Files
voidea/app/services/llm_service.py
T

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