124 lines
4.4 KiB
Python
124 lines
4.4 KiB
Python
"""
|
|
LLMService — обращение к языковой модели через OpenAI-совместимый API.
|
|
|
|
Поддерживает несколько профилей (разные модели/промпты) —
|
|
пользователь выбирает, к какому ЛЛМ отправить запрос.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, Optional
|
|
|
|
import requests
|
|
|
|
from config import LLM_PROFILES
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class LLMService:
|
|
"""
|
|
Сервис общения с языковой моделью.
|
|
|
|
Хранит набор профилей (model + system_prompt + температура + ...).
|
|
Метод chat() принимает id профиля и текст пользователя, возвращает ответ.
|
|
"""
|
|
|
|
def __init__(self, profiles: list = None):
|
|
"""
|
|
Args:
|
|
profiles: Список словарей с ключами:
|
|
id, name, description, url, key, model,
|
|
system_prompt, temperature, max_tokens.
|
|
По умолчанию — LLM_PROFILES из config.py.
|
|
"""
|
|
self._profiles: Dict[str, dict] = {}
|
|
|
|
for p in (profiles or LLM_PROFILES):
|
|
pid = p["id"]
|
|
self._profiles[pid] = p
|
|
log.info("LLM: зарегистрирован профиль '%s' (модель=%s)", pid, p["model"])
|
|
|
|
@property
|
|
def profile_list(self) -> list:
|
|
"""
|
|
Возвращает список профилей для фронтенда
|
|
(без ключей, только публичная информация).
|
|
"""
|
|
return [
|
|
{
|
|
"id": p["id"],
|
|
"name": p["name"],
|
|
"description": p["description"],
|
|
}
|
|
for p in self._profiles.values()
|
|
]
|
|
|
|
def get_profile(self, profile_id: str) -> Optional[dict]:
|
|
"""Возвращает полный профиль по id или None."""
|
|
return self._profiles.get(profile_id)
|
|
|
|
def chat(self, profile_id: str, user_text: str) -> str:
|
|
"""
|
|
Отправляет текст пользователя в указанную LLM и возвращает ответ.
|
|
|
|
Args:
|
|
profile_id: Идентификатор профиля (ddm, qwen, deepseek, ...).
|
|
user_text: Текст, который нужно отправить модели.
|
|
|
|
Returns:
|
|
Ответ модели (строка).
|
|
|
|
Raises:
|
|
ValueError: Если профиль не найден.
|
|
RuntimeError: При ошибке API.
|
|
"""
|
|
profile = self._profiles.get(profile_id)
|
|
if not profile:
|
|
available = ", ".join(self._profiles.keys())
|
|
raise ValueError(
|
|
f"Неизвестный LLM-профиль '{profile_id}'. Доступны: {available}"
|
|
)
|
|
|
|
if not profile["key"]:
|
|
raise RuntimeError(f"API-ключ для профиля '{profile_id}' не задан")
|
|
|
|
log.info(
|
|
"LLM [%s]: отправка запроса (модель=%s, %d символов)",
|
|
profile_id,
|
|
profile["model"],
|
|
len(user_text),
|
|
)
|
|
|
|
resp = requests.post(
|
|
profile["url"],
|
|
headers={
|
|
"Authorization": f"Bearer {profile['key']}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
json={
|
|
"model": profile["model"],
|
|
"messages": [
|
|
{"role": "system", "content": profile["system_prompt"]},
|
|
{"role": "user", "content": user_text},
|
|
],
|
|
"temperature": profile["temperature"],
|
|
"max_tokens": profile["max_tokens"],
|
|
},
|
|
timeout=60,
|
|
)
|
|
|
|
if resp.status_code != 200:
|
|
log.error("LLM [%s] API error %d: %s", profile_id, resp.status_code, resp.text)
|
|
raise RuntimeError(
|
|
f"Ошибка LLM ({resp.status_code}): {resp.text[:200]}"
|
|
)
|
|
|
|
answer = resp.json()["choices"][0]["message"]["content"].strip()
|
|
|
|
log.info("LLM [%s]: получен ответ (%d символов)", profile_id, len(answer))
|
|
return answer
|
|
|
|
|
|
# Глобальный экземпляр сервиса
|
|
llm_service = LLMService()
|