124 lines
3.9 KiB
Python
124 lines
3.9 KiB
Python
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
|