""" 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()