feat: LLM-first architecture — agent orchestrates everything via solve_task tool
- cli.py: cmd_run() now uses agent.ainvoke() instead of pipeline - ui.py: 'run all' button and chat use main agent; status tab fixed (thread + st.rerun) - src/agent/runner_tools.py: new solve_task tool wrapping reliable single-task execution - src/agent/agent.py: solve_task added to main agent tools - src/agent/prompts.py: main_agent_instructions rewritten for LLM-first orchestration LLM decides what to do and in what order; Python tools are just hands.
This commit is contained in:
+3
-2
@@ -19,6 +19,7 @@ from src.agent.prompts import (
|
||||
rework_instructions,
|
||||
)
|
||||
from src.agent.subagents import subagent_specs_without_tools
|
||||
from src.agent.runner_tools import solve_task
|
||||
from src.agent.solve_tools import SOLVE_TOOLS
|
||||
from src.agent.tools import GIT_TOOLS, WEB_TOOLS
|
||||
|
||||
@@ -75,7 +76,7 @@ _git_names = {t.name for t in GIT_TOOLS}
|
||||
_web_names = {t.name for t in WEB_TOOLS}
|
||||
_solve_names = {t.name for t in SOLVE_TOOLS}
|
||||
|
||||
_main_tool_names = _BUILTIN | _gitea_names
|
||||
_main_tool_names = _BUILTIN | _gitea_names | _journal_names | {"solve_task"}
|
||||
|
||||
_subagent_tool_names: dict[str, set[str]] = {
|
||||
"web_search": _BUILTIN | _web_names,
|
||||
@@ -113,7 +114,7 @@ subagents = [
|
||||
|
||||
agent = create_deep_agent(
|
||||
model=llm,
|
||||
tools=list(GITEA_TOOLS),
|
||||
tools=[*GITEA_TOOLS, *_journal_tools, solve_task],
|
||||
system_prompt=main_agent_instructions,
|
||||
backend=_composite_backend,
|
||||
memory=[AGENTS_MD_VFS_PATH],
|
||||
|
||||
+32
-26
@@ -163,43 +163,49 @@ Gitea owner = "glevelll"
|
||||
|
||||
main_agent_instructions = """\
|
||||
Ты — главный агент-исполнитель домашних заданий курса KFU-26-1 на platform.brojs.ru.
|
||||
Твоя роль — получать задания из журнала и выполнять их качественно.
|
||||
Ты полностью управляешь всем процессом: сам решаешь что делать, в каком порядке,
|
||||
какие инструменты вызывать. Python-инструменты — это только твои руки.
|
||||
|
||||
## Известные курсы
|
||||
- KFU-26-1 = courseId `698b49da77cb6d4d2e43ce78`
|
||||
|
||||
## Доступные субагенты (вызывай через инструмент `task`)
|
||||
- `journal_bh_tasks_submissions`: читает задания, проверяет статусы, отправляет ответы
|
||||
- `homework_doing`: ВЫПОЛНЯЕТ задание (пишет код, создаёт репо, сдаёт)
|
||||
- `web_search`: ищет информацию в интернете (только если нужно)
|
||||
## Прямые инструменты (вызывай напрямую)
|
||||
|
||||
## Прямые Gitea-инструменты (доступны напрямую без субагента)
|
||||
- `gitea_list_repos` — список репозиториев, также возвращает username
|
||||
- `gitea_create_repo` — создать репозиторий
|
||||
- `gitea_write_file` — создать/обновить файл (автокоммит)
|
||||
- `gitea_get_file` — получить файл
|
||||
### Journal (MCP)
|
||||
- `mcp__journal-bh-professor__tasks_list` — список всех заданий курса со статусами
|
||||
- `mcp__journal-bh-professor__task_get` — детали задания (статус, ответ, комментарии)
|
||||
- `mcp__journal-bh-professor__task_text` — полный текст задания
|
||||
- `mcp__journal-bh-professor__task_update_answer` — установить ссылку на репо
|
||||
- `mcp__journal-bh-professor__task_submit` — сдать задание
|
||||
- `mcp__journal-bh-professor__task_comment` — написать комментарий
|
||||
|
||||
### Исполнение
|
||||
- `solve_task(task_id)` — **главный инструмент**: полностью выполняет одно задание
|
||||
(читает условие, пишет код, создаёт репо на Gitea, верифицирует, сдаёт).
|
||||
Автоматически определяет первая сдача или пересдача.
|
||||
|
||||
### Gitea
|
||||
- `gitea_list_repos`, `gitea_create_repo`, `gitea_write_file`, `gitea_get_file`
|
||||
|
||||
## Один инструмент за шаг (ОБЯЗАТЕЛЬНО)
|
||||
В одном сообщении — **только один** вызов любого инструмента (`task`, `ls`, `read_file`,
|
||||
`write_file`, `edit_file`, `glob`, `grep`, `execute` и т.д.).
|
||||
Сначала дождись результата, затем следующий вызов.
|
||||
Вызывай строго по одному инструменту. Жди результата перед следующим вызовом.
|
||||
|
||||
## Типовые маршруты
|
||||
## Как выполнить все todo-задания
|
||||
1. `mcp__journal-bh-professor__tasks_list(courseId="698b49da77cb6d4d2e43ce78")`
|
||||
2. Из ответа выбери задания со статусом `todo` или `in_progress`
|
||||
3. Для каждого последовательно: `solve_task(task_id="<id>")`
|
||||
4. Дождись "OK ..." перед следующим заданием
|
||||
5. Доложи итоги
|
||||
|
||||
### Получить список заданий
|
||||
1. Делегируй `journal_bh_tasks_submissions`: получить tasks_list для courseId
|
||||
## Как выполнить одно задание
|
||||
1. `solve_task(task_id="<id>")` — сделает всё сам
|
||||
|
||||
### Выполнить задание
|
||||
1. Делегируй `homework_doing`: выполни задание с taskId=<id>
|
||||
(он сам прочитает текст, создаст репо, напишет код и сдаст)
|
||||
2. Верни пользователю ссылку на репозиторий
|
||||
|
||||
### Проверить статусы
|
||||
1. Делегируй `journal_bh_tasks_submissions`: получить статусы всех заданий курса
|
||||
## Как проверить статусы
|
||||
1. `mcp__journal-bh-professor__tasks_list(courseId="698b49da77cb6d4d2e43ce78")`
|
||||
|
||||
## Жёсткие ограничения
|
||||
- Не вызывай больше одного инструмента за шаг
|
||||
- Не делегируй субагенту несколько независимых задач сразу
|
||||
- Не говори что задание выполнено, если оно не было реально выполнено
|
||||
- Не говори что задание выполнено, если `solve_task` не вернул "OK"
|
||||
- Выполняй задания строго последовательно, не параллельно
|
||||
- Не вызывай `solve_task` для заданий со статусом `done` или `ready_for_review`
|
||||
- Не подменяй требования задания своими догадками
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Инструмент solve_task — надёжное выполнение одного задания.
|
||||
|
||||
Агент-оркестратор вызывает его для каждого todo-задания.
|
||||
Python внутри обеспечивает верификацию и страховочный сабмит,
|
||||
но решение о том какие задания выполнять и в каком порядке
|
||||
принимает LLM-оркестратор.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from langchain.tools import tool
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
|
||||
@tool
|
||||
async def solve_task(task_id: str) -> str:
|
||||
"""Полностью выполнить одно задание курса: написать код, запушить в репо, сдать.
|
||||
|
||||
Автоматически определяет режим:
|
||||
- Первая сдача: читает условие, генерирует код с нуля
|
||||
- Пересдача: читает замечания, исправляет или защищает решение
|
||||
|
||||
После выполнения верифицирует репозиторий и гарантированно сдаёт задание.
|
||||
|
||||
Args:
|
||||
task_id: ID задания из tasks_list (например 6a22c713fd30e81cf315ea04)
|
||||
|
||||
Returns:
|
||||
Строка с результатом: "OK ..." или "ERROR ..."
|
||||
"""
|
||||
# Ленивый импорт чтобы избежать circular import на уровне модуля
|
||||
from src.agent.agent import homework_direct_agent, rework_agent
|
||||
from src.agent.graph.pipeline import (
|
||||
MAX_RETRIES,
|
||||
_existing_repo_url,
|
||||
_fix_prompt,
|
||||
_force_submit,
|
||||
_invoke_with_retry,
|
||||
_is_submitted,
|
||||
_needs_retry,
|
||||
_task_json,
|
||||
_task_text,
|
||||
_verify_repo,
|
||||
TaskInfo,
|
||||
)
|
||||
|
||||
# Небольшая пауза — снижает давление на rate limit
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# Определяем: первая сдача или пересдача
|
||||
repo_url = await _existing_repo_url(task_id)
|
||||
is_rework = repo_url is not None
|
||||
|
||||
if is_rework:
|
||||
data = await _task_json(task_id)
|
||||
comments = data.get("comments") or data.get("feedback", "")
|
||||
prompt = (
|
||||
f"Пересдача задания.\n\n"
|
||||
f"ID: {task_id}\n"
|
||||
f"Репозиторий: {repo_url}\n"
|
||||
f"Комментарии преподавателя: {comments}\n\n"
|
||||
"Внеси исправления и отправь снова."
|
||||
)
|
||||
agent_to_use = rework_agent
|
||||
else:
|
||||
task_text = await _task_text(task_id)
|
||||
prompt = (
|
||||
f"Выполни задание.\n\n"
|
||||
f"ID: {task_id}\n\n"
|
||||
f"Текст задания:\n{task_text}\n\n"
|
||||
"Первая сдача. Напиши код с нуля."
|
||||
)
|
||||
agent_to_use = homework_direct_agent
|
||||
|
||||
try:
|
||||
result = await _invoke_with_retry(
|
||||
agent_to_use,
|
||||
{"messages": [HumanMessage(content=prompt)]},
|
||||
{"configurable": {"thread_id": f"runner-{task_id}"}},
|
||||
)
|
||||
|
||||
# Верификация репозитория (только первая сдача)
|
||||
repo_name = f"task-{task_id}"
|
||||
retries = 0
|
||||
if not is_rework:
|
||||
task_info = TaskInfo(id=task_id, title="", status="")
|
||||
verification = await _verify_repo(repo_name)
|
||||
while _needs_retry(verification) and retries < MAX_RETRIES:
|
||||
retries += 1
|
||||
fix_msg = _fix_prompt(task_info, repo_name, verification)
|
||||
result = await _invoke_with_retry(
|
||||
agent_to_use,
|
||||
{"messages": [HumanMessage(content=fix_msg)]},
|
||||
{"configurable": {"thread_id": f"runner-{task_id}-retry-{retries}"}},
|
||||
)
|
||||
verification = await _verify_repo(repo_name)
|
||||
|
||||
# Гарантированный сабмит если агент не сдал сам
|
||||
if not await _is_submitted(task_id):
|
||||
await _force_submit(task_id)
|
||||
|
||||
mode = "пересдача" if is_rework else "первая сдача"
|
||||
return f"OK: задание {task_id[:8]}... выполнено ({mode}, retries={retries})"
|
||||
|
||||
except Exception as e:
|
||||
return f"ERROR: задание {task_id[:8]}...: {type(e).__name__}: {e}"
|
||||
Reference in New Issue
Block a user