import asyncio import json import logging import time from collections.abc import Sequence from openai import APIConnectionError, APIStatusError, AsyncOpenAI, RateLimitError from app.llm.prompts import SYSTEM_PROMPT logger = logging.getLogger(__name__) class LLMUnavailable(RuntimeError): pass class DeepSeekClient: def __init__( self, api_key: str, base_url: str, model: str, timeout: float = 45, retries: int = 3 ): self.client = AsyncOpenAI( api_key=api_key, base_url=base_url, timeout=timeout, max_retries=0 ) self.model, self.retries = model, retries async def complete( self, messages: Sequence[dict[str, str]], *, temperature: float = 0.1 ) -> str: started = time.monotonic() payload = [{"role": "system", "content": SYSTEM_PROMPT}, *messages] for attempt in range(self.retries): try: response = await self.client.chat.completions.create( model=self.model, messages=list(payload), temperature=temperature ) content = response.choices[0].message.content or "" logger.info( "llm_complete duration_ms=%d tokens=%s", int((time.monotonic() - started) * 1000), getattr(response.usage, "total_tokens", "unknown"), ) return content except (RateLimitError, APIConnectionError, APIStatusError) as error: if ( isinstance(error, APIStatusError) and error.status_code < 500 and error.status_code != 429 ): break if attempt + 1 < self.retries: await asyncio.sleep(2**attempt) logger.warning("llm_unavailable duration_ms=%d", int((time.monotonic() - started) * 1000)) raise LLMUnavailable("Модель временно недоступна. Попробуйте ещё раз позже.") async def json(self, messages: Sequence[dict[str, str]]) -> dict[str, object]: content = await self.complete(messages, temperature=0) try: return json.loads(content.removeprefix("```json").removesuffix("```").strip()) except json.JSONDecodeError: return {}