Deployment
This commit is contained in:
@@ -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
|
||||
+3
-1
@@ -1,2 +1,4 @@
|
||||
.env
|
||||
projects
|
||||
projects
|
||||
role_id
|
||||
wrapped_secret_id
|
||||
Generated
+2
-1
@@ -6,8 +6,9 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/agents/issue_triage/tests" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/projects/agents" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.14 (Agents)" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (agents) (2)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+1
-1
@@ -3,5 +3,5 @@
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.14 (Agents)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 (Agents)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (agents) (2)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
@@ -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
|
||||
```
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from agents.codebase_analyst.start import Project
|
||||
|
||||
|
||||
class CoderAgent:
|
||||
"""Агент-исполнитель: выполняет подзадачу, генерирует код и применяет изменения."""
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import json
|
||||
from typing import List, Dict
|
||||
|
||||
from agents.codebase_analyst.start import Project
|
||||
|
||||
|
||||
class PlannerAgent:
|
||||
"""Агент-планировщик: анализирует проект и создаёт план подзадач."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
Executable
+36
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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")),
|
||||
|
||||
+14
-99
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user