Riy777 commited on
Commit
0771ac3
·
verified ·
1 Parent(s): 5034a9d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -93
app.py CHANGED
@@ -136,7 +136,7 @@ class MedicalLabBot:
136
  logger.info("🔄 جاري تهيئة تطبيق التليجرام...")
137
 
138
  # ========== الحل الجذري (DNS) ==========
139
- logger.info("🔧 إعداد عملي HTTP مخصص مع CustomDNSTransport...")
140
  custom_transport = CustomDNSTransport()
141
  custom_client = httpx.AsyncClient(transport=custom_transport)
142
  # ============================================
@@ -328,14 +328,13 @@ class MedicalLabBot:
328
  match = re.search(r'(?:lecture|lec|محاضرة)\s*(\d+)', name_lower)
329
  if not match:
330
  # إذا لم يجد الصيغة السابقة، يبحث عن رقم لوحده في بداية أو نهاية الاسم
331
- match = re.search(r('^(\d+)\s*-|[\s_-](\d+)$', name_lower)
332
- if match:
333
- # يأخذ الرقم الثاني إذا وجد (المجموعة الثانية)، وإلا الأول
334
- lecture_num_str = match.group(2) or match.group(1)
335
- lecture_num = int(lecture_num_str) if lecture_num_str else None
336
  else:
337
- lecture_num = int(match.group(1))
338
-
339
 
340
  # تحديد نوع الملف
341
  if 'lab' in name_lower or 'عملي' in name_lower:
@@ -348,8 +347,7 @@ class MedicalLabBot:
348
  file_type = 'lecture'
349
  # إذا لم يكن أي مما سبق وكان هناك رقم محاضرة، افترضه محاضرة
350
  elif lecture_num is not None:
351
- file_type = 'lecture'
352
-
353
 
354
  return {
355
  'name': file_name,
@@ -395,11 +393,11 @@ class MedicalLabBot:
395
  # 2. محاولة البحث برقم المحاضرة الدقيق (مثل "محاضرة 3", "lec 5")
396
  match_lec_num = re.search(r'(?:lecture|lec|محاضرة)\s*(\d+)', query_lower)
397
  if match_lec_num:
398
- num = int(match_lec_num.group(1))
399
- for file_info in files:
400
- if file_info['lecture_number'] == num:
401
- logger.info(f"Found file by exact lecture number {num}")
402
- return file_info
403
 
404
  # 3. محاولة البحث بجزء من الاسم
405
  best_match = None
@@ -417,16 +415,14 @@ class MedicalLabBot:
417
  best_match = file_info
418
  # إذا تطابق الرقم المستخرج من الاستعلام مع رقم المحاضرة، نعتبره تطابق جيد
419
  elif highest_score > 0 and file_info['lecture_number'] is not None:
420
- num_in_query = re.findall(r'\d+', query_lower)
421
- if num_in_query and file_info['lecture_number'] == int(num_in_query[0]):
422
- logger.info(f"Found file by partial name match with lecture number heuristic: {file_info['name']}")
423
- return file_info
424
-
425
 
426
  if best_match and highest_score > 0:
427
- logger.info(f"Found file by best partial name match: {best_match['name']} (score: {highest_score})")
428
- return best_match
429
-
430
 
431
  logger.warning(f"Could not find file matching query: '{query}' in subject: {subject}")
432
  return None # لم يتم العثور
@@ -452,21 +448,21 @@ class MedicalLabBot:
452
 
453
  if local_path.lower().endswith('.pdf'):
454
  # استخدام PyMuPDF (fitz) لاستخراج أدق
455
- with fitz.open(local_path) as doc:
456
- for page_num, page in enumerate(doc):
457
- text_content += page.get_text("text", sort=True) # محاولة فرز النص
458
- # text_content += f"\n\n--- Page {page_num + 1} ---\n\n" # إضافة فاصل صفحات (اختياري)
459
 
460
  elif local_path.lower().endswith('.txt'):
461
  with open(local_path, 'r', encoding='utf-8', errors='ignore') as f: # تجاهل أخطاء الترميز
462
  text_content = f.read()
463
 
464
  elif local_path.lower().endswith('.docx'):
465
- doc_obj = await asyncio.to_thread(docx.Document, local_path) # تشغيل في thread
466
- full_text = []
467
- for para in doc_obj.paragraphs:
468
- full_text.append(para.text)
469
- text_content = '\n'.join(full_text)
470
 
471
  else:
472
  logger.warning(f"Unsupported file type: {local_path}")
@@ -478,7 +474,7 @@ class MedicalLabBot:
478
  text_content = re.sub(r' +', ' ', text_content).strip() # تقليل المسافات المتتالية
479
 
480
  if len(text_content) < 50:
481
- logger.warning(f"File {file_path} content is very short or empty after extraction.")
482
 
483
  self.file_cache[file_path] = text_content
484
  # logger.debug(f"Content for {file_path}: {text_content[:500]}...") # إلغاء التعليق للتتبع
@@ -507,7 +503,7 @@ class MedicalLabBot:
507
  return f"❌ خطأ في معالجة الملف: {file_name}\n{content}"
508
 
509
  if not content.strip():
510
- return f"❌ المحتوى فارغ للملف: {file_name}"
511
 
512
  memory = self.get_user_memory(user_id)
513
  memory['history'].append({"role": "user", "content": f"اشرح لي النقاط الأساسية في هذه المحاضرة: {file_name}"})
@@ -518,8 +514,7 @@ class MedicalLabBot:
518
  content_snippet = content[:max_content_chars] + "\n\n[... المحتوى مقطوع ...]"
519
  logger.warning(f"Content for {file_name} truncated to {max_content_chars} chars.")
520
  else:
521
- content_snippet = content
522
-
523
 
524
  system_prompt = f"أنت مساعد أكاديمي متخصص في مادة {subject}. مهمتك هي شرح النقاط الأساسية في محتوى المحاضرة المقدم لك. ركز على المفاهيم الجوهرية، التعريفات الهامة، النتائج الرئيسية، وأي معلومات ضرورية لفهم الموضوع. قدم الشرح بطريقة منظمة وواضحة باستخدام نقاط Markdown. تجنب التفاصيل الثانوية غير الضرورية."
525
 
@@ -590,21 +585,20 @@ class MedicalLabBot:
590
  logger.error(f"Failed to get content for question generation from {file_name}. Content: {content[:100]}")
591
  # محاولة اختيار ملف آخر مرة واحدة
592
  if len(files) > 1:
593
- logger.info("Retrying with another file for question generation...")
594
- remaining_files = [f for f in files if f['path'] != file_path]
595
- if remaining_files:
596
- file_info = random.choice(remaining_files)
597
- file_path = file_info['path']
598
- file_name = file_info['name']
599
- content = await self.download_and_extract_content(file_path, subject)
600
- if content.startswith("Error:") or not content.strip():
601
- logger.error(f"Retry failed for question generation from {file_name}.")
602
- return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة من ملفات المادة."
603
- else:
604
- return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة من ملفات المادة."
605
  else:
606
- return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة من ملفات المادة."
607
-
608
 
609
  max_content_chars = 7000
610
  content_snippet = content[:max_content_chars] if len(content) > max_content_chars else content
@@ -633,7 +627,7 @@ class MedicalLabBot:
633
  summary_file = next((f for f in files if f['type'] == 'lecture'), None)
634
  # إذا لم يوجد محاضرة أيضاً، اختر أول ملف
635
  if not summary_file:
636
- summary_file = files[0]
637
 
638
  file_path = summary_file['path']
639
  file_name = summary_file['name']
@@ -690,11 +684,11 @@ class MedicalLabBot:
690
  try:
691
  await update.callback_query.edit_message_text(welcome_text, reply_markup=reply_markup)
692
  except Exception as e:
693
- logger.error(f"Error editing message in start: {e}")
694
- # إذا فشل التعديل (ربما الرسالة قديمة), أرسل رسالة جديدة
695
- await update.effective_message.reply_text(welcome_text, reply_markup=reply_markup)
696
  else:
697
- await update.message.reply_text(welcome_text, reply_markup=reply_markup)
698
 
699
  return SELECTING_SUBJECT
700
 
@@ -713,7 +707,6 @@ class MedicalLabBot:
713
  keyboard.append(row)
714
  row = []
715
 
716
-
717
  keyboard.append([InlineKeyboardButton("🔄 تحديث قائمة المواد", callback_data="refresh_materials")])
718
  keyboard.append([InlineKeyboardButton("❓ مساعدة", callback_data="general_help")])
719
  return keyboard
@@ -777,8 +770,8 @@ class MedicalLabBot:
777
 
778
  subject = context.user_data.get('current_subject')
779
  if not subject or subject not in self.available_materials:
780
- logger.warning("Subject context lost or invalid in handle_back_actions. Returning to main menu.")
781
- return await self.start(update, context) # إذا ضاع السياق، عد للبداية
782
 
783
  subject_files = self.available_materials[subject]['files']
784
  subject_name = subject.replace('_', ' ').title()
@@ -844,39 +837,36 @@ class MedicalLabBot:
844
  subject = context.user_data.get('current_subject')
845
  logger.info(f"User {user_id} selected action: {action} for subject: {subject}")
846
 
847
-
848
  if not subject or subject not in self.available_materials:
849
- logger.error("Subject context lost or invalid in handle_action_selection.")
850
- await query.edit_message_text("❌ حدث خطأ في السياق. يرجى البدء من جديد.")
851
- return await self.start(update, context)
852
 
853
  # زر العودة للقائمة الرئيسية
854
  back_button = InlineKeyboardButton("🔙 رجوع", callback_data="back_to_actions")
855
  keyboard_with_back = [[back_button]]
856
  reply_markup_back = InlineKeyboardMarkup(keyboard_with_back)
857
 
858
-
859
  if action == "main_menu":
860
  return await self.start(update, context)
861
 
862
  elif action == "browse_files":
863
- files_text = await self.get_files_list_text(subject)
864
- keyboard = [
865
- # يمكن إضافة زر لشرح ملف من هنا مباشرة إذا أردت
866
- [InlineKeyboardButton("📖 طلب شرح ملف محدد", callback_data="explain_lecture")],
867
- [back_button]
868
- ]
869
- reply_markup = InlineKeyboardMarkup(keyboard)
870
- await query.edit_message_text(files_text, reply_markup=reply_markup, parse_mode='Markdown') # إضافة parse_mode
871
- # البقاء في نفس الحالة لعرض الخيارات مرة أخرى
872
- return SELECTING_ACTION
873
-
874
 
875
  elif action == "explain_lecture":
876
  files_list = await self.get_files_list_text(subject)
877
  if files_list.startswith("❌"): # إذا لم توجد ملفات
878
- await query.edit_message_text(files_list, reply_markup=reply_markup_back)
879
- return SELECTING_ACTION
880
 
881
  await query.edit_message_text(
882
  f"📖 **شرح محاضرة**\n\n"
@@ -917,13 +907,11 @@ class MedicalLabBot:
917
  await query.message.reply_text("عذراً، لم أتعرف على هذا الخيار.")
918
  return SELECTING_ACTION
919
 
920
-
921
  async def browse_available_files(self, query, context):
922
  """عرض الملفات المتاحة للمادة - تم دمجه في handle_action_selection"""
923
  # هذه الدالة لم تعد مستخدمة بشكل مباشر، الكود موجود في handle_action_selection
924
  pass
925
 
926
-
927
  async def get_files_list_text(self, subject):
928
  """إنشاء نص لقائمة الملفات"""
929
  if subject not in self.available_materials:
@@ -962,11 +950,10 @@ class MedicalLabBot:
962
 
963
  return files_text
964
 
965
-
966
  async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
967
  """معالجة الرسائل النصية من المستخدم"""
968
  if not update.message or not update.message.text:
969
- return # تجاهل الرسائل غير النصية أو الفارغة
970
 
971
  user_message = update.message.text
972
  user_id = update.effective_user.id
@@ -974,7 +961,6 @@ class MedicalLabBot:
974
  subject = context.user_data.get('current_subject', 'general')
975
  logger.info(f"User {user_id} sent message: '{user_message}' | waiting_for: {waiting_for} | subject: {subject}")
976
 
977
-
978
  await update.message.chat.send_action(action="typing")
979
 
980
  response = "عذراً، لم أفهم طلبك. هل يمكنك توضيحه؟" # رد افتراضي
@@ -996,14 +982,14 @@ class MedicalLabBot:
996
 
997
  # إذا نجح الرد، أرسل قائمة الخيارات التالية
998
  if next_state == SELECTING_ACTION:
999
- context.user_data['waiting_for'] = None # إنهاء حالة الانتظار
1000
- keyboard = [
1001
- [InlineKeyboardButton("🔄 طرح سؤال آخر", callback_data="more_questions")],
1002
- [InlineKeyboardButton("📚 تغيير المادة", callback_data="change_subject")],
1003
- [InlineKeyboardButton("🏠 القائمة الرئيسية", callback_data="main_menu")]
1004
- ]
1005
- reply_markup = InlineKeyboardMarkup(keyboard)
1006
- await update.message.reply_text("هل تحتاج مساعدة أخرى؟", reply_markup=reply_markup)
1007
 
1008
  return next_state # العودة للحالة المناسبة (إما خيارات أو انتظار سؤال آخر)
1009
 
@@ -1013,7 +999,6 @@ class MedicalLabBot:
1013
  context.user_data['waiting_for'] = None # إنهاء الانتظار عند حدوث خطأ فادح
1014
  return SELECTING_ACTION # العودة لقائمة الخيارات كحل احتياطي آمن
1015
 
1016
-
1017
  # ========== إنشاء كائن البوت ==========
1018
  bot = MedicalLabBot()
1019
 
@@ -1038,7 +1023,6 @@ async def root():
1038
  status_color = "#dc3545" # أحمر
1039
  init_details = "فشل الاتصال بـ Telegram أو ضبط الويب هوك بعد عدة محاولات."
1040
 
1041
-
1042
  return f"""
1043
  <html>
1044
  <head>
@@ -1140,7 +1124,6 @@ async def health_check():
1140
  service_status = "unhealthy_failed_init"
1141
  status_code = 500 # Internal Server Error
1142
 
1143
-
1144
  response_payload = {
1145
  "status": service_status,
1146
  "service": "medical-lab-bot",
@@ -1165,4 +1148,4 @@ if __name__ == "__main__":
1165
  port=port,
1166
  log_level=log_level,
1167
  reload=False # تعطيل إعادة التحميل التلقائي في الإنتاج
1168
- )
 
136
  logger.info("🔄 جاري تهيئة تطبيق التليجرام...")
137
 
138
  # ========== الحل الجذري (DNS) ==========
139
+ logger.info("🔧 إعداد عميل HTTP مخصص مع CustomDNSTransport...")
140
  custom_transport = CustomDNSTransport()
141
  custom_client = httpx.AsyncClient(transport=custom_transport)
142
  # ============================================
 
328
  match = re.search(r'(?:lecture|lec|محاضرة)\s*(\d+)', name_lower)
329
  if not match:
330
  # إذا لم يجد الصيغة السابقة، يبحث عن رقم لوحده في بداية أو نهاية الاسم
331
+ match = re.search(r'^(\d+)\s*-|[\s_-](\d+)$', name_lower)
332
+ if match:
333
+ # يأخذ الرقم الثاني إذا وجد (المجموعة الثانية)، وإلا الأول
334
+ lecture_num_str = match.group(2) or match.group(1)
335
+ lecture_num = int(lecture_num_str) if lecture_num_str else None
336
  else:
337
+ lecture_num = int(match.group(1))
 
338
 
339
  # تحديد نوع الملف
340
  if 'lab' in name_lower or 'عملي' in name_lower:
 
347
  file_type = 'lecture'
348
  # إذا لم يكن أي مما سبق وكان هناك رقم محاضرة، افترضه محاضرة
349
  elif lecture_num is not None:
350
+ file_type = 'lecture'
 
351
 
352
  return {
353
  'name': file_name,
 
393
  # 2. محاولة البحث برقم المحاضرة الدقيق (مثل "محاضرة 3", "lec 5")
394
  match_lec_num = re.search(r'(?:lecture|lec|محاضرة)\s*(\d+)', query_lower)
395
  if match_lec_num:
396
+ num = int(match_lec_num.group(1))
397
+ for file_info in files:
398
+ if file_info['lecture_number'] == num:
399
+ logger.info(f"Found file by exact lecture number {num}")
400
+ return file_info
401
 
402
  # 3. محاولة البحث بجزء من الاسم
403
  best_match = None
 
415
  best_match = file_info
416
  # إذا تطابق الرقم المستخرج من الاستعلام مع رقم المحاضرة، نعتبره تطابق جيد
417
  elif highest_score > 0 and file_info['lecture_number'] is not None:
418
+ num_in_query = re.findall(r'\d+', query_lower)
419
+ if num_in_query and file_info['lecture_number'] == int(num_in_query[0]):
420
+ logger.info(f"Found file by partial name match with lecture number heuristic: {file_info['name']}")
421
+ return file_info
 
422
 
423
  if best_match and highest_score > 0:
424
+ logger.info(f"Found file by best partial name match: {best_match['name']} (score: {highest_score})")
425
+ return best_match
 
426
 
427
  logger.warning(f"Could not find file matching query: '{query}' in subject: {subject}")
428
  return None # لم يتم العثور
 
448
 
449
  if local_path.lower().endswith('.pdf'):
450
  # استخدام PyMuPDF (fitz) لاستخراج أدق
451
+ with fitz.open(local_path) as doc:
452
+ for page_num, page in enumerate(doc):
453
+ text_content += page.get_text("text", sort=True) # محاولة فرز النص
454
+ # text_content += f"\n\n--- Page {page_num + 1} ---\n\n" # إضافة فاصل صفحات (اختياري)
455
 
456
  elif local_path.lower().endswith('.txt'):
457
  with open(local_path, 'r', encoding='utf-8', errors='ignore') as f: # تجاهل أخطاء الترميز
458
  text_content = f.read()
459
 
460
  elif local_path.lower().endswith('.docx'):
461
+ doc_obj = await asyncio.to_thread(docx.Document, local_path) # تشغيل في thread
462
+ full_text = []
463
+ for para in doc_obj.paragraphs:
464
+ full_text.append(para.text)
465
+ text_content = '\n'.join(full_text)
466
 
467
  else:
468
  logger.warning(f"Unsupported file type: {local_path}")
 
474
  text_content = re.sub(r' +', ' ', text_content).strip() # تقليل المسافات المتتالية
475
 
476
  if len(text_content) < 50:
477
+ logger.warning(f"File {file_path} content is very short or empty after extraction.")
478
 
479
  self.file_cache[file_path] = text_content
480
  # logger.debug(f"Content for {file_path}: {text_content[:500]}...") # إلغاء التعليق للتتبع
 
503
  return f"❌ خطأ في معالجة الملف: {file_name}\n{content}"
504
 
505
  if not content.strip():
506
+ return f"❌ المحتوى فارغ للملف: {file_name}"
507
 
508
  memory = self.get_user_memory(user_id)
509
  memory['history'].append({"role": "user", "content": f"اشرح لي النقاط الأساسية في هذه المحاضرة: {file_name}"})
 
514
  content_snippet = content[:max_content_chars] + "\n\n[... المحتوى مقطوع ...]"
515
  logger.warning(f"Content for {file_name} truncated to {max_content_chars} chars.")
516
  else:
517
+ content_snippet = content
 
518
 
519
  system_prompt = f"أنت مساعد أكاديمي متخصص في مادة {subject}. مهمتك هي شرح النقاط الأساسية في محتوى المحاضرة المقدم لك. ركز على المفاهيم الجوهرية، التعريفات الهامة، النتائج الرئيسية، وأي معلومات ضرورية لفهم الموضوع. قدم الشرح بطريقة منظمة وواضحة باستخدام نقاط Markdown. تجنب التفاصيل الثانوية غير الضرورية."
520
 
 
585
  logger.error(f"Failed to get content for question generation from {file_name}. Content: {content[:100]}")
586
  # محاولة اختيار ملف آخر مرة واحدة
587
  if len(files) > 1:
588
+ logger.info("Retrying with another file for question generation...")
589
+ remaining_files = [f for f in files if f['path'] != file_path]
590
+ if remaining_files:
591
+ file_info = random.choice(remaining_files)
592
+ file_path = file_info['path']
593
+ file_name = file_info['name']
594
+ content = await self.download_and_extract_content(file_path, subject)
595
+ if content.startswith("Error:") or not content.strip():
596
+ logger.error(f"Retry failed for question generation from {file_name}.")
597
+ return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة من ملفات المادة."
598
+ else:
599
+ return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة من ملفات المادة."
600
  else:
601
+ return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة من ملفات المادة."
 
602
 
603
  max_content_chars = 7000
604
  content_snippet = content[:max_content_chars] if len(content) > max_content_chars else content
 
627
  summary_file = next((f for f in files if f['type'] == 'lecture'), None)
628
  # إذا لم يوجد محاضرة أيضاً، اختر أول ملف
629
  if not summary_file:
630
+ summary_file = files[0]
631
 
632
  file_path = summary_file['path']
633
  file_name = summary_file['name']
 
684
  try:
685
  await update.callback_query.edit_message_text(welcome_text, reply_markup=reply_markup)
686
  except Exception as e:
687
+ logger.error(f"Error editing message in start: {e}")
688
+ # إذا فشل التعديل (ربما الرسالة قديمة), أرسل رسالة جديدة
689
+ await update.effective_message.reply_text(welcome_text, reply_markup=reply_markup)
690
  else:
691
+ await update.message.reply_text(welcome_text, reply_markup=reply_markup)
692
 
693
  return SELECTING_SUBJECT
694
 
 
707
  keyboard.append(row)
708
  row = []
709
 
 
710
  keyboard.append([InlineKeyboardButton("🔄 تحديث قائمة المواد", callback_data="refresh_materials")])
711
  keyboard.append([InlineKeyboardButton("❓ مساعدة", callback_data="general_help")])
712
  return keyboard
 
770
 
771
  subject = context.user_data.get('current_subject')
772
  if not subject or subject not in self.available_materials:
773
+ logger.warning("Subject context lost or invalid in handle_back_actions. Returning to main menu.")
774
+ return await self.start(update, context) # إذا ضاع السياق، عد للبداية
775
 
776
  subject_files = self.available_materials[subject]['files']
777
  subject_name = subject.replace('_', ' ').title()
 
837
  subject = context.user_data.get('current_subject')
838
  logger.info(f"User {user_id} selected action: {action} for subject: {subject}")
839
 
 
840
  if not subject or subject not in self.available_materials:
841
+ logger.error("Subject context lost or invalid in handle_action_selection.")
842
+ await query.edit_message_text("❌ حدث خطأ في السياق. يرجى البدء من جديد.")
843
+ return await self.start(update, context)
844
 
845
  # زر العودة للقائمة الرئيسية
846
  back_button = InlineKeyboardButton("🔙 رجوع", callback_data="back_to_actions")
847
  keyboard_with_back = [[back_button]]
848
  reply_markup_back = InlineKeyboardMarkup(keyboard_with_back)
849
 
 
850
  if action == "main_menu":
851
  return await self.start(update, context)
852
 
853
  elif action == "browse_files":
854
+ files_text = await self.get_files_list_text(subject)
855
+ keyboard = [
856
+ # يمكن إضافة زر لشرح ملف من هنا مباشرة إذا أردت
857
+ [InlineKeyboardButton("📖 طلب شرح ملف محدد", callback_data="explain_lecture")],
858
+ [back_button]
859
+ ]
860
+ reply_markup = InlineKeyboardMarkup(keyboard)
861
+ await query.edit_message_text(files_text, reply_markup=reply_markup, parse_mode='Markdown') # إضافة parse_mode
862
+ # البقاء في نفس الحالة لعرض الخيارات مرة أخرى
863
+ return SELECTING_ACTION
 
864
 
865
  elif action == "explain_lecture":
866
  files_list = await self.get_files_list_text(subject)
867
  if files_list.startswith("❌"): # إذا لم توجد ملفات
868
+ await query.edit_message_text(files_list, reply_markup=reply_markup_back)
869
+ return SELECTING_ACTION
870
 
871
  await query.edit_message_text(
872
  f"📖 **شرح محاضرة**\n\n"
 
907
  await query.message.reply_text("عذراً، لم أتعرف على هذا الخيار.")
908
  return SELECTING_ACTION
909
 
 
910
  async def browse_available_files(self, query, context):
911
  """عرض الملفات المتاحة للمادة - تم دمجه في handle_action_selection"""
912
  # هذه الدالة لم تعد مستخدمة بشكل مباشر، الكود موجود في handle_action_selection
913
  pass
914
 
 
915
  async def get_files_list_text(self, subject):
916
  """إنشاء نص لقائمة الملفات"""
917
  if subject not in self.available_materials:
 
950
 
951
  return files_text
952
 
 
953
  async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
954
  """معالجة الرسائل النصية من المستخدم"""
955
  if not update.message or not update.message.text:
956
+ return # تجاهل الرسائل غير النصية أو الفارغة
957
 
958
  user_message = update.message.text
959
  user_id = update.effective_user.id
 
961
  subject = context.user_data.get('current_subject', 'general')
962
  logger.info(f"User {user_id} sent message: '{user_message}' | waiting_for: {waiting_for} | subject: {subject}")
963
 
 
964
  await update.message.chat.send_action(action="typing")
965
 
966
  response = "عذراً، لم أفهم طلبك. هل يمكنك توضيحه؟" # رد افتراضي
 
982
 
983
  # إذا نجح الرد، أرسل قائمة الخيارات التالية
984
  if next_state == SELECTING_ACTION:
985
+ context.user_data['waiting_for'] = None # إنهاء حالة الانتظار
986
+ keyboard = [
987
+ [InlineKeyboardButton("🔄 طرح سؤال آخر", callback_data="more_questions")],
988
+ [InlineKeyboardButton("📚 تغيير المادة", callback_data="change_subject")],
989
+ [InlineKeyboardButton("🏠 القائمة الرئيسية", callback_data="main_menu")]
990
+ ]
991
+ reply_markup = InlineKeyboardMarkup(keyboard)
992
+ await update.message.reply_text("هل تحتاج مساعدة أخرى؟", reply_markup=reply_markup)
993
 
994
  return next_state # العودة للحالة المناسبة (إما خيارات أو انتظار سؤال آخر)
995
 
 
999
  context.user_data['waiting_for'] = None # إنهاء الانتظار عند حدوث خطأ فادح
1000
  return SELECTING_ACTION # العودة لقائمة الخيارات كحل احتياطي آمن
1001
 
 
1002
  # ========== إنشاء كائن البوت ==========
1003
  bot = MedicalLabBot()
1004
 
 
1023
  status_color = "#dc3545" # أحمر
1024
  init_details = "فشل الاتصال بـ Telegram أو ضبط الويب هوك بعد عدة محاولات."
1025
 
 
1026
  return f"""
1027
  <html>
1028
  <head>
 
1124
  service_status = "unhealthy_failed_init"
1125
  status_code = 500 # Internal Server Error
1126
 
 
1127
  response_payload = {
1128
  "status": service_status,
1129
  "service": "medical-lab-bot",
 
1148
  port=port,
1149
  log_level=log_level,
1150
  reload=False # تعطيل إعادة التحميل التلقائي في الإنتاج
1151
+ )