436 lines
17 KiB
Python
436 lines
17 KiB
Python
"""
|
||
GigaChat API Tool для Telegram CLI Bot
|
||
|
||
Инструмент для работы с GigaChat API (Сбер).
|
||
Поддерживает генерацию текста, чат-сессии и различные модели.
|
||
|
||
Документация: https://developers.sber.ru/docs/ru/gigachat
|
||
"""
|
||
|
||
import os
|
||
import base64
|
||
import httpx
|
||
import asyncio
|
||
import uuid
|
||
import logging
|
||
from typing import Optional, List, Dict, Any
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime, timedelta
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@dataclass
|
||
class GigaChatMessage:
|
||
"""Сообщение для чата с GigaChat"""
|
||
role: str # 'user', 'assistant', 'system'
|
||
content: str
|
||
|
||
|
||
@dataclass
|
||
class GigaChatConfig:
|
||
"""Конфигурация подключения к GigaChat API"""
|
||
client_id: str
|
||
client_secret: str
|
||
scope: str = "GIGACHAT_API_PERS"
|
||
auth_url: str = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth"
|
||
model: str = "GigaChat"
|
||
api_url: str = "https://gigachat.devices.sberbank.ru/api/v1"
|
||
timeout: int = 60
|
||
|
||
|
||
class GigaChatTool:
|
||
"""
|
||
Инструмент для работы с GigaChat API
|
||
|
||
Пример использования:
|
||
config = GigaChatConfig(
|
||
client_id=os.getenv("GIGACHAT_CLIENT_ID"),
|
||
client_secret=os.getenv("GIGACHAT_CLIENT_SECRET"),
|
||
)
|
||
tool = GigaChatTool(config)
|
||
|
||
# Простой запрос
|
||
response = await tool.chat("Привет, как дела?")
|
||
|
||
# Чат с историей
|
||
messages = [
|
||
GigaChatMessage(role="system", content="Ты полезный ассистент."),
|
||
GigaChatMessage(role="user", content="Расскажи про Python"),
|
||
]
|
||
response = await tool.chat(messages=messages)
|
||
"""
|
||
|
||
def __init__(self, config: Optional[GigaChatConfig] = None):
|
||
self.config = config or self._load_config_from_env()
|
||
self._access_token: Optional[str] = None
|
||
self._token_expires: Optional[datetime] = None
|
||
self._chat_history: List[GigaChatMessage] = []
|
||
|
||
def _load_config_from_env(self) -> GigaChatConfig:
|
||
"""Загрузка конфигурации из переменных окружения"""
|
||
return GigaChatConfig(
|
||
client_id=os.getenv("GIGACHAT_CLIENT_ID", ""),
|
||
client_secret=os.getenv("GIGACHAT_CLIENT_SECRET", ""),
|
||
scope=os.getenv("GIGACHAT_SCOPE", "GIGACHAT_API_PERS"),
|
||
auth_url=os.getenv("GIGACHAT_AUTH_URL", "https://ngw.devices.sberbank.ru:9443/api/v2/oauth"),
|
||
model=os.getenv("GIGACHAT_MODEL", "GigaChat-Pro"),
|
||
)
|
||
|
||
def _get_auth_headers(self) -> Dict[str, str]:
|
||
"""Получение заголовков для авторизации"""
|
||
# GigaChat требует RqUID (UUID) и Content-Type для OAuth
|
||
return {
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
"RqUID": str(uuid.uuid4()),
|
||
}
|
||
|
||
async def _get_access_token(self) -> str:
|
||
"""Получение access токена для API"""
|
||
# Проверяем кэш токена
|
||
if self._access_token and self._token_expires:
|
||
if datetime.now() < self._token_expires - timedelta(minutes=5):
|
||
return self._access_token
|
||
|
||
# Запрашиваем новый токен с использованием Basic Auth
|
||
credentials = f"{self.config.client_id}:{self.config.client_secret}"
|
||
encoded_credentials = base64.b64encode(credentials.encode()).decode()
|
||
|
||
# GigaChat использует самоподписанные сертификаты - отключаем верификацию
|
||
async with httpx.AsyncClient(verify=False) as client:
|
||
response = await client.post(
|
||
self.config.auth_url,
|
||
headers={
|
||
"Authorization": f"Basic {encoded_credentials}",
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
"RqUID": str(uuid.uuid4()),
|
||
},
|
||
data={"scope": self.config.scope},
|
||
timeout=30,
|
||
)
|
||
|
||
# Логируем для отладки
|
||
logger.debug(f"GigaChat auth status: {response.status_code}")
|
||
logger.debug(f"GigaChat auth response: {response.text[:200]}")
|
||
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
|
||
self._access_token = data["access_token"]
|
||
# Токен действителен 30 минут, кэшируем на 25 минут
|
||
self._token_expires = datetime.now() + timedelta(minutes=25)
|
||
|
||
# Логируем начало токена для проверки (первые 50 символов)
|
||
logger.info(f"GigaChat токен получен: {self._access_token[:50]}...")
|
||
|
||
return self._access_token
|
||
|
||
async def chat(
|
||
self,
|
||
messages: Optional[List[GigaChatMessage]] = None,
|
||
model: Optional[str] = None,
|
||
temperature: float = 0.7,
|
||
max_tokens: int = 2000,
|
||
top_p: float = 0.1,
|
||
repetition_penalty: float = 1.0,
|
||
use_history: bool = True,
|
||
user_id: Optional[str] = None,
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
Отправка запроса к GigaChat API
|
||
|
||
Args:
|
||
messages: Список сообщений (если None, используется история чата)
|
||
model: Модель для генерации (если None, используется модель из конфига)
|
||
temperature: Температура генерации (0.0 - 2.0)
|
||
max_tokens: Максимальное количество токенов в ответе
|
||
top_p: Параметр top-p sampling
|
||
repetition_penalty: Штраф за повторения
|
||
use_history: Использовать ли историю чата
|
||
user_id: ID пользователя для заголовка X-User-Id
|
||
|
||
Returns:
|
||
Dict с ответом API:
|
||
- content: Текст ответа
|
||
- model: Использованная модель
|
||
- usage: Статистика использования токенов
|
||
- finish_reason: Причина завершения
|
||
"""
|
||
token = await self._get_access_token()
|
||
|
||
# Формируем сообщения
|
||
if messages is None:
|
||
if use_history:
|
||
messages = self._chat_history.copy()
|
||
else:
|
||
messages = []
|
||
elif use_history:
|
||
# Добавляем новые сообщения к истории
|
||
self._chat_history.extend(messages)
|
||
messages = self._chat_history.copy()
|
||
|
||
# Преобразуем сообщения в формат API
|
||
api_messages = [
|
||
{"role": msg.role, "content": msg.content}
|
||
for msg in messages
|
||
]
|
||
|
||
payload = {
|
||
"model": model or self.config.model,
|
||
"messages": api_messages,
|
||
"temperature": temperature,
|
||
"max_tokens": max_tokens,
|
||
"top_p": top_p,
|
||
"repetition_penalty": repetition_penalty,
|
||
}
|
||
|
||
headers = {
|
||
"Authorization": f"Bearer {token}",
|
||
"Content-Type": "application/json",
|
||
"X-User-Id": str(user_id) if user_id else "telegram-bot",
|
||
}
|
||
|
||
# Логируем запрос для отладки
|
||
logger.debug(f"GigaChat API URL: {self.config.api_url}/chat/completions")
|
||
logger.debug(f"GigaChat headers: {headers}")
|
||
logger.debug(f"GigaChat payload: model={model or self.config.model}, messages={len(api_messages)}, max_tokens={max_tokens}")
|
||
|
||
# GigaChat использует самоподписанные сертификаты - отключаем верификацию
|
||
async with httpx.AsyncClient(verify=False) as client:
|
||
try:
|
||
response = await client.post(
|
||
f"{self.config.api_url}/chat/completions",
|
||
headers=headers,
|
||
json=payload,
|
||
timeout=self.config.timeout,
|
||
)
|
||
|
||
# Логируем для отладки
|
||
logger.debug(f"GigaChat chat status: {response.status_code}")
|
||
logger.debug(f"GigaChat response headers: {dict(response.headers)}")
|
||
if response.status_code != 200:
|
||
logger.error(f"GigaChat error response: {response.text[:1000]}")
|
||
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
except httpx.HTTPStatusError as e:
|
||
logger.error(f"GigaChat HTTP error: {e}")
|
||
logger.error(f"Response: {e.response.text[:500]}")
|
||
return {
|
||
"content": "",
|
||
"error": f"HTTP {e.response.status_code}: {e.response.text[:200]}",
|
||
}
|
||
except httpx.HTTPError as e:
|
||
logger.error(f"GigaChat request error: {e}")
|
||
return {
|
||
"content": "",
|
||
"error": f"Request error: {str(e)}",
|
||
}
|
||
|
||
# Добавляем ответ ассистента в историю
|
||
if use_history and data.get("choices"):
|
||
assistant_message = data["choices"][0]["message"]
|
||
self._chat_history.append(GigaChatMessage(
|
||
role=assistant_message["role"],
|
||
content=assistant_message["content"],
|
||
))
|
||
|
||
return {
|
||
"content": data["choices"][0]["message"]["content"] if data.get("choices") else "",
|
||
"model": data.get("model", self.config.model),
|
||
"usage": data.get("usage", {}),
|
||
"finish_reason": data["choices"][0]["finish_reason"] if data.get("choices") else "",
|
||
}
|
||
|
||
def clear_history(self):
|
||
"""Очистка истории чата"""
|
||
self._chat_history = []
|
||
|
||
def get_history(self) -> List[GigaChatMessage]:
|
||
"""Получение истории чата"""
|
||
return self._chat_history.copy()
|
||
|
||
def set_system_prompt(self, prompt: str):
|
||
"""Установка системного промпта (добавляется в начало истории)"""
|
||
# Удаляем старый системный промпт если есть
|
||
self._chat_history = [
|
||
msg for msg in self._chat_history if msg.role != "system"
|
||
]
|
||
# Добавляем новый в начало
|
||
self._chat_history.insert(0, GigaChatMessage(role="system", content=prompt))
|
||
|
||
async def generate_image(
|
||
self,
|
||
prompt: str,
|
||
model: str = " Kandinsky-2",
|
||
size: str = "1024x1024",
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
Генерация изображений через GigaChat (Kandinsky)
|
||
|
||
Args:
|
||
prompt: Текстовое описание изображения
|
||
model: Модель для генерации
|
||
size: Размер изображения
|
||
|
||
Returns:
|
||
Dict с результатом генерации
|
||
"""
|
||
token = await self._get_access_token()
|
||
|
||
payload = {
|
||
"model": model,
|
||
"prompt": prompt,
|
||
"size": size,
|
||
}
|
||
|
||
headers = {
|
||
"Authorization": f"Bearer {token}",
|
||
"Content-Type": "application/json",
|
||
}
|
||
|
||
# GigaChat использует самоподписанные сертификаты - отключаем верификацию
|
||
async with httpx.AsyncClient(verify=False) as client:
|
||
# Запуск генерации
|
||
response = await client.post(
|
||
f"{self.config.api_url}/images/generations",
|
||
headers=headers,
|
||
json=payload,
|
||
timeout=self.config.timeout,
|
||
)
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
|
||
return data
|
||
|
||
async def get_models(self) -> List[str]:
|
||
"""Получение списка доступных моделей"""
|
||
token = await self._get_access_token()
|
||
|
||
headers = {
|
||
"Authorization": f"Bearer {token}",
|
||
}
|
||
|
||
# GigaChat использует самоподписанные сертификаты - отключаем верификацию
|
||
async with httpx.AsyncClient(verify=False) as client:
|
||
response = await client.get(
|
||
f"{self.config.api_url}/models",
|
||
headers=headers,
|
||
timeout=30,
|
||
)
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
|
||
return [model["id"] for model in data.get("data", [])]
|
||
|
||
|
||
# Утилита для создания инструмента в формате бота
|
||
def create_gigachat_tool():
|
||
"""
|
||
Создает экземпляр GigaChatTool с конфигурацией из окружения
|
||
|
||
Returns:
|
||
GigaChatTool или None если конфигурация не задана
|
||
"""
|
||
if not os.getenv("GIGACHAT_CLIENT_ID") or not os.getenv("GIGACHAT_CLIENT_SECRET"):
|
||
return None
|
||
|
||
return GigaChatTool()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# Пример использования
|
||
async def main():
|
||
tool = create_gigachat_tool()
|
||
if not tool:
|
||
print("GigaChat не настроен. Проверьте переменные окружения.")
|
||
return
|
||
|
||
# Простой запрос
|
||
response = await tool.chat("Привет! Расскажи кратко про себя.")
|
||
print(f"Ответ: {response['content']}")
|
||
print(f"Модель: {response['model']}")
|
||
print(f"Токены: {response['usage']}")
|
||
|
||
asyncio.run(main())
|
||
|
||
# ===========================================
|
||
# Интеграция с реестром инструментов бота
|
||
# ===========================================
|
||
|
||
from bot.tools import BaseTool, ToolResult, register_tool
|
||
|
||
|
||
@register_tool
|
||
class GigaChatCapability(BaseTool):
|
||
"""
|
||
Capability-обёртка для GigaChat API.
|
||
|
||
Позволяет использовать GigaChat через реестр инструментов бота.
|
||
"""
|
||
|
||
name = "gigachat"
|
||
description = "Генерация ответов AI через GigaChat API (Сбер). Альтернатива Qwen Code."
|
||
category = "ai"
|
||
|
||
def __init__(self):
|
||
self._provider = None
|
||
|
||
def _get_provider(self):
|
||
"""Ленивая инициализация провайдера"""
|
||
if self._provider is None:
|
||
from qwen_integration import GigaChatProvider
|
||
self._provider = GigaChatProvider()
|
||
return self._provider
|
||
|
||
async def execute(
|
||
self,
|
||
prompt: str,
|
||
system_prompt: Optional[str] = None,
|
||
temperature: float = 0.7,
|
||
max_tokens: int = 2000,
|
||
**kwargs
|
||
) -> ToolResult:
|
||
"""
|
||
Выполнить запрос к GigaChat API.
|
||
|
||
Args:
|
||
prompt: Запрос пользователя
|
||
system_prompt: Системный промпт (роль ассистента)
|
||
temperature: Температура генерации (0.0-2.0)
|
||
max_tokens: Максимум токенов в ответе
|
||
"""
|
||
provider = self._get_provider()
|
||
|
||
if not provider.is_available():
|
||
return ToolResult(
|
||
success=False,
|
||
error=provider.get_error() or "GigaChat не доступен",
|
||
)
|
||
|
||
result = await provider.chat(
|
||
prompt=prompt,
|
||
system_prompt=system_prompt,
|
||
temperature=temperature,
|
||
max_tokens=max_tokens,
|
||
)
|
||
|
||
if result.get("success"):
|
||
return ToolResult(
|
||
success=True,
|
||
data={
|
||
"content": result.get("content", ""),
|
||
"model": result.get("model", "GigaChat-Pro"),
|
||
"usage": result.get("usage", {}),
|
||
},
|
||
metadata={
|
||
"model": result.get("model"),
|
||
"tokens": result.get("usage"),
|
||
}
|
||
)
|
||
else:
|
||
return ToolResult(
|
||
success=False,
|
||
error=result.get("error", "Неизвестная ошибка GigaChat"),
|
||
)
|