0f38d8d588
Includes deep-agents-ui integration, rework detection via Gitea, tool-call sanitization fixes, and startup scripts.
568 lines
24 KiB
Markdown
568 lines
24 KiB
Markdown
# Архитектура папки `src/agent`
|
||
|
||
## Обзор проекта
|
||
|
||
Проект **brojs-agent** — это мультиагентная система для автоматизации выполнения заданий курса **KFU-26-1** на платформе BroJS (platform.brojs.ru). Система использует **OpenRouter** (облачный LLM) и координирует несколько специализированных агентов через фреймворк **deepagents** и **LangGraph**.
|
||
|
||
### Основная задача
|
||
1. Получить список незакрытых заданий из журнала BroJS
|
||
2. Запустить агента для выполнения каждого задания
|
||
3. Агент пишет код, создаёт репозиторий на Gitea, тестирует, отправляет ответ
|
||
4. При отклонении (пересдача) — клонирует код, исправляет по замечаниям, пересдаёт
|
||
|
||
---
|
||
|
||
## Структура файлов
|
||
|
||
```
|
||
src/agent/
|
||
├── agent.py # 🎯 Инициализация всех агентов (главный орк, ДЗ, пересдача)
|
||
├── constants.py # ⚙️ Константы: ID курса, пути, Gitea-параметры
|
||
├── llm.py # 🧠 Инициализация LLM через OpenRouter
|
||
├── mcp_client.py # 📡 Загрузка инструментов Journal через MCP
|
||
├── gitea_tools.py # 🔧 REST-инструменты для git.brojs.ru
|
||
├── tools.py # 🌐 Git и веб-инструменты
|
||
├── prompts.py # 📝 Системные промпты для всех агентов
|
||
├── subagents.py # 🤖 Спецификации субагентов
|
||
├── runner_tools.py # ⚙️ Инструменты запуска заданий
|
||
├── solve_tools.py # 💻 Инструменты для решения (генерация кода, валидация)
|
||
├── solve_prompts.py # 📄 Промпты для решения заданий
|
||
├── middlewares/ # 🛡️ Middleware для обработки ошибок и валидации
|
||
│ ├── retry_on_rate_limit.py # Retry при ошибках 429
|
||
│ ├── sanitize_tool_calls.py # Фильтрация незнакомых инструментов
|
||
│ └── validate_journal_workflow.py # Валидация workflow Journal
|
||
├── graph/ # 📊 LangGraph pipeline
|
||
│ ├── pipeline.py # Основной pipeline последовательного выполнения
|
||
│ └── __init__.py
|
||
├── agent_workspace/ # 💾 Рабочая папка агента
|
||
│ ├── AGENTS.md # Память агента между запусками
|
||
│ └── large_tool_results/ # Кэш больших результатов
|
||
└── __pycache__/
|
||
```
|
||
|
||
---
|
||
|
||
## Компоненты системы
|
||
|
||
### 1️⃣ **LLM (Большая языковая модель)**
|
||
**Файл:** [llm.py](llm.py)
|
||
|
||
```python
|
||
ChatOpenAI(
|
||
model="openai/gpt-oss-20b:free",
|
||
base_url="https://openrouter.ai/api/v1",
|
||
api_key=os.getenv("OPENAI_API_KEY"), # sk-or-v1-...
|
||
temperature=0.0,
|
||
)
|
||
```
|
||
|
||
- **Провайдер:** OpenRouter (облачный сервис)
|
||
- **Модель:** `gpt-oss-20b:free` (бесплатная, 20B параметров)
|
||
- **Температура:** 0.0 (детерминированные ответы)
|
||
- **Зачем OpenRouter вместо Ollama?**
|
||
- Нет требований к GPU/RAM
|
||
- Совместим с LangChain из коробки
|
||
- Работает в CI/CD
|
||
- Бесплатный тир
|
||
|
||
---
|
||
|
||
### 2️⃣ **Konstantes & пути**
|
||
**Файл:** [constants.py](constants.py)
|
||
|
||
Определяет ключевые параметры:
|
||
|
||
```python
|
||
COURSE_ID = "698b49da77cb6d4d2e43ce78" # ID курса KFU-26-1
|
||
GITEA_OWNER = "dapa46" # Владелец репозиториев на git.brojs.ru
|
||
AGENT_WORKSPACE_DIR = /path/to/agent_workspace # Рабочая папка
|
||
AGENTS_MD_VFS_PATH = "/AGENTS.md" # Виртуальный путь памяти
|
||
```
|
||
|
||
Функция `ensure_agents_md_file()` создаёт `AGENTS.md` при первом запуске — файл служит **памятью** агента между сообщениями.
|
||
|
||
---
|
||
|
||
### 3️⃣ **MCP: Инструменты Journal**
|
||
**Файл:** [mcp_client.py](mcp_client.py)
|
||
|
||
**MCP (Model Context Protocol)** — стандарт для подключения инструментов к LLM.
|
||
|
||
#### Как это работает:
|
||
1. **Загрузка асинхронно** через `MultiServerMCPClient`
|
||
2. **HTTP transport** к `https://platform.brojs.ru/jrnl-bh/api/mcp`
|
||
3. **Авторизация** Bearer токеном из `.env` (`JOURNAL_TOKEN=jrnl_...`)
|
||
4. **Раздает инструменты в две группы:**
|
||
|
||
| Группа | Инструменты | Назначение |
|
||
|--------|-----------|-----------|
|
||
| **Courses & Lessons** | `courses_list`, `lessons_list` | Получить структуру курса |
|
||
| **Tasks & Submissions** | `tasks_list`, `task_text`, `task_get`, `task_update_answer`, `task_submit`, `task_submission_status` | Работа с заданиями |
|
||
|
||
#### Обработка ошибок:
|
||
- **Exponential backoff** при ошибке 429 (rate limit): 15, 30, 60, 120, 240 сек
|
||
- **Персистентный клиент** переиспользуется для всего запуска
|
||
- **Минимальная пауза** 1.5 сек между вызовами (предотвращает burst)
|
||
|
||
---
|
||
|
||
### 4️⃣ **Gitea REST API**
|
||
**Файл:** [gitea_tools.py](gitea_tools.py)
|
||
|
||
Инструменты для работы с git.brojs.ru:
|
||
|
||
```python
|
||
@tool()
|
||
def gitea_list_repos() → str
|
||
# Список своих репозиториев
|
||
|
||
@tool()
|
||
def gitea_create_repo(name: str) → str
|
||
# Создать новый репозиторий
|
||
|
||
@tool()
|
||
def gitea_get_file(repo: str, path: str) → str
|
||
# Прочитать файл из репозитория
|
||
|
||
@tool()
|
||
def gitea_create_file(repo: str, path: str, content: str, message: str) → str
|
||
# Создать/обновить файл
|
||
|
||
@tool()
|
||
def gitea_delete_repo(repo: str) → str
|
||
# Удалить репозиторий
|
||
```
|
||
|
||
**Авторизация:** Token через заголовок `Authorization: token {GITEA_TOKEN}`
|
||
|
||
**Эндпоинты:** REST API v1: `/api/v1/repos/{owner}/{repo}/...`
|
||
|
||
---
|
||
|
||
### 5️⃣ **Git & Web tools**
|
||
**Файл:** [tools.py](tools.py)
|
||
|
||
#### Git инструменты (работают в `agent_workspace`):
|
||
- `git_clone(url)` — клонировать репозиторий
|
||
- `git_commit(repo, files, message)` — коммитить файлы
|
||
- `git_push(repo)` — пушить в origin
|
||
- `git_get_commit_history(repo)` — получить историю
|
||
|
||
#### Web инструменты:
|
||
- `web_search(query)` — поиск через Tavily (если настроен API ключ)
|
||
- `get_page_content(url)` — загрузить страницу и вернуть Markdown
|
||
|
||
---
|
||
|
||
### 6️⃣ **Solve tools: Генерация и валидация кода**
|
||
**Файл:** [solve_tools.py](solve_tools.py)
|
||
|
||
Специализированные инструменты для решения заданий:
|
||
|
||
```python
|
||
@tool()
|
||
async def generate_code_solution(
|
||
task_id: str,
|
||
task_text: str,
|
||
previous_code: str = ""
|
||
) → str
|
||
# Генерирует/улучшает Python-решение
|
||
# Вызывает субагента с prompt из solve_prompts.py
|
||
|
||
@tool()
|
||
async def validate_teacher_comment(
|
||
task_id: str,
|
||
comment: str
|
||
) → dict
|
||
# Валидирует замечание преподавателя
|
||
# Разбирает комментарий на claim'ы, каждый валидирует отдельно
|
||
```
|
||
|
||
**Кэширование:** Результаты кэшируются в `agent_workspace/large_tool_results/`
|
||
|
||
---
|
||
|
||
### 7️⃣ **Агенты (deepagents)**
|
||
**Файл:** [agent.py](agent.py)
|
||
|
||
Система создаёт **3 основных агента:**
|
||
|
||
#### 🎯 Главный агент (`agent`)
|
||
- **Инструменты:** Gitea, Journal, solve_task
|
||
- **Роль:** Оркестратор — управляет всем процессом
|
||
- **Субагенты:** web_search, homework_doing, journal_bh_tasks_submissions
|
||
- **Middleware:** SanitizeToolCallsMiddleware (фильтрует недоступные инструменты)
|
||
|
||
#### 💻 Агент ДЗ (`homework_direct_agent`)
|
||
- **Инструменты:** Git, Gitea, Web, Journal, Solve tools
|
||
- **Роль:** Выполняет одно задание от начала до конца
|
||
- **Процесс:**
|
||
1. Читает текст задания
|
||
2. Генерирует решение (код)
|
||
3. Тестирует локально
|
||
4. Создаёт/обновляет репозиторий
|
||
5. Пушит решение
|
||
6. Отправляет ответ в Journal
|
||
7. Сдаёт на проверку
|
||
- **Middleware:**
|
||
- `RetryOnRateLimitMiddleware()` — retry при 429
|
||
- `SanitizeToolCallsMiddleware` — фильтрация
|
||
- `ValidateJournalWorkflowMiddleware()` — проверка workflow
|
||
|
||
#### 🔄 Агент пересдачи (`rework_agent`)
|
||
- **Инструменты:** Такие же как homework_direct_agent
|
||
- **Роль:** При отклонении — клонирует, исправляет, пересдаёт
|
||
- **Процесс:**
|
||
1. Клонирует предыдущее решение
|
||
2. Читает замечания преподавателя
|
||
3. Исправляет код по замечаниям
|
||
4. Пушит обновление
|
||
5. Переотправляет ответ
|
||
6. Пересдаёт
|
||
|
||
---
|
||
|
||
### 8️⃣ **Субагенты (для делегирования)**
|
||
**Файл:** [subagents.py](subagents.py)
|
||
|
||
LangGraph поддерживает делегирование задач субагентам. Определены 3 субагента:
|
||
|
||
```python
|
||
subagent_specs = [
|
||
{
|
||
"name": "web_search",
|
||
"description": "Ищет информацию в интернете",
|
||
"system_prompt": research_instructions,
|
||
# tools добавляются в agent.py
|
||
},
|
||
{
|
||
"name": "homework_doing",
|
||
"description": "Выполняет домашние задания",
|
||
"system_prompt": homework_doing_instructions,
|
||
},
|
||
{
|
||
"name": "journal_bh_tasks_submissions",
|
||
"description": "Работает с Journal: задания и сдачи",
|
||
"system_prompt": journal_tasks_submissions_instructions,
|
||
},
|
||
]
|
||
```
|
||
|
||
Каждый субагент получает свой набор инструментов и промпт через middleware.
|
||
|
||
---
|
||
|
||
### 9️⃣ **Middleware: фильтрация и обработка ошибок**
|
||
**Папка:** [middlewares/](middlewares/)
|
||
|
||
#### `retry_on_rate_limit.py`
|
||
Перехватывает ошибки 429 и retry'т с exponential backoff.
|
||
|
||
```python
|
||
class RetryOnRateLimitMiddleware:
|
||
async def before_tool_call(tool, input):
|
||
# Ищет 429 в предыдущих ошибках
|
||
if rate_limit_error:
|
||
await asyncio.sleep(backoff)
|
||
return retry_tool_call()
|
||
```
|
||
|
||
#### `sanitize_tool_calls.py`
|
||
Фильтрует вызовы инструментов — дозволяет только известные.
|
||
|
||
```python
|
||
class SanitizeToolCallsMiddleware:
|
||
def __init__(self, known_tools: set[str]):
|
||
self.known_tools = known_tools
|
||
|
||
async def before_tool_call(tool, input):
|
||
if tool.name not in self.known_tools:
|
||
raise ValueError(f"Unknown tool: {tool.name}")
|
||
```
|
||
|
||
#### `validate_journal_workflow.py`
|
||
Проверяет корректность workflow при работе с Journal:
|
||
- После получения задания → обновить ответ
|
||
- Перед сдачей → проверить статус
|
||
- После сдачи → подтвердить
|
||
|
||
---
|
||
|
||
### 🔟 **LangGraph Pipeline**
|
||
**Файл:** [graph/pipeline.py](graph/pipeline.py)
|
||
|
||
**LangGraph** — это граф для управления состоянием и последовательностью выполнения.
|
||
|
||
#### Состояние (`PipelineState`):
|
||
```python
|
||
class PipelineState(TypedDict):
|
||
tasks: list[TaskInfo] # Список всех заданий
|
||
current_index: int # Индекс текущего задания
|
||
results: list[dict] # Результаты выполнения
|
||
errors: list[str] # Ошибки
|
||
```
|
||
|
||
#### Узлы графа:
|
||
1. **`fetch_tasks`** → Получить список незакрытых заданий из Journal
|
||
2. **`process_task`** → Запустить `homework_direct_agent` на текущее задание
|
||
3. **`check_result`** → Проверить результат (успех/ошибка)
|
||
4. **`next_or_done`** → Перейти к следующему заданию или завершить
|
||
|
||
#### Переходы:
|
||
```
|
||
START → fetch_tasks → process_task → check_result → (success) → next_or_done
|
||
→ (error) → next_or_done
|
||
→ (need_rework) → run_rework_agent → next_or_done
|
||
```
|
||
|
||
---
|
||
|
||
### 1️⃣1️⃣ **Промпты (системные инструкции)**
|
||
**Файл:** [prompts.py](prompts.py)
|
||
|
||
Содержит системные промпты для каждого агента:
|
||
|
||
#### `main_agent_instructions`
|
||
Инструкции для главного оркестратора:
|
||
- Управляет процессом в целом
|
||
- Делегирует задачи субагентам
|
||
- Координирует работу
|
||
|
||
#### `homework_doing_instructions`
|
||
Инструкции для агента выполнения ДЗ:
|
||
- Как читать задание
|
||
- Как генерировать код
|
||
- Как тестировать
|
||
- Как создавать репозиторий
|
||
- Как отправлять ответ
|
||
|
||
#### `rework_instructions`
|
||
Инструкции для агента пересдачи:
|
||
- Как интерпретировать замечания
|
||
- Как исправлять код
|
||
- Как пересдавать
|
||
|
||
#### `research_instructions`
|
||
Инструкции для веб-поиска:
|
||
- Искать только из реально открытых страниц
|
||
- Не выдумывать факты
|
||
- Подтверждать URL
|
||
|
||
---
|
||
|
||
## Поток выполнения
|
||
|
||
### 📊 Общий сценарий
|
||
|
||
```
|
||
1. Пользователь запускает pipeline.py
|
||
↓
|
||
2. pipeline.fetch_tasks()
|
||
→ Запрашивает Journal: "Дай мне все незакрытые задания курса KFU-26-1"
|
||
→ Получает список TaskInfo (id, title, status)
|
||
↓
|
||
3. Для каждого задания (while current_index < len(tasks)):
|
||
↓
|
||
4. pipeline.process_task()
|
||
→ Вызывает homework_direct_agent.invoke({
|
||
"messages": [HumanMessage("Выполни задание XYZ")]
|
||
})
|
||
↓
|
||
5. homework_direct_agent работает:
|
||
|
||
5a. Читает текст задания через task_text()
|
||
5b. Вызывает generate_code_solution() → получает Python-код
|
||
5c. Тестирует код локально (git_clone → test.py → git_push)
|
||
5d. Обновляет ответ в Journal через task_update_answer()
|
||
5e. Отправляет на проверку через task_submit()
|
||
↓
|
||
6. pipeline.check_result()
|
||
→ Проверяет: успех? ошибка? нужна пересдача?
|
||
↓
|
||
7. Если успех → перейти к следующему заданию
|
||
Если ошибка → залогировать и перейти
|
||
Если пересдача → запустить rework_agent
|
||
↓
|
||
8. pipeline.next_or_done()
|
||
→ current_index += 1
|
||
→ Если есть ещё задания → goto 4
|
||
→ Иначе → завершить, вернуть results
|
||
```
|
||
|
||
---
|
||
|
||
## Интеграции
|
||
|
||
### 🔌 Внешние сервисы
|
||
|
||
| Сервис | Назначение | Авторизация | Endpoint |
|
||
|--------|-----------|-----------|----------|
|
||
| **OpenRouter** | LLM (генерация кода) | `OPENAI_API_KEY=sk-or-v1-...` | `https://openrouter.ai/api/v1` |
|
||
| **BroJS Journal** | Задания и сдачи | `JOURNAL_TOKEN=jrnl_...` | `https://platform.brojs.ru/jrnl-bh/api/mcp` |
|
||
| **Gitea (git.brojs.ru)** | Хранилище кода | `GITEA_TOKEN=...` | `https://git.brojs.ru/api/v1` |
|
||
| **Tavily** | Веб-поиск (опционально) | `TAVILY_API_KEY=tvly-...` | `https://api.tavily.com` |
|
||
|
||
### 🛠️ Локальные компоненты
|
||
|
||
- **agent_workspace/** — кэш результатов, память агента (AGENTS.md)
|
||
- **Python subprocess** — выполнение git команд, тестирование кода
|
||
|
||
---
|
||
|
||
## Конфигурация (`.env`)
|
||
|
||
```bash
|
||
# Обязательные
|
||
JOURNAL_TOKEN=jrnl_8b251e8e5b345e310269a697968abecad08043a2a30f07229cefe488e59fb951
|
||
GITEA_TOKEN=f5892316061c124f447710bb242f2f90c565a411
|
||
OPENAI_API_KEY=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
|
||
|
||
# Опциональные
|
||
TAVILY_API_KEY=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 # для web_search
|
||
```
|
||
|
||
**Где получить:**
|
||
- `JOURNAL_TOKEN` → platform.brojs.ru → Settings → Access Tokens
|
||
- `GITEA_TOKEN` → git.brojs.ru → Settings → Applications → Access Tokens
|
||
- `OPENAI_API_KEY` → openrouter.ai → API Keys (формат `sk-or-v1-...`)
|
||
- `TAVILY_API_KEY` → tavily.com → API Keys
|
||
|
||
---
|
||
|
||
## Ключевые паттерны
|
||
|
||
### 🎯 Паттерн: Tool Middleware Pipeline
|
||
|
||
```python
|
||
# В deepagents каждый вызов инструмента проходит через цепь middleware:
|
||
|
||
инструмент → middleware1.before → middleware2.before → ... → ИСПОЛНЕНИЕ
|
||
↓
|
||
результат ← middleware1.after ← middleware2.after ← ... ← результат
|
||
```
|
||
|
||
### 🔄 Паттерн: Субагент как инструмент
|
||
|
||
```python
|
||
# Главный агент не делает работу сам, а вызывает:
|
||
agent.invoke()
|
||
→ Видит "нужно выполнить ДЗ"
|
||
→ Вызывает инструмент: homework_doing (это субагент)
|
||
→ Субагент имеет свой LLM, свой prompt, свои инструменты
|
||
→ Возвращает результат главному агенту
|
||
```
|
||
|
||
### 📝 Паттерн: Виртуальная файловая система (VFS)
|
||
|
||
```python
|
||
# Агент видит файловую систему как:
|
||
/AGENTS.md # памятью агента (бинд к agent_workspace/AGENTS.md)
|
||
/skills/... # bundled skills (бинд к src/agent/skills/)
|
||
~/project/... # рабочая папка (бинд к agent_workspace/)
|
||
```
|
||
|
||
Это позволяет агенту иметь консистентный вид файловой системы, хотя на самом деле это виртуальная проекция.
|
||
|
||
### 💾 Паттерн: Асинхронная загрузка MCP
|
||
|
||
```python
|
||
# MCP может быть долгой операцией (сетевой запрос)
|
||
# Решение: asyncio.run() в отдельном потоке при наличии event loop
|
||
|
||
try:
|
||
asyncio.get_running_loop() # есть ли event loop?
|
||
# Да → запустить в ThreadPoolExecutor
|
||
with ThreadPoolExecutor() as pool:
|
||
result = pool.submit(lambda: asyncio.run(fetch())).result()
|
||
except RuntimeError:
|
||
# Нет → просто asyncio.run()
|
||
result = asyncio.run(fetch())
|
||
```
|
||
|
||
---
|
||
|
||
## Отладка
|
||
|
||
### 📋 Логирование
|
||
|
||
Система выводит логи при инициализации:
|
||
```
|
||
=== Загружено: journal=X, gitea=5, git=5 ===
|
||
```
|
||
|
||
При ошибке MCP:
|
||
```
|
||
MCP 'journal-bh-professor': не удалось загрузить инструменты — ...
|
||
```
|
||
|
||
### 🧪 Тестирование компонентов
|
||
|
||
```python
|
||
# Импорт отдельного агента
|
||
from src.agent.agent import homework_direct_agent
|
||
|
||
# Запуск на тестовом задании
|
||
result = homework_direct_agent.invoke({
|
||
"messages": [HumanMessage("Выполни задание ...")]
|
||
})
|
||
```
|
||
|
||
### 🔍 Проверка инструментов
|
||
|
||
```python
|
||
# Какие инструменты есть у агента?
|
||
print(homework_direct_agent.tools)
|
||
|
||
# Какие middleware установлены?
|
||
# Нужно смотреть в create_deep_agent(middleware=[...])
|
||
```
|
||
|
||
---
|
||
|
||
## Возможные ошибки и решения
|
||
|
||
| Ошибка | Причина | Решение |
|
||
|--------|---------|---------|
|
||
| `ImportError: No module named 'src.agent.agent'` | Неправильный PYTHONPATH | `cd /path/to/project && python -m src.agent.graph.pipeline` |
|
||
| `MCP journal: не удалось загрузить` | Токен неправильный/устарел | Сгенерировать новый `JOURNAL_TOKEN` на platform.brojs.ru |
|
||
| `429 Too Many Requests` | Rate limit OpenRouter | Middleware автоматически retry'т с backoff |
|
||
| `gitea_create_repo: 401 Unauthorized` | Неправильный Gitea токен | Проверить `GITEA_TOKEN` в `.env` |
|
||
| `generate_code_solution timeout` | Генерация долгая | Увеличить timeout в LLM конфиге |
|
||
|
||
---
|
||
|
||
## Резюме: Как это работает вместе
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ PIPELINE (LangGraph) │
|
||
│ Последовательно: fetch_tasks → process_task → check │
|
||
└──────────────────────┬──────────────────────────────────┘
|
||
│
|
||
├─→ JOURNAL (MCP)
|
||
│ Получить задания, отправить ответы
|
||
│
|
||
├─→ HOMEWORK_DIRECT_AGENT
|
||
│ ├─ generate_code_solution (LLM)
|
||
│ ├─ gitea_* (REST API)
|
||
│ ├─ git_* (subprocess)
|
||
│ └─ task_submit (Journal)
|
||
│
|
||
├─→ REWORK_AGENT
|
||
│ └─ Аналогично, но для пересдачи
|
||
│
|
||
└─→ MIDDLEWARES
|
||
├─ SanitizeToolCallsMiddleware
|
||
├─ RetryOnRateLimitMiddleware
|
||
└─ ValidateJournalWorkflowMiddleware
|
||
|
||
┌─────────────────────┐
|
||
│ OpenRouter LLM │
|
||
│ gpt-oss-20b:free │
|
||
└─────────────────────┘
|
||
(Генерация кода и рассуждения)
|
||
```
|
||
|
||
Система автоматизирует весь цикл выполнения и сдачи заданий, обрабатывая ошибки, пересдачи и координируя несколько специализированных агентов через единый фреймворк.
|
||
|