import asyncio import logging from collections.abc import Callable, Coroutine from typing import Any import anthropic import httpx from bot.models import Depth, FocusArea from bot.prompts.chunk_summary import build_chunk_prompt from bot.prompts.synthesis import build_synthesis_prompt from bot.services.ai_client import AIClient, get_ai_client log = logging.getLogger(__name__) _semaphore = asyncio.Semaphore(1) MAX_RETRIES = 5 ProgressCallback = Callable[[str], Coroutine[Any, Any, None]] async def _call_with_retry( client: AIClient, system: str, user: str, max_tokens: int, on_progress: ProgressCallback | None = None, ) -> str: for attempt in range(MAX_RETRIES): try: async with _semaphore: return await client.complete(system, user, max_tokens, on_progress) except anthropic.RateLimitError as e: wait = getattr(e, "retry_after", None) or 60 log.warning("Rate limited, waiting %ds (attempt %d/%d)", wait, attempt + 1, MAX_RETRIES) if on_progress: await on_progress(f"Rate limited, waiting {wait}s...") await asyncio.sleep(wait) except anthropic.APIStatusError as e: if e.status_code >= 500: wait = 10 * (attempt + 1) log.warning("Server error %d, retrying in %ds", e.status_code, wait) await asyncio.sleep(wait) else: raise except httpx.HTTPStatusError as e: if e.response.status_code == 429 or e.response.status_code >= 500: wait = 10 * (attempt + 1) log.warning("HTTP %d, retrying in %ds", e.response.status_code, wait) await asyncio.sleep(wait) else: raise raise RuntimeError("Max retries exceeded") async def analyze_channel( depth: Depth, focus_areas: list[FocusArea], lang: str, model_id: str, chunks: list[str], channel_title: str, channel_username: str | None, subscribers: int | None, msg_count: int, on_progress: ProgressCallback | None = None, ) -> str: client = get_ai_client(model_id) total = len(chunks) summaries: list[str] = [] for i, chunk_text in enumerate(chunks, 1): if on_progress: await on_progress(f"Analyzing chunk {i}/{total}...") prompt = build_chunk_prompt( depth, focus_areas, lang, channel_title, chunk_text, i, total ) summary = await _call_with_retry( client, system="You are an expert Telegram channel analyst.", user=prompt, max_tokens=4096, on_progress=on_progress, ) summaries.append(summary) log.info("Chunk %d/%d summarized (%d chars)", i, total, len(summary)) if i < total: if on_progress: await on_progress(f"Chunk {i}/{total} done. Cooling down 60s...") await asyncio.sleep(60) if on_progress: await on_progress("Generating final report...") synthesis_prompt = build_synthesis_prompt( depth, focus_areas, lang, channel_title, channel_username, subscribers, msg_count, summaries, ) report = await _call_with_retry( client, system="You are an expert Telegram channel analyst producing a final report.", user=synthesis_prompt, max_tokens=16000, on_progress=on_progress, ) log.info("Final report generated (%d chars)", len(report)) return report