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