tg-channel-analysis/bot/services/analyzer.py
Sergei Poljanski c30b7e4675 Add multi-user features: i18n, payments, multi-model AI, focus areas
Replace single report type with composable analysis: depth levels
(basic/standard/full) + focus area multi-select (psychology, business,
marketing, content, audience, sentiment). Multi-step inline keyboard
flow guides users through selection.

- i18n: English + Russian, auto-detect from Telegram, /lang override
- AI providers: Anthropic + OpenRouter via AIClient abstraction
- Telegram Stars payments with per-depth pricing and free trial
- SQLite (aiosqlite) for users, analyses, payments tracking
- User middleware for auto-registration and language detection
- Report persistence: save .md locally, offer file download
- New commands: /features, /prices, /lang
- Composable prompt system: depth modifiers + focus area fragments
2026-02-23 01:18:12 +02:00

112 lines
3.5 KiB
Python

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