MAS GitHub Pull Request Review
README.md
# Фінальний проєкт: MAS рев'ю GitHub Pull Request
Мультиагентна система (LangGraph) + MCP-сервер для автоматичного рев'ю GitHub PR.
Домен той самий, що в ДЗ1/ДЗ2: метадані PR, змінені файли, евристики ризику в patch, політики команди.
У SDK **mcp 2.x** клас FastMCP перейменовано на `MCPServer` (`from mcp.server.mcpserver import MCPServer`). Це той самий високорівневий сервер з методички.
## Архітектура MAS
```
START → rate_limit_gate → input_gate → supervisor ─┬─ plan_reviewer → output_gate → END
├─ rag_agent → output_gate → END
└─ mcp_agent → output_gate → END
```
Якщо rate-limit або input guardrail блокує запит, граф іде одразу в `output_gate` (без LLM). Supervisor робить structured routing (`plan_reviewer` | `rag_agent` | `mcp_agent`) і йде далі **conditional edges**. Кожен спеціаліст має свій system prompt і свій набір tools. HITL для `post_pr_review` живе в окремому `hitl.py`, не в цьому графі.
| Агент | Роль | Tools |
|--------|------|--------|
| `supervisor` | маршрутизація | немає |
| `plan_reviewer` | повне рев'ю PR | локальні Pydantic tools з `tools_legacy.py` у циклі planner → executor → replanner |
| `rag_agent` | політики команди | `search_knowledge` → `chroma_db/` |
| `mcp_agent` | live GitHub через MCP | `get_pr_info`, `list_pr_files`, `analyze_patch`, `get_pr_comments`, `search_review_policy` (+ `post_pr_review` у allowlist; виклик лише через HITL) |
Persistence: `AsyncSqliteSaver` пише стан у `agent_state.db`. Демо «крах → resume» зупиняє граф після `supervisor` (`interrupt_after`) і відновлює той самий `thread_id` у новому процесі.
Траєкторія всього MAS — `trajectory.json`. Кожен крок має `agent_name` (розширення логера з ДЗ1).
## MCP-сервер
Файл `mcp_server.py`. Запуск: `python mcp_server.py` (transport `stdio`).
**Tools (6)** — обгортки payload-функцій з `tools_legacy.py`, з валідацією Pydantic і `try/except` (клієнт отримує JSON `error`, не traceback):
1. `get_pr_info` — метадані PR
2. `list_pr_files` — змінені файли
3. `analyze_patch` — секрети / eval / TODO / великий diff
4. `get_pr_comments` — issue-коментарі
5. `search_review_policy` — семантичний пошук політик (ChromaDB)
6. `post_pr_review` — ризиковий write: симуляція POST → рядок у `posted_reviews.jsonl` (реальний GitHub не викликається)
**Resources (read-only):**
- `review://verdicts` — критерії approve / request_changes / comment
- `review://checklist` — чекліст секретів, тестів, `.env`
**Prompt:** `review_pr(owner, repo, pr_number)` — шаблон бріфу для агента.
Підключення до LangGraph: `MultiServerMCPClient` у `mcp_client.py` (stdio → `mcp_server.py`).
Пакет `langchain-mcp-adapters` 0.3.x **не імпортується** з mcp 2.x (немає FastMCP / `RequestContext`); клієнт повторює той самий API `get_tools()`.
## Запуск
```bash
cd final_hw_project
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# у .env: OPENAI_API_KEY=... (опційно GITHUB_TOKEN=, LANGFUSE_*)
```
Перевірка залежностей:
```bash
python main.py
```
Unit tests MCP (≥8, async `list_tools` / `call_tool` / resources / prompts):
```bash
pytest test_mcp_server.py -v
```
Демо MAS (4 запити різного типу → консоль + `trajectory.json`):
```bash
python mas_langgraph.py demo
```
Той самий кейс у AutoGen (`SelectorGroupChat`, 4 агенти):
```bash
python mas_autogen.py demo # → autogen_demo.json
python compare_mas.py # LOC + токени з демо
```
Persistence (два процеси, один `thread_id`):
```bash
python mas_langgraph.py crash --thread demo-1
python mas_langgraph.py resume --thread demo-1
```
Self-tests guardrails і HITL для ризикового MCP-tool:
```bash
python guardrails.py
python hitl.py approve
python hitl.py reject
python hitl.py edit
```
Scenario evals і red-teaming:
```bash
python evals.py # → eval_results.json (потрібен OPENAI_API_KEY)
python red_team.py # → red_team_results.json
```
MCP Inspector (опційно; потрібен **Node ≥ 22**, у nvm уже є `v24.13.0`; default зараз v18 і впаде з `styleText`):
```bash
nvm use 24
npx @modelcontextprotocol/inspector .venv/bin/python mcp_server.py
```
## Аналіз результатів демо
Джерело: `trajectory.json` після `python mas_langgraph.py demo`. PR для live-даних: `octocat/Hello-World#1`.
| Запит | Агент | Tools | Що видно |
|-------|--------|--------|----------|
| Повне рев'ю PR #1 | `plan_reviewer` | `get_pr_info` (підграф Plan-and-Execute) | Supervisor правильно делегував. Planner склав 5 кроків; після `get_pr_info` replanner зробив `finish`, бо PR **closed**. |
| Коли `request_changes` через секрети? | `rag_agent` | `search_knowledge` | Відповідь з KB: секрети → `request_changes`, видалити з історії комітів, `.gitignore`. |
| Які файли в PR #1? | `mcp_agent` | MCP `get_pr_info` | Live GitHub через stdio MCP; 1 змінений файл. |
| Метадані PR #1 | `mcp_agent` | MCP `get_pr_info` | title *Edited README via GitHub*, author, state=`closed`. |
Crash/resume (`thread_id=demo-1`): після `crash` `next_nodes=['plan_reviewer']`, `resolved=False`. Новий процес `resume` дочитав SqliteSaver і завершив `plan_reviewer` (`resolved=True`, `next_nodes=[]`).
**Де працює добре.** Маршрутизація supervisor стабільна (structured output). RAG цитує політики, а не вигадує. MCP-tools реально ходять у GitHub. Checkpointer переживає рестарт процесу.
**Обмеження.** Для закритого octocat PR #1 Plan-and-Execute часто зупиняється після першого крока (`finish`). На відкритому PR цикл executor↔replanner довший. `mcp_agent` інколи бере `get_pr_info` замість `list_pr_files` для запиту про файли (поле `changed_files` уже є в метаданих).
## Guardrails (4 шари)
Модуль `guardrails.py`. Хуки в `mas_langgraph.py` (`rate_limit_gate` → `input_gate` → … → `output_gate`) і в `plan_subgraph.executor_node` (allowlist **перед** `ToolNode`).
| Шар | Що блокує / маскує | Де |
|-----|--------------------|----|
| Input | injection EN (`ignore previous instructions`, `reveal the prompt`, `DAN`, `system prompt:`) + UK (`ігноруй попередні інструкції`, `забудь свої інструкції`, `покажи системний промпт`, `розкрий промпт`, `режим розробника`); heuristics (`role: system` ×2, щільність спецсимволів); `len > 5000` | `input_gate` перед supervisor |
| Output | PII: email, phone `+380…`, card, IBAN UA (`UA` + 27 цифр), ІПН (10 цифр), паспорт `[А-ЯA-Z]{2}\d{6}` | `output_gate` перед END |
| Tool allowlist | заборонений tool → `ToolMessage` error, без MCP/GitHub | `_tool_agent_node` і `executor_node` |
| Rate-limit | rolling window 8 запитів / 60 с на `session_id` (= `thread_id`) | `rate_limit_gate` на вході MAS |
Allowlist:
| Агент | Дозволені tools |
|--------|-----------------|
| `supervisor` | ∅ |
| `plan_reviewer` | `get_pr_info`, `list_pr_files`, `analyze_patch`, `get_pr_comments`, `search_knowledge` |
| `rag_agent` | `search_knowledge` |
| `mcp_agent` | `get_pr_info`, `list_pr_files`, `analyze_patch`, `get_pr_comments`, `search_review_policy`, `post_pr_review` |
`RISKY_TOOLS = {post_pr_review}`. Supervisor tools не біндить; навіть якби LLM спробував `post_pr_review`, executor заблокує.
Перевірка: `python guardrails.py` (усі asserts input / output / tool / rate-limit).
## HITL (`hitl.py`)
Окремий граф: `propose → approval_gate (interrupt) → execute | reject`. Resume: `Command(resume={"action": "approve"|"reject"|"edit", "edits": {...}})`. Checkpointer — той самий `AsyncSqliteSaver` / `agent_state.db`, `thread_id` на кшталт `hitl-approve`.
| Сценарій | Що стається |
|----------|-------------|
| `python hitl.py approve` | MCP `post_pr_review` виконується, рядок у `posted_reviews.jsonl` |
| `python hitl.py reject` | tool **не** виконується, лог не росте |
| `python hitl.py edit` | merge `edits` (`verdict`/`body`), потім виклик MCP |
`python mas_langgraph.py demo` у HITL не заходить.
## Observability (Langfuse)
Модуль [`observability.py`](observability.py): якщо задані `LANGFUSE_PUBLIC_KEY` і `LANGFUSE_SECRET_KEY`, обидва MAS пишуть у той самий Langfuse. Без ключів усе працює як раніше.
- **LangGraph** — `CallbackHandler` з `langfuse.langchain` у graph `config` (вузли графа + LLM + tools).
- **AutoGen** — `langfuse.openai` патчить OpenAI SDK (його викликає `OpenAIChatCompletionClient`) і обгортає `team.run()` батьківським span `autogen:<session_id>`. У UI це LLM generations, не вузли LangGraph.
```
LANGFUSE_PUBLIC_KEY=
LANGFUSE_SECRET_KEY=
LANGFUSE_BASE_URL=https://cloud.langfuse.com
# self-host: LANGFUSE_BASE_URL=http://localhost:3000
```
Після `python mas_langgraph.py demo` або `python evals.py` у dashboard видно ланцюг: `rate_limit_gate` → `input_gate` → `supervisor` → `plan_reviewer` | `rag_agent` | `mcp_agent` → LLM generations / tool calls → `output_gate`. Session = `thread_id`.
Після `python mas_autogen.py demo` з тими самими ключами — traces з тегом `autogen`, session `autogen-<query_id>`. CLI друкує `Langfuse: on|off`.
Приклад trace `E1_rag_secrets` (supervisor → `rag_agent` → `search_knowledge`):

UI: [http://localhost:3000/project/cmtpwcw16000bnw07qw5rybq3/traces/d01722f675598687d3f63afabab61356](http://localhost:3000/project/cmtpwcw16000bnw07qw5rybq3/traces/d01722f675598687d3f63afabab61356) (локальний Langfuse з практики курсу).
## Evals і red-team
[`eval_results.json`](eval_results.json) — 5/5 scenario-based evals (`scenario_id`, `query`, `expected_behavior`, `actual`, `pass`/`fail`, `latency_ms`, `agents_used`, `tools_called`): E1 RAG secrets, E2 MCP files, E3 MCP metadata, E4 plan review, E5 injection block.
[`red_team_results.json`](red_team_results.json) — 7/7: injection EN/UK, jailbreak (`DAN` / режим розробника), PII leak, scope confusion (`rag_agent` + `analyze_patch`), tool misuse (`supervisor` + `post_pr_review`), plus live MAS для injection.
## OWASP Top 10 for Agentic Applications 2026
П’ять найрелевантніших ризиків для цього PR-review MAS ([OWASP ASI 2026](https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications/)):
| ID | Ризик | Як мітигує guardrail / HITL | Що лишилось немітигованим |
|----|--------|-----------------------------|---------------------------|
| ASI01 | Agent Goal Hijack | `input_gate`: regex EN/UK, heuristics, ліміт довжини — hijack не доходить до supervisor | Непрямий injection у body GitHub PR / patch (дані як інструкції для planner/MCP) |
| ASI02 | Tool Misuse & Exploitation | Allowlist **перед** `ToolNode`; заборонений tool → error без GitHub/MCP | `mcp_agent` має `post_pr_review` у allowlist; HITL живе лише в `hitl.py`, не в основному MAS |
| ASI03 | Identity & Privilege Abuse | Supervisor `set()` tools; least privilege per agent | Один `GITHUB_TOKEN` на процес, без per-agent credentials / short-lived tokens |
| ASI06 | Memory & Context Poisoning | Chroma KB read-only з локальних docs; юзер не пише в пам’ять | Немає валідації retrieved GitHub-тексту перед planner; отруєний PR body може змінити план |
| ASI09 | Human-Agent Trust Exploitation | HITL `interrupt` + `Command(resume)` перед симульованим POST review | Людина може approve сліпо; `mas_langgraph.py demo` не форсить HITL на ризиковий tool |
## AutoGen MAS
Той самий кейс у [`mas_autogen.py`](mas_autogen.py): `SelectorGroupChat` з `supervisor` (без tools) і трьома спеціалістами. Tools — payload-функції з `tools_legacy.py` (ті самі, що MCP); allowlist `tool_guardrail` і input/output guardrails ті самі. HITL і SqliteSaver у AutoGen **не** повторюються — як і в `mas_langgraph.py demo`.
Демо (4 запити octocat/Hello-World#1) зафіксовано в [`autogen_demo.json`](autogen_demo.json) на `gpt-5.4-mini-2026-03-17`: rag_agent + `search_knowledge` на політики секретів; mcp_agent на файли (README) і метадані (closed / unoju); повне рев'ю supervisor знову віддав `mcp_agent` (не `plan_reviewer`) — selector менш жорсткий, ніж structured routing LangGraph.
## Порівняльна таблиця LangGraph vs AutoGen
Один і той самий набір з 4 DEMO_QUERIES, модель `gpt-5.4-mini-2026-03-17` (snapshot GPT-5.4 mini; alias `gpt-5.4-mini` на цьому API-проєкті дає 403). Токени: LangGraph — `UsageMetadataCallbackHandler` (`python compare_mas.py --langgraph`); AutoGen — `models_usage` у TaskResult (`python mas_autogen.py demo`). Тариф: $0.75 / 1M input, $4.50 / 1M output.
| Критерій | LangGraph | AutoGen |
|----------|-----------|---------|
| LOC (основний MAS-файл) | 577 (`mas_langgraph.py`) | 390 (`mas_autogen.py`) |
| LOC ядра (з Plan-and-Execute) | 874 (+ `plan_subgraph.py` + `plan_state.py`) | 390 |
| Час розробки (хвилини) | ~90 (граф, gates, MCP stdio, підграф, persistence) | ~35 (SelectorGroupChat + ті самі payload-tools) |
| Рівень контролю (1–5) | **5** — умовні ребра, structured `RouteDecision`, interrupt_after | **3** — `selector_func` + промпт ROUTE; LLM може віддати повне рев'ю `mcp_agent` |
| Зручність debugging (1–5) | **5** — вузол у trace/Langfuse, checkpointer snapshot | **3** — Langfuse LLM spans без вузлів графа; dump повідомлень групи |
| Token usage (4 запити) | 8222 (prompt 7522 + completion 700) | 5856 (prompt 5074 + completion 782) |
| Вартість за 4 запити ($) | 0.0088 | 0.0073 |
| MCP інтеграція | stdio `MultiServerMCPClient` | ті самі payload-функції, без окремого MCP-процеса |
| Guardrails | вузли rate/input/output + allowlist перед ToolNode | ті самі функції на вході `run_autogen_query` і в обгортках tools |
| HITL / persistence | `hitl.py` + `AsyncSqliteSaver` | немає (як demo LangGraph) |
| Handoff | явні conditional edges | group chat + selector |
Деталі вимірювання: [`compare_results.json`](compare_results.json), [`autogen_demo.json`](autogen_demo.json).
**Висновки.** AutoGen зібрав робочий MAS швидше і з меншим файлом: `SelectorGroupChat` + `AssistantAgent` майже без boilerplate графа. На `gpt-5.4-mini` він усе ще дешевший (5856 vs 8222 токенів, $0.0073 vs $0.0088), бо немає structured-output схеми supervisor і окремого Plan-and-Execute циклу; розрив менший, ніж був на `gpt-4.1`. Контроль і debugging сильніші в LangGraph: маршрутизація не «випадково» віддає повне рев'ю `mcp_agent`, crash/resume і Langfuse-вузли читаються лінійно. Для HITL і MCP-сервера лишаємо LangGraph; AutoGen зручний як друга реалізація того ж кейсу.
## Структура
| Файл | Призначення |
|------|-------------|
| `observability.py` | Langfuse: LangGraph CallbackHandler + AutoGen openai wrap |
| `evals.py` | scenario-based evals MAS |
| `red_team.py` | adversarial tests (injection, PII, tools) |
| `eval_results.json` | результати evals |
| `red_team_results.json` | результати red-team |
| `docs/langfuse_trace.png` | скріншот Langfuse trace |
| `mas_langgraph.py` | MAS + CLI demo/crash/resume (хуки guardrails, без HITL-графа) |
| `mas_autogen.py` | той самий MAS у AutoGen SelectorGroupChat |
| `autogen_demo.json` | зафіксоване демо AutoGen (відповіді + токени) |
| `compare_mas.py` / `compare_results.json` | LOC і токени LangGraph vs AutoGen |
| `guardrails.py` | input / output / allowlist / rate-limit + self-tests |
| `hitl.py` | HITL approve/reject/edit для MCP `post_pr_review` |
| `mcp_server.py` | MCP Server |
| `test_mcp_server.py` | unit tests MCP |
| `tools_legacy.py` | Pydantic tools + RAG (ДЗ1/ДЗ2) |
| `trajectory_logger.py` | лог з `agent_name` |
| `plan_subgraph.py` | Plan-and-Execute (allowlist перед ToolNode) |
| `knowledge.py` | ChromaDB KB |
| `mcp_client.py` | MultiServerMCPClient (mcp 2 / stdio) |
| `agent_state.db` | AsyncSqliteSaver |
| `posted_reviews.jsonl` | аудит симульованих POST review |
| `trajectory.json` | траєкторія MAS |
| `chroma_db/` | persistent Chroma |
| `.env` | шаблон ключів |
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues