feat: initial brojs-agent implementation

AI-агент для автоматического выполнения заданий курса KFU-26-1.
Стек: deepagents + LangGraph + LangChain + OpenRouter (gpt-oss-20b:free).

- Главный агент-оркестратор с субагентами
- homework_direct_agent и rework_agent для сдачи/пересдачи
- Gitea REST API инструменты (git.brojs.ru/glevelll)
- BroJS Journal MCP клиент (platform.brojs.ru)
- LangGraph pipeline: последовательное выполнение всех заданий
- Middleware: SanitizeToolCalls + ValidateJournalWorkflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 23:08:14 +03:00
commit c95d7375cc
23 changed files with 1663 additions and 0 deletions
@@ -0,0 +1,39 @@
"""Middleware: блокирует вызовы несуществующих инструментов."""
from __future__ import annotations
from typing import Any
from langchain.agents.middleware import AgentMiddleware, AgentState
from langchain_core.messages import ToolMessage
class SanitizeToolCallsMiddleware(AgentMiddleware[AgentState[Any], Any]):
"""Перехватывает вызовы инструментов с неизвестными именами и возвращает
понятное сообщение об ошибке вместо падения рантайма."""
known_tools: set[str]
def _reject(self, request) -> ToolMessage:
name = request.tool_call.get("name", "")
available = sorted(self.known_tools)
return ToolMessage(
content=(
f"Инструмент '{name}' не существует. "
f"Доступные инструменты: {available}. "
"Исправь имя инструмента и попробуй снова."
),
tool_call_id=request.tool_call["id"],
name=name,
)
def wrap_tool_call(self, request, handler):
name = request.tool_call.get("name", "")
if name not in self.known_tools:
return self._reject(request)
return handler(request)
async def awrap_tool_call(self, request, handler):
name = request.tool_call.get("name", "")
if name not in self.known_tools:
return self._reject(request)
return await handler(request)