llm-agent-backend/app/llm_client.py

470 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Этот модуль предоставляет инструменты для взаимодействия с различными LLM,
включая OpenAI (Gemini) и MistralAI, обеспечивая унифицированный интерфейс.
"""
from typing import Dict, Any, List
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from openai import OpenAI
from mistralai import Mistral
# --- Конфигурация LLM ---
# Пустой словарь для локальных моделей (Ollama)
LOCAL_MODELS: Dict[str, Any] = {}
# Конфигурация внешних моделей
MODELS: Dict[str, Dict[str, Any]] = {
"gemini-2.0-flash": {
"name": "gemini-2.0-flash",
"provider": "openai", # Изменено на "openai"
"model_name": "gemini-2.0-flash", # Добавлено имя модели для LangChain
"apiBase": "https://generativelanguage.googleapis.com/v1beta",
"apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
"stream": True,
"capabilities": ["vision"],
},
"gemini-2.5-flash": {
"name": "gemini-2.5-flash",
"provider": "openai", # Изменено на "openai"
"model_name": "gemini-2.5-flash", # Добавлено имя модели для LangChain
"apiBase": "https://generativelanguage.googleapis.com/v1beta",
"apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
"stream": True,
"capabilities": ["vision"],
},
"gemini-2.0-flash-r": {
"name": "gemini-2.0-flash-r",
"provider": "openai", # Изменено на "openai"
"model_name": "gemini-2.0-flash", # Добавлено имя модели для LangChain
"apiBase":
"https://render-service-gsu7.onrender.com/g/v1beta", # "generativelanguage.googleapis.com", # "https://render-service-gsu7.onrender.com/g/v1beta",
"apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
"stream": True,
"capabilities": ["vision"],
},
"gemini-2.0-flash-lite": {
"name": "google/gemini-2.0-flash-lite-001",
"provider": "openai",
"model_name": "google/gemini-2.0-flash-lite-001",
"apiBase": "https://openrouter.ai/api/v1", # Добавлено /api/v1
"apiKey":
"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",
"stream": True,
"capabilities": ["vision", "reasoning"],
"model_kwargs": {
"include_reasoning": True
}
},
"gemini-2.5-flash-r": {
"name": "gemini-2.5-flash-r",
"provider": "openai", # Изменено на "openai"
"model_name": "gemini-2.5-flash", # Добавлено имя модели для LangChain
"apiBase":
"https://render-service-gsu7.onrender.com/g/v1beta", # "generativelanguage.googleapis.com", # "https://render-service-gsu7.onrender.com/g/v1beta",
"apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
"stream": True,
"capabilities": ["vision"],
},
"gemini-2.5-flash-openrouter": {
"name": "google/gemini-2.5-flash",
"provider": "openai",
"model_name": "google/gemini-2.5-flash",
"apiBase": "https://openrouter.ai/api/v1", # Добавлено /api/v1
"apiKey":
"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",
"stream": True,
"capabilities": ["vision", "reasoning"],
"model_kwargs": {
"include_reasoning": True
}
},
"gemini-2.5-flash-bh": {
"name": "gemini-2.5-flash",
"provider": "openai", # Изменено на "openai"
"model_name": "gemini-2.5-flash", # Добавлено имя модели для LangChain
"apiBase": "https://bothub.chat/api/v2/openai/v1",
"apiKey":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImQ3MTMwYTk0LTJiNmMtNDgzZi04Y2NiLWM5MjgwNzlmNDFlZSIsImlzRGV2ZWxvcGVyIjp0cnVlLCJpYXQiOjE3NTQ0NzYwMDcsImV4cCI6MjA3MDA1MjAwN30._64_DXNZatdd4-4VbXp8dOXdDhIlNWMQt9A_Nn6mu3g",
"stream": True,
"capabilities": ["vision"],
},
"gemini-3-flash-preview-bh": {
"name": "gemini-3-flash-preview",
"provider": "openai", # Изменено на "openai"
"model_name":
"gemini-3-flash-preview", # Добавлено имя модели для LangChain
"apiBase": "https://bothub.chat/api/v2/openai/v1",
"apiKey":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImQ3MTMwYTk0LTJiNmMtNDgzZi04Y2NiLWM5MjgwNzlmNDFlZSIsImlzRGV2ZWxvcGVyIjp0cnVlLCJpYXQiOjE3NTQ0NzYwMDcsImV4cCI6MjA3MDA1MjAwN30._64_DXNZatdd4-4VbXp8dOXdDhIlNWMQt9A_Nn6mu3g",
"stream": True,
"capabilities": ["vision"],
},
"gemini-3.0-flash-openrouter": {
"name": "google/gemini-3-flash-preview",
"provider": "openai",
"model_name": "google/gemini-3-flash-preview",
"apiBase": "https://openrouter.ai/api/v1", # Добавлено /api/v1
"apiKey":
"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",
"stream": True,
"capabilities": ["vision", "reasoning"],
"model_kwargs": {
"include_reasoning": True
}
},
"gemini-3.1-pro-openrouter": {
"name": "google/gemini-3.1-pro-preview",
"provider": "openai",
"model_name": "google/gemini-3.1-pro-preview",
"apiBase": "https://openrouter.ai/api/v1", # Добавлено /api/v1
"apiKey":
"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",
"stream": True,
"capabilities": ["vision", "reasoning"],
"model_kwargs": {
"include_reasoning": True
}
},
"gemini-3.1-pro-bh": {
"name": "gemini-3.1-pro-preview",
"provider": "openai", # Изменено на "openai"
"model_name":
"gemini-3.1-pro-preview", # Добавлено имя модели для LangChain
"apiBase": "https://bothub.chat/api/v2/openai/v1",
"apiKey":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImQ3MTMwYTk0LTJiNmMtNDgzZi04Y2NiLWM5MjgwNzlmNDFlZSIsImlzRGV2ZWxvcGVyIjp0cnVlLCJpYXQiOjE3NTQ0NzYwMDcsImV4cCI6MjA3MDA1MjAwN30._64_DXNZatdd4-4VbXp8dOXdDhIlNWMQt9A_Nn6mu3g",
"stream": True,
"capabilities": ["vision"],
},
"claude-4.5-sonnet-bh": {
"name": "claude-4.5-sonnet-bh",
"provider": "openai", # Изменено на "openai"
"model_name":
"claude-sonnet-4.5", # Добавлено имя модели для LangChain
"apiBase": "https://bothub.chat/api/v2/openai/v1",
"apiKey":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImQ3MTMwYTk0LTJiNmMtNDgzZi04Y2NiLWM5MjgwNzlmNDFlZSIsImlzRGV2ZWxvcGVyIjp0cnVlLCJpYXQiOjE3NTQ0NzYwMDcsImV4cCI6MjA3MDA1MjAwN30._64_DXNZatdd4-4VbXp8dOXdDhIlNWMQt9A_Nn6mu3g",
"stream": True,
"capabilities": ["vision"],
},
"mistral-small-latest": {
"name": "mistral-small-latest",
"provider": "mistralai",
"model_name":
"mistral-small-latest", # Добавлено имя модели для LangChain
"apiBase": "https://api.mistral.ai",
"apiKey": "Q0m29fvxBY0Cfdj4sjHaKqccy1NjonLW",
"stream": True,
"capabilities": ["vision"],
},
"mistral-small-latest-r": {
"name": "mistral-small-latest",
"provider": "mistralai",
"model_name":
"mistral-small-latest", # Добавлено имя модели для LangChain
"apiBase": "https://render-service-gsu7.onrender.com/m",
"apiKey": "Q0m29fvxBY0Cfdj4sjHaKqccy1NjonLW",
"stream": True,
"capabilities": ["vision"],
},
"mistral-small-latest-error": {
"name": "mistral-small-latest",
"provider": "mistralai",
"model_name":
"mistral-small-latest-error", # Добавлено имя модели для LangChain
"apiBase": "https://render-service-gsu7.onrender.com/m",
"apiKey": "Q0m29fvxBY0Cfdj4sjHaKqccy1NjonLW",
"stream": True,
"capabilities": ["vision"],
},
}
class CustomLLM:
"""
Класс-обертка для взаимодействия с различными LLM.
"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.name = config["name"]
self.provider = config["provider"]
try:
if self.provider == "openai":
# Инициализация Gemini через OpenAI API
self.client = OpenAI(api_key=config["apiKey"],
base_url=config["apiBase"])
self.model_name = config["model_name"]
self.stream = config["stream"]
elif self.provider == "mistralai":
# Для Mistral используем прямые HTTP-запросы
self.client = Mistral(api_key=config["apiKey"],
server_url=config["apiBase"])
self.model_name = config["model_name"]
self.stream = config["stream"]
# self.capabilities = config.get("capabilities", [])
else:
raise ValueError(f"Неизвестный провайдер LLM: {self.provider}")
except Exception as e:
print(f"Ошибка при вызове OpenAI API: {e}")
raise
def invoke(self, messages: List[Any]) -> str:
"""
Отправляет запрос к LLM и возвращает ответ.
Messages могут быть строкой или списком объектов Langchain Message.
"""
if self.provider == "openai":
# OpenAI ожидает список сообщений в определенном формате
openai_messages = []
if isinstance(messages, str):
openai_messages.append({"role": "user", "content": messages})
elif isinstance(messages, list):
for msg in messages:
if isinstance(msg, HumanMessage):
openai_messages.append({
"role": "user",
"content": msg.content
})
elif isinstance(msg, SystemMessage):
openai_messages.append({
"role": "system",
"content": msg.content
})
elif isinstance(
msg, AIMessage): # Добавляем обработку AIMessage
openai_messages.append({
"role": "assistant",
"content": msg.content
})
else: # Предполагаем, что это другие типы сообщений Langchain или словари
openai_messages.append({
"role":
msg.type if hasattr(msg, 'type') else 'user',
"content":
msg.content
})
try:
response = self.client.chat.completions.create(
model=self.model_name,
messages=openai_messages,
stream=self.stream,
)
if self.stream:
# Handle streaming responses
collected_chunks = []
collected_messages = []
for chunk in response:
collected_chunks.append(chunk)
chunk_message = chunk.choices[0].delta.content
if chunk_message is not None:
collected_messages.append(chunk_message)
full_reply_content = ''.join(collected_messages)
return full_reply_content
else:
return response.choices[0].message.content
except Exception as e:
print(f"Ошибка при вызове OpenAI API: {e}")
raise
elif self.provider == "mistralai":
mistralai_messages = []
if isinstance(messages, str):
mistralai_messages.append({
"role": "user",
"content": messages
})
elif isinstance(messages, list):
for msg in messages:
if isinstance(msg, HumanMessage):
mistralai_messages.append({
"role": "user",
"content": msg.content
})
elif isinstance(msg, SystemMessage):
mistralai_messages.append({
"role": "system",
"content": msg.content
})
elif isinstance(
msg, AIMessage): # Добавляем обработку AIMessage
mistralai_messages.append({
"role": "assistant",
"content": msg.content
})
else: # Предполагаем, что это другие типы сообщений Langchain или словари
mistralai_messages.append({
"role":
msg.type if hasattr(msg, 'type') else 'user',
"content":
msg.content
})
try:
response = self.client.chat.stream(model=self.model_name,
messages=mistralai_messages)
if self.stream:
collected_messages = []
for chunk in response:
delta = chunk.data.choices[0].delta
chunk_message = getattr(
delta, "content",
None) # или delta.content, если всегда есть
if chunk_message is not None:
collected_messages.append(chunk_message)
if getattr(delta, "finish_reason", None) is not None:
break
full_reply_content = "".join(collected_messages)
return full_reply_content
else:
return response.choices[0].message.content
except Exception as e:
print(f"Ошибка при вызове OpenAI API: {e}")
raise
return ""
def stream2(self, messages: List[Any]):
"""
Генератор для стриминга ответа от LLM.
Возвращает чанки контента по мере их получения.
"""
if self.provider == "openai":
openai_messages = self._prepare_openai_messages(messages)
try:
response = self.client.chat.completions.create(
model=self.model_name,
messages=openai_messages,
stream=True,
)
for chunk in response:
chunk_message = chunk.choices[0].delta.content
if chunk_message is not None:
yield chunk_message
except GeneratorExit:
# НОВОЕ: Обработка прерывания генератора
print(f"⚠️ Стрим OpenAI прерван для модели {self.model_name}")
raise
except Exception as e:
print(f"Ошибка при стриминге OpenAI API: {e}")
raise
elif self.provider == "mistralai":
mistralai_messages = self._prepare_mistralai_messages(messages)
try:
response = self.client.chat.stream(model=self.model_name,
messages=mistralai_messages)
for chunk in response:
delta = chunk.data.choices[0].delta
chunk_message = getattr(delta, "content", None)
if chunk_message is not None:
yield chunk_message
if getattr(delta, "finish_reason", None) is not None:
break
except GeneratorExit:
# НОВОЕ: Обработка прерывания генератора
print(f"⚠️ Стрим Mistral прерван для модели {self.model_name}")
raise
except Exception as e:
print(f"Ошибка при стриминге Mistral API: {e}")
raise
def _prepare_openai_messages(self,
messages: List[Any]) -> List[Dict[str, str]]:
"""Подготавливает сообщения в формате OpenAI."""
openai_messages = []
if isinstance(messages, str):
openai_messages.append({"role": "user", "content": messages})
elif isinstance(messages, list):
for msg in messages:
if isinstance(msg, HumanMessage):
openai_messages.append({
"role": "user",
"content": msg.content
})
elif isinstance(msg, SystemMessage):
openai_messages.append({
"role": "system",
"content": msg.content
})
elif isinstance(msg, AIMessage):
openai_messages.append({
"role": "assistant",
"content": msg.content
})
else:
openai_messages.append({
"role":
msg.type if hasattr(msg, 'type') else 'user',
"content":
msg.content
})
return openai_messages
def _prepare_mistralai_messages(
self, messages: List[Any]) -> List[Dict[str, str]]:
"""Подготавливает сообщения в формате Mistral AI."""
mistralai_messages = []
if isinstance(messages, str):
mistralai_messages.append({"role": "user", "content": messages})
elif isinstance(messages, list):
for msg in messages:
if isinstance(msg, HumanMessage):
mistralai_messages.append({
"role": "user",
"content": msg.content
})
elif isinstance(msg, SystemMessage):
mistralai_messages.append({
"role": "system",
"content": msg.content
})
elif isinstance(msg, AIMessage):
mistralai_messages.append({
"role": "assistant",
"content": msg.content
})
else:
mistralai_messages.append({
"role":
msg.type if hasattr(msg, 'type') else 'user',
"content":
msg.content
})
return mistralai_messages
def get_llm(name: str) -> CustomLLM:
"""
Возвращает экземпляр CustomLLM для заданной модели.
"""
config = MODELS.get(name)
if not config:
raise ValueError(f"Модель '{name}' не сконфигурирована.")
return CustomLLM(config)
def get_available_models():
"""Возвращает список доступных моделей."""
return list(MODELS.keys())