Files
hotels/llm_client.py

257 lines
8.4 KiB
Python
Raw Permalink Normal View History

"""
Универсальный клиент для работы с разными LLM провайдерами
"""
import requests
import os
from typing import List, Dict, Optional
from llm_config import (
ACTIVE_PROVIDER, CURRENT_CONFIG, CHAT_MODEL,
TEMPERATURE, MAX_TOKENS, OPENAI_CONFIG, OPENROUTER_CONFIG, OLLAMA_CONFIG
)
class LLMClient:
"""Универсальный клиент для OpenAI/OpenRouter/Ollama"""
def __init__(self):
# Читаем настройки из переменных окружения или используем дефолтные
self.provider = os.environ.get('ACTIVE_PROVIDER', ACTIVE_PROVIDER)
self.model = os.environ.get('LLM_MODEL', CHAT_MODEL)
self.temperature = TEMPERATURE
self.max_tokens = MAX_TOKENS
# Определяем конфиг на основе провайдера
if self.provider == 'openai':
self.config = OPENAI_CONFIG
elif self.provider == 'openrouter':
self.config = OPENROUTER_CONFIG
elif self.provider == 'ollama':
self.config = OLLAMA_CONFIG
else:
self.config = CURRENT_CONFIG
@property
def provider_config(self):
"""Получить конфиг текущего провайдера"""
return {
"provider": self.provider,
"model": self.model,
"api_base": self.config.get('api_base', ''),
"has_key": bool(self.config.get('api_key'))
}
def chat_completion(
self,
messages: List[Dict[str, str]],
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
model: Optional[str] = None
) -> Dict:
"""
Универсальный метод для chat completion
Args:
messages: Список сообщений [{'role': 'user', 'content': '...'}]
temperature: Креативность (0.0-1.0)
max_tokens: Макс токенов в ответе
model: Модель (опционально)
Returns:
{'text': str, 'usage': dict}
"""
temperature = temperature or self.temperature
max_tokens = max_tokens or self.max_tokens
model = model or self.model
if self.provider == 'openai' or self.provider == 'openrouter':
return self._openai_style_request(messages, temperature, max_tokens, model)
elif self.provider == 'ollama':
return self._ollama_request(messages, temperature, max_tokens, model)
else:
raise ValueError(f"Unknown provider: {self.provider}")
def _openai_style_request(
self,
messages: List[Dict],
temperature: float,
max_tokens: int,
model: str
) -> Dict:
"""Запрос к OpenAI или OpenRouter"""
url = f"{self.config['api_base']}/chat/completions"
headers = {
"Authorization": f"Bearer {self.config['api_key']}",
"Content-Type": "application/json"
}
# OpenRouter требует дополнительные заголовки
if self.provider == 'openrouter':
headers["HTTP-Referer"] = "https://hotel-audit.ru"
headers["X-Title"] = "Hotel Audit System"
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens
}
# Настройка прокси
proxies = None
if self.config.get('proxy'):
proxies = {
'http': self.config['proxy'],
'https': self.config['proxy']
}
try:
response = requests.post(
url,
headers=headers,
json=payload,
proxies=proxies,
timeout=60
)
if response.status_code == 200:
data = response.json()
return {
'text': data['choices'][0]['message']['content'],
'usage': data.get('usage', {}),
'model': data.get('model', model)
}
else:
error_msg = f"API Error {response.status_code}: {response.text}"
return {
'text': f"Ошибка API: {response.status_code}",
'error': error_msg
}
except Exception as e:
return {
'text': f"Ошибка соединения: {str(e)}",
'error': str(e)
}
def _ollama_request(
self,
messages: List[Dict],
temperature: float,
max_tokens: int,
model: str
) -> Dict:
"""Запрос к локальной Ollama"""
url = f"{self.config['api_base']}/api/chat"
# Конвертируем формат сообщений
payload = {
"model": model,
"messages": messages,
"stream": False,
"options": {
"temperature": temperature,
"num_predict": max_tokens
}
}
try:
response = requests.post(url, json=payload, timeout=120)
if response.status_code == 200:
data = response.json()
return {
'text': data['message']['content'],
'usage': {
'prompt_tokens': data.get('prompt_eval_count', 0),
'completion_tokens': data.get('eval_count', 0)
},
'model': model
}
else:
return {
'text': f"Ошибка Ollama: {response.status_code}",
'error': response.text
}
except requests.exceptions.ConnectionError:
return {
'text': "Ошибка: Ollama не запущена. Запустите: ollama serve",
'error': 'Connection refused'
}
except Exception as e:
return {
'text': f"Ошибка Ollama: {str(e)}",
'error': str(e)
}
def simple_chat(self, prompt: str, system: Optional[str] = None) -> str:
"""
Простой метод для быстрого чата
Args:
prompt: Вопрос пользователя
system: Системный промпт (опционально)
Returns:
Ответ модели (только текст)
"""
messages = []
if system:
messages.append({'role': 'system', 'content': system})
messages.append({'role': 'user', 'content': prompt})
result = self.chat_completion(messages)
return result['text']
def get_info(self) -> Dict:
"""Информация о текущей конфигурации"""
return {
'provider': self.provider,
'model': self.model,
'temperature': self.temperature,
'max_tokens': self.max_tokens,
'api_base': self.config['api_base']
}
# Глобальный экземпляр (singleton pattern)
llm = LLMClient()
# ==================== ТЕСТЫ ====================
if __name__ == "__main__":
print("=" * 70)
print("🤖 ТЕСТ LLM КЛИЕНТА")
print("=" * 70)
info = llm.get_info()
print(f"\n📊 Конфигурация:")
print(f" Провайдер: {info['provider']}")
print(f" Модель: {info['model']}")
print(f" Temperature: {info['temperature']}")
print(f" Max tokens: {info['max_tokens']}")
print(f" API: {info['api_base']}")
print(f"\n💬 Тестовый запрос...")
response = llm.simple_chat(
prompt="Сколько будет 2+2? Ответь одним числом.",
system="Ты математический ассистент."
)
print(f" Ответ: {response}")
print("\n" + "=" * 70)
print("✅ Клиент работает!")
print("=" * 70)