import logging import os from contextlib import asynccontextmanager 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__) _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}") return value @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() 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"} @app.get("/hello/{name}") async def say_hello(name: str): return {"message": f"Hello {name}"} @app.get("/consume_task") 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) logger.info("Task planning successful.") logger.info(plan.render()) return {"plan": plan} 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)} @app.get("/code_issue") 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")), 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"}