131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
"""Unified LLM service for VoIdeaAI.
|
|
|
|
Supports OpenAI-compatible APIs (OpenAI, YandexGPT via API).
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from app.core.config import get_settings
|
|
from app.core.database import async_session_maker
|
|
from app.models.log import LogEntry
|
|
from app.services.debug_service import DebugService
|
|
|
|
logger = logging.getLogger("voidea.llm")
|
|
|
|
OPENAI_URL = "https://api.openai.com/v1/chat/completions"
|
|
|
|
|
|
async def _log_llm_call(messages: list[dict], response: str | None, model: str, duration_ms: int):
|
|
"""Log LLM call details if debug mode is active."""
|
|
try:
|
|
async with async_session_maker() as db:
|
|
svc = DebugService(db)
|
|
if await svc.is_debug_mode():
|
|
log = LogEntry(
|
|
level="DEBUG",
|
|
source="llm",
|
|
message=f"LLM call: model={model} duration={duration_ms}ms success={response is not None}",
|
|
details=json.dumps({
|
|
"messages": messages,
|
|
"response": response,
|
|
"model": model,
|
|
}, ensure_ascii=False),
|
|
created_at=datetime.now(timezone.utc),
|
|
)
|
|
db.add(log)
|
|
await db.commit()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
async def chat_completion(
|
|
messages: list[dict[str, str]],
|
|
model: str = "gpt-4o-mini",
|
|
temperature: float = 0.7,
|
|
max_tokens: int = 1024,
|
|
) -> str | None:
|
|
"""Call an LLM with OpenAI-compatible chat completions format.
|
|
|
|
Args:
|
|
messages: List of {"role": "system"|"user"|"assistant", "content": "..."}
|
|
model: Model name
|
|
temperature: Response creativity
|
|
max_tokens: Max tokens in response
|
|
|
|
Returns:
|
|
Response text or None on failure
|
|
"""
|
|
import time
|
|
start = time.time()
|
|
|
|
settings = get_settings()
|
|
api_key = settings.openai_api_key or settings.ai_yandex_key or ""
|
|
if not api_key:
|
|
await _log_llm_call(messages, None, model, 0)
|
|
return None
|
|
|
|
url = settings.ai_yandex_url.rstrip("/") + "/chat/completions" if settings.ai_yandex_key else OPENAI_URL
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
try:
|
|
resp = await client.post(
|
|
url,
|
|
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
|
json={
|
|
"model": model,
|
|
"messages": messages,
|
|
"temperature": temperature,
|
|
"max_tokens": max_tokens,
|
|
},
|
|
)
|
|
elapsed = int((time.time() - start) * 1000)
|
|
if resp.status_code != 200:
|
|
await _log_llm_call(messages, None, model, elapsed)
|
|
return None
|
|
data = resp.json()
|
|
content = data["choices"][0]["message"]["content"]
|
|
await _log_llm_call(messages, content, model, elapsed)
|
|
return content
|
|
except Exception as e:
|
|
elapsed = int((time.time() - start) * 1000)
|
|
await _log_llm_call(messages, None, model, elapsed)
|
|
return None
|
|
|
|
|
|
async def classify_intent(text: str, agents: list[dict[str, Any]]) -> str | None:
|
|
"""Use LLM to classify user intent and select the best agent.
|
|
|
|
Args:
|
|
text: User input text
|
|
agents: List of agent dicts with name and description
|
|
|
|
Returns:
|
|
Selected agent name or None
|
|
"""
|
|
agent_list = "\n".join(f"- {a['name']}: {a['description']}" for a in agents)
|
|
|
|
prompt = f"""Ты — дирижёр умных ассистентов. Определи, какой агент лучше всего подходит для ответа пользователю.
|
|
|
|
Доступные агенты:
|
|
{agent_list}
|
|
|
|
Ответь ТОЛЬКО именем агента, без пояснений."""
|
|
|
|
messages = [
|
|
{"role": "system", "content": prompt},
|
|
{"role": "user", "content": text},
|
|
]
|
|
|
|
result = await chat_completion(messages, temperature=0.3, max_tokens=64)
|
|
if not result:
|
|
return None
|
|
|
|
result = result.strip().strip('"').strip("'")
|
|
agent_names = {a["name"] for a in agents}
|
|
return result if result in agent_names else None
|