Initial commit: VoIdeaAI - voice-first AI idea assistant
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""Fallback chain for AI providers.
|
||||
|
||||
Tries providers in order with retries per provider.
|
||||
YandexGPT -> GigaChat -> Error
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.integrations.ai.base import AIProvider, AIResult
|
||||
from app.integrations.ai.yandex_gpt import YandexGPTProvider
|
||||
from app.integrations.ai.gigachat import GigaChatProvider
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
class FallbackChain:
|
||||
"""Chain multiple AI providers with retry logic.
|
||||
|
||||
Tries providers in order (YandexGPT -> GigaChat).
|
||||
Each provider gets up to `max_retries` attempts before falling through.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
providers: list[AIProvider] | None = None,
|
||||
max_retries: int = 2,
|
||||
timeout: int = 30,
|
||||
):
|
||||
self.providers = providers or [
|
||||
YandexGPTProvider(),
|
||||
GigaChatProvider(),
|
||||
]
|
||||
self.max_retries = max_retries
|
||||
self.timeout = timeout
|
||||
|
||||
async def analyze(self, prompt: str, **kwargs: Any) -> AIResult:
|
||||
"""Try each provider in order with retries.
|
||||
|
||||
Args:
|
||||
prompt: The formatted prompt to send
|
||||
**kwargs: Additional parameters passed to each provider
|
||||
|
||||
Returns:
|
||||
AIResult from the first successful provider,
|
||||
or the last error result if all fail.
|
||||
"""
|
||||
last_error: AIResult | None = None
|
||||
|
||||
for provider in self.providers:
|
||||
for attempt in range(self.max_retries + 1):
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
provider.analyze(prompt, **kwargs),
|
||||
timeout=self.timeout,
|
||||
)
|
||||
|
||||
if result.success:
|
||||
await self._cleanup_providers(provider)
|
||||
return result
|
||||
|
||||
last_error = result
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
last_error = AIResult(
|
||||
success=False,
|
||||
error=f"{provider.name} timeout after {self.timeout}s",
|
||||
provider=provider.name,
|
||||
duration_ms=int(self.timeout * 1000),
|
||||
)
|
||||
except Exception as e:
|
||||
last_error = AIResult(
|
||||
success=False,
|
||||
error=f"{provider.name} error: {str(e)}",
|
||||
provider=provider.name,
|
||||
)
|
||||
|
||||
if attempt < self.max_retries:
|
||||
await asyncio.sleep(1 * (attempt + 1))
|
||||
|
||||
await self._cleanup_providers()
|
||||
return last_error or AIResult(
|
||||
success=False,
|
||||
error="All providers failed",
|
||||
provider="fallback_chain",
|
||||
)
|
||||
|
||||
async def _cleanup_providers(self, skip: AIProvider | None = None) -> None:
|
||||
"""Close all provider clients except the one to keep."""
|
||||
for provider in self.providers:
|
||||
if provider is skip:
|
||||
continue
|
||||
if hasattr(provider, "close"):
|
||||
try:
|
||||
await provider.close()
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user