Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
# main.py (
|
2 |
import os
|
3 |
import re
|
4 |
import logging
|
@@ -65,7 +65,7 @@ logging.getLogger('starlette').setLevel(logging.INFO)
|
|
65 |
if _gemini_available: logging.getLogger("google.ai.generativelanguage").setLevel(logging.WARNING)
|
66 |
logger = logging.getLogger(__name__)
|
67 |
logger.info(f"Logging configured. Using BS4 parser: {DEFAULT_PARSER}")
|
68 |
-
if not _gemini_available: logger.warning("google-generativeai library not found. Gemini
|
69 |
|
70 |
|
71 |
# --- Global variable for PTB app ---
|
@@ -80,22 +80,28 @@ def get_secret(secret_name):
|
|
80 |
return value
|
81 |
|
82 |
TELEGRAM_TOKEN = get_secret('TELEGRAM_TOKEN')
|
83 |
-
OPENROUTER_API_KEY = get_secret('OPENROUTER_API_KEY')
|
84 |
URLTOTEXT_API_KEY = get_secret('URLTOTEXT_API_KEY')
|
85 |
SUPADATA_API_KEY = get_secret('SUPADATA_API_KEY')
|
86 |
APIFY_API_TOKEN = get_secret('APIFY_API_TOKEN')
|
87 |
WEBHOOK_SECRET = get_secret('WEBHOOK_SECRET')
|
88 |
-
GEMINI_API_KEY = get_secret('GEMINI_API_KEY')
|
89 |
|
90 |
-
|
|
|
91 |
APIFY_ACTOR_ID = os.environ.get("APIFY_ACTOR_ID", "karamelo~youtube-transcripts")
|
92 |
-
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.0-flash")
|
93 |
|
94 |
if not TELEGRAM_TOKEN: logger.critical("β FATAL: TELEGRAM_TOKEN not found."); raise RuntimeError("Exiting: Telegram token missing.")
|
95 |
-
if not
|
96 |
-
|
97 |
-
|
98 |
-
|
|
|
|
|
|
|
|
|
|
|
99 |
|
100 |
|
101 |
if not URLTOTEXT_API_KEY: pass
|
@@ -104,16 +110,15 @@ if not APIFY_API_TOKEN: pass
|
|
104 |
if not WEBHOOK_SECRET: logger.info("Optional secret 'WEBHOOK_SECRET' not found. Webhook security disabled.")
|
105 |
|
106 |
logger.info("Secret loading and configuration check finished.")
|
107 |
-
logger.info(f"Using
|
108 |
-
|
109 |
-
else: logger.info("Gemini Fallback: Disabled")
|
110 |
logger.info(f"Using Apify Actor (via REST): {APIFY_ACTOR_ID}")
|
111 |
_apify_token_exists = bool(APIFY_API_TOKEN)
|
112 |
|
113 |
|
114 |
-
if
|
115 |
try: genai.configure(api_key=GEMINI_API_KEY); logger.info("Google GenAI client configured successfully.")
|
116 |
-
except Exception as e: logger.error(f"Failed to configure Google GenAI client: {e}");
|
117 |
|
118 |
# --- Retry Decorator ---
|
119 |
@retry( stop=stop_after_attempt(4), wait=wait_exponential(multiplier=1, min=2, max=15), retry=retry_if_exception_type((NetworkError, RetryAfter, TimedOut, BadRequest)), before_sleep=before_sleep_log(logger, logging.WARNING), reraise=True )
|
@@ -322,12 +327,16 @@ async def get_website_content_via_api(url: str, api_key: str) -> Optional[str]:
|
|
322 |
except Exception as e: logger.error(f"[Fallback Web API] Unexpected error during urltotext.com API call for {url}: {e}", exc_info=True); return None
|
323 |
|
324 |
# --- Summarization Functions ---
|
325 |
-
async def generate_summary_gemini(text: str, summary_type: str) -> str:
|
326 |
-
"""Generates summary using Google Gemini API (Fallback)."""
|
327 |
-
global GEMINI_MODEL, _gemini_fallback_enabled
|
328 |
-
if not _gemini_fallback_enabled: logger.error("[Gemini Fallback] Called but is disabled."); return "Error: Fallback AI service not available."
|
329 |
-
logger.info(f"[Gemini Fallback] Generating {summary_type} summary using {GEMINI_MODEL}. Input length: {len(text)}")
|
330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
331 |
if summary_type == "paragraph": prompt = ("You are an AI model designed to provide concise summaries using British English spellings. Your output MUST be:\n" "β’ Clear and simple language suitable for someone unfamiliar with the topic.\n" "β’ Uses British English spellings throughout.\n" "β’ Straightforward and understandable vocabulary; avoid complex terms.\n" "β’ Presented as ONE SINGLE PARAGRAPH.\n" "β’ No more than 85 words maximum; but does not have to be exactly 85.\n" "β’ Considers the entire text content equally.\n" "β’ Uses semicolons (;) instead of em dashes (β or β).\n\n" "Here is the text to summarise:")
|
332 |
else: # points summary
|
333 |
prompt = ("You are an AI model designed to provide concise summaries using British English spellings. Your output MUST strictly follow this Markdown format:\n\n"
|
@@ -345,47 +354,74 @@ async def generate_summary_gemini(text: str, summary_type: str) -> str:
|
|
345 |
"β’ Consider the entire text's content, not just the beginning or a few topics.\n"
|
346 |
"β’ Use semicolons (;) instead of em dashes (β or β).\n\n"
|
347 |
"Here is the text to summarise:")
|
348 |
-
|
349 |
-
|
|
|
|
|
|
|
|
|
350 |
full_prompt = f"{prompt}\n\n{text}"
|
351 |
-
|
|
|
352 |
safety_settings = {
|
353 |
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
|
354 |
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
|
355 |
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
|
356 |
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
|
357 |
-
# getattr used for safety in case the category doesn't exist in the SDK version
|
358 |
getattr(HarmCategory, 'HARM_CATEGORY_CIVIC_INTEGRITY', None): HarmBlockThreshold.BLOCK_NONE
|
359 |
}
|
360 |
safety_settings = {k: v for k, v in safety_settings.items() if k is not None}
|
361 |
-
logger.debug(f"[Gemini
|
362 |
|
363 |
try:
|
364 |
-
logger.debug(f"[Gemini
|
365 |
model = genai.GenerativeModel(GEMINI_MODEL)
|
366 |
-
logger.info(f"[Gemini
|
367 |
-
request_options = {"timeout": 120}
|
368 |
response = await model.generate_content_async( full_prompt, safety_settings=safety_settings, request_options=request_options )
|
369 |
-
logger.info("[Gemini
|
370 |
-
|
371 |
-
if response.prompt_feedback.block_reason: logger.warning(f"[Gemini Fallback] Request blocked unexpectedly. Reason: {response.prompt_feedback.block_reason}");
|
372 |
-
for cand in response.candidates:
|
373 |
-
if cand.finish_reason == 'SAFETY': logger.warning(f"[Gemini Fallback] Candidate blocked due to SAFETY. Ratings: {cand.safety_ratings}")
|
374 |
-
|
375 |
-
try: summary = response.text
|
376 |
-
except ValueError as e: logger.warning(f"[Gemini Fallback] Error accessing response text (likely blocked content): {e}"); summary = None
|
377 |
|
378 |
-
|
379 |
-
|
380 |
-
|
|
|
381 |
|
|
|
|
|
|
|
|
|
|
|
382 |
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
388 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
389 |
if summary_type == "paragraph": prompt = ("You are an AI model designed to provide concise summaries using British English spellings. Your output MUST be:\n" "β’ Clear and simple language suitable for someone unfamiliar with the topic.\n" "β’ Uses British English spellings throughout.\n" "β’ Straightforward and understandable vocabulary; avoid complex terms.\n" "β’ Presented as ONE SINGLE PARAGRAPH.\n" "β’ No more than 85 words maximum; but does not have to be exactly 85.\n" "β’ Considers the entire text content equally.\n" "β’ Uses semicolons (;) instead of em dashes (β or β).\n\n" "Here is the text to summarise:")
|
390 |
else: # points summary
|
391 |
prompt = ("You are an AI model designed to provide concise summaries using British English spellings. Your output MUST strictly follow this Markdown format:\n\n"
|
@@ -403,57 +439,124 @@ async def generate_summary(text: str, summary_type: str) -> str:
|
|
403 |
"β’ Consider the entire text's content, not just the beginning or a few topics.\n"
|
404 |
"β’ Use semicolons (;) instead of em dashes (β or β).\n\n"
|
405 |
"Here is the text to summarise:")
|
406 |
-
|
407 |
-
|
|
|
|
|
|
|
|
|
408 |
full_prompt = f"{prompt}\n\n{text}"
|
409 |
-
|
410 |
-
|
|
|
|
|
|
|
|
|
411 |
response = None
|
412 |
|
413 |
try:
|
414 |
async with httpx.AsyncClient(timeout=api_timeouts) as client:
|
415 |
-
logger.info(f"[
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
453 |
except Exception as e:
|
454 |
-
logger.error(f"[
|
455 |
-
|
456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
457 |
|
458 |
|
459 |
# (process_summary_task, handlers, setup, lifespan, routes, etc. remain the same)
|
@@ -495,7 +598,7 @@ async def process_summary_task( user_id: int, chat_id: int, message_id_to_edit:
|
|
495 |
if content:
|
496 |
logger.info(f"[Task {task_id}] Content fetched (len:{len(content)}). Generating summary.")
|
497 |
await retry_bot_operation(bot.send_chat_action, chat_id=chat_id, action='typing')
|
498 |
-
final_summary = await generate_summary(content, summary_type) # This now
|
499 |
if final_summary.startswith("Error:") or final_summary.startswith("Sorry,"): user_feedback_message = final_summary; logger.warning(f"[Task {task_id}] Summary generation failed: {final_summary}")
|
500 |
else:
|
501 |
max_length = 4096; summary_parts = [final_summary[i:i+max_length] for i in range(0, len(final_summary), max_length)]
|
@@ -565,17 +668,23 @@ async def handle_summary_type_callback(update: Update, context: ContextTypes.DEF
|
|
565 |
|
566 |
context.user_data.pop('url_to_summarize', None); context.user_data.pop('original_message_id', None); logger.debug(f"Cleared URL context for user {user.id}")
|
567 |
|
568 |
-
global TELEGRAM_TOKEN, OPENROUTER_API_KEY
|
569 |
if not TELEGRAM_TOKEN:
|
570 |
logger.critical("TG TOKEN missing!")
|
571 |
try: await query.edit_message_text(text="β Bot config error.")
|
572 |
except Exception: pass
|
573 |
return
|
574 |
-
if not
|
575 |
-
logger.
|
576 |
-
try: await query.edit_message_text(text="β AI config error.")
|
577 |
except Exception: pass
|
578 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
579 |
|
580 |
logger.info(f"Scheduling task for user {user.id}, chat {query.message.chat_id}, msg {message_id_to_edit}")
|
581 |
asyncio.create_task( process_summary_task( user_id=user.id, chat_id=query.message.chat_id, message_id_to_edit=message_id_to_edit, url=url, summary_type=summary_type, bot_token=TELEGRAM_TOKEN ), name=f"SummaryTask-{user.id}-{message_id_to_edit}" )
|
@@ -639,14 +748,14 @@ async def lifespan(app: Starlette):
|
|
639 |
logger.info("ASGI Lifespan: Shutdown complete.")
|
640 |
|
641 |
async def health_check(request: Request) -> PlainTextResponse:
|
642 |
-
global OPENROUTER_MODEL, GEMINI_MODEL, APIFY_ACTOR_ID, _apify_token_exists,
|
643 |
bot_status = "Not Initialized"
|
644 |
if ptb_app and ptb_app.bot:
|
645 |
try:
|
646 |
if ptb_app.running: bot_info = await ptb_app.bot.get_me(); bot_status = f"Running (@{bot_info.username})"
|
647 |
else: bot_status = "Initialized/Not running"
|
648 |
except Exception as e: bot_status = f"Error checking status: {e}"
|
649 |
-
return PlainTextResponse( f"TG Bot Summarizer - Status: {bot_status}\n" f"Primary Model: {
|
650 |
|
651 |
async def telegram_webhook(request: Request) -> Response:
|
652 |
global WEBHOOK_SECRET
|
|
|
1 |
+
# main.py (Corrected SyntaxError at line 424 - Now with Gemini 2.0 as primary)
|
2 |
import os
|
3 |
import re
|
4 |
import logging
|
|
|
65 |
if _gemini_available: logging.getLogger("google.ai.generativelanguage").setLevel(logging.WARNING)
|
66 |
logger = logging.getLogger(__name__)
|
67 |
logger.info(f"Logging configured. Using BS4 parser: {DEFAULT_PARSER}")
|
68 |
+
if not _gemini_available: logger.warning("google-generativeai library not found. Gemini functionality disabled.")
|
69 |
|
70 |
|
71 |
# --- Global variable for PTB app ---
|
|
|
80 |
return value
|
81 |
|
82 |
TELEGRAM_TOKEN = get_secret('TELEGRAM_TOKEN')
|
83 |
+
OPENROUTER_API_KEY = get_secret('OPENROUTER_API_KEY') # Now Fallback
|
84 |
URLTOTEXT_API_KEY = get_secret('URLTOTEXT_API_KEY')
|
85 |
SUPADATA_API_KEY = get_secret('SUPADATA_API_KEY')
|
86 |
APIFY_API_TOKEN = get_secret('APIFY_API_TOKEN')
|
87 |
WEBHOOK_SECRET = get_secret('WEBHOOK_SECRET')
|
88 |
+
GEMINI_API_KEY = get_secret('GEMINI_API_KEY') # Now Primary
|
89 |
|
90 |
+
# Models (User can still configure via env vars)
|
91 |
+
OPENROUTER_MODEL = os.environ.get("OPENROUTER_MODEL", "deepseek/deepseek-chat-v3-0324:free") # Fallback Model
|
92 |
APIFY_ACTOR_ID = os.environ.get("APIFY_ACTOR_ID", "karamelo~youtube-transcripts")
|
93 |
+
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.0-flash") # Primary Model
|
94 |
|
95 |
if not TELEGRAM_TOKEN: logger.critical("β FATAL: TELEGRAM_TOKEN not found."); raise RuntimeError("Exiting: Telegram token missing.")
|
96 |
+
if not GEMINI_API_KEY: logger.error("β ERROR: GEMINI_API_KEY not found. Primary summarization (Gemini) will fail.")
|
97 |
+
if not OPENROUTER_API_KEY: logger.warning("β οΈ WARNING: OPENROUTER_API_KEY not found. Fallback summarization will fail.")
|
98 |
+
|
99 |
+
_gemini_primary_enabled = _gemini_available and bool(GEMINI_API_KEY)
|
100 |
+
if not _gemini_available: logger.warning("β οΈ WARNING: google-generativeai library missing. Gemini disabled.")
|
101 |
+
elif not GEMINI_API_KEY: logger.warning("β οΈ WARNING: GEMINI_API_KEY not found or empty. Gemini disabled.")
|
102 |
+
|
103 |
+
_openrouter_fallback_enabled = bool(OPENROUTER_API_KEY)
|
104 |
+
if not _openrouter_fallback_enabled: logger.warning("β οΈ WARNING: OPENROUTER_API_KEY not found. Fallback disabled.")
|
105 |
|
106 |
|
107 |
if not URLTOTEXT_API_KEY: pass
|
|
|
110 |
if not WEBHOOK_SECRET: logger.info("Optional secret 'WEBHOOK_SECRET' not found. Webhook security disabled.")
|
111 |
|
112 |
logger.info("Secret loading and configuration check finished.")
|
113 |
+
logger.info(f"Using Gemini Model (Primary): {GEMINI_MODEL if _gemini_primary_enabled else 'DISABLED'}")
|
114 |
+
logger.info(f"Using OpenRouter Model (Fallback): {OPENROUTER_MODEL if _openrouter_fallback_enabled else 'DISABLED'}")
|
|
|
115 |
logger.info(f"Using Apify Actor (via REST): {APIFY_ACTOR_ID}")
|
116 |
_apify_token_exists = bool(APIFY_API_TOKEN)
|
117 |
|
118 |
|
119 |
+
if _gemini_primary_enabled:
|
120 |
try: genai.configure(api_key=GEMINI_API_KEY); logger.info("Google GenAI client configured successfully.")
|
121 |
+
except Exception as e: logger.error(f"Failed to configure Google GenAI client: {e}"); _gemini_primary_enabled = False
|
122 |
|
123 |
# --- Retry Decorator ---
|
124 |
@retry( stop=stop_after_attempt(4), wait=wait_exponential(multiplier=1, min=2, max=15), retry=retry_if_exception_type((NetworkError, RetryAfter, TimedOut, BadRequest)), before_sleep=before_sleep_log(logger, logging.WARNING), reraise=True )
|
|
|
327 |
except Exception as e: logger.error(f"[Fallback Web API] Unexpected error during urltotext.com API call for {url}: {e}", exc_info=True); return None
|
328 |
|
329 |
# --- Summarization Functions ---
|
|
|
|
|
|
|
|
|
|
|
330 |
|
331 |
+
async def _call_gemini(text: str, summary_type: str) -> Tuple[Optional[str], Optional[str]]:
|
332 |
+
"""Internal function to call Gemini API. Returns (summary, error_message)."""
|
333 |
+
global GEMINI_MODEL, _gemini_primary_enabled
|
334 |
+
if not _gemini_primary_enabled:
|
335 |
+
logger.error("[Gemini Primary] Called but is disabled.");
|
336 |
+
return None, "Error: Primary AI service (Gemini) not configured/available."
|
337 |
+
logger.info(f"[Gemini Primary] Generating {summary_type} summary using {GEMINI_MODEL}. Input length: {len(text)}")
|
338 |
+
|
339 |
+
# Define prompts (same logic as before)
|
340 |
if summary_type == "paragraph": prompt = ("You are an AI model designed to provide concise summaries using British English spellings. Your output MUST be:\n" "β’ Clear and simple language suitable for someone unfamiliar with the topic.\n" "β’ Uses British English spellings throughout.\n" "β’ Straightforward and understandable vocabulary; avoid complex terms.\n" "β’ Presented as ONE SINGLE PARAGRAPH.\n" "β’ No more than 85 words maximum; but does not have to be exactly 85.\n" "β’ Considers the entire text content equally.\n" "β’ Uses semicolons (;) instead of em dashes (β or β).\n\n" "Here is the text to summarise:")
|
341 |
else: # points summary
|
342 |
prompt = ("You are an AI model designed to provide concise summaries using British English spellings. Your output MUST strictly follow this Markdown format:\n\n"
|
|
|
354 |
"β’ Consider the entire text's content, not just the beginning or a few topics.\n"
|
355 |
"β’ Use semicolons (;) instead of em dashes (β or β).\n\n"
|
356 |
"Here is the text to summarise:")
|
357 |
+
|
358 |
+
# Input Length Check (Gemini-specific limits if known, otherwise use a large default)
|
359 |
+
MAX_INPUT_LENGTH_GEMINI = 1000000 # Default limit for Gemini
|
360 |
+
if len(text) > MAX_INPUT_LENGTH_GEMINI:
|
361 |
+
logger.warning(f"[Gemini Primary] Input length ({len(text)}) exceeds limit ({MAX_INPUT_LENGTH_GEMINI}). Truncating.");
|
362 |
+
text = text[:MAX_INPUT_LENGTH_GEMINI] + "... (Content truncated)"
|
363 |
full_prompt = f"{prompt}\n\n{text}"
|
364 |
+
|
365 |
+
# Safety Settings (Block None)
|
366 |
safety_settings = {
|
367 |
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
|
368 |
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
|
369 |
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
|
370 |
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
|
|
|
371 |
getattr(HarmCategory, 'HARM_CATEGORY_CIVIC_INTEGRITY', None): HarmBlockThreshold.BLOCK_NONE
|
372 |
}
|
373 |
safety_settings = {k: v for k, v in safety_settings.items() if k is not None}
|
374 |
+
logger.debug(f"[Gemini Primary] Using safety settings: {safety_settings}")
|
375 |
|
376 |
try:
|
377 |
+
logger.debug(f"[Gemini Primary] Initializing model {GEMINI_MODEL}")
|
378 |
model = genai.GenerativeModel(GEMINI_MODEL)
|
379 |
+
logger.info(f"[Gemini Primary] Sending request to Gemini ({GEMINI_MODEL})...")
|
380 |
+
request_options = {"timeout": 120} # Generous timeout for Gemini
|
381 |
response = await model.generate_content_async( full_prompt, safety_settings=safety_settings, request_options=request_options )
|
382 |
+
logger.info("[Gemini Primary] Received response from Gemini.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
383 |
|
384 |
+
# Check for immediate blocking reasons
|
385 |
+
if response.prompt_feedback.block_reason:
|
386 |
+
logger.warning(f"[Gemini Primary] Request blocked by API. Reason: {response.prompt_feedback.block_reason}");
|
387 |
+
return None, f"Sorry, the primary AI model ({GEMINI_MODEL}) blocked the request (Reason: {response.prompt_feedback.block_reason})."
|
388 |
|
389 |
+
# Check candidate-level blocking
|
390 |
+
for cand in response.candidates:
|
391 |
+
if cand.finish_reason == 'SAFETY':
|
392 |
+
logger.warning(f"[Gemini Primary] Candidate blocked due to SAFETY. Ratings: {cand.safety_ratings}")
|
393 |
+
return None, f"Sorry, the primary AI model ({GEMINI_MODEL}) blocked the response due to safety filters."
|
394 |
|
395 |
+
# Try to get text, handle potential ValueError if blocked
|
396 |
+
try:
|
397 |
+
summary = response.text
|
398 |
+
except ValueError as e:
|
399 |
+
logger.warning(f"[Gemini Primary] Error accessing response text (likely blocked content): {e}");
|
400 |
+
summary = None
|
401 |
+
|
402 |
+
if summary:
|
403 |
+
logger.info(f"[Gemini Primary] Success generating summary. Output len: {len(summary)}");
|
404 |
+
# Escape Markdown for Telegram
|
405 |
+
escaped_summary = summary.strip().replace('_', r'\_').replace('*', r'\*').replace('[', r'\[').replace('`', r'\`')
|
406 |
+
return escaped_summary, None
|
407 |
+
else:
|
408 |
+
finish_reason = response.candidates[0].finish_reason if response.candidates else 'N/A'
|
409 |
+
logger.warning(f"[Gemini Primary] Gemini returned empty summary or content was blocked. Finish reason: {finish_reason}");
|
410 |
+
return None, f"Sorry, the primary AI model ({GEMINI_MODEL}) did not provide a summary (Finish Reason: {finish_reason})."
|
411 |
|
412 |
+
except Exception as e:
|
413 |
+
logger.error(f"[Gemini Primary] Unexpected error during Gemini API call: {e}", exc_info=True);
|
414 |
+
return None, f"Sorry, an unexpected error occurred while using the primary AI service ({GEMINI_MODEL})."
|
415 |
+
|
416 |
+
async def _call_openrouter(text: str, summary_type: str) -> Tuple[Optional[str], Optional[str]]:
|
417 |
+
"""Internal function to call OpenRouter API (Fallback). Returns (summary, error_message)."""
|
418 |
+
global OPENROUTER_API_KEY, OPENROUTER_MODEL, _openrouter_fallback_enabled
|
419 |
+
if not _openrouter_fallback_enabled:
|
420 |
+
logger.error("[OpenRouter Fallback] Called but is disabled.");
|
421 |
+
return None, "Error: Fallback AI service (OpenRouter) not configured/available."
|
422 |
+
logger.info(f"[OpenRouter Fallback] Generating {summary_type} summary using {OPENROUTER_MODEL}. Input length: {len(text)}")
|
423 |
+
|
424 |
+
# Define prompts (same logic as before)
|
425 |
if summary_type == "paragraph": prompt = ("You are an AI model designed to provide concise summaries using British English spellings. Your output MUST be:\n" "β’ Clear and simple language suitable for someone unfamiliar with the topic.\n" "β’ Uses British English spellings throughout.\n" "β’ Straightforward and understandable vocabulary; avoid complex terms.\n" "β’ Presented as ONE SINGLE PARAGRAPH.\n" "β’ No more than 85 words maximum; but does not have to be exactly 85.\n" "β’ Considers the entire text content equally.\n" "β’ Uses semicolons (;) instead of em dashes (β or β).\n\n" "Here is the text to summarise:")
|
426 |
else: # points summary
|
427 |
prompt = ("You are an AI model designed to provide concise summaries using British English spellings. Your output MUST strictly follow this Markdown format:\n\n"
|
|
|
439 |
"β’ Consider the entire text's content, not just the beginning or a few topics.\n"
|
440 |
"β’ Use semicolons (;) instead of em dashes (β or β).\n\n"
|
441 |
"Here is the text to summarise:")
|
442 |
+
|
443 |
+
# Input Length Check (OpenRouter-specific limit)
|
444 |
+
MAX_INPUT_LENGTH_OR = 500000
|
445 |
+
if len(text) > MAX_INPUT_LENGTH_OR:
|
446 |
+
logger.warning(f"[OpenRouter Fallback] Input length ({len(text)}) exceeds limit ({MAX_INPUT_LENGTH_OR}). Truncating.");
|
447 |
+
text = text[:MAX_INPUT_LENGTH_OR] + "... (Content truncated)"
|
448 |
full_prompt = f"{prompt}\n\n{text}"
|
449 |
+
|
450 |
+
headers = { "Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json" }
|
451 |
+
payload = { "model": OPENROUTER_MODEL, "messages": [{"role": "user", "content": full_prompt}] }
|
452 |
+
openrouter_api_endpoint = "https://openrouter.ai/api/v1/chat/completions"
|
453 |
+
# Use reasonable timeouts for fallback
|
454 |
+
api_timeouts = httpx.Timeout(25.0, read=20.0, write=25.0, pool=60.0)
|
455 |
response = None
|
456 |
|
457 |
try:
|
458 |
async with httpx.AsyncClient(timeout=api_timeouts) as client:
|
459 |
+
logger.info(f"[OpenRouter Fallback] Sending request to OpenRouter ({OPENROUTER_MODEL}) with read timeout {api_timeouts.read}s...")
|
460 |
+
response = await client.post(openrouter_api_endpoint, headers=headers, json=payload)
|
461 |
+
if response: logger.info(f"[OpenRouter Fallback] Received response from OpenRouter. Status code: {response.status_code}")
|
462 |
+
else: logger.error("[OpenRouter Fallback] No response from OpenRouter (unexpected)."); return None, "Sorry, fallback AI service failed unexpectedly."
|
463 |
+
|
464 |
+
if response.status_code == 200:
|
465 |
+
try:
|
466 |
+
data = response.json()
|
467 |
+
if data.get("choices") and isinstance(data["choices"], list) and len(data["choices"]) > 0:
|
468 |
+
message = data["choices"][0].get("message")
|
469 |
+
if message and isinstance(message, dict):
|
470 |
+
summary = message.get("content")
|
471 |
+
if summary:
|
472 |
+
logger.info(f"[OpenRouter Fallback] Success via OpenRouter. Output len: {len(summary)}")
|
473 |
+
# Escape Markdown for Telegram
|
474 |
+
escaped_summary = summary.strip().replace('_', r'\_').replace('*', r'\*').replace('[', r'\[').replace('`', r'\`')
|
475 |
+
return escaped_summary, None
|
476 |
+
else:
|
477 |
+
logger.warning(f"[OpenRouter Fallback] OpenRouter success but content empty. Resp: {data}")
|
478 |
+
return None, "Sorry, the fallback AI model returned an empty summary."
|
479 |
+
else:
|
480 |
+
logger.error(f"[OpenRouter Fallback] Unexpected message structure: {message}. Full: {data}")
|
481 |
+
return None, "Sorry, could not parse fallback AI response (format)."
|
482 |
+
else:
|
483 |
+
logger.error(f"[OpenRouter Fallback] Unexpected choices structure: {data.get('choices')}. Full: {data}")
|
484 |
+
return None, "Sorry, could not parse fallback AI response (choices)."
|
485 |
+
except json.JSONDecodeError:
|
486 |
+
logger.error(f"[OpenRouter Fallback] Failed JSON decode OpenRouter. Status:{response.status_code}. Resp:{response.text[:500]}")
|
487 |
+
return None, "Sorry, failed to understand fallback AI response."
|
488 |
+
except Exception as e:
|
489 |
+
logger.error(f"[OpenRouter Fallback] Error processing OpenRouter success response: {e}", exc_info=True)
|
490 |
+
return None, "Sorry, error processing fallback AI response."
|
491 |
+
# Handle specific error codes
|
492 |
+
elif response.status_code == 401: logger.error("[OpenRouter Fallback] API key invalid (401)."); return None, "Error: Fallback AI model configuration key is invalid."
|
493 |
+
elif response.status_code == 402: logger.error("[OpenRouter Fallback] Payment Required (402)."); return None, "Sorry, fallback AI service limits/payment issue."
|
494 |
+
elif response.status_code == 429: logger.warning("[OpenRouter Fallback] Rate Limit Exceeded (429)."); return None, "Sorry, fallback AI model is busy. Try again."
|
495 |
+
elif response.status_code == 500: logger.error(f"[OpenRouter Fallback] Internal Server Error (500). Resp:{response.text[:500]}"); return None, "Sorry, fallback AI service internal error."
|
496 |
+
else:
|
497 |
+
error_info = "";
|
498 |
+
try: error_info = response.json().get("error", {}).get("message", "")
|
499 |
+
except Exception: pass
|
500 |
+
logger.error(f"[OpenRouter Fallback] Unexpected status {response.status_code}. Error: '{error_info}' Resp:{response.text[:500]}");
|
501 |
+
return None, f"Sorry, fallback AI service returned unexpected status ({response.status_code})."
|
502 |
+
|
503 |
+
except httpx.TimeoutException as e:
|
504 |
+
logger.error(f"[OpenRouter Fallback] Timeout error ({type(e)}) connecting/reading from OpenRouter API: {e}")
|
505 |
+
return None, f"Sorry, the fallback AI service ({OPENROUTER_MODEL}) timed out."
|
506 |
+
except httpx.RequestError as e:
|
507 |
+
logger.error(f"[OpenRouter Fallback] Request error connecting to OpenRouter API: {e}")
|
508 |
+
return None, "Sorry, there was an error connecting to the fallback AI model service."
|
509 |
except Exception as e:
|
510 |
+
logger.error(f"[OpenRouter Fallback] Unexpected error during OpenRouter call: {e}", exc_info=True)
|
511 |
+
return None, "Sorry, an unexpected error occurred while using the fallback AI service."
|
512 |
+
|
513 |
+
async def generate_summary(text: str, summary_type: str) -> str:
|
514 |
+
"""
|
515 |
+
Generates summary using Gemini (Primary) and falls back to OpenRouter if needed.
|
516 |
+
"""
|
517 |
+
global _gemini_primary_enabled, _openrouter_fallback_enabled, GEMINI_MODEL, OPENROUTER_MODEL
|
518 |
+
logger.info(f"[Summary Generation] Starting process. Primary: Gemini ({GEMINI_MODEL}), Fallback: OpenRouter ({OPENROUTER_MODEL})")
|
519 |
+
|
520 |
+
final_summary: Optional[str] = None
|
521 |
+
error_message: Optional[str] = None
|
522 |
+
|
523 |
+
# --- Attempt 1: Gemini (Primary) ---
|
524 |
+
if _gemini_primary_enabled:
|
525 |
+
logger.info(f"[Summary Generation] Attempting primary AI: Gemini ({GEMINI_MODEL})")
|
526 |
+
final_summary, error_message = await _call_gemini(text, summary_type)
|
527 |
+
if final_summary:
|
528 |
+
logger.info(f"[Summary Generation] Success with primary AI (Gemini).")
|
529 |
+
return final_summary
|
530 |
+
else:
|
531 |
+
logger.warning(f"[Summary Generation] Primary AI (Gemini) failed or returned unusable result. Error: {error_message}. Proceeding to fallback.")
|
532 |
+
else:
|
533 |
+
logger.warning("[Summary Generation] Primary AI (Gemini) is disabled or unavailable. Proceeding directly to fallback.")
|
534 |
+
error_message = "Primary AI (Gemini) unavailable." # Set initial error message
|
535 |
+
|
536 |
+
# --- Attempt 2: OpenRouter (Fallback) ---
|
537 |
+
if _openrouter_fallback_enabled:
|
538 |
+
logger.info(f"[Summary Generation] Attempting fallback AI: OpenRouter ({OPENROUTER_MODEL})")
|
539 |
+
fallback_summary, fallback_error = await _call_openrouter(text, summary_type)
|
540 |
+
if fallback_summary:
|
541 |
+
logger.info(f"[Summary Generation] Success with fallback AI (OpenRouter).")
|
542 |
+
return fallback_summary
|
543 |
+
else:
|
544 |
+
logger.error(f"[Summary Generation] Fallback AI (OpenRouter) also failed. Error: {fallback_error}")
|
545 |
+
# Combine error messages if possible
|
546 |
+
if error_message: # Keep the primary error if it exists
|
547 |
+
return f"{error_message} Fallback AI ({OPENROUTER_MODEL}) also failed: {fallback_error}"
|
548 |
+
else: # If primary was skipped
|
549 |
+
return f"Fallback AI ({OPENROUTER_MODEL}) failed: {fallback_error}"
|
550 |
+
else:
|
551 |
+
logger.error("[Summary Generation] Fallback AI (OpenRouter) is disabled or unavailable. Cannot proceed.")
|
552 |
+
if error_message: # Return the primary error
|
553 |
+
return f"{error_message} Fallback AI is also unavailable."
|
554 |
+
else: # Should not happen if logic is correct, but safeguard
|
555 |
+
return "Error: Both primary and fallback AI services are unavailable."
|
556 |
+
|
557 |
+
# Should ideally not be reached if logic above is sound, but as a final catch
|
558 |
+
logger.error("[Summary Generation] Reached end of function without returning a summary or specific error.")
|
559 |
+
return "Sorry, an unknown error occurred during summary generation after trying all available models."
|
560 |
|
561 |
|
562 |
# (process_summary_task, handlers, setup, lifespan, routes, etc. remain the same)
|
|
|
598 |
if content:
|
599 |
logger.info(f"[Task {task_id}] Content fetched (len:{len(content)}). Generating summary.")
|
600 |
await retry_bot_operation(bot.send_chat_action, chat_id=chat_id, action='typing')
|
601 |
+
final_summary = await generate_summary(content, summary_type) # This now calls Gemini first, then OpenRouter
|
602 |
if final_summary.startswith("Error:") or final_summary.startswith("Sorry,"): user_feedback_message = final_summary; logger.warning(f"[Task {task_id}] Summary generation failed: {final_summary}")
|
603 |
else:
|
604 |
max_length = 4096; summary_parts = [final_summary[i:i+max_length] for i in range(0, len(final_summary), max_length)]
|
|
|
668 |
|
669 |
context.user_data.pop('url_to_summarize', None); context.user_data.pop('original_message_id', None); logger.debug(f"Cleared URL context for user {user.id}")
|
670 |
|
671 |
+
global TELEGRAM_TOKEN, GEMINI_API_KEY, OPENROUTER_API_KEY, _gemini_primary_enabled, _openrouter_fallback_enabled
|
672 |
if not TELEGRAM_TOKEN:
|
673 |
logger.critical("TG TOKEN missing!")
|
674 |
try: await query.edit_message_text(text="β Bot config error.")
|
675 |
except Exception: pass
|
676 |
return
|
677 |
+
if not _gemini_primary_enabled and not _openrouter_fallback_enabled:
|
678 |
+
logger.critical("Neither Gemini nor OpenRouter API keys are configured/valid!")
|
679 |
+
try: await query.edit_message_text(text="β AI config error: No models available.")
|
680 |
except Exception: pass
|
681 |
return
|
682 |
+
elif not _gemini_primary_enabled:
|
683 |
+
logger.warning("Primary AI (Gemini) is unavailable, will rely on fallback.")
|
684 |
+
# No need to inform user unless fallback also fails later
|
685 |
+
elif not _openrouter_fallback_enabled:
|
686 |
+
logger.warning("Fallback AI (OpenRouter) is unavailable.")
|
687 |
+
# No need to inform user unless primary fails later
|
688 |
|
689 |
logger.info(f"Scheduling task for user {user.id}, chat {query.message.chat_id}, msg {message_id_to_edit}")
|
690 |
asyncio.create_task( process_summary_task( user_id=user.id, chat_id=query.message.chat_id, message_id_to_edit=message_id_to_edit, url=url, summary_type=summary_type, bot_token=TELEGRAM_TOKEN ), name=f"SummaryTask-{user.id}-{message_id_to_edit}" )
|
|
|
748 |
logger.info("ASGI Lifespan: Shutdown complete.")
|
749 |
|
750 |
async def health_check(request: Request) -> PlainTextResponse:
|
751 |
+
global OPENROUTER_MODEL, GEMINI_MODEL, APIFY_ACTOR_ID, _apify_token_exists, _gemini_primary_enabled, _openrouter_fallback_enabled
|
752 |
bot_status = "Not Initialized"
|
753 |
if ptb_app and ptb_app.bot:
|
754 |
try:
|
755 |
if ptb_app.running: bot_info = await ptb_app.bot.get_me(); bot_status = f"Running (@{bot_info.username})"
|
756 |
else: bot_status = "Initialized/Not running"
|
757 |
except Exception as e: bot_status = f"Error checking status: {e}"
|
758 |
+
return PlainTextResponse( f"TG Bot Summarizer - Status: {bot_status}\n" f"Primary Model: {GEMINI_MODEL if _gemini_primary_enabled else 'N/A (Disabled)'}\n" f"Fallback Model: {OPENROUTER_MODEL if _openrouter_fallback_enabled else 'N/A (Disabled)'}\n" f"Apify Actor: {APIFY_ACTOR_ID if _apify_token_exists else 'N/A (No Token)'}" )
|
759 |
|
760 |
async def telegram_webhook(request: Request) -> Response:
|
761 |
global WEBHOOK_SECRET
|