2026-09-19 23:47:45 +03:00
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
from contextlib import asynccontextmanager
|
2026-09-19 14:37:16 +03:00
|
|
|
|
2026-09-25 19:44:48 +03:00
|
|
|
from fastapi import FastAPI, HTTPException, Depends
|
2026-09-25 19:54:01 +03:00
|
|
|
from pydantic import BaseModel
|
2026-09-22 23:21:59 +03:00
|
|
|
|
|
|
|
|
from agents.coders.BaseCoder import CoderAgent
|
2026-09-20 20:56:36 +03:00
|
|
|
from agents.issue_triage.IssueTriageAgent import IssueTriageAgent
|
|
|
|
|
from agents.issue_triage.context_builder import YouTrackContextBuilder
|
|
|
|
|
from agents.registry import AgentRegistry
|
2026-09-22 23:21:59 +03:00
|
|
|
from common.gitea_mcp_client import GiteaMCPClient
|
2026-09-25 19:44:48 +03:00
|
|
|
from common.hot_env import load_env, reload_if_changed
|
2026-09-19 23:47:45 +03:00
|
|
|
from common.llm_client import LLMClient
|
2026-09-25 19:44:48 +03:00
|
|
|
from common.singleton import AgentSingleton
|
2026-09-19 23:47:45 +03:00
|
|
|
from common.youtrack_mcp_client import YouTrackMCPClient, IssueNotFound
|
2026-09-19 14:37:16 +03:00
|
|
|
|
2026-09-19 23:47:45 +03:00
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2026-09-25 19:44:48 +03:00
|
|
|
_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
|
|
|
|
|
|
2026-09-19 23:47:45 +03:00
|
|
|
def env(key: str) -> str:
|
2026-09-25 19:44:48 +03:00
|
|
|
reload_if_changed()
|
2026-09-19 23:47:45 +03:00
|
|
|
value = os.environ.get(key)
|
|
|
|
|
if not value:
|
|
|
|
|
raise RuntimeError(f"Missing required env var: {key}")
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
async def lifespan(app: FastAPI):
|
2026-09-25 19:44:48 +03:00
|
|
|
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())
|
|
|
|
|
|
2026-09-19 23:47:45 +03:00
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
# ---- shutdown ----
|
2026-09-25 19:44:48 +03:00
|
|
|
watcher.cancel()
|
|
|
|
|
|
2026-09-19 23:47:45 +03:00
|
|
|
app = FastAPI(lifespan=lifespan)
|
2026-09-19 14:37:16 +03:00
|
|
|
|
2026-09-25 19:44:48 +03:00
|
|
|
|
|
|
|
|
async def get_agent() -> IssueTriageAgent:
|
|
|
|
|
singleton = await AgentSingleton.get()
|
|
|
|
|
await singleton.ensure_initialized()
|
|
|
|
|
return singleton.agent
|
|
|
|
|
|
|
|
|
|
|
2026-09-19 14:37:16 +03:00
|
|
|
@app.get("/")
|
|
|
|
|
async def root():
|
|
|
|
|
return {"message": "Hello World"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/hello/{name}")
|
|
|
|
|
async def say_hello(name: str):
|
|
|
|
|
return {"message": f"Hello {name}"}
|
2026-09-19 23:47:45 +03:00
|
|
|
|
|
|
|
|
|
2026-09-25 19:54:01 +03:00
|
|
|
class IncomingTask(BaseModel):
|
|
|
|
|
issue: str
|
|
|
|
|
|
|
|
|
|
@app.post("/consume_task")
|
|
|
|
|
async def decomposing_issue(body: IncomingTask, agent: IssueTriageAgent = Depends(get_agent)):
|
2026-09-19 23:47:45 +03:00
|
|
|
try:
|
2026-09-25 19:54:01 +03:00
|
|
|
logger.info(f"Received an issue: {body.issue}")
|
|
|
|
|
plan = await agent.plan_issue(body.issue)
|
2026-09-19 23:47:45 +03:00
|
|
|
logger.info("Task planning successful.")
|
2026-09-20 20:56:36 +03:00
|
|
|
logger.info(plan.render())
|
2026-09-19 23:47:45 +03:00
|
|
|
return {"plan": plan}
|
|
|
|
|
except IssueNotFound:
|
2026-09-25 19:54:01 +03:00
|
|
|
logger.error(f"Issue {body.issue} not found.")
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"Issue {body.issue} not found")
|
2026-09-19 23:47:45 +03:00
|
|
|
except Exception as e:
|
|
|
|
|
logger.exception("An unexpected error occurred")
|
|
|
|
|
return {"error": str(e)}
|
2026-09-22 23:21:59 +03:00
|
|
|
|
|
|
|
|
@app.get("/code_issue")
|
2026-09-25 19:44:48 +03:00
|
|
|
async def code_issue(issue: str, issue_agent: IssueTriageAgent = Depends(get_agent)):
|
2026-09-22 23:21:59 +03:00
|
|
|
gitea_mcp_client = GiteaMCPClient()
|
|
|
|
|
coder_agent: CoderAgent = CoderAgent(
|
|
|
|
|
str(os.getenv("CODER_ID")),
|
|
|
|
|
issue_agent.ctx_builder,
|
|
|
|
|
issue_agent.llm,
|
|
|
|
|
issue_agent.agent_registry,
|
|
|
|
|
issue_agent.youtrack_mcp,
|
|
|
|
|
gitea_mcp_client
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
logger.info(f"Resolve an issue: {issue}")
|
|
|
|
|
issue_ctx = await issue_agent.ctx_builder.build(issue)
|
|
|
|
|
if issue_ctx:
|
|
|
|
|
response = await coder_agent.fix_an_issue(issue_ctx)
|
|
|
|
|
logger.info("Task planning successful.")
|
|
|
|
|
return {"status": "ok", "response": response}
|
|
|
|
|
except IssueNotFound:
|
|
|
|
|
logger.error(f"Issue {issue} not found.")
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"Issue {issue} not found")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.exception("An unexpected error occurred")
|
|
|
|
|
return {"error": str(e)}
|
|
|
|
|
|
|
|
|
|
return {"error": "No context was created"}
|