470 lines
20 KiB
Python
470 lines
20 KiB
Python
"""
|
||
Этот модуль предоставляет инструменты для взаимодействия с различными 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())
|