107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
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
|