from __future__ import annotations import logging from typing import Optional from collections import deque from config import settings logger = logging.getLogger(__name__) class LLMService: def __init__(self) -> None: self._ready = False self._error: Optional[str] = None self._client = None self._model = (settings.llm_model or "").strip() self._history: deque[dict] = deque(maxlen=12) # 6 turns (user+assistant) self._system_prompt = "请用中文口语化简短回复,1-2句。" if not settings.llm_api_key: return if not self._model: base = (settings.llm_base_url or "").lower() self._model = "deepseek-chat" if "deepseek.com" in base else "gpt-4o-mini" try: from openai import AsyncOpenAI self._client = AsyncOpenAI( api_key=settings.llm_api_key, base_url=settings.llm_base_url, timeout=settings.llm_timeout, ) self._ready = True except Exception as exc: # pragma: no cover self._error = str(exc) logger.warning("LLM online mode unavailable: %s", exc) async def reply_with_meta(self, text: str) -> tuple[str, str]: if not self._ready or self._client is None: return f"收到:{text}。这是本地兜底回复。", "fallback" try: resp = await self._client.chat.completions.create( model=self._model, messages=[{"role": "system", "content": self._system_prompt}, *list(self._history), {"role": "user", "content": text}], max_tokens=settings.llm_max_tokens, temperature=0.7, ) content = (resp.choices[0].message.content or "").strip() if content: self._history.append({"role": "user", "content": text}) self._history.append({"role": "assistant", "content": content}) return content, "online" except Exception as exc: # pragma: no cover self._error = str(exc) logger.warning("LLM request failed, fallback used: %s", exc) return f"收到:{text}。这是本地兜底回复。", "fallback" async def reply(self, text: str) -> str: content, _ = await self.reply_with_meta(text) return content @property def health(self) -> dict: return { "ready": self._ready, "model": self._model, "history_items": len(self._history), "error": self._error, } def clear_history(self) -> None: self._history.clear()