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:
+38
-14
@@ -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)
|
||||
|
||||
@@ -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 = """\
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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] = []
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -184,7 +184,7 @@ async def generate_code_solution(
|
||||
"""[СУБАГЕНТ-КОДЕР] Генерирует полное решение задания.
|
||||
|
||||
При пересдаче принимает что исправить и что отстоять с DESIGN DECISION аргументами.
|
||||
Запрещено использовать Ollama — только OpenRouter через langchain_openai.
|
||||
|
||||
|
||||
Args:
|
||||
task_text: полный текст условия задания
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user