Initial commit: VoIdeaAI - voice-first AI idea assistant
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user