llm.py 1.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
  1. from __future__ import annotations
  2. import json
  3. from typing import Any
  4. import httpx
  5. from .config import settings
  6. class LLMUnavailable(RuntimeError):
  7. pass
  8. class OpenAICompatibleClient:
  9. """项目使用的最小 OpenAI 兼容对话客户端。"""
  10. @property
  11. def configured(self) -> bool:
  12. return bool(settings.llm_api_key and settings.llm_base_url and settings.llm_model)
  13. async def chat(self, system: str, user: str, temperature: float = 0.2) -> str:
  14. if not self.configured:
  15. raise LLMUnavailable("未配置 LLM_API_KEY / LLM_BASE_URL / LLM_MODEL")
  16. async with httpx.AsyncClient(timeout=60) as client:
  17. response = await client.post(
  18. f"{settings.llm_base_url}/chat/completions",
  19. headers={"Authorization": f"Bearer {settings.llm_api_key}"},
  20. json={
  21. "model": settings.llm_model,
  22. "temperature": temperature,
  23. "messages": [
  24. {"role": "system", "content": system},
  25. {"role": "user", "content": user},
  26. ],
  27. },
  28. )
  29. response.raise_for_status()
  30. data = response.json()
  31. return data["choices"][0]["message"]["content"]
  32. async def json(self, system: str, user: str) -> dict[str, Any]:
  33. """调用模型并从回复中提取 JSON 对象。"""
  34. raw = await self.chat(system, user, temperature=0.1)
  35. cleaned = raw.strip()
  36. if "```" in cleaned:
  37. cleaned = cleaned.replace("```json", "").replace("```", "").strip()
  38. start, end = cleaned.find("{"), cleaned.rfind("}")
  39. if start >= 0 and end > start:
  40. cleaned = cleaned[start : end + 1]
  41. return json.loads(cleaned)
  42. llm = OpenAICompatibleClient()