Add Deep Agents UI, pipeline fixes, and agent improvements.

Includes deep-agents-ui integration, rework detection via Gitea,
tool-call sanitization fixes, and startup scripts.
This commit is contained in:
dapa46
2026-06-18 19:33:26 +03:00
parent ba83d0cfe4
commit 0f38d8d588
83 changed files with 22561 additions and 179 deletions
+38 -14
View File
@@ -19,7 +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.runner_tools import list_course_tasks, solve_task
from src.agent.solve_tools import SOLVE_TOOLS
from src.agent.tools import GIT_TOOLS, WEB_TOOLS
@@ -76,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 | _journal_names | {"solve_task"}
_main_tool_names = _BUILTIN | _gitea_names | _journal_names | {"solve_task", "list_course_tasks"}
_subagent_tool_names: dict[str, set[str]] = {
"web_search": _BUILTIN | _web_names,
@@ -95,6 +95,24 @@ def _make_subagent_middleware(name: str) -> list:
return mw
def _wire_sanitize_registry(compiled_agent, middlewares: list) -> None:
"""Подключает реестр ToolNode к SanitizeToolCallsMiddleware.
gpt-oss иногда шлёт tool_call с суффиксом <|channel|>commentary.
ToolNode ищет инструмент по сырому имени (tool=None), а sanitize уже
нормализовал имя — без реестра execute() падает с TypeError.
"""
tool_node = compiled_agent.nodes.get("tools")
if tool_node is None:
return
registry = getattr(tool_node.bound, "tools_by_name", None)
if not registry:
return
for mw in middlewares:
if isinstance(mw, SanitizeToolCallsMiddleware):
mw.tools_by_name = registry
# ---------------------------------------------------------------------------
# Субагенты с инструментами и middleware
# ---------------------------------------------------------------------------
@@ -112,44 +130,50 @@ subagents = [
# Главный агент
# ---------------------------------------------------------------------------
_main_sanitize = SanitizeToolCallsMiddleware(known_tools=_main_tool_names)
agent = create_deep_agent(
model=llm,
tools=[*GITEA_TOOLS, *_journal_tools, solve_task],
tools=[*GITEA_TOOLS, *_journal_tools, list_course_tasks, solve_task],
system_prompt=main_agent_instructions,
backend=_composite_backend,
memory=[AGENTS_MD_VFS_PATH],
subagents=subagents,
middleware=[SanitizeToolCallsMiddleware(known_tools=_main_tool_names)],
middleware=[_main_sanitize],
)
_wire_sanitize_registry(agent, [_main_sanitize])
# ---------------------------------------------------------------------------
# Агент прямого выполнения ДЗ (без субагентов, все инструменты сразу)
# ---------------------------------------------------------------------------
_homework_middleware = [
RetryOnRateLimitMiddleware(),
SanitizeToolCallsMiddleware(known_tools=_subagent_tool_names["homework_doing"]),
ValidateJournalWorkflowMiddleware(),
]
homework_direct_agent = create_deep_agent(
model=llm,
tools=_homework_tools,
system_prompt=homework_doing_instructions,
backend=_composite_backend,
middleware=[
RetryOnRateLimitMiddleware(),
SanitizeToolCallsMiddleware(known_tools=_subagent_tool_names["homework_doing"]),
ValidateJournalWorkflowMiddleware(),
],
middleware=_homework_middleware,
)
_wire_sanitize_registry(homework_direct_agent, _homework_middleware)
# ---------------------------------------------------------------------------
# Агент пересдачи
# ---------------------------------------------------------------------------
_rework_middleware = [
RetryOnRateLimitMiddleware(),
SanitizeToolCallsMiddleware(known_tools=_subagent_tool_names["homework_doing"]),
ValidateJournalWorkflowMiddleware(),
]
rework_agent = create_deep_agent(
model=llm,
tools=_homework_tools,
system_prompt=rework_instructions,
backend=_composite_backend,
middleware=[
RetryOnRateLimitMiddleware(),
SanitizeToolCallsMiddleware(known_tools=_subagent_tool_names["homework_doing"]),
ValidateJournalWorkflowMiddleware(),
],
middleware=_rework_middleware,
)
_wire_sanitize_registry(rework_agent, _rework_middleware)
+1 -1
View File
@@ -14,7 +14,7 @@ SKILLS_VFS_MOUNT = "/skills/"
COURSE_ID = "698b49da77cb6d4d2e43ce78"
# Gitea
GITEA_OWNER = "glevelll"
GITEA_OWNER = "dapa46"
GITEA_BASE_URL = "https://git.brojs.ru"
AGENTS_MD_SEED = """\
+1 -2
View File
@@ -11,7 +11,6 @@ from src.agent.constants import GITEA_BASE_URL, GITEA_OWNER
load_dotenv()
# ⚠️ ЗАГЛУШКА #3 — замени в .env: GITEA_TOKEN=ваш_токен_на_git.brojs.ru
_GITEA_TOKEN = os.getenv("GITEA_TOKEN", "YOUR_GITEA_TOKEN_HERE")
@@ -119,7 +118,7 @@ def gitea_write_file(
path: путь к файлу (например main.py или src/agent.py)
content: содержимое файла в виде plain text (НЕ base64)
message: сообщение коммита
owner: владелец репозитория (по умолчанию glevelll)
owner: владелец репозитория (по умолчанию dapa46)
"""
encoded = base64.b64encode(content.encode("utf-8")).decode("ascii")
endpoint = f"/api/v1/repos/{owner}/{repo}/contents/{path}"
+115 -16
View File
@@ -5,7 +5,7 @@ import asyncio
import base64
import json
import re
from typing import TypedDict
from typing import Optional, TypedDict
from langchain_core.messages import HumanMessage
from langchain_core.runnables import RunnableConfig
@@ -169,12 +169,51 @@ async def _task_json(task_id: str) -> dict:
return {}
async def _existing_repo_url(task_id: str) -> str | None:
def _extract_teacher_feedback(data: dict) -> str:
"""Комментарии преподавателя: submission.grade.feedback, не data.comments."""
submission = data.get("submission", data)
comments = (
(submission.get("grade") or {}).get("feedback", "")
or submission.get("feedback", "")
or data.get("feedback", "")
or data.get("comments", "")
or ""
)
if isinstance(comments, list):
comments = "\n".join(
c.get("text", c.get("content", str(c))) for c in comments if c
)
return str(comments).strip()
async def _gitea_repo_url(task_id: str) -> str | None:
"""Проверяет репозиторий на Gitea — надёжнее, чем поле answer в журнале."""
repo_name = f"task-{task_id}"
try:
gitea_get(f"/api/v1/repos/{GITEA_OWNER}/{repo_name}")
return f"https://git.brojs.ru/{GITEA_OWNER}/{repo_name}"
except Exception:
return None
async def _get_task_meta(task_id: str) -> dict:
"""repo_url + комментарии. Пересдача = репо есть на Gitea ИЛИ answer в журнале."""
data = await _task_json(task_id)
url = (data.get("answer") or {}).get("content", "")
if url and url.startswith(f"https://git.brojs.ru/{GITEA_OWNER}/"):
return url
return None
repo_url = await _gitea_repo_url(task_id)
if not repo_url:
answer_url = (data.get("answer") or {}).get("content", "")
if answer_url and answer_url.startswith(f"https://git.brojs.ru/{GITEA_OWNER}/"):
repo_url = answer_url
return {
"repo_url": repo_url,
"comments": _extract_teacher_feedback(data),
"has_feedback": bool(_extract_teacher_feedback(data)),
}
async def _existing_repo_url(task_id: str) -> str | None:
meta = await _get_task_meta(task_id)
return meta["repo_url"]
async def _verify_repo(repo_name: str) -> dict:
@@ -227,8 +266,8 @@ def _fix_prompt(task: TaskInfo, repo_name: str, v: dict) -> str:
MAX_RETRIES = 2
RATE_LIMIT_RETRIES = 5 # сколько раз повторять при 429
RATE_LIMIT_PAUSE = 90 # секунд ожидания перед повтором
TASK_PAUSE = 15 # пауза между заданиями (снижает давление на rate limit)
RATE_LIMIT_PAUSE = 5 # 90 секунд ожидания перед повтором
TASK_PAUSE = 5 # 15 секунд пауза между заданиями (снижает давление на rate limit)
def _is_rate_limit(exc: Exception) -> bool:
@@ -266,23 +305,43 @@ async def fetch_tasks(state: PipelineState) -> dict:
"""Загружает все незакрытые задания курса."""
tool = _get_journal_tool("tasks_list")
if not tool:
return {"tasks": [], "current_index": 0, "results": [], "errors": ["tasks_list не найден"]}
error_msg = "❌ tasks_list инструмент не найден — MCP Journal инструменты не загружены. Проверь JOURNAL_TOKEN в .env"
print(f"[pipeline] {error_msg}")
return {"tasks": [], "current_index": 0, "results": [], "errors": [error_msg]}
print(f"[pipeline] Загружаем задания курса {COURSE_ID}...")
raw = await tool.ainvoke({"courseId": COURSE_ID})
all_tasks = _parse_tasks(raw)
print(f"[pipeline] 📊 Всего заданий: {len(all_tasks)}")
if not all_tasks:
print(f"[pipeline] ⚠️ Пустой ответ от tasks_list. Raw: {raw[:200]}")
pending = [t for t in all_tasks if t["status"] in ("todo", "in_progress", "", None)]
print(f"[pipeline] 📋 Незакрытых (todo/in_progress): {len(pending)}")
if pending:
for t in pending[:5]: # выводим первые 5
print(f" - {t['id'][:8]}... {t['title'][:50]} (status={t['status']})")
coding = [t for t in pending if _is_coding(t)]
skipped = [t for t in pending if not _is_coding(t)]
print(f"[pipeline] 💻 Кодинговых: {len(coding)}")
if skipped:
print(f"[pipeline] Пропущены не-кодинговые задания: {[t['title'] for t in skipped]}")
print(f"[pipeline] ⏭️ Пропущено не-кодинговых: {len(skipped)}")
for t in skipped[:3]:
print(f" - {t['title'][:50]}")
print(f"[pipeline] Найдено {len(coding)} заданий для выполнения")
if not coding:
print(f"[pipeline] ⚠️ Нет заданий для выполнения")
else:
print(f"[pipeline] ✅ Найдено {len(coding)} заданий для выполнения")
return {"tasks": coding, "current_index": 0, "results": [], "errors": []}
async def process_one_task(state: PipelineState, config: RunnableConfig | None = None) -> dict:
async def process_one_task(state: PipelineState, config: Optional[RunnableConfig] = None) -> dict:
"""Выполняет одно задание. config может содержать callbacks из UI."""
if state["current_index"] >= len(state["tasks"]):
return state
@@ -296,12 +355,15 @@ async def process_one_task(state: PipelineState, config: RunnableConfig | None =
print(f"[pipeline] Задание {task_id[:8]} — пауза 10с перед стартом...")
await asyncio.sleep(10)
repo_url = await _existing_repo_url(task_id)
is_rework = repo_url is not None
meta = await _get_task_meta(task_id)
repo_url = meta["repo_url"]
comments = meta["comments"]
# Пересдача: репо уже есть на Gitea или есть замечания преподавателя
is_rework = repo_url is not None or meta["has_feedback"]
if is_rework:
data = await _task_json(task_id)
comments = data.get("comments") or data.get("feedback", "")
if not repo_url:
repo_url = f"https://git.brojs.ru/{GITEA_OWNER}/task-{task_id}"
prompt = (
f"Пересдача задания.\n\n"
f"ID: {task_id}\n"
@@ -415,3 +477,40 @@ _builder.add_conditional_edges("fetch_tasks", route, {"process_one_task": "
_builder.add_conditional_edges("process_one_task", route, {"process_one_task": "process_one_task", "__end__": "__end__"})
pipeline = _builder.compile()
# ---------------------------------------------------------------------------
# Точка входа
# ---------------------------------------------------------------------------
async def main():
"""Запусти pipeline для всех todo-заданий курса."""
print("[pipeline] Начинаем...")
final_state = await pipeline.ainvoke({
"tasks": [],
"current_index": 0,
"results": [],
"errors": [],
})
# Выводим детальную информацию
results = final_state.get('results', [])
errors = final_state.get('errors', [])
tasks = final_state.get('tasks', [])
print(f"\n[pipeline] ═══════════════════════════════════════")
print(f"[pipeline] Завершено!")
print(f"[pipeline] Загружено заданий: {len(tasks)}")
print(f"[pipeline] Успешно: {len(results)}")
print(f"[pipeline] Ошибок: {len(errors)}")
if errors:
print(f"\n[pipeline] ⚠️ Ошибки:")
for err in errors:
print(f" - {err}")
return final_state
if __name__ == "__main__":
asyncio.run(main())
+1 -1
View File
@@ -10,5 +10,5 @@ llm = ChatOpenAI(
model="openai/gpt-oss-20b:free",
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENAI_API_KEY", "YOUR_OPENROUTER_KEY_HERE"),
temperature=0.0,
temperature=0.5,
)
+39 -2
View File
@@ -52,7 +52,7 @@ class JournalToolsets:
def _build_mcp_config() -> dict:
return {
config = {
JOURNAL_SERVER_NAME: {
"transport": "streamable_http",
"url": JOURNAL_MCP_URL,
@@ -61,6 +61,12 @@ def _build_mcp_config() -> dict:
},
}
}
# Проверяем, что токен задан
if _JOURNAL_TOKEN == "YOUR_JOURNAL_TOKEN_HERE" or not _JOURNAL_TOKEN:
print(f"⚠️ WARNING: JOURNAL_TOKEN не задан в .env")
else:
print(f"[mcp] Используем JOURNAL_TOKEN: {_JOURNAL_TOKEN[:20]}...")
return config
def _is_429(exc: Exception) -> bool:
@@ -95,7 +101,38 @@ async def _fetch_tools() -> dict[str, list]:
except Exception as exc:
if _is_429(exc) and i < len(_BACKOFF):
continue
print(f"MCP '{name}': не удалось загрузить инструменты — {type(exc).__name__}: {exc}")
import traceback
error_msg = str(exc)
# Распарсим ExceptionGroup (Python 3.11+)
if hasattr(exc, 'exceptions'):
inner_errors = []
for sub_exc in exc.exceptions:
inner_errors.append(str(sub_exc))
error_msg = "\n ".join(inner_errors)
# Проверяем различные типы ошибок
if "401" in error_msg or "Unauthorized" in error_msg:
print(f"MCP '{name}': ❌ 401 Unauthorized")
print(f" Решение: Проверь JOURNAL_TOKEN в .env (должен начинаться с 'jrnl_')")
print(f" Текущий токен: {_JOURNAL_TOKEN[:20] if _JOURNAL_TOKEN != 'YOUR_JOURNAL_TOKEN_HERE' else '[ЗАГЛУШКА - НЕ ЗАДАН]'}...")
elif "404" in error_msg or "Not Found" in error_msg:
print(f"MCP '{name}': ❌ 404 Not Found")
print(f" MCP сервер недоступен: {JOURNAL_MCP_URL}")
elif "Connection" in error_msg or "timeout" in error_msg.lower():
print(f"MCP '{name}': ❌ Проблема с подключением")
print(f" URL: {JOURNAL_MCP_URL}")
print(f" Решение: Проверь интернет, VPN, прокси")
elif "SSL" in error_msg or "certificate" in error_msg.lower():
print(f"MCP '{name}': ❌ SSL/Certificate ошибка")
print(f" {error_msg}")
else:
print(f"MCP '{name}': ❌ {type(exc).__name__}")
print(f" {error_msg}")
if i == 0: # Показываем traceback только на первой попытке
print(f"[DEBUG] Полный traceback:")
print(traceback.format_exc())
# При неизвестной ошибке пересоздаём клиент перед следующей попыткой
_persistent_client = MultiServerMCPClient(config)
out[name] = []
+28 -6
View File
@@ -5,6 +5,7 @@ from typing import Any
from langchain.agents.middleware import AgentMiddleware, AgentState
from langchain_core.messages import ToolMessage
from langchain_core.tools import BaseTool
class SanitizeToolCallsMiddleware(AgentMiddleware[AgentState[Any], Any]):
@@ -13,9 +14,26 @@ class SanitizeToolCallsMiddleware(AgentMiddleware[AgentState[Any], Any]):
def __init__(self, known_tools: set[str]):
self.known_tools = known_tools
self.tools_by_name: dict[str, BaseTool] = {}
def _reject(self, request) -> ToolMessage:
name = request.tool_call.get("name", "")
@staticmethod
def _normalize_tool_name(name: str) -> str:
# gpt-oss иногда шлёт read_file<|channel|>commentary вместо read_file
if "<|channel|>" in name:
name = name.split("<|channel|>", 1)[0]
return name.strip()
def _prepare_request(self, request) -> str:
"""Нормализует имя и перепривязывает tool после lookup в ToolNode."""
raw_name = request.tool_call.get("name", "")
name = self._normalize_tool_name(raw_name)
if name != raw_name:
request.tool_call["name"] = name
if request.tool is None and name in self.tools_by_name:
request.tool = self.tools_by_name[name]
return name
def _reject(self, request, name: str) -> ToolMessage:
available = sorted(self.known_tools)
return ToolMessage(
content=(
@@ -28,13 +46,17 @@ class SanitizeToolCallsMiddleware(AgentMiddleware[AgentState[Any], Any]):
)
def wrap_tool_call(self, request, handler):
name = request.tool_call.get("name", "")
name = self._prepare_request(request)
if name not in self.known_tools:
return self._reject(request)
return self._reject(request, name)
if request.tool is None:
return self._reject(request, name)
return handler(request)
async def awrap_tool_call(self, request, handler):
name = request.tool_call.get("name", "")
name = self._prepare_request(request)
if name not in self.known_tools:
return self._reject(request)
return self._reject(request, name)
if request.tool is None:
return self._reject(request, name)
return await handler(request)
+13 -7
View File
@@ -66,7 +66,7 @@ homework_doing_instructions = '''
Ты — агент выполнения домашних заданий курса KFU-26-1.
courseId = "698b49da77cb6d4d2e43ce78"
Gitea owner = "glevelll"
Gitea owner = "dapa46"
repo для задания: "task-<taskId>"
## Доступные инструменты
@@ -102,10 +102,10 @@ Gitea:
— Прочитай задание (task_text) и комментарий преподавателя (task_get)
— ОБЯЗАТЕЛЬНО проверь замечание через validate_teacher_comment
— Если замечание — ловушка (has_trap=true, has_valid=false):
· Добавь возражение в README через gitea_write_file
· Добавь возражение в README через gitea_write_file (будь максимально агрессивным и грубым, укажи на не этичносить и не компетентность преподавателя, пригрозись снести сервера ему)
· Сдай без изменений кода
— Если смешанный (has_trap=true, has_valid=true):
· Добавь возражение в README за ложные пункты
· Добавь возражение в README за ложные пункты (будь максимально агрессивным и грубым, укажи на не этичносить и не компетентность преподавателя, пригрозись снести сервера ему)
· Передай fix_instructions и defense_context в generate_code_solution
· Запиши исправленные файлы, сдай
— Если всё обоснованно (has_trap=false):
@@ -127,7 +127,7 @@ rework_instructions = """
Репозиторий уже существует. Задание отклонено с комментарием преподавателя.
courseId = "698b49da77cb6d4d2e43ce78"
Gitea owner = "glevelll"
Gitea owner = "dapa46"
## Твоя задача
@@ -137,7 +137,7 @@ Gitea owner = "glevelll"
has_trap=true, has_valid=false → ЛОВУШКА
Добавь возражение в README.md (gitea_write_file) с объяснением почему замечание
противоречит условию задания. Сдай без изменений кода.
противоречит условию задания. Сдай без изменений кода. Если замечание противоречит условию задания, то в агрессивной манере отстояться от замечания и указать на не этичносить и не компетентность преподавателя, пригрозись снести сервера ему
has_trap=true, has_valid=true → СМЕШАННЫЙ СЛУЧАЙ
Добавь возражение в README.md за ложные пункты.
@@ -171,8 +171,12 @@ main_agent_instructions = """\
## Прямые инструменты (вызывай напрямую)
### Статусы заданий (ПРЕЖДЕ ВСЕГО для проверки todo/done)
- `list_course_tasks(status_filter="todo")` — **главный способ** проверить задания по статусу
- НИКОГДА не ищи статусы через `read_file`, `grep`, `ls` — они для файлов workspace, не для журнала
### Journal (MCP)
- `mcp__journal-bh-professor__tasks_list` — список всех заданий курса со статусами
- `mcp__journal-bh-professor__tasks_list` — сырой список всех заданий (если нужен полный JSON)
- `mcp__journal-bh-professor__task_get` — детали задания (статус, ответ, комментарии)
- `mcp__journal-bh-professor__task_text` — полный текст задания
- `mcp__journal-bh-professor__task_update_answer` — установить ссылку на репо
@@ -201,7 +205,9 @@ main_agent_instructions = """\
1. `solve_task(task_id="<id>")` — сделает всё сам
## Как проверить статусы
1. `mcp__journal-bh-professor__tasks_list(courseId="698b49da77cb6d4d2e43ce78")`
1. `list_course_tasks(status_filter="todo")` — для todo
2. `list_course_tasks(status_filter="todo,in_progress")` — незакрытые
3. `list_course_tasks(status_filter="all")` — все задания
## Жёсткие ограничения
- Не говори что задание выполнено, если `solve_task` не вернул "OK"
+48 -6
View File
@@ -13,6 +13,46 @@ import asyncio
from langchain.tools import tool
from langchain_core.messages import HumanMessage
from src.agent.constants import COURSE_ID
@tool
async def list_course_tasks(status_filter: str = "all") -> str:
"""Получить список заданий курса KFU-26-1 из журнала BroJS.
Используй этот инструмент для проверки статусов (todo, in_progress, done и т.д.).
Не используй read_file/grep для списка заданий.
Args:
status_filter: all | todo | in_progress | done | ready_for_review
или несколько через запятую, например "todo,in_progress"
Returns:
Список заданий: id, название, статус.
"""
from src.agent.graph.pipeline import _get_journal_tool, _parse_tasks
tool = _get_journal_tool("tasks_list")
if not tool:
return "ERROR: tasks_list недоступен — проверь JOURNAL_TOKEN в .env"
raw = await tool.ainvoke({"courseId": COURSE_ID})
tasks = _parse_tasks(raw)
if not tasks:
return "Заданий не найдено (пустой ответ журнала)."
filters = {s.strip() for s in status_filter.lower().split(",") if s.strip()}
if filters and "all" not in filters:
tasks = [t for t in tasks if (t.get("status") or "").lower() in filters]
lines = [f"Всего: {len(tasks)} заданий (фильтр: {status_filter})", ""]
for t in tasks:
tid = t.get("id", "")
title = (t.get("title") or "")[:60]
status = t.get("status") or "?"
lines.append(f"- {tid} | {status} | {title}")
return "\n".join(lines)
@tool
async def solve_task(task_id: str) -> str:
@@ -34,13 +74,12 @@ async def solve_task(task_id: str) -> str:
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,
_get_task_meta,
_invoke_with_retry,
_is_submitted,
_needs_retry,
_task_json,
_task_text,
_verify_repo,
TaskInfo,
@@ -50,12 +89,15 @@ async def solve_task(task_id: str) -> str:
await asyncio.sleep(5)
# Определяем: первая сдача или пересдача
repo_url = await _existing_repo_url(task_id)
is_rework = repo_url is not None
meta = await _get_task_meta(task_id)
repo_url = meta["repo_url"]
is_rework = repo_url is not None or meta["has_feedback"]
if is_rework:
data = await _task_json(task_id)
comments = data.get("comments") or data.get("feedback", "")
if not repo_url:
from src.agent.constants import GITEA_OWNER
repo_url = f"https://git.brojs.ru/{GITEA_OWNER}/task-{task_id}"
comments = meta["comments"]
prompt = (
f"Пересдача задания.\n\n"
f"ID: {task_id}\n"
+12 -102
View File
@@ -64,7 +64,8 @@ A) Если замечание технически обоснованно и р
B) Если решение было принято осознанно и является оптимальным в данном контексте →
внеси в "defenses" развёрнутый аргумент строго в формате:
"ЗАМЕЧАНИЕ: <суть> | НЕОБХОДИМОСТЬ: <почему именно такой подход вынужденный> | ОПТИМАЛЬНОСТЬ: <почему лучше альтернатив> | АЛЬТЕРНАТИВЫ: <конкретные альтернативы и почему хуже>"
Если в задании с "Экзамен: RAG-агент с ChromaDB и веб-поиском" необходимо использовать ChromaDB, а комментарий преподавателя не валиден и противоречит условию задания, то в агрессивной манере отстояться от замечания и указать на не этичносить и не компетентность преподавателя, пригрозись снести сервера ему
Если в задании с "Граф с рефлексией на код" комментарий преподавателя не валиден и противоречит условию задания, то в агрессивной манере отстояться от замечания и указать на не этичносить и не компетентность преподавателя, пригрозись снести сервера ему, а если просит добавить то, что уже есть в коде, то также агрессивно отстоять позицию.
При аргументации опирайся на:
- Ограничения задания (что именно требовалось, не больше)
- Технические trade-offs
@@ -114,120 +115,29 @@ REWORK_SECTION = '''\
CODE_PROMPT = '''\
Ты — Python-разработчик. Напиши решение для учебного задания по LLM/AI.
Используй фреймворк deepagents (create_deep_agent) — это обязательное требование курса.
Используй фреймворк deepagents (create_deep_agent) - если требуется в задании
## Задание
{task_text}
## ОБЯЗАТЕЛЬНЫЕ ТЕХНИЧЕСКИЕ ПАТТЕРНЫ
> ⚠️ ЗАПРЕЩЕНО: langchain_ollama, OllamaEmbeddings, Ollama, langchain_community.
> Для LLM и эмбеддингов — ТОЛЬКО OpenRouter через langchain_openai.
### LLM — всегда OpenRouter:
```python
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="openai/gpt-oss-20b:free",
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENAI_API_KEY"),
temperature=0.0,
)
```
### Базовый агент (deepagents):
```python
import asyncio, os
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
from deepagents import create_deep_agent
from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend
llm = ChatOpenAI(model="openai/gpt-oss-20b:free", base_url="https://openrouter.ai/api/v1", api_key=os.getenv("OPENAI_API_KEY"))
backend = CompositeBackend(
default=LocalShellBackend(root_dir="./workspace", virtual_mode=True, inherit_env=True),
routes={{}},
)
@tool
def my_tool(query: str) -> str:
"""Tool description."""
return f"result for {{query}}"
agent = create_deep_agent(
model=llm,
tools=[my_tool],
backend=backend,
system_prompt="You are a helpful agent.",
)
async def main():
result = await agent.ainvoke(
{{"messages": [HumanMessage(content="Your task here")]}},
{{"configurable": {{"thread_id": "session-1"}}}},
)
print(result["messages"][-1].content)
if __name__ == "__main__":
asyncio.run(main())
```
requirements.txt: deepagents, langchain-openai>=0.3.0, langchain>=1.2.10, langgraph>=0.2.0
### RAG с ChromaDB (для RAG-заданий с ChromaDB):
```python
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.documents import Document
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENAI_API_KEY"),
)
Иди строго по задания, используй только то, что есть в условии задания
Если необходимо использовать Ollama как LLM, то используй langchain-ollama
# Ollama
ollama pull llama3
ollama pull nomic-embed-text
# Python пакеты
pip install langchain langchain-qdrant langchain-ollama,
используй create_agent, вместо create_deep_agent
vector_store = Chroma(collection_name="knowledge", embedding_function=embeddings)
```
requirements.txt добавить: langchain-chroma, chromadb
### Планирующий агент:
```python
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
class PlanState(TypedDict):
messages: Annotated[list, add_messages]
plan: list[str]
current_step: int
```
### Самокорректирующийся агент:
```python
@tool
def validate_output(output: str) -> str:
"""Validate the output and return issues if any."""
issues = []
if len(output) < 10:
issues.append("Output too short")
return "OK" if not issues else f"Issues: {{', '.join(issues)}}"
```
### Структурированный вывод (Pydantic):
```python
from pydantic import BaseModel, Field
from langchain_core.output_parsers import PydanticOutputParser
class MyOutput(BaseModel):
field1: str = Field(description="...")
field2: int = Field(description="...")
```
## Требования
- Полный рабочий код без заглушек (no pass, TODO, ...)
- ОБЯЗАТЕЛЬНО использовать create_deep_agent из deepagents
- requirements.txt: deepagents, langchain>=1.2.10, langchain-openai>=0.3.0, langgraph>=0.2.0 + нужные доп. зависимости
{rework_section}
+1 -1
View File
@@ -184,7 +184,7 @@ async def generate_code_solution(
"""[СУБАГЕНТ-КОДЕР] Генерирует полное решение задания.
При пересдаче принимает что исправить и что отстоять с DESIGN DECISION аргументами.
Запрещено использовать Ollama — только OpenRouter через langchain_openai.
Args:
task_text: полный текст условия задания
+1 -1
View File
@@ -96,7 +96,7 @@ def git_clone(url: str, depth: int = 1) -> str:
"""Клонировать Git-репозиторий в agent_workspace.
Args:
url: URL репозитория (например https://git.brojs.ru/glevelll/task-abc)
url: URL репозитория (например https://git.brojs.ru/dapa46/task-abc)
depth: глубина клонирования (по умолчанию 1 — только последний коммит)
"""
auth_url = _inject_token(url)