import asyncio import logging from collections.abc import Callable, Coroutine from typing import Any import anthropic from bot.config import settings from bot.models import ReportType from bot.prompts.chunk_summary import build_chunk_prompt from bot.prompts.synthesis import build_synthesis_prompt log = logging.getLogger(__name__) # Only 1 concurrent request to stay within rate limits _semaphore = asyncio.Semaphore(1) MAX_RETRIES = 5 async def _call_claude( client: anthropic.AsyncAnthropic, system: str, user: str, max_tokens: int, on_progress: "ProgressCallback | None" = None, ) -> str: for attempt in range(MAX_RETRIES): try: async with _semaphore: kwargs: dict[str, Any] = { "model": settings.claude_model, "max_tokens": max_tokens, "system": system, "messages": [{"role": "user", "content": user}], } # Adaptive thinking only works on Opus 4.6 if "opus-4-6" in settings.claude_model: kwargs["thinking"] = {"type": "adaptive"} async with client.messages.stream(**kwargs) as stream: response = await stream.get_final_message() return "".join( block.text for block in response.content if block.type == "text" ) 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 raise RuntimeError("Max retries exceeded due to rate limiting") ProgressCallback = Callable[[str], Coroutine[Any, Any, None]] async def analyze_channel( report_type: ReportType, chunks: list[str], channel_title: str, channel_username: str | None, subscribers: int | None, msg_count: int, on_progress: ProgressCallback | None = None, ) -> str: client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key) 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( report_type, channel_title, chunk_text, i, total ) summary = await _call_claude( 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)) # Wait 60s between chunks — rate limit is 30K input tokens/min if i < total: if on_progress: await on_progress(f"Chunk {i}/{total} done. Cooling down 60s for rate limit...") await asyncio.sleep(60) if on_progress: await on_progress("Generating final report...") synthesis_prompt = build_synthesis_prompt( report_type, channel_title, channel_username, subscribers, msg_count, summaries, ) report = await _call_claude( 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