fmab777 commited on
Commit
b5cdfb8
Β·
verified Β·
1 Parent(s): c2c94e2

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +205 -96
main.py CHANGED
@@ -1,4 +1,4 @@
1
- # main.py (Correcting SyntaxError at line 424)
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 fallback disabled.")
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
- OPENROUTER_MODEL = os.environ.get("OPENROUTER_MODEL", "deepseek/deepseek-chat-v3-0324:free")
 
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 OPENROUTER_API_KEY: logger.error("❌ ERROR: OPENROUTER_API_KEY not found. Primary summarization will fail.")
96
- _gemini_fallback_enabled = _gemini_available and bool(GEMINI_API_KEY)
97
- if _gemini_fallback_enabled and not GEMINI_API_KEY: logger.warning("⚠️ WARNING: GEMINI_API_KEY found in env but value seems empty. Fallback disabled.")
98
- elif not _gemini_fallback_enabled and _gemini_available : logger.warning("⚠️ WARNING: GEMINI_API_KEY not found. Fallback disabled.")
 
 
 
 
 
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 OpenRouter Model (Primary): {OPENROUTER_MODEL}")
108
- if _gemini_fallback_enabled: logger.info(f"Using Gemini Model (Fallback): {GEMINI_MODEL}")
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 _gemini_fallback_enabled:
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}"); _gemini_fallback_enabled = False
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
- MAX_INPUT_LENGTH = 1000000
349
- if len(text) > MAX_INPUT_LENGTH: logger.warning(f"[Gemini Fallback] Input length ({len(text)}) exceeds limit ({MAX_INPUT_LENGTH}). Truncating."); text = text[:MAX_INPUT_LENGTH] + "... (Content truncated)"
 
 
 
 
350
  full_prompt = f"{prompt}\n\n{text}"
351
- # *** FIX: Set all safety settings to BLOCK_NONE ***
 
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 Fallback] Using safety settings: {safety_settings}")
362
 
363
  try:
364
- logger.debug(f"[Gemini Fallback] Initializing model {GEMINI_MODEL}")
365
  model = genai.GenerativeModel(GEMINI_MODEL)
366
- logger.info(f"[Gemini Fallback] Sending request to Gemini ({GEMINI_MODEL})...")
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 Fallback] Received response from 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
- if summary: logger.info(f"[Gemini Fallback] Success generating summary. Output len: {len(summary)}"); return summary.strip().replace('_', r'\_').replace('*', r'\*').replace('[', r'\[').replace('`', r'\`')
379
- else: logger.warning(f"[Gemini Fallback] Gemini returned empty summary or content was blocked. Finish reason: {response.candidates[0].finish_reason if response.candidates else 'N/A'}"); return "Sorry, the fallback AI model did not provide a summary (possibly due to content filters)."
380
- except Exception as e: logger.error(f"[Gemini Fallback] Unexpected error during Gemini API call: {e}", exc_info=True); return "Sorry, an unexpected error occurred while using the fallback AI service."
 
381
 
 
 
 
 
 
382
 
383
- async def generate_summary(text: str, summary_type: str) -> str:
384
- """Generates summary using OpenRouter (Primary) with Gemini fallback on 10s ReadTimeout."""
385
- global OPENROUTER_API_KEY, OPENROUTER_MODEL, _gemini_fallback_enabled
386
- logger.info(f"[Primary Summary] Generating {summary_type} summary using {OPENROUTER_MODEL}. Input length: {len(text)}")
387
- if not OPENROUTER_API_KEY: logger.error("[Primary Summary] OpenRouter key missing."); return "Error: AI model configuration key missing."
 
 
 
 
 
 
 
 
 
 
 
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
- MAX_INPUT_LENGTH = 500000
407
- if len(text) > MAX_INPUT_LENGTH: logger.warning(f"[Primary Summary] Input length ({len(text)}) exceeds limit ({MAX_INPUT_LENGTH}). Truncating."); text = text[:MAX_INPUT_LENGTH] + "... (Content truncated)"
 
 
 
 
408
  full_prompt = f"{prompt}\n\n{text}"
409
- headers = { "Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json" }; payload = { "model": OPENROUTER_MODEL, "messages": [{"role": "user", "content": full_prompt}] }; openrouter_api_endpoint = "https://openrouter.ai/api/v1/chat/completions"
410
- api_timeouts = httpx.Timeout(15.0, read=10.0, write=15.0, pool=60.0)
 
 
 
 
411
  response = None
412
 
413
  try:
414
  async with httpx.AsyncClient(timeout=api_timeouts) as client:
415
- logger.info(f"[Primary Summary] Sending request to OpenRouter ({OPENROUTER_MODEL}) with read timeout {api_timeouts.read}s...")
416
- try:
417
- response = await client.post(openrouter_api_endpoint, headers=headers, json=payload)
418
- if response: logger.info(f"[Primary Summary] Received response from OpenRouter. Status code: {response.status_code}")
419
- else: logger.error("[Primary Summary] No response from OpenRouter (unexpected)."); return "Sorry, primary AI service failed unexpectedly."
420
-
421
- if response.status_code == 200:
422
- try:
423
- data = response.json()
424
- if data.get("choices") and isinstance(data["choices"], list) and len(data["choices"]) > 0:
425
- message = data["choices"][0].get("message")
426
- if message and isinstance(message, dict):
427
- summary = message.get("content")
428
- if summary: logger.info(f"[Primary Summary] Success via OpenRouter. Output len: {len(summary)}"); return summary.strip().replace('_', r'\_').replace('*', r'\*').replace('[', r'\[').replace('`', r'\`')
429
- else: logger.warning(f"[Primary Summary] OpenRouter success but content empty. Resp: {data}"); return "Sorry, the primary AI model returned an empty summary."
430
- else: logger.error(f"[Primary Summary] Unexpected message structure: {message}. Full: {data}"); return "Sorry, could not parse primary AI response (format)."
431
- else: logger.error(f"[Primary Summary] Unexpected choices structure: {data.get('choices')}. Full: {data}"); return "Sorry, could not parse primary AI response (choices)."
432
- except json.JSONDecodeError: logger.error(f"[Primary Summary] Failed JSON decode OpenRouter. Status:{response.status_code}. Resp:{response.text[:500]}"); return "Sorry, failed to understand primary AI response."
433
- except Exception as e: logger.error(f"[Primary Summary] Error processing OpenRouter success response: {e}", exc_info=True); return "Sorry, error processing primary AI response."
434
- elif response.status_code == 401: logger.error("[Primary Summary] OpenRouter API key invalid (401)."); return "Error: Primary AI model configuration key is invalid."
435
- elif response.status_code == 402: logger.error("[Primary Summary] OpenRouter Payment Required (402)."); return "Sorry, primary AI service limits/payment issue."
436
- elif response.status_code == 429: logger.warning("[Primary Summary] OpenRouter Rate Limit Exceeded (429)."); return "Sorry, primary AI model is busy. Try again."
437
- elif response.status_code == 500: logger.error(f"[Primary Summary] OpenRouter Internal Server Error (500). Resp:{response.text[:500]}"); return "Sorry, primary AI service internal error."
438
- else:
439
- error_info = "";
440
- try: # Correct Indentation
441
- error_info = response.json().get("error", {}).get("message", "")
442
- except Exception: pass
443
- logger.error(f"[Primary Summary] Unexpected status {response.status_code} from OpenRouter. Error: '{error_info}' Resp:{response.text[:500]}");
444
- return f"Sorry, primary AI service returned unexpected status ({response.status_code})."
445
-
446
- except httpx.ReadTimeout:
447
- logger.warning(f"[Primary Summary] Read Timeout ({api_timeouts.read}s) waiting for OpenRouter. Attempting Gemini fallback...")
448
- if _gemini_fallback_enabled: return await generate_summary_gemini(text, summary_type)
449
- else: logger.error("[Fallback Attempt] Gemini fallback skipped (disabled or key missing)."); return f"Sorry, the primary AI service timed out after {api_timeouts.read} seconds, and the fallback service is not available."
450
- except httpx.TimeoutException as e: logger.error(f"[Primary Summary] Timeout error ({type(e)}) connecting/writing to OpenRouter API: {e}"); return "Sorry, the request to the primary AI model timed out. Please try again."
451
-
452
- except httpx.RequestError as e: logger.error(f"[Primary Summary] Request error connecting to OpenRouter API: {e}"); return "Sorry, there was an error connecting to the primary AI model service."
 
 
 
 
 
 
 
 
 
 
 
 
453
  except Exception as e:
454
- logger.error(f"[Primary Summary] Unexpected error in generate_summary (Outer try): {e}", exc_info=True)
455
- if response: logger.error(f"--> Last OpenRouter response status before error: {response.status_code}")
456
- return "Sorry, an unexpected error occurred while trying to generate the summary."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 handles the fallback internally
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 OPENROUTER_API_KEY:
575
- logger.error("OpenRouter key missing!")
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, _gemini_fallback_enabled
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: {OPENROUTER_MODEL}\n" f"Fallback Model: {GEMINI_MODEL if _gemini_fallback_enabled else 'N/A (Disabled)'}\n" f"Apify Actor: {APIFY_ACTOR_ID if _apify_token_exists else 'N/A (No Token)'}" )
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