diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..eaab2b6
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,16 @@
+.aiassistant
+.env
+.git/
+.gitignore
+.idea/
+.vscode/
+.venv/
+venv/
+projects-data
+CHANGELOG.md
+__pycache__
+deployment
+projects
+test_main.http
+role_id
+wrapped_secret_id
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 0555f7b..ad421e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
.env
-projects
\ No newline at end of file
+projects
+role_id
+wrapped_secret_id
\ No newline at end of file
diff --git a/.idea/Agents.iml b/.idea/Agents.iml
index 9d65d31..e0d6de8 100644
--- a/.idea/Agents.iml
+++ b/.idea/Agents.iml
@@ -6,8 +6,9 @@
+
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index c1fc2a9..1bec7e5 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,5 +3,5 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f18c515
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+### Сборка
+```shell
+docker build -t triage_agent -f deployment/Dockerfile .
+```
+
+### Запуск
+```shell
+./deployment/generate-wrapped-secret-id.sh && docker compose -f deployment/docker-compose.yml up --build
+```
\ No newline at end of file
diff --git a/agents/codebase_analyst/graph_tool.py b/agents/codebase_analyst/graph_tool.py
index eda997c..dadcb74 100644
--- a/agents/codebase_analyst/graph_tool.py
+++ b/agents/codebase_analyst/graph_tool.py
@@ -751,7 +751,7 @@ def analyze_repository(inp: AnalysisInput, run_ctx: RunContext) -> AnalysisResul
return AnalysisResult(success=True, graph=graph, res_all=res_all, leaf_clusters=leaf_clusters)
-def to_networkx(dep_graph: "DependencyGraph", include_external: bool = False) -> nx.DiGraph[DependencyNode]:
+def to_networkx(dep_graph: "DependencyGraph", include_external: bool = False) -> "nx.DiGraph[DependencyNode]":
"""
Convert a DependencyGraph into a networkx DiGraph.
@@ -792,7 +792,7 @@ COLS = [
def analyze(
- G: nx.DiGraph[DependencyNode],
+ G: "nx.DiGraph[DependencyNode]",
graph: DependencyGraph, # NOTE: currently unused in the body
label: str = "",
) -> AnalyzeResult:
diff --git a/agents/codebase_analyst/start.py b/agents/codebase_analyst/start.py
index e77d8f3..c5a436f 100644
--- a/agents/codebase_analyst/start.py
+++ b/agents/codebase_analyst/start.py
@@ -13,8 +13,10 @@ import sys
from pathlib import Path
from typing import List, Dict, Any
import requests
-from playwright.sync_api import sync_playwright
from agents.architect_ts.analyzer import CodeAnalyzer
+from agents.coder.CoderAgent import CoderAgent
+from agents.solution_architect.PlannerAgent import PlannerAgent
+from agents.tester_web.TesterAgent import TesterAgent
# ---------- Конфигурация ----------
CONFIG_FILE = "config.json"
diff --git a/agents/coder/CoderAgent.py b/agents/coder/CoderAgent.py
index 40ee545..e962b83 100644
--- a/agents/coder/CoderAgent.py
+++ b/agents/coder/CoderAgent.py
@@ -1,3 +1,9 @@
+import os
+from typing import List
+
+from agents.codebase_analyst.start import Project
+
+
class CoderAgent:
"""Агент-исполнитель: выполняет подзадачу, генерирует код и применяет изменения."""
diff --git a/agents/coders/state.py b/agents/coders/state.py
index 98a711a..ca1fcb5 100644
--- a/agents/coders/state.py
+++ b/agents/coders/state.py
@@ -108,7 +108,7 @@ class AgentState(BaseModel):
}
@classmethod
- def from_dict(cls, data: Dict[str, Any], state_file: Path) -> AgentState:
+ def from_dict(cls, data: Dict[str, Any], state_file: Path) -> "AgentState":
known = {
"schema_version", "issue_id", "agent_id", "state", "updated_at",
"last_processed_comment_id", "pending_question_comment_id",
diff --git a/agents/issue_triage/IssueTriageAgent.py b/agents/issue_triage/IssueTriageAgent.py
index 8dd37bd..c272228 100644
--- a/agents/issue_triage/IssueTriageAgent.py
+++ b/agents/issue_triage/IssueTriageAgent.py
@@ -14,7 +14,7 @@ from agents.issue_triage.triage import render_validation_failure, render_plan_co
from agents.registry import agent, AgentRegistry, Capability
from common.llm_client import LLMClient
from common.youtrack_mcp_client import IssueNotFound, YouTrackMCPClient
-from contracts.IssueContext import IssueContext, IssueComment
+from contracts.IssueContext import IssueContext
logger = logging.getLogger("triage_agent")
logger.setLevel(logging.DEBUG)
@@ -23,7 +23,7 @@ logger.setLevel(logging.DEBUG)
class TriageContext:
issue: IssueContext
registry: AgentRegistry
- triage_agent: IssueTriageAgent
+ triage_agent: "IssueTriageAgent"
async def run_triage(ctx: TriageContext) -> TriageMarker:
diff --git a/agents/solution_architect/PlannerAgent.py b/agents/solution_architect/PlannerAgent.py
index d6cc321..35f5c1f 100644
--- a/agents/solution_architect/PlannerAgent.py
+++ b/agents/solution_architect/PlannerAgent.py
@@ -1,3 +1,9 @@
+import json
+from typing import List, Dict
+
+from agents.codebase_analyst.start import Project
+
+
class PlannerAgent:
"""Агент-планировщик: анализирует проект и создаёт план подзадач."""
diff --git a/agents/tester_web/TesterAgent.py b/agents/tester_web/TesterAgent.py
index 6e64c5c..615f72f 100644
--- a/agents/tester_web/TesterAgent.py
+++ b/agents/tester_web/TesterAgent.py
@@ -1,3 +1,8 @@
+import time
+from typing import List, Dict, Any
+
+from playwright.sync_api import sync_playwright
+
from agents.codebase_analyst.start import Project
diff --git a/common/hot_env.py b/common/hot_env.py
new file mode 100644
index 0000000..5abaa1a
--- /dev/null
+++ b/common/hot_env.py
@@ -0,0 +1,82 @@
+import asyncio
+import hashlib
+import logging
+import os
+import time
+from pathlib import Path
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+ENV_PATH = Path(os.environ.get("VAULT_ENV_PATH", "/vault-secrets/.env"))
+MAX_WAIT_SECONDS = 30
+WATCH_INTERVAL_SECONDS = 5
+
+def _file_hash() -> str | None:
+ if not ENV_PATH.exists():
+ return None
+ try:
+ return hashlib.sha256(ENV_PATH.read_bytes()).hexdigest()
+ except OSError:
+ return None
+
+
+def _parse_env_file(path: Path) -> dict[str, str]:
+ result: dict[str, str] = {}
+ with path.open() as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith("#") or "=" not in line:
+ continue
+ key, _, value = line.partition("=")
+ result[key.strip()] = value.strip().strip('"').strip("'")
+ return result
+
+
+def wait_for_env_file(timeout: int = MAX_WAIT_SECONDS) -> None:
+ """Блокирующее ожидание появления .env (для entrypoint до старта uvicorn)."""
+ start = time.time()
+ while not ENV_PATH.exists():
+ if time.time() - start > timeout:
+ raise RuntimeError(
+ f"Vault Agent did not render {ENV_PATH} within {timeout}s"
+ )
+ logger.info("Waiting for %s ...", ENV_PATH)
+ time.sleep(0.5)
+ time.sleep(1.5)
+
+
+def load_env(force: bool = False) -> bool:
+ if not ENV_PATH.exists():
+ return False
+
+ try:
+ values = _parse_env_file(ENV_PATH)
+ except OSError as e:
+ logger.warning("Failed to read %s: %s", ENV_PATH, e)
+ return False
+
+ for key, value in values.items():
+ if force or key not in os.environ:
+ os.environ[key] = value
+
+ global _last_hash
+ _last_hash = _file_hash()
+ return True
+
+
+def reload_if_changed() -> bool:
+ current = _file_hash()
+ if current is None or current == _last_hash:
+ return False
+ logger.info("Detected change in %s, reloading secrets", ENV_PATH)
+ return load_env(force=True)
+
+
+async def watch_env_file(interval: int = WATCH_INTERVAL_SECONDS) -> None:
+ while True:
+ try:
+ reload_if_changed()
+ except Exception:
+ logger.exception("env watcher failed")
+ await asyncio.sleep(interval)
diff --git a/common/singleton.py b/common/singleton.py
new file mode 100644
index 0000000..bfdbeb5
--- /dev/null
+++ b/common/singleton.py
@@ -0,0 +1,107 @@
+import asyncio
+import logging
+from typing import Optional
+
+import httpx
+
+from agents.issue_triage.IssueTriageAgent import IssueTriageAgent
+from agents.issue_triage.context_builder import YouTrackContextBuilder
+from agents.registry import AgentRegistry
+from common.llm_client import LLMClient
+from common.youtrack_mcp_client import YouTrackMCPClient
+
+logger = logging.getLogger(__name__)
+
+
+class AgentSingleton:
+ """
+ Синглтон для IssueTriageAgent и его зависимостей.
+ Потокобезопасен через asyncio.Lock — инициализация не гоняется.
+ """
+
+ _instance: Optional["AgentSingleton"] = None
+ _lock = asyncio.Lock()
+
+ def __init__(self) -> None:
+ self._agent: Optional[IssueTriageAgent] = None
+ self._youtrack_mcp: Optional[YouTrackMCPClient] = None
+ self._http_for_attachments: Optional[httpx.AsyncClient] = None
+ self._initialized = False
+
+ @classmethod
+ async def get(cls) -> "AgentSingleton":
+ if cls._instance is None:
+ async with cls._lock:
+ if cls._instance is None:
+ cls._instance = cls()
+ return cls._instance
+
+ async def ensure_initialized(self) -> None:
+ """Инициализация при первом обращении или после reset()."""
+ if self._initialized:
+ return
+ async with self._lock:
+ if self._initialized:
+ return
+ await self._initialize()
+ self._initialized = True
+
+ async def _initialize(self) -> None:
+ from main import env, env_optional # локальный импорт, чтобы не плодить циклы
+
+ logger.info("Initializing AgentSingleton (or re-initializing)")
+
+ self._youtrack_mcp = await YouTrackMCPClient(
+ env("YOUTRACK_MCP_SERVER"),
+ env("YOUTRACK_MCP_TOKEN"),
+ ).connect()
+
+ self._http_for_attachments = httpx.AsyncClient(
+ headers={"Authorization": f"Bearer {env('YOUTRACK_MCP_TOKEN')}"},
+ proxy=env_optional("HTTPS_PROXY"),
+ timeout=httpx.Timeout(30.0, connect=10.0, read=60.0),
+ follow_redirects=True,
+ )
+
+ self._agent = IssueTriageAgent(
+ YouTrackContextBuilder(self._youtrack_mcp, self._http_for_attachments),
+ LLMClient(
+ base_url=env("LLM_ADDRESS"),
+ api_key=env("LLM_API_KEY"),
+ model=env("LLM_MODEL"),
+ ),
+ AgentRegistry(),
+ self._youtrack_mcp,
+ )
+
+ @property
+ def agent(self) -> IssueTriageAgent:
+ if self._agent is None:
+ raise RuntimeError("AgentSingleton not initialized. Call ensure_initialized() first.")
+ return self._agent
+
+ @property
+ def youtrack_mcp(self) -> YouTrackMCPClient:
+ if self._youtrack_mcp is None:
+ raise RuntimeError("AgentSingleton not initialized.")
+ return self._youtrack_mcp
+
+ async def reset(self) -> None:
+ """Закрывает старые ресурсы и помечает синглтон как неинициализированный."""
+ async with self._lock:
+ logger.info("Resetting AgentSingleton (secrets rotated?)")
+ if self._http_for_attachments is not None:
+ await self._http_for_attachments.aclose()
+ if self._youtrack_mcp is not None:
+ await self._youtrack_mcp.close()
+ self._agent = None
+ self._youtrack_mcp = None
+ self._http_for_attachments = None
+ self._initialized = False
+
+ @classmethod
+ async def destroy(cls) -> None:
+ """Полное уничтожение синглтона — для shutdown."""
+ if cls._instance is not None:
+ await cls._instance.reset()
+ cls._instance = None
\ No newline at end of file
diff --git a/contracts/IssueContext.py b/contracts/IssueContext.py
index ed5e290..da467bb 100644
--- a/contracts/IssueContext.py
+++ b/contracts/IssueContext.py
@@ -7,6 +7,15 @@ from pydantic import BaseModel
from contracts.ProjectContext import ProjectContext
+class IssueComment(BaseModel):
+ id: str
+ author: str
+ body: str
+ created_at: int
+ is_agent: bool = False
+ marker: dict[str, str] = field(default_factory=dict)
+
+
@dataclass
class IssueContext:
issue_id: str
@@ -49,11 +58,3 @@ class IssueContext:
parts.append(f"- @{c.author} ({c.created_at}): {c.body}")
return "\n".join(parts)
-
-class IssueComment(BaseModel):
- id: str
- author: str
- body: str
- created_at: int
- is_agent: bool = False
- marker: dict[str, str] = field(default_factory=dict)
diff --git a/contracts/Task.py b/contracts/Task.py
index 5bfbc5d..73d5096 100644
--- a/contracts/Task.py
+++ b/contracts/Task.py
@@ -1,7 +1,7 @@
import uuid
from dataclasses import dataclass, field
from enum import Enum
-from typing import Optional
+from typing import Optional, Literal
from contracts.base import BaseContract
@@ -58,7 +58,7 @@ class Task(BaseContract):
lines = [
f"# Task: {self.description}",
f"# Before start you must ensure that another tasks have been done: {', '.join(self.depends_on)}" if self.depends_on else "",
- f"# Acceptance criteria: \n{"\n - ".join(self.acceptance_criteria)}",
+ f"# Acceptance criteria: \n{"\n - ".join(self.acceptance_criteria)}"
]
return "\n".join(lines)
diff --git a/deployment/Dockerfile b/deployment/Dockerfile
new file mode 100644
index 0000000..6ba89d2
--- /dev/null
+++ b/deployment/Dockerfile
@@ -0,0 +1,18 @@
+FROM python:3.13-slim-trixie AS builder
+WORKDIR /build
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ gcc \
+ && rm -rf /var/lib/apt/lists/*
+COPY requirements.txt .
+RUN pip install --no-cache-dir --target=/app/deps -r requirements.txt
+
+### Runtime
+FROM gcr.io/distroless/python3-debian13:nonroot
+WORKDIR /app
+COPY --from=builder /app/deps /app/deps
+ENV PYTHONPATH="/app/deps"
+ENV PYTHONUNBUFFERED=1
+COPY . .
+USER nonroot
+EXPOSE 8000
+CMD ["entrypoint.py"]
\ No newline at end of file
diff --git a/deployment/agent.hcl b/deployment/agent.hcl
new file mode 100644
index 0000000..bd28458
--- /dev/null
+++ b/deployment/agent.hcl
@@ -0,0 +1,29 @@
+pid_file = "/tmp/pidfile"
+
+vault {
+ address = "https://10.6.2.1:8200"
+ tls_skip_verify = true
+ retry {
+ num_retries = 5
+ }
+}
+
+auto_auth {
+ method "approle" {
+ mount_path = "auth/approle"
+ config = {
+ role_id_file_path = "/vault-auth/role_id"
+ secret_id_file_path = "/vault-auth/wrapped_secret_id"
+ secret_id_response_wrapping_path = "auth/approle/role/triage_agent/secret-id"
+ remove_secret_id_file_after_reading = false
+ }
+ }
+ # sink не нужен, если приложению не требуется сам токен Vault
+}
+
+template {
+ source = "/vault-template/template.ctmpl"
+ destination = "/vault-secrets/.env"
+ error_on_missing_key = true
+ perms = "0644"
+}
\ No newline at end of file
diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml
new file mode 100644
index 0000000..27a451d
--- /dev/null
+++ b/deployment/docker-compose.yml
@@ -0,0 +1,48 @@
+volumes:
+ vault-secrets:
+ driver_opts:
+ type: tmpfs
+ device: tmpfs
+ o: "size=1m,mode=0777,uid=65532,gid=65532"
+ projects-data:
+
+services:
+ vault-agent:
+ image: hashicorp/vault:1.17
+ command: agent -config=/vault-agent/agent.hcl
+ cap_add:
+ - IPC_LOCK
+ volumes:
+ - ./agent.hcl:/vault-agent/agent.hcl:ro
+ - ./env_template.ctmpl:/vault-template/template.ctmpl:ro
+ - ./../role_id:/vault-auth/role_id:ro
+ - ./../wrapped_secret_id:/vault-auth/wrapped_secret_id:ro
+ - vault-secrets:/vault-secrets
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ networks:
+ - agent-net
+
+ api:
+ build:
+ context: ..
+ dockerfile: deployment/Dockerfile
+ ports:
+ - "8173:8000"
+ group_add:
+ - "100"
+ volumes:
+ - ./../projects-data:/projects
+ - vault-secrets:/vault-secrets
+ depends_on:
+ vault-agent:
+ condition: service_started
+ networks:
+ - agent-net
+
+networks:
+ agent-net:
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 10.6.7.0/24
\ No newline at end of file
diff --git a/deployment/env_template.ctmpl b/deployment/env_template.ctmpl
new file mode 100644
index 0000000..bd5458a
--- /dev/null
+++ b/deployment/env_template.ctmpl
@@ -0,0 +1,10 @@
+{{ with secret "internal_secrets/data/agents" }}
+YOUTRACK_MCP_SERVER={{ .Data.data.youtrack_mcp_server }}
+YOUTRACK_MCP_TOKEN={{ .Data.data.youtrack_agent_mcp_token }}
+YOUTRACK_TRIAGE_AUTHOR={{ .Data.data.youtrack_triage_author }}
+
+LLM_ADDRESS={{ .Data.data.llm_address }}
+LLM_API_KEY={{ .Data.data.llm_api_key }}
+LLM_MODEL={{ .Data.data.llm_model }}
+
+{{ end }}
diff --git a/deployment/generate-wrapped-secret-id.sh b/deployment/generate-wrapped-secret-id.sh
new file mode 100755
index 0000000..97a7e24
--- /dev/null
+++ b/deployment/generate-wrapped-secret-id.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+set -euo pipefail
+
+: '
+vault write auth/approle/role/triage_agent \
+ token_policies="agent_policy" \
+ token_ttl=1h \
+ token_max_ttl=4h \
+ secret_id_ttl=24h \
+ token_bound_cidrs="10.6.0.0/16"
+'
+
+VAULT_ADDR="https://10.6.2.1:8200"
+if [ -z "${VAULT_TOKEN:-}" ]; then
+ read -r -s -p "VAULT_TOKEN: " VAULT_TOKEN
+ echo
+fi
+
+TARGET_DIR=${1:-'.'}
+
+ROLE_NAME="triage_agent"
+WRAP_TTL="600s"
+CONFIG=$(mktemp)
+chmod 600 "$CONFIG"
+trap 'rm -f "$CONFIG"' EXIT
+
+{
+ printf 'header = "X-Vault-Token: %s"\n' "$VAULT_TOKEN"
+ printf 'header = "X-Vault-Wrap-TTL: %s"\n' "$WRAP_TTL"
+} > "$CONFIG"
+
+curl -s -X POST -k -K "$CONFIG" \
+ "$VAULT_ADDR/v1/auth/approle/role/${ROLE_NAME}/secret-id" \
+ | jq -r '.wrap_info.token // .errors[0]' | tr -d '\n' > wrapped_secret_id
+
+
diff --git a/entrypoint.py b/entrypoint.py
new file mode 100644
index 0000000..8da356c
--- /dev/null
+++ b/entrypoint.py
@@ -0,0 +1,17 @@
+import logging
+import sys
+
+from common.hot_env import wait_for_env_file, load_env
+
+logging.basicConfig(level=logging.INFO)
+
+if __name__ == "__main__":
+ try:
+ wait_for_env_file()
+ load_env(force=True)
+ except RuntimeError as e:
+ print(f"FATAL: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ import uvicorn
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, log_level="info")
\ No newline at end of file
diff --git a/main.py b/main.py
index 09fb744..7d5b414 100644
--- a/main.py
+++ b/main.py
@@ -2,23 +2,35 @@ import logging
import os
from contextlib import asynccontextmanager
-import httpx
-from dotenv import load_dotenv
-from fastapi import FastAPI, HTTPException
+from fastapi import FastAPI, HTTPException, Depends
from agents.coders.BaseCoder import CoderAgent
from agents.issue_triage.IssueTriageAgent import IssueTriageAgent
from agents.issue_triage.context_builder import YouTrackContextBuilder
from agents.registry import AgentRegistry
from common.gitea_mcp_client import GiteaMCPClient
+from common.hot_env import load_env, reload_if_changed
from common.llm_client import LLMClient
+from common.singleton import AgentSingleton
from common.youtrack_mcp_client import YouTrackMCPClient, IssueNotFound
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
-load_dotenv()
+_last_hash: str | None = None
+
+if not load_env(force=False):
+ logger.warning("No .env file found at startup, will rely on os.environ")
+
+
+def env_optional(key: str, default: str | None = None) -> str:
+ value = os.environ.get(key)
+ if value is None:
+ return default
+ return value
+
def env(key: str) -> str:
+ reload_if_changed()
value = os.environ.get(key)
if not value:
raise RuntimeError(f"Missing required env var: {key}")
@@ -26,42 +38,43 @@ def env(key: str) -> str:
@asynccontextmanager
async def lifespan(app: FastAPI):
+ import asyncio
+
+ singleton = await AgentSingleton.get()
+ await singleton.ensure_initialized()
+
+ async def watch_and_reset():
+ while True:
+ try:
+ if reload_if_changed():
+ await singleton.reset()
+ await singleton.ensure_initialized()
+ except Exception:
+ logger.exception("watch_and_reset failed")
+ await asyncio.sleep(5)
+
+ watcher = asyncio.create_task(watch_and_reset())
+
app.state.youtrack_mcp = await YouTrackMCPClient(
str(os.getenv('YOUTRACK_MCP_SERVER')),
str(os.getenv('YOUTRACK_MCP_TOKEN')),
).connect()
- #
- #
- # res = await mcp.call_tool(
- # "add_issue_comment",
- # {"issueId": "ARCH-229", "text": "test"}
- # )
- # logger.info(res)
-
- http_for_attachments = httpx.AsyncClient(
- headers={"Authorization": f"Bearer {env('YOUTRACK_MCP_TOKEN')}"},
- proxy=env("HTTPS_PROXY"),
- timeout=httpx.Timeout(30.0, connect=10.0, read=60.0),
- follow_redirects=True,
- )
-
- app.state.issue_reader_agent = IssueTriageAgent(
- YouTrackContextBuilder(app.state.youtrack_mcp, http_for_attachments),
- LLMClient(
- base_url=env("LLM_ADDRESS"),
- api_key=env("LLM_API_KEY"),
- model=env("LLM_MODEL"),
- ),
- AgentRegistry(),
- app.state.youtrack_mcp
- )
yield
# ---- shutdown ----
+ watcher.cancel()
await app.state.youtrack_mcp.close()
+
app = FastAPI(lifespan=lifespan)
+
+async def get_agent() -> IssueTriageAgent:
+ singleton = await AgentSingleton.get()
+ await singleton.ensure_initialized()
+ return singleton.agent
+
+
@app.get("/")
async def root():
return {"message": "Hello World"}
@@ -73,8 +86,7 @@ async def say_hello(name: str):
@app.get("/consume_task")
-async def decomposing_issue(issue: str):
- agent: IssueTriageAgent = app.state.issue_reader_agent
+async def decomposing_issue(issue: str, agent: IssueTriageAgent = Depends(get_agent)):
try:
logger.info(f"Received an issue: {issue}")
plan = await agent.plan_issue(issue)
@@ -89,8 +101,7 @@ async def decomposing_issue(issue: str):
return {"error": str(e)}
@app.get("/code_issue")
-async def code_issue(issue: str):
- issue_agent: IssueTriageAgent = app.state.issue_reader_agent
+async def code_issue(issue: str, issue_agent: IssueTriageAgent = Depends(get_agent)):
gitea_mcp_client = GiteaMCPClient()
coder_agent: CoderAgent = CoderAgent(
str(os.getenv("CODER_ID")),
diff --git a/requirements.txt b/requirements.txt
index 8a5e5c9..b3f2a29 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,103 +1,18 @@
-a2wsgi==1.10.10
-adbc_driver_manager==1.12.0
-adbc_driver_postgresql==1.12.0
-adbc_driver_sqlite==1.12.0
-aiohttp==3.14.3
-AppKit==0.2.8
-atheris==3.1.0
-beautifulsoup4==4.15.0
-botocore==1.43.102
-brotli==1.2.0
-brotlicffi==1.2.0.2
-cffi==2.1.1
-chardet==7.6.0
-checks==0.2
-ConfigParser==7.2.0
-curio==1.6
-cycler==0.12.1
-Cython==3.3.0
-docutils==0.23
-email_validator==2.3.0
-eval_type_backport==0.4.0
-exceptiongroup==1.3.1
-fastapi_cli==0.0.32
-fastparquet==2026.5.0
-filelock==4.0.3
-Foundation==0.1.0a0.dev1
-fqdn==1.5.1
-fsspec==2026.9.0
-gunicorn==26.2.0
-h2==4.4.1
-HTMLParser==0.0.2
-httptools==0.8.0
-httpx2_jsfetch==1.0
-hypothesis==6.168.1
-importlib_resources==7.1.0
-ipython==9.17.1
-ipywidgets==8.1.9
-isoduration==20.11.0
-itsdangerous==2.2.0
-Jinja2==3.1.6
-jnius==1.1.0
-jsonpath_ng==1.8.0
-jsonpointer==3.1.1
-keyring==25.7.0
-lxml==6.1.3
-MarkupSafe==3.0.3
-matplotlib==3.11.2
-mtrand==0.1
-numexpr==2.14.2
-odfpy==1.4.1
-openpyxl==3.1.5
-outcome==1.3.0.post0
-Pillow==12.3.0
-protobuf==7.36.2
-psutil==7.2.2
-pydantic_extra_types==2.11.1
-pydot==4.0.1
-pygraphviz==2.0.2
-PyInstaller==6.22.3
-pyodide==0.0.2
-pyOpenSSL==26.4.0
-pyperf==2.10.0
-PyQt4==4.11.4
-PyQt5==5.15.11
-pytest==9.1.1
-pytest_run_parallel==0.10.0
-python_bcrypt==0.3.2
-python_calamine==0.8.2
-pyxlsb==1.0.10
-qtpy==2.4.3
-redis==8.1.0
-rfc3339_validator==0.1.4
-rfc3986_validator==0.1.1
-rfc3987==1.3.8
-rfc3987_syntax==1.1.0
-s3fs==2026.9.0
-scipy==1.18.1
-scipy_doctest==2.2.0
-setuptools==84.0.0
-simplejson==4.1.2
-sounddevice==0.5.6
-Sphinx==9.1.0
-SQLAlchemy==2.1.0
-sympy==1.14.0
-threadpoolctl==3.7.0
-toml==0.10.2
-tomlkit==0.15.1
-traitlets==5.16.1
+fastapi==0.141.1
+httpx==0.28.1
+mcp==2.2.0
+mcp_types==2.2.0
+networkx==3.7
+openai==3.19.2
+pandas==3.0.6
+playwright==1.63.0
+pydantic==2.13.5
+python-dotenv==1.2.3
+PyYAML==6.0.3
+Requests==2.34.2
+tree_sitter==0.26.0
tree_sitter_html==0.23.2
tree_sitter_javascript==0.25.0
tree_sitter_python==0.25.0
tree_sitter_typescript==0.23.2
-typer==0.27.2
-urllib3_secure_extra==0.1.0
-watchfiles==1.3.0
-webcolors==25.10.0
-winloop==0.6.4
-wsproto==1.3.2
-xlrd==2.0.2
-xlsxwriter==3.2.9
-xmlrpclib==1.0.1
-zstandard==0.25.0
-zttp==0.0.34
+uvicorn==0.54.0