Glevel 177dea2769 fix: переписать UI на st.rerun() polling — кнопки и события работают корректно
Проблема: blocking while-цикл блокировал Streamlit, кнопка «Стоп» не работала
(Streamlit не может обработать клик пока скрипт заблокирован), кнопка мигала
каждые 0.3с из-за динамического ключа.

Решение: rerun-based polling — каждая «итерация» это полный rerun скрипта:
- chat_running / pipe_running флаги в session_state
- thread + queue живут в session_state между рерандами
- time.sleep(0.5) → st.rerun() вместо while-цикла
- кнопка «Стоп» рендерится нормально и реагирует мгновенно
- « Агент работает... Nс» при простое >15с
- лог событий накапливается в session_state.chat_events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 17:05:09 +03:00

brojs-agent

AI-агент для автоматического выполнения заданий курса KFU-26-1 на platform.brojs.ru.

Что делает агент

  1. Читает незакрытые задания из журнала через BroJS MCP
  2. Пишет Python-решение для каждого задания
  3. Создаёт репозиторий на git.brojs.ru/glevelll/task-<id>
  4. Коммитит решение через Gitea API
  5. Отправляет ссылку на репозиторий как ответ
  6. Сдаёт задание на проверку
  7. При пересдаче — клонирует, исправляет, пушит снова

Стек

  • Python 3.11+
  • deepagents — мультиагентный фреймворк
  • LangGraph — граф пайплайна
  • LangChain — инструменты и MCP-адаптеры
  • OpenRouter → gpt-oss-20b:free
  • Gitea REST API (git.brojs.ru)
  • BroJS Journal MCP (platform.brojs.ru)

Почему OpenRouter, а не Ollama

Все решения используют OpenRouter (https://openrouter.ai/api/v1) с моделью openai/gpt-oss-20b:free через стандартный langchain_openai.ChatOpenAI.

Критерий OpenRouter Ollama (локальная)
Требования к железу Нет (облако) GPU / 816 GB RAM
Совместимость с LangChain Полная (OpenAI-совместимый API) Требует отдельного провайдера
Смена модели Одна строка в коде Скачать новую модель (~GB)
CI / автоматизация Работает из любой среды Нужен локальный сервер
Стоимость Бесплатный тир (gpt-oss-20b:free) Бесплатно, но ресурсоёмко

Ключ хранится в .env как OPENAI_API_KEY=sk-or-v1-... — формат совместим с OpenAI SDK, поэтому смена провайдера (OpenAI, Azure, Ollama) не требует изменений в коде, только .env.

Установка

# Клонировать репозиторий
git clone https://github.com/Glevelll/brojs-agent.git
cd brojs-agent

# Установить зависимости
pip install -r requirements.txt
# или через uv:
# uv sync

# Создать .env из шаблона
cp .env.example .env
# Отредактировать .env — заменить заглушки на реальные ключи

Настройка .env

OPENAI_API_KEY=sk-or-...        # OpenRouter ключ
JOURNAL_TOKEN=jrnl_...          # BroJS токен (генерируется на platform.brojs.ru)
GITEA_TOKEN=...                 # Токен на git.brojs.ru
TAVILY_API_KEY=...              # Опционально, для web_search

Где взять токены

Токен Где получить
OPENAI_API_KEY https://openrouter.ai/settings/keys
JOURNAL_TOKEN platform.brojs.ru → профиль → API токены
GITEA_TOKEN git.brojs.ru → Settings → Applications → Access Tokens
TAVILY_API_KEY https://tavily.com (опционально)

⚠️ ВАЖНО: Никогда не коммить .env в репозиторий! Он добавлен в .gitignore.

Запуск

Запуск пайплайна (выполнить все незакрытые задания)

python -c "
import asyncio
from src.agent.graph.pipeline import pipeline
from langchain_core.messages import HumanMessage

async def main():
    result = await pipeline.ainvoke(
        {'tasks': [], 'current_index': 0, 'results': [], 'errors': []},
        {'configurable': {'thread_id': 'pipeline-run-1'}}
    )
    print(f'Выполнено: {len(result[\"results\"])} заданий')
    print(f'Ошибки: {result[\"errors\"]}')

asyncio.run(main())
"

LangGraph dev server (UI в браузере)

# Установить langgraph-cli
pip install "langgraph-cli[inmem]"

# Запустить dev server
langgraph dev --allow-blocking --port 2024
# Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024

Интерактивный чат с главным агентом

python -c "
import asyncio
from src.agent.agent import agent
from langchain_core.messages import HumanMessage

async def main():
    config = {'configurable': {'thread_id': 'chat-1'}}
    while True:
        user = input('Вы: ')
        if user == 'exit': break
        result = await agent.ainvoke(
            {'messages': [HumanMessage(content=user)]}, config
        )
        print('Агент:', result['messages'][-1].content)

asyncio.run(main())
"

Структура проекта

brojs-agent/
├── .env.example              # шаблон переменных окружения
├── .gitignore
├── requirements.txt
├── pyproject.toml
├── langgraph.json
├── agent.py                  # точка входа для langgraph dev
└── src/
    └── agent/
        ├── agent.py          # создание агентов (main, homework, rework)
        ├── constants.py      # COURSE_ID, GITEA_OWNER, пути
        ├── llm.py            # LLM через OpenRouter
        ├── gitea_tools.py    # Gitea REST API инструменты
        ├── mcp_client.py     # BroJS Journal MCP клиент
        ├── tools.py          # git + web инструменты
        ├── prompts.py        # системные промпты
        ├── subagents.py      # спецификации субагентов
        ├── middlewares/
        │   ├── sanitize_tool_calls.py       # блокировка несуществующих инструментов
        │   └── validate_journal_workflow.py # порядок task_update_answer → task_submit
        ├── graph/
        │   └── pipeline.py   # LangGraph пайплайн по всем заданиям
        └── agent_workspace/  # рабочая директория агента (клонированные репо)

Заглушки в коде

Файл Переменная Что заменить
src/agent/llm.py OPENAI_API_KEY OpenRouter ключ в .env
src/agent/mcp_client.py JOURNAL_TOKEN BroJS токен в .env
src/agent/gitea_tools.py GITEA_TOKEN Gitea токен в .env
S
Description
AI-агент для автоматического выполнения заданий курса KFU-26-1
Readme 343 KiB
Languages
Python 100%