#!/usr/bin/env python3 """ AI Provider Manager - управление переключением между AI-провайдерами. Поддерживаемые провайдеры: - qwen: Qwen Code CLI (основной) - gigachat: GigaChat API (Сбер) Использует единый интерфейс BaseAIProvider для всех провайдеров. """ import logging from typing import Optional, Dict, Any, Callable, List from dataclasses import dataclass from enum import Enum from bot.base_ai_provider import BaseAIProvider, ProviderResponse logger = logging.getLogger(__name__) class AIProvider(Enum): """Доступные AI-провайдеры.""" QWEN = "qwen" GIGACHAT = "gigachat" OPENCODE = "opencode" @dataclass class ProviderInfo: """Информация о провайдере.""" id: str name: str description: str available: bool is_active: bool class AIProviderManager: """ Менеджер управления AI-провайдерами. Позволяет переключаться между провайдерами и выполнять запросы через активного провайдера с поддержкой инструментов. """ def __init__(self, qwen_manager=None): self._qwen_manager = qwen_manager self._provider_status: Dict[str, bool] = {} self._providers: Dict[str, BaseAIProvider] = {} self._tools_registry: Dict[str, Any] = {} # Инициализируем провайдеров self._init_providers() # Проверяем доступность провайдеров при инициализации self._check_provider_status() def _init_providers(self): """Инициализировать AI-провайдеров.""" # Qwen Code Provider if self._qwen_manager: from bot.providers.qwen_provider import QwenCodeProvider self._providers[AIProvider.QWEN.value] = QwenCodeProvider(self._qwen_manager) logger.info("Qwen Code Provider инициализирован") # GigaChat Provider - создаём новый экземпляр напрямую from bot.providers.gigachat_provider import GigaChatProvider self._providers[AIProvider.GIGACHAT.value] = GigaChatProvider() logger.info("GigaChat Provider инициализирован") # Opencode Provider from bot.providers.opencode_provider import OpencodeProvider self._providers[AIProvider.OPENCODE.value] = OpencodeProvider() logger.info("Opencode Provider инициализирован") def set_tools_registry(self, tools_registry: Dict[str, Any]): """Установить реестр инструментов для всех провайдеров.""" self._tools_registry = tools_registry def get_provider(self, provider_id: str) -> Optional[BaseAIProvider]: """Получить экземпляр провайдера.""" return self._providers.get(provider_id) def _check_provider_status(self): """Проверка доступности провайдеров.""" # Проверяем Qwen self._provider_status[AIProvider.QWEN.value] = True # Qwen всегда доступен # Проверяем GigaChat gigachat_provider = self._providers.get(AIProvider.GIGACHAT.value) if gigachat_provider: self._provider_status[AIProvider.GIGACHAT.value] = gigachat_provider.is_available() else: self._provider_status[AIProvider.GIGACHAT.value] = False # Проверяем Opencode opencode_provider = self._providers.get(AIProvider.OPENCODE.value) if opencode_provider: self._provider_status[AIProvider.OPENCODE.value] = opencode_provider.is_available() else: self._provider_status[AIProvider.OPENCODE.value] = False def get_available_providers(self) -> List[str]: """Получить список доступных провайдеров.""" return [ provider_id for provider_id, available in self._provider_status.items() if available ] def is_provider_available(self, provider_id: str) -> bool: """Проверить доступен ли провайдер.""" return self._provider_status.get(provider_id, False) def get_provider_info(self, provider_id: str, is_active: bool = False) -> ProviderInfo: """Получить информацию о провайдере.""" providers = { AIProvider.QWEN.value: ProviderInfo( id=AIProvider.QWEN.value, name="Qwen Code", description="Alibaba Qwen Code CLI — мощный AI-ассистент с поддержкой инструментов", available=self.is_provider_available(AIProvider.QWEN.value), is_active=is_active ), AIProvider.GIGACHAT.value: ProviderInfo( id=AIProvider.GIGACHAT.value, name="GigaChat", description="Sber GigaChat API — российская AI-модель от Сбера", available=self.is_provider_available(AIProvider.GIGACHAT.value), is_active=is_active ), AIProvider.OPENCODE.value: ProviderInfo( id=AIProvider.OPENCODE.value, name="Opencode", description="Opencode AI — бесплатные модели (minimax, big-pickle, gpt-5-nano)", available=self.is_provider_available(AIProvider.OPENCODE.value), is_active=is_active ) } return providers.get(provider_id, ProviderInfo( id=provider_id, name=provider_id, description="Unknown provider", available=False, is_active=is_active )) def get_all_providers_info(self, active_provider_id: str) -> List[ProviderInfo]: """Получить информацию обо всех провайдерах.""" return [ self.get_provider_info(AIProvider.QWEN.value, AIProvider.QWEN.value == active_provider_id), self.get_provider_info(AIProvider.GIGACHAT.value, AIProvider.GIGACHAT.value == active_provider_id), self.get_provider_info(AIProvider.OPENCODE.value, AIProvider.OPENCODE.value == active_provider_id) ] def switch_provider(self, user_id: int, provider_id: str, state_manager) -> tuple[bool, str]: """ Переключить AI-провайдер для пользователя. Args: user_id: ID пользователя provider_id: ID провайдера ("qwen" или "gigachat") state_manager: Менеджер состояний для обновления состояния пользователя Returns: (success: bool, message: str) """ if not self.is_provider_available(provider_id): return False, f"❌ Провайдер {provider_id} недоступен" state = state_manager.get(user_id) state.current_ai_provider = provider_id provider_info = self.get_provider_info(provider_id) logger.info(f"Пользователь {user_id} переключен на {provider_id}") return True, f"✅ Переключен на {provider_info.name}" def get_current_provider(self, state) -> str: """Получить текущего провайдера пользователя.""" return state.current_ai_provider async def execute_request( self, provider_id: str, user_id: int, prompt: str, system_prompt: Optional[str] = None, on_output: Optional[Callable[[str], Any]] = None, on_chunk: Optional[Callable[[str], Any]] = None, on_event: Optional[Callable[[Any], Any]] = None, context: Optional[List[Dict[str, str]]] = None, use_tools: bool = True ) -> Dict[str, Any]: """ Выполнить запрос через указанного провайдера с поддержкой инструментов. Args: provider_id: ID провайдера user_id: ID пользователя prompt: Запрос system_prompt: Системный промпт on_output: Callback для вывода on_chunk: Callback для потокового вывода on_event: Callback для событий context: История диалога use_tools: Использовать ли инструменты Returns: Dict с результатом: - success: bool - content: str - error: str (если ошибка) - provider: str - metadata: dict """ provider = self._providers.get(provider_id) if not provider: return { "success": False, "error": f"Провайдер {provider_id} не найден", "provider": provider_id } try: # Используем универсальный метод process_with_tools response = await provider.process_with_tools( prompt=prompt, system_prompt=system_prompt, context=context, tools_registry=self._tools_registry if use_tools else None, on_chunk=on_chunk, user_id=user_id ) if response.success: # Получаем информацию о модели из metadata ответа model_name = None if response.message and response.message.metadata: model_name = response.message.metadata.get("model") return { "success": True, "content": response.message.content if response.message else "", "provider": provider_id, "metadata": { "provider_name": response.provider_name, "usage": response.usage, "tool_calls": len(response.message.tool_calls) if response.message and response.message.tool_calls else 0, "model": model_name # Добавляем модель } } else: return { "success": False, "error": response.error, "provider": provider_id } except Exception as e: logger.error(f"Ошибка выполнения запроса через {provider_id}: {e}") return { "success": False, "error": str(e), "provider": provider_id } # Глобальный менеджер (будет инициализирован в bot.py) ai_provider_manager: Optional[AIProviderManager] = None def init_ai_provider_manager(qwen_manager, tools_registry=None) -> AIProviderManager: """Инициализировать глобальный AIProviderManager.""" global ai_provider_manager ai_provider_manager = AIProviderManager(qwen_manager) # Устанавливаем реестр инструментов если предоставлен if tools_registry: ai_provider_manager.set_tools_registry(tools_registry) logger.info(f"AIProviderManager инициализирован. Доступные провайдеры: {ai_provider_manager.get_available_providers()}") return ai_provider_manager def get_ai_provider_manager() -> AIProviderManager: """Получить глобальный AIProviderManager.""" global ai_provider_manager if ai_provider_manager is None: raise RuntimeError("AIProviderManager не инициализирован. Вызовите init_ai_provider_manager().") return ai_provider_manager