Update app.py
Browse files
app.py
CHANGED
|
@@ -136,7 +136,7 @@ class MedicalLabBot:
|
|
| 136 |
logger.info("🔄 جاري تهيئة تطبيق التليجرام...")
|
| 137 |
|
| 138 |
# ========== الحل الجذري (DNS) ==========
|
| 139 |
-
logger.info("🔧 إعداد
|
| 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 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
else:
|
| 337 |
-
|
| 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 |
-
|
| 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 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 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 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
|
| 426 |
if best_match and highest_score > 0:
|
| 427 |
-
|
| 428 |
-
|
| 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 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 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 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
else:
|
| 606 |
-
|
| 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 |
-
|
| 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 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
else:
|
| 697 |
-
|
| 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 |
-
|
| 781 |
-
|
| 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 |
-
|
| 850 |
-
|
| 851 |
-
|
| 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 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
|
| 875 |
elif action == "explain_lecture":
|
| 876 |
files_list = await self.get_files_list_text(subject)
|
| 877 |
if files_list.startswith("❌"): # إذا لم توجد ملفات
|
| 878 |
-
|
| 879 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 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 |
+
)
|