Riy777 commited on
Commit
3db389f
·
verified ·
1 Parent(s): 8f7ce57

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +518 -313
app.py CHANGED
@@ -63,32 +63,39 @@ class CustomDNSTransport(AsyncHTTPTransport):
63
  async def handle_async_request(self, request: Request) -> Response:
64
  try:
65
  host = request.url.host
66
-
67
  # التحقق إذا كان Host هو أصلاً IP
68
  is_ip = True
69
  try:
 
70
  parts = host.split('.')
71
- if len(parts) != 4 or not all(p.isdigit() for p in parts):
72
- is_ip = False
 
73
  except Exception:
74
  is_ip = False
75
 
76
  if is_ip:
77
- logger.info(f"Host {host} is already an IP. Skipping resolution.")
 
78
  else:
79
  logger.info(f"🔧 Resolving host: {host}")
80
  result = await self.resolver.resolve(host, 'A')
81
  ip = result[0].address
82
  logger.info(f"✅ Resolved {host} to {ip}")
83
-
84
  # تعيين SNI (مهم جداً لـ SSL/HTTPS)
85
- request.extensions["sni_hostname"] = host
86
  # تحديث الـ URL لاستخدام الـ IP
87
  request.url = request.url.copy_with(host=ip)
88
 
 
 
 
 
89
  except Exception as e:
90
  logger.error(f"❌ DNS resolution failed for {host}: {e}. Trying request with original host...")
91
- # إذا فشل الحل، اسمح للطلب بالاستمرار مع اسم المضيف الأصلي
92
  pass
93
 
94
  # استدعاء المعالج الأصلي (الأب) لإكمال الطلب
@@ -118,15 +125,15 @@ class MedicalLabBot:
118
  self.is_initialized = False
119
  self.initialization_status = "pending" # تتبع حالة التهيئة
120
  self.load_all_materials()
121
-
122
  async def initialize_application(self):
123
  """تهيئة تطبيق التليجرام بشكل غير متزامن مع حل DNS المخصص"""
124
  try:
125
  if self.is_initialized:
126
  return True
127
-
128
  logger.info("🔄 جاري تهيئة تطبيق التليجرام...")
129
-
130
  # ========== الحل الجذري (DNS) ==========
131
  logger.info("🔧 إعداد عميل HTTP مخصص مع CustomDNSTransport...")
132
  custom_transport = CustomDNSTransport()
@@ -140,17 +147,17 @@ class MedicalLabBot:
140
  .build()
141
  )
142
  await self.setup_handlers()
143
-
144
- max_retries = 3
145
  retry_delay = 5
146
-
147
  for attempt in range(max_retries):
148
  try:
149
  logger.info(f"🚀 محاولة تهيئة الاتصال بـ Telegram (محاولة {attempt + 1}/{max_retries})...")
150
  # 1. الاتصال بـ Telegram (getMe)
151
- await self.application.initialize()
152
  logger.info("✅ تم تهيئة الاتصال بـ Telegram بنجاح.")
153
-
154
  # 2. ضبط الـ Webhook (فقط بعد نجاح الاتصال)
155
  if SPACE_URL:
156
  webhook_url = f"{SPACE_URL.rstrip('/')}/telegram"
@@ -163,13 +170,13 @@ class MedicalLabBot:
163
  logger.info(f"✅ Webhook set to: {webhook_url}")
164
  else:
165
  logger.warning("⚠️ SPACE_URL not set. Webhook cannot be set.")
166
-
167
  # 3. ضبط الحالة على "ناجح"
168
  self.is_initialized = True
169
  self.initialization_status = "success"
170
  logger.info("✅✅✅ التطبيق جاهز لاستقبال الطلبات.")
171
  return True # نجح، اخرج من الدالة
172
-
173
  except Exception as e:
174
  logger.warning(f"⚠️ فشلت محاولة التهيئة {attempt + 1}: {e}")
175
  if attempt < max_retries - 1:
@@ -177,7 +184,7 @@ class MedicalLabBot:
177
  await asyncio.sleep(retry_delay)
178
  else:
179
  logger.error(f"❌ فشل تهيئة التطبيق نهائياً بعد {max_retries} محاولات (حتى مع DNS المخصص).")
180
-
181
  # فشل بعد كل المحاولات
182
  self.is_initialized = False
183
  self.initialization_status = "failed"
@@ -188,7 +195,7 @@ class MedicalLabBot:
188
  self.is_initialized = False
189
  self.initialization_status = "failed"
190
  return False
191
-
192
  async def setup_handlers(self):
193
  """إعداد معالجات التليجرام"""
194
  # إعداد Conversation Handler مع إعدادات محسنة
@@ -216,14 +223,14 @@ class MedicalLabBot:
216
  persistent=False,
217
  per_message=False
218
  )
219
-
220
  # إضافة الـ handlers
221
  self.application.add_handler(conv_handler)
222
-
223
  # إضافة handlers منفصلة للاستعلامات العامة
224
  self.application.add_handler(CallbackQueryHandler(self.handle_more_questions, pattern='^more_questions$'))
225
  self.application.add_handler(CallbackQueryHandler(self.handle_change_subject, pattern='^change_subject$'))
226
-
227
  logger.info("✅ تم إعداد معالجات التليجرام")
228
 
229
  def load_all_materials(self):
@@ -231,28 +238,29 @@ class MedicalLabBot:
231
  try:
232
  logger.info("جاري تحميل قائمة المواد من Hugging Face...")
233
  all_files = list_repo_files(repo_id=REPO_ID, repo_type="dataset")
234
-
235
  materials = {}
236
-
237
  for file_path in all_files:
238
  try:
239
  path_parts = file_path.split('/')
240
-
241
  if len(path_parts) >= 2:
242
  subject = path_parts[0]
243
  file_name = path_parts[-1]
244
-
245
  if subject not in materials:
246
  materials[subject] = {
247
  'files': [],
248
  'file_details': {}
249
  }
250
-
251
  file_info = self.extract_file_info(file_name, file_path)
252
  materials[subject]['files'].append(file_info)
253
  materials[subject]['file_details'][file_name] = file_info
254
-
255
  else:
 
256
  if 'general' not in materials:
257
  materials['general'] = {
258
  'files': [],
@@ -261,14 +269,19 @@ class MedicalLabBot:
261
  file_info = self.extract_file_info(file_path, file_path)
262
  materials['general']['files'].append(file_info)
263
  materials['general']['file_details'][file_path] = file_info
264
-
265
  except Exception as e:
266
  logger.error(f"خطأ في معالجة الملف {file_path}: {e}")
267
  continue
268
-
 
 
 
 
 
269
  self.available_materials = materials
270
  logger.info(f"✅ تم تحميل {len(materials)} مادة بنجاح")
271
-
272
  except Exception as e:
273
  logger.error(f"❌ خطأ في تحميل المواد: {e}")
274
  self.available_materials = {'Biochemistry': {'files': [], 'file_details': {}}} # مادة افتراضية عند الفشل
@@ -285,9 +298,9 @@ class MedicalLabBot:
285
  """عرض رسالة المساعدة"""
286
  help_text = """
287
  ❓ **مساعدة** ❓
288
-
289
  هذا البوت مصمم لمساعدتك في دراسة مواد المختبرات الطبية.
290
-
291
  1. **ابدأ** باختيار مادة من القائمة الرئيسية.
292
  2. **اختر الخدمة:**
293
  * **شرح محاضرة:** يعطيك ملخص للملف الذي تختاره.
@@ -299,7 +312,7 @@ class MedicalLabBot:
299
  """
300
  keyboard = [[InlineKeyboardButton("🔙 العودة للقائمة الرئيسية", callback_data="main_menu")]]
301
  reply_markup = InlineKeyboardMarkup(keyboard)
302
-
303
  await query.edit_message_text(help_text, reply_markup=reply_markup)
304
  return SELECTING_SUBJECT
305
 
@@ -308,22 +321,35 @@ class MedicalLabBot:
308
  name_lower = file_name.lower()
309
  lecture_num = None
310
  file_type = 'unknown'
311
-
312
- # استخراج رقم المحاضرة
313
- match = re.search(r'(lecture|lec|محاضرة)\s*(\d+)', name_lower)
314
- if match:
315
- lecture_num = int(match.group(2))
316
-
 
 
 
 
 
 
 
 
 
317
  # تحديد نوع الملف
318
  if 'lab' in name_lower or 'عملي' in name_lower:
319
  file_type = 'lab'
320
- elif 'exam' in name_lower or 'امتحان' in name_lower:
321
  file_type = 'exam'
322
  elif 'summary' in name_lower or 'ملخص' in name_lower:
323
  file_type = 'summary'
324
  elif 'lecture' in name_lower or 'محاضرة' in name_lower:
325
  file_type = 'lecture'
326
-
 
 
 
 
327
  return {
328
  'name': file_name,
329
  'path': file_path,
@@ -334,7 +360,8 @@ class MedicalLabBot:
334
  async def _call_nvidia_api(self, messages, max_tokens=1500):
335
  """دالة مساعدة لاستدعاء NVIDIA API"""
336
  try:
337
- completion = nvidia_client.chat.completions.create(
 
338
  model="meta/llama3-70b-instruct", # استخدام موديل قوي
339
  messages=messages,
340
  temperature=0.5,
@@ -350,129 +377,174 @@ class MedicalLabBot:
350
  """البحث عن ملف بناءً على استعلام المستخدم (رقم، اسم، الخ)"""
351
  files = self.available_materials[subject]['files']
352
  query_lower = query.lower().strip()
353
-
354
- # محاولة البحث بالرقم (مثل "1", "2")
355
  try:
356
  # استخراج الرقم فقط
357
- match_num = re.findall(r'\d+', query_lower)
358
  if match_num:
359
  index = int(match_num[0])
360
  if 1 <= index <= len(files):
 
361
  return files[index - 1]
362
  except (IndexError, ValueError):
363
  pass # ليس استعلام رقمي
364
-
365
- # محاولة البحث برقم المحاضرة (مثل "محاضرة 3")
366
- match = re.search(r'(\d+)', query_lower)
367
- if match:
368
- num = int(match.group(1))
369
- for file_info in files:
370
- if file_info['lecture_number'] == num:
371
- return file_info
372
-
373
- # محاولة البحث بالاسم
 
 
 
374
  for file_info in files:
375
- if query_lower in file_info['name'].lower():
376
- return file_info
377
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  return None # لم يتم العثور
379
 
380
  async def download_and_extract_content(self, file_path, subject):
381
  """تحميل الملف من HF واستخراج النص منه"""
382
  if file_path in self.file_cache:
383
- logger.info(f"💾 Using cached content for {file_path}")
384
  return self.file_cache[file_path]
385
-
386
  logger.info(f"⏳ Downloading {file_path} from Hugging Face...")
387
  try:
388
- local_path = hf_hub_download(
 
 
389
  repo_id=REPO_ID,
390
  filename=file_path,
391
  repo_type="dataset"
392
  )
393
-
394
  text_content = ""
395
  logger.info(f"📄 Extracting content from {local_path}")
396
-
397
  if local_path.lower().endswith('.pdf'):
398
  # استخدام PyMuPDF (fitz) لاستخراج أدق
399
- with fitz.open(local_path) as doc:
400
- for page in doc:
401
- text_content += page.get_text()
402
-
 
403
  elif local_path.lower().endswith('.txt'):
404
- with open(local_path, 'r', encoding='utf-8') as f:
405
  text_content = f.read()
406
-
407
  elif local_path.lower().endswith('.docx'):
408
- doc_obj = docx.Document(local_path)
409
- for para in doc_obj.paragraphs:
410
- text_content += para.text + "\n"
411
-
 
 
412
  else:
413
  logger.warning(f"Unsupported file type: {local_path}")
414
- return f"Error: Unsupported file type ({file_path})."
 
 
 
 
 
415
 
416
- # تنظيف أساسي
417
- text_content = re.sub(r'\s+', ' ', text_content).strip()
418
-
419
  if len(text_content) < 50:
420
- logger.warning(f"File {file_path} content is very short or empty.")
421
- # قد لا يكون هذا خطأ، ربما الملف قصير فعلاً
422
-
423
  self.file_cache[file_path] = text_content
 
424
  return text_content
425
-
426
  except Exception as e:
427
- logger.error(f"❌ Error downloading/extracting {file_path}: {e}")
428
- return f"Error: Could not retrieve file {file_path}."
 
 
429
 
430
- # ========== (الدوال الرئيسية التي كانت ناقصة) ==========
431
-
432
  async def explain_lecture(self, user_query, subject, user_id):
433
  """شرح محاضرة بناءً على استعلام المستخدم"""
434
  file_info = self._find_file_by_query(user_query, subject)
435
-
436
  if not file_info:
437
- return "❌ لم أتمكن من العثور على الملف. يرجى تحديد رقم أو اسم الملف بوضوح."
438
-
 
439
  file_path = file_info['path']
 
440
  await bot.application.bot.send_chat_action(chat_id=user_id, action="typing")
441
  content = await self.download_and_extract_content(file_path, subject)
442
-
443
  if content.startswith("Error:"):
444
- return f"❌ خطأ في معالجة الملف: {file_path}\n{content}"
445
-
446
- if not content:
447
- return f"❌ المحتوى فارغ للملف: {file_path}"
448
-
449
  memory = self.get_user_memory(user_id)
450
- memory['history'].append({"role": "user", "content": f"اشرح لي النقاط الأساسية في هذه المحاضرة: {file_info['name']}"})
451
-
452
- system_prompt = f"أنت مساعد مختبرات طبية متخصص. اشرح النقاط الأساسية في المحتوى التالي من مادة {subject} (ملف: {file_info['name']}). ركز على المفاهيم الرئيسية والنتائج المهمة. استخدم تنسيق Markdown ونقاط واضحة."
453
-
 
 
 
 
 
 
 
 
 
454
  messages = [
455
  {"role": "system", "content": system_prompt},
456
- {"role": "user", "content": f"المحتوى:\n```\n{content[:8000]}\n```\n\nاشرح النقاط الأساسية."} # قص المحتوى ليلائم نافذة السياق
457
  ]
458
-
459
  response = await self._call_nvidia_api(messages, 1500)
460
  memory['history'].append({"role": "assistant", "content": response})
461
- return f"📝 **شرح لأهم نقاط ملف: {file_info['name']}**\n\n{response}"
462
 
463
  async def explain_concept(self, user_query, subject, user_id):
464
  """شرح مفهوم أو مصطلح طبي"""
465
  memory = self.get_user_memory(user_id)
466
  memory['history'].append({"role": "user", "content": user_query})
467
-
468
- system_prompt = f"أنت خبير في المختبرات الطبية. اشرح المفهوم التالي بوضوح وبشكل مبسط، مع التركيز على أهميته في مادة {subject} إذا كان ذا صلة."
469
-
470
  messages = [
471
  {"role": "system", "content": system_prompt},
472
- {"role": "user", "content": user_query}
 
 
473
  ]
474
-
475
- response = await self._call_nvidia_api(messages)
476
  memory['history'].append({"role": "assistant", "content": response})
477
  return f"🧪 **شرح مفهوم: {user_query}**\n\n{response}"
478
 
@@ -480,13 +552,14 @@ class MedicalLabBot:
480
  """معالجة استعلام عام من المستخدم"""
481
  memory = self.get_user_memory(user_id)
482
  history = memory.get('history', [])
483
-
484
- system_prompt = f"أنت مساعد ذكي متخصص في المختبرات الطبية. المادة الحالية التي يركز عليها الطالب هي {subject}. أجب على سؤال الطالب بناءً على سياق المحادثة إن وجد."
485
-
486
  messages = [{"role": "system", "content": system_prompt}]
487
- messages.extend(history[-5:]) # إضافة آخر 5 تفاعلات للسياق
 
488
  messages.append({"role": "user", "content": user_message})
489
-
490
  response = await self._call_nvidia_api(messages)
491
  memory['history'].append({"role": "user", "content": user_message})
492
  memory['history'].append({"role": "assistant", "content": response})
@@ -494,148 +567,201 @@ class MedicalLabBot:
494
 
495
  async def generate_questions_for_subject(self, subject, user_id):
496
  """توليد أسئلة من ملف عشوائي في المادة"""
497
- files = self.available_materials[subject]['files']
498
- if not files:
499
  return "❌ لا توجد ملفات في هذه المادة لتوليد أسئلة منها."
500
-
501
- # اختيار ملف عشوائي
502
- file_info = random.choice(files)
 
 
 
 
 
 
503
  file_path = file_info['path']
504
-
 
505
  await bot.application.bot.send_chat_action(chat_id=user_id, action="typing")
506
  content = await self.download_and_extract_content(file_path, subject)
507
-
508
- if content.startswith("Error:") or not content:
509
- return f" لم أتمكن من قراءة ملف ({file_info['name']}) لتوليد أسئلة."
510
-
511
- system_prompt = f"أنت خبير في مادة {subject}. قم بإنشاء 5 أسئلة متنوعة (MCQ أو أسئلة قصيرة) بناءً على المحتوى التالي من ملف {file_info['name']}."
512
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  messages = [
514
  {"role": "system", "content": system_prompt},
515
- {"role": "user", "content": f"المحتوى:\n```\n{content[:8000]}\n```\n\nقم بإنشاء 5 أسئلة متنوعة وواضحة."}
516
  ]
517
-
518
  response = await self._call_nvidia_api(messages)
519
- return f"❓ **أسئلة مقترحة من ملف: {file_info['name']}**\n\n{response}"
520
 
521
  async def generate_summary(self, subject, user_id):
522
  """تلخيص ملف مهم من المادة"""
523
- files = self.available_materials[subject]['files']
524
- if not files:
525
  return "❌ لا توجد ملفات في هذه المادة لتلخيصها."
526
-
527
- # محاولة العثور على ملف ملخص
 
 
528
  summary_file = next((f for f in files if f['type'] == 'summary'), None)
 
 
 
 
529
  if not summary_file:
530
- # إذا لم يوجد ملخص، اختر أول محاضرة
531
- summary_file = next((f for f in files if f['type'] == 'lecture'), files[0])
532
-
533
  file_path = summary_file['path']
 
 
534
  await bot.application.bot.send_chat_action(chat_id=user_id, action="typing")
535
  content = await self.download_and_extract_content(file_path, subject)
536
-
537
- if content.startswith("Error:") or not content:
538
- return f"❌ لم أتمكن من قراءة ملف ({summary_file['name']}) للتلخيص."
539
 
540
- system_prompt = f"أنت خبير في مادة {subject}. قم بتلخيص المحتوى التالي من ملف {summary_file['name']} في 5 نقاط رئيسية وواضحة."
541
-
 
 
 
 
 
 
 
542
  messages = [
543
  {"role": "system", "content": system_prompt},
544
- {"role": "user", "content": f"المحتوى:\n```\n{content[:8000]}\n```\n\nلخص المحتوى في 5 نقاط رئيسية."}
545
  ]
546
-
547
  response = await self._call_nvidia_api(messages)
548
- return f"📋 **ملخص ملف: {summary_file['name']}**\n\n{response}"
549
 
550
  # ========== (باقي دوال المعالجة الأصلية) ==========
551
-
552
  async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
553
  """بدء المحادثة وعرض القائمة الرئيسية"""
554
  user_id = update.effective_user.id
555
-
 
556
  welcome_text = """
557
  🏥 **مرحباً بك في بوت المختبرات الطبية الذكي** 🔬
558
 
559
- 📚 **المواد المتاحة حالياً:**
 
 
560
  """
561
-
562
  if not self.available_materials:
563
- welcome_text += "\n\n عذراً، لم أتمكن من تحميل أي مواد دراسية حالياً. حاول تحديث القائمة."
564
  else:
565
- for subject in self.available_materials.keys():
566
  file_count = len(self.available_materials[subject]['files'])
567
  welcome_text += f"\n• {subject} ({file_count} ملف)"
568
-
569
- welcome_text += "\n\nاختر المادة التي تريد البدء بها:"
570
-
571
  keyboard = self.create_subjects_keyboard()
572
  reply_markup = InlineKeyboardMarkup(keyboard)
573
-
 
574
  if update.callback_query:
575
- await update.callback_query.edit_message_text(welcome_text, reply_markup=reply_markup)
 
 
 
 
 
576
  else:
577
- await update.message.reply_text(welcome_text, reply_markup=reply_markup)
578
-
579
  return SELECTING_SUBJECT
580
-
581
  def create_subjects_keyboard(self):
582
  """إنشاء لوحة مفاتيح للمواد المتاحة"""
583
  keyboard = []
584
- for subject in self.available_materials.keys():
 
 
 
 
585
  file_count = len(self.available_materials[subject]['files'])
586
  display_name = f"{subject} ({file_count})"
587
- keyboard.append([InlineKeyboardButton(display_name, callback_data=f"subject_{subject}")])
588
-
 
 
 
 
589
  keyboard.append([InlineKeyboardButton("🔄 تحديث قائمة المواد", callback_data="refresh_materials")])
590
  keyboard.append([InlineKeyboardButton("❓ مساعدة", callback_data="general_help")])
591
  return keyboard
592
-
593
  async def handle_subject_selection(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
594
  """معالجة اختيار المادة"""
595
  query = update.callback_query
596
  await query.answer()
597
-
598
  user_id = query.from_user.id
599
  callback_data = query.data
600
-
 
601
  if callback_data == "general_help":
602
  return await self.handle_general_help(query, context)
603
  elif callback_data == "refresh_materials":
604
  await query.edit_message_text("🔄 جاري تحديث قائمة المواد...")
605
- self.load_all_materials()
606
  keyboard = self.create_subjects_keyboard()
607
  reply_markup = InlineKeyboardMarkup(keyboard)
608
- await query.edit_message_text("✅ تم تحديث قائمة المواد\nاختر المادة:", reply_markup=reply_markup)
609
  return SELECTING_SUBJECT
610
-
611
  subject = callback_data.replace("subject_", "")
612
  context.user_data['current_subject'] = subject
613
-
614
  memory = self.get_user_memory(user_id)
615
  memory['last_subject'] = subject
616
-
617
  # التأكد من أن المادة موجودة (احتياطي)
618
  if subject not in self.available_materials:
619
- await query.edit_message_text(" خطأ: المادة غير موجودة. ربما تحتاج لتحديث القائمة؟")
620
- return await self.start(update, context)
 
621
 
622
  subject_files = self.available_materials[subject]['files']
623
  subject_name = subject.replace('_', ' ').title()
624
-
625
  keyboard = [
626
- [InlineKeyboardButton("📖 شرح محاضرة محددة", callback_data="explain_lecture")],
627
- [InlineKeyboardButton("🔍 استعراض جميع الملفات", callback_data="browse_files")],
628
- [InlineKeyboardButton("❓ أسئلة عن المادة", callback_data="generate_questions")],
629
- [InlineKeyboardButton("📝 ملخص المادة", callback_data="summarize_content")],
630
  [InlineKeyboardButton("🧪 تفسير مفهوم", callback_data="explain_concept")],
631
- [InlineKeyboardButton("🏠 القائمة الرئيسية", callback_data="main_menu")]
632
  ]
633
  reply_markup = InlineKeyboardMarkup(keyboard)
634
-
635
  await query.edit_message_text(
636
  f"📚 **{subject_name}**\n\n"
637
- f"عدد الملفات المتاحة: {len(subject_files)}\n"
638
- f"اختر الخدمة التي تريدها:",
639
  reply_markup=reply_markup,
640
  parse_mode='Markdown'
641
  )
@@ -645,206 +771,243 @@ class MedicalLabBot:
645
  """معالجة العودة إلى قائمة الإجراءات"""
646
  query = update.callback_query
647
  await query.answer()
648
-
 
649
  subject = context.user_data.get('current_subject')
650
- if not subject:
 
651
  return await self.start(update, context) # إذا ضاع السياق، عد للبداية
652
 
653
  subject_files = self.available_materials[subject]['files']
654
  subject_name = subject.replace('_', ' ').title()
655
-
656
  keyboard = [
657
- [InlineKeyboardButton("📖 شرح محاضرة محددة", callback_data="explain_lecture")],
658
- [InlineKeyboardButton("🔍 استعراض جميع الملفات", callback_data="browse_files")],
659
- [InlineKeyboardButton("❓ أسئلة عن المادة", callback_data="generate_questions")],
660
- [InlineKeyboardButton("📝 ملخص المادة", callback_data="summarize_content")],
661
  [InlineKeyboardButton("🧪 تفسير مفهوم", callback_data="explain_concept")],
662
- [InlineKeyboardButton("🏠 القائمة الرئيسية", callback_data="main_menu")]
663
  ]
664
  reply_markup = InlineKeyboardMarkup(keyboard)
665
-
666
  await query.edit_message_text(
667
  f"📚 **{subject_name}**\n\n"
668
- f"اختر الخدمة التي تريدها:",
669
  reply_markup=reply_markup,
670
  parse_mode='Markdown'
671
  )
672
  return SELECTING_ACTION
673
-
674
  async def handle_main_menu(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
675
  """معالجة العودة للقائمة الرئيسية"""
676
  query = update.callback_query
677
  await query.answer()
 
678
  context.user_data.clear() # تنظيف السياق عند العودة للرئيسية
679
  return await self.start(update, context)
680
-
681
  async def handle_more_questions(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
682
- """معالجة طلب المزيد من الأسئلة"""
683
  query = update.callback_query
684
  await query.answer()
685
-
 
686
  subject = context.user_data.get('current_subject', 'عام')
687
-
688
  keyboard = [
689
- [InlineKeyboardButton("🔙 العودة للإجراءات", callback_data="back_to_actions")],
690
  ]
691
  reply_markup = InlineKeyboardMarkup(keyboard)
692
-
693
- await query.edit_message_text(f"💬 اكتب سؤالك أو طلبك (في سياق مادة {subject}):", reply_markup=reply_markup)
694
- context.user_data['waiting_for'] = 'general'
695
  return WAITING_FOR_QUESTION
696
-
697
  async def handle_change_subject(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
698
  """معالجة تغيير المادة"""
699
  query = update.callback_query
700
  await query.answer()
 
701
  keyboard = self.create_subjects_keyboard()
702
  reply_markup = InlineKeyboardMarkup(keyboard)
703
- await query.edit_message_text("اختر المادة:", reply_markup=reply_markup)
704
  return SELECTING_SUBJECT
705
 
706
  async def handle_action_selection(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
707
  """معالجة اختيار الإجراء"""
708
  query = update.callback_query
709
  await query.answer()
710
-
711
  action = query.data
712
  user_id = query.from_user.id
713
  subject = context.user_data.get('current_subject')
 
714
 
715
- if not subject:
 
 
716
  await query.edit_message_text("❌ حدث خطأ في السياق. يرجى البدء من جديد.")
717
  return await self.start(update, context)
718
 
 
 
 
 
 
 
719
  if action == "main_menu":
720
  return await self.start(update, context)
721
-
722
  elif action == "browse_files":
723
- return await self.browse_available_files(query, context)
724
-
 
 
 
 
 
 
 
 
 
 
725
  elif action == "explain_lecture":
726
  files_list = await self.get_files_list_text(subject)
727
- keyboard = [[InlineKeyboardButton("🔙 العودة", callback_data="back_to_actions")]]
728
- reply_markup = InlineKeyboardMarkup(keyboard)
 
 
729
  await query.edit_message_text(
730
  f"📖 **شرح محاضرة**\n\n"
731
- f"{files_list}\n"
732
- f"اكتب رقم المحاضرة أو اسمها:\n"
733
- f"مثال: '1' أو 'محاضرة 3' أو 'الملف الثاني'",
734
- reply_markup=reply_markup
735
  )
736
  context.user_data['waiting_for'] = 'lecture_explanation'
737
  return WAITING_FOR_QUESTION
738
-
739
  elif action == "generate_questions":
740
- await query.edit_message_text("⏳ جاري توليد الأسئلة...")
741
  questions = await self.generate_questions_for_subject(subject, user_id)
742
- keyboard = [[InlineKeyboardButton("🔙 العودة", callback_data="back_to_actions")]]
743
- reply_markup = InlineKeyboardMarkup(keyboard)
744
- await query.edit_message_text(questions, reply_markup=reply_markup)
745
- return SELECTING_ACTION
746
-
747
  elif action == "explain_concept":
748
- keyboard = [[InlineKeyboardButton("🔙 العودة", callback_data="back_to_actions")]]
749
- reply_markup = InlineKeyboardMarkup(keyboard)
750
  await query.edit_message_text(
751
  "🧪 **تفسير مفهوم**\n\n"
752
- "ما هو المفهوم أو المصطلح الذي تريد شرحه؟\n"
753
- "مثال: 'ما هو تحليل الإنزيمات' أو 'اشرح لي تحليل البول'",
754
- reply_markup=reply_markup
755
  )
756
  context.user_data['waiting_for'] = 'concept_explanation'
757
  return WAITING_FOR_QUESTION
758
-
759
  elif action == "summarize_content":
760
- await query.edit_message_text("⏳ جاري تلخيص المادة...")
761
  summary = await self.generate_summary(subject, user_id)
762
- keyboard = [[InlineKeyboardButton("🔙 العودة", callback_data="back_to_actions")]]
763
- reply_markup = InlineKeyboardMarkup(keyboard)
764
- await query.edit_message_text(summary, reply_markup=reply_markup)
765
- return SELECTING_ACTION
766
 
767
- async def browse_available_files(self, query, context):
768
- """عرض الملفات المتاحة للمادة"""
769
- subject = context.user_data.get('current_subject')
770
- files_text = await self.get_files_list_text(subject)
771
-
772
- keyboard = [
773
- [InlineKeyboardButton("📖 طلب شرح ملف", callback_data="explain_lecture")],
774
- [InlineKeyboardButton("🔙 العودة", callback_data="back_to_actions")]
775
- ]
776
- reply_markup = InlineKeyboardMarkup(keyboard)
777
-
778
- await query.edit_message_text(files_text, reply_markup=reply_markup)
779
  return SELECTING_ACTION
780
 
 
 
 
 
 
 
 
781
  async def get_files_list_text(self, subject):
782
  """إنشاء نص لقائمة الملفات"""
783
  if subject not in self.available_materials:
 
784
  return "❌ خطأ: لم يتم العثور على المادة المحددة."
785
 
786
  files = self.available_materials[subject]['files']
787
-
788
  if not files:
789
- return "❌ لا توجد ملفات متاحة لهذه المادة."
790
-
791
- files_text = "📁 **الملفات المتاحة:**\n\n"
792
- for i, file_info in enumerate(files[:20], 1): # زيادة الحد إلى 20
 
 
793
  file_name = file_info['name']
794
  lecture_num = file_info['lecture_number']
795
  file_type = file_info['type']
796
-
797
  type_emoji = {
798
  'lecture': '📖',
799
- 'lab': '🧪',
800
  'exam': '📝',
801
  'summary': '📋',
802
  'unknown': '📄'
803
  }.get(file_type, '📄')
804
-
805
- num_text = f" - (محاضرة {lecture_num})" if lecture_num else ""
806
- files_text += f"{i}. {type_emoji} {file_name}{num_text}\n"
807
-
808
- if len(files) > 20:
809
- files_text += f"\n... وغيرها {len(files) - 20} ملف آخر"
810
-
 
811
  return files_text
812
 
 
813
  async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
814
  """معالجة الرسائل النصية من المستخدم"""
 
 
 
815
  user_message = update.message.text
816
  user_id = update.effective_user.id
817
  waiting_for = context.user_data.get('waiting_for')
818
  subject = context.user_data.get('current_subject', 'general')
819
-
 
 
820
  await update.message.chat.send_action(action="typing")
821
-
 
 
 
822
  try:
823
  if waiting_for == 'lecture_explanation':
824
  response = await self.explain_lecture(user_message, subject, user_id)
 
825
  elif waiting_for == 'concept_explanation':
826
  response = await self.explain_concept(user_message, subject, user_id)
827
- else: # يتضمن 'general' أو أي شيء آخر
 
828
  response = await self.process_general_query(user_message, subject, user_id)
829
-
 
 
830
  await update.message.reply_text(response, parse_mode='Markdown')
831
-
832
- keyboard = [
833
- [InlineKeyboardButton("🔄 أسئلة أخرى", callback_data="more_questions")],
834
- [InlineKeyboardButton("📚 تغيير المادة", callback_data="change_subject")],
835
- [InlineKeyboardButton("🏠 القائمة الرئيسية", callback_data="main_menu")]
836
- ]
837
- reply_markup = InlineKeyboardMarkup(keyboard)
838
-
839
- await update.message.reply_text("ماذا تريد أن تفعل بعد؟", reply_markup=reply_markup)
840
-
841
- context.user_data['waiting_for'] = None # إنهاء حالة الانتظار
842
- return SELECTING_ACTION # العودة إلى قائمة الإجراءات
843
-
 
844
  except Exception as e:
845
- logger.error(f"❌ Error processing message: {e}")
846
- await update.message.reply_text("❌ حدث خطأ أثناء معالجة طلبك. يرجى المحاولة مرة أخرى.")
847
- return SELECTING_ACTION
 
 
848
 
849
  # ========== إنشاء كائن البوت ==========
850
  bot = MedicalLabBot()
@@ -856,48 +1019,60 @@ async def root():
856
  """الصفحة الرئيسية"""
857
  materials_count = len(bot.available_materials)
858
  total_files = sum(len(material['files']) for material in bot.available_materials.values())
859
-
860
  # تحديد رسالة الحالة بناءً على حالة التهيئة
861
  status_message = "⏳ جاري التهيئة (الاتصال بـ Telegram)..."
862
  status_color = "#ffc107" # أصفر
 
863
  if bot.initialization_status == "success":
864
  status_message = "✅ نشط ومهيأ"
865
  status_color = "#28a745" # أخضر
 
866
  elif bot.initialization_status == "failed":
867
- status_message = "❌ فشل التهيئة (خطأ في الاتصال بـ Telegram)"
868
  status_color = "#dc3545" # أحمر
869
-
 
 
870
  return f"""
871
  <html>
872
  <head>
873
- <title>Medical Lab Bot</title>
874
  <style>
875
- body {{ font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }}
876
- .container {{ max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }}
877
- .status {{ padding: 20px; background: #f0f8ff; border-radius: 10px; border-left: 5px solid {status_color}; }}
878
- h1 {{ color: #2c3e50; text-align: center; }}
879
- ul {{ line-height: 1.8; }}
880
- li {{ margin: 10px 0; }}
 
 
 
 
 
 
881
  </style>
882
  </head>
883
  <body>
884
  <div class="container">
885
- <h1>🏥 بوت المختبرات الطبية الذكي</h1>
886
- <div class="status">
887
- <h2>حالة البوت: {status_message}</h2>
 
888
  <p><strong>المواد المحملة:</strong> {materials_count} مادة</p>
889
  <p><strong>إجمالي الملفات:</strong> {total_files} ملف</p>
890
  </div>
891
- <h3>🎯 المميزات:</h3>
892
  <ul>
893
- <li>📚 شرح المواد الدراسية من ملفات PDF وWord</li>
894
- <li>❓ توليد أسئلة متنوعة للمراجعة</li>
895
- <li>📖 تلخيص المحتوى الدراسي</li>
896
- <li>🧪 تفسير المفاهيم العلمية</li>
897
- <li>💾 ذاكرة محادثة لكل مستخدم</li>
 
898
  </ul>
899
- <div class="status" style="margin-top: 20px;">
900
- <p><strong>ℹ️ معلومات:</strong> هذا البوت يستخدم الذكاء الاصطناعي من NVIDIA وملفات Hugging Face</p>
901
  </div>
902
  </div>
903
  </body>
@@ -912,35 +1087,53 @@ async def handle_telegram_update(request: Request):
912
  # التحقق من أن التطبيق مهيأ
913
  if not bot.is_initialized or not bot.application:
914
  logger.error("❌ التطبيق غير مهيأ، لا يمكن معالجة التحديث (ربما لا يزال قيد التهيئة).")
 
915
  raise HTTPException(status_code=503, detail="Application not initialized or still initializing")
916
-
917
  update_data = await request.json()
 
918
  update = Update.de_json(update_data, bot.application.bot)
919
-
920
- # معالجة التحديث
921
- await bot.application.process_update(update)
 
 
922
  return {"status": "ok"}
923
-
 
 
 
 
 
 
924
  except Exception as e:
925
- logger.error(f"❌ Error processing update: {e}")
926
- raise HTTPException(status_code=400, detail="Invalid update")
 
 
927
 
928
  @app.get("/health")
929
  async def health_check():
930
  """فحص صحة الخدمة"""
931
  materials_count = len(bot.available_materials)
932
  total_files = sum(len(material['files']) for material in bot.available_materials.values())
933
-
934
- status = "unhealthy"
 
935
  if bot.initialization_status == "pending":
936
- status = "initializing"
 
 
937
  elif bot.initialization_status == "success":
938
- status = "healthy"
 
939
  elif bot.initialization_status == "failed":
940
- status = "unhealthy_failed_init"
941
-
942
- return {
943
- "status": status,
 
 
944
  "service": "medical-lab-bot",
945
  "initialization_status": bot.initialization_status,
946
  "materials_loaded": materials_count,
@@ -948,8 +1141,20 @@ async def health_check():
948
  "timestamp": datetime.now().isoformat()
949
  }
950
 
 
 
 
 
951
  # ========== التشغيل الرئيسي ==========
952
  if __name__ == "__main__":
953
  port = int(os.environ.get("PORT", 7860))
954
- logger.info(f"🚀 Starting Medical Lab Bot on port {port}")
955
- uvicorn.run(app, host="0.0.0.0", port=port)
 
 
 
 
 
 
 
 
 
63
  async def handle_async_request(self, request: Request) -> Response:
64
  try:
65
  host = request.url.host
66
+
67
  # التحقق إذا كان Host هو أصلاً IP
68
  is_ip = True
69
  try:
70
+ # تحقق مبسط لـ IPv4
71
  parts = host.split('.')
72
+ if len(parts) != 4 or not all(p.isdigit() and 0 <= int(p) <= 255 for p in parts):
73
+ is_ip = False
74
+ # يمكن إضافة تحقق لـ IPv6 إذا لزم الأمر
75
  except Exception:
76
  is_ip = False
77
 
78
  if is_ip:
79
+ # logger.info(f"Host {host} is already an IP. Skipping resolution.") # يمكن إلغاء التعليق عند الحاجة للتتبع
80
+ pass
81
  else:
82
  logger.info(f"🔧 Resolving host: {host}")
83
  result = await self.resolver.resolve(host, 'A')
84
  ip = result[0].address
85
  logger.info(f"✅ Resolved {host} to {ip}")
86
+
87
  # تعيين SNI (مهم جداً لـ SSL/HTTPS)
88
+ request.extensions["sni_hostname"] = host
89
  # تحديث الـ URL لاستخدام الـ IP
90
  request.url = request.url.copy_with(host=ip)
91
 
92
+ except dns.resolver.NoAnswer:
93
+ logger.error(f"❌ DNS NoAnswer for {host}. Trying request with original host...")
94
+ except dns.resolver.NXDOMAIN:
95
+ logger.error(f"❌ DNS NXDOMAIN for {host}. Trying request with original host...")
96
  except Exception as e:
97
  logger.error(f"❌ DNS resolution failed for {host}: {e}. Trying request with original host...")
98
+ # إذا فشل الحل لأي سبب، اسمح للطلب بالاستمرار مع اسم المضيف الأصلي
99
  pass
100
 
101
  # استدعاء المعالج الأصلي (الأب) لإكمال الطلب
 
125
  self.is_initialized = False
126
  self.initialization_status = "pending" # تتبع حالة التهيئة
127
  self.load_all_materials()
128
+
129
  async def initialize_application(self):
130
  """تهيئة تطبيق التليجرام بشكل غير متزامن مع حل DNS المخصص"""
131
  try:
132
  if self.is_initialized:
133
  return True
134
+
135
  logger.info("🔄 جاري تهيئة تطبيق التليجرام...")
136
+
137
  # ========== الحل الجذري (DNS) ==========
138
  logger.info("🔧 إعداد عميل HTTP مخصص مع CustomDNSTransport...")
139
  custom_transport = CustomDNSTransport()
 
147
  .build()
148
  )
149
  await self.setup_handlers()
150
+
151
+ max_retries = 3
152
  retry_delay = 5
153
+
154
  for attempt in range(max_retries):
155
  try:
156
  logger.info(f"🚀 محاولة تهيئة الاتصال بـ Telegram (محاولة {attempt + 1}/{max_retries})...")
157
  # 1. الاتصال بـ Telegram (getMe)
158
+ await self.application.initialize()
159
  logger.info("✅ تم تهيئة الاتصال بـ Telegram بنجاح.")
160
+
161
  # 2. ضبط الـ Webhook (فقط بعد نجاح الاتصال)
162
  if SPACE_URL:
163
  webhook_url = f"{SPACE_URL.rstrip('/')}/telegram"
 
170
  logger.info(f"✅ Webhook set to: {webhook_url}")
171
  else:
172
  logger.warning("⚠️ SPACE_URL not set. Webhook cannot be set.")
173
+
174
  # 3. ضبط الحالة على "ناجح"
175
  self.is_initialized = True
176
  self.initialization_status = "success"
177
  logger.info("✅✅✅ التطبيق جاهز لاستقبال الطلبات.")
178
  return True # نجح، اخرج من الدالة
179
+
180
  except Exception as e:
181
  logger.warning(f"⚠️ فشلت محاولة التهيئة {attempt + 1}: {e}")
182
  if attempt < max_retries - 1:
 
184
  await asyncio.sleep(retry_delay)
185
  else:
186
  logger.error(f"❌ فشل تهيئة التطبيق نهائياً بعد {max_retries} محاولات (حتى مع DNS المخصص).")
187
+
188
  # فشل بعد كل المحاولات
189
  self.is_initialized = False
190
  self.initialization_status = "failed"
 
195
  self.is_initialized = False
196
  self.initialization_status = "failed"
197
  return False
198
+
199
  async def setup_handlers(self):
200
  """إعداد معالجات التليجرام"""
201
  # إعداد Conversation Handler مع إعدادات محسنة
 
223
  persistent=False,
224
  per_message=False
225
  )
226
+
227
  # إضافة الـ handlers
228
  self.application.add_handler(conv_handler)
229
+
230
  # إضافة handlers منفصلة للاستعلامات العامة
231
  self.application.add_handler(CallbackQueryHandler(self.handle_more_questions, pattern='^more_questions$'))
232
  self.application.add_handler(CallbackQueryHandler(self.handle_change_subject, pattern='^change_subject$'))
233
+
234
  logger.info("✅ تم إعداد معالجات التليجرام")
235
 
236
  def load_all_materials(self):
 
238
  try:
239
  logger.info("جاري تحميل قائمة المواد من Hugging Face...")
240
  all_files = list_repo_files(repo_id=REPO_ID, repo_type="dataset")
241
+
242
  materials = {}
243
+
244
  for file_path in all_files:
245
  try:
246
  path_parts = file_path.split('/')
247
+
248
  if len(path_parts) >= 2:
249
  subject = path_parts[0]
250
  file_name = path_parts[-1]
251
+
252
  if subject not in materials:
253
  materials[subject] = {
254
  'files': [],
255
  'file_details': {}
256
  }
257
+
258
  file_info = self.extract_file_info(file_name, file_path)
259
  materials[subject]['files'].append(file_info)
260
  materials[subject]['file_details'][file_name] = file_info
261
+
262
  else:
263
+ # ملفات في الجذر الرئيسي للمستودع (إن وجدت)
264
  if 'general' not in materials:
265
  materials['general'] = {
266
  'files': [],
 
269
  file_info = self.extract_file_info(file_path, file_path)
270
  materials['general']['files'].append(file_info)
271
  materials['general']['file_details'][file_path] = file_info
272
+
273
  except Exception as e:
274
  logger.error(f"خطأ في معالجة الملف {file_path}: {e}")
275
  continue
276
+
277
+ # فرز الملفات داخل كل مادة (اختياري، للترتيب)
278
+ for subject in materials:
279
+ materials[subject]['files'].sort(key=lambda x: (x['lecture_number'] if x['lecture_number'] is not None else float('inf'), x['name']))
280
+
281
+
282
  self.available_materials = materials
283
  logger.info(f"✅ تم تحميل {len(materials)} مادة بنجاح")
284
+
285
  except Exception as e:
286
  logger.error(f"❌ خطأ في تحميل المواد: {e}")
287
  self.available_materials = {'Biochemistry': {'files': [], 'file_details': {}}} # مادة افتراضية عند الفشل
 
298
  """عرض رسالة المساعدة"""
299
  help_text = """
300
  ❓ **مساعدة** ❓
301
+
302
  هذا البوت مصمم لمساعدتك في دراسة مواد المختبرات الطبية.
303
+
304
  1. **ابدأ** باختيار مادة من القائمة الرئيسية.
305
  2. **اختر الخدمة:**
306
  * **شرح محاضرة:** يعطيك ملخص للملف الذي تختاره.
 
312
  """
313
  keyboard = [[InlineKeyboardButton("🔙 العودة للقائمة الرئيسية", callback_data="main_menu")]]
314
  reply_markup = InlineKeyboardMarkup(keyboard)
315
+
316
  await query.edit_message_text(help_text, reply_markup=reply_markup)
317
  return SELECTING_SUBJECT
318
 
 
321
  name_lower = file_name.lower()
322
  lecture_num = None
323
  file_type = 'unknown'
324
+
325
+ # استخراج رقم المحاضرة (محاولة أكثر مرونة)
326
+ # يبحث عن "lec" أو "lecture" أو "محاضرة" متبوعة برقم
327
+ match = re.search(r'(?:lecture|lec|محاضرة)\s*(\d+)', name_lower)
328
+ if not match:
329
+ # إذا لم يجد الصيغة السابقة، يبحث عن رقم لوحده في بداية أو نهاية الاسم
330
+ match = re.search(r'^(\d+)\s*-|[\s_-](\d+)$', name_lower)
331
+ if match:
332
+ # يأخذ الرقم الثاني إذا وجد (المجموعة الثانية)، وإلا الأول
333
+ lecture_num_str = match.group(2) or match.group(1)
334
+ lecture_num = int(lecture_num_str) if lecture_num_str else None
335
+ else:
336
+ lecture_num = int(match.group(1))
337
+
338
+
339
  # تحديد نوع الملف
340
  if 'lab' in name_lower or 'عملي' in name_lower:
341
  file_type = 'lab'
342
+ elif 'exam' in name_lower or 'امتحان' in name_lower or 'اسئلة' in name_lower:
343
  file_type = 'exam'
344
  elif 'summary' in name_lower or 'ملخص' in name_lower:
345
  file_type = 'summary'
346
  elif 'lecture' 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
+
353
  return {
354
  'name': file_name,
355
  'path': file_path,
 
360
  async def _call_nvidia_api(self, messages, max_tokens=1500):
361
  """دالة مساعدة لاستدعاء NVIDIA API"""
362
  try:
363
+ completion = await asyncio.to_thread(
364
+ nvidia_client.chat.completions.create,
365
  model="meta/llama3-70b-instruct", # استخدام موديل قوي
366
  messages=messages,
367
  temperature=0.5,
 
377
  """البحث عن ملف بناءً على استعلام المستخدم (رقم، اسم، الخ)"""
378
  files = self.available_materials[subject]['files']
379
  query_lower = query.lower().strip()
380
+
381
+ # 1. محاولة البحث بالرقم التسلسلي في القائمة (مثل "1", "2")
382
  try:
383
  # استخراج الرقم فقط
384
+ match_num = re.findall(r'^\d+$', query_lower) # يبحث عن رقم فقط في الاستعلام
385
  if match_num:
386
  index = int(match_num[0])
387
  if 1 <= index <= len(files):
388
+ logger.info(f"Found file by index {index}")
389
  return files[index - 1]
390
  except (IndexError, ValueError):
391
  pass # ليس استعلام رقمي
392
+
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
404
+ highest_score = 0
405
  for file_info in files:
406
+ name_lower = file_info['name'].lower()
407
+ # حساب درجة التشابه (بسيط) - عدد الكلمات المشتركة
408
+ query_words = set(query_lower.split())
409
+ name_words = set(re.findall(r'\w+', name_lower)) # استخراج الكلمات من اسم الملف
410
+ common_words = query_words.intersection(name_words)
411
+ score = len(common_words)
412
+
413
+ if score > highest_score:
414
+ highest_score = score
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
+
424
+ if best_match and highest_score > 0:
425
+ logger.info(f"Found file by best partial name match: {best_match['name']} (score: {highest_score})")
426
+ return best_match
427
+
428
+
429
+ logger.warning(f"Could not find file matching query: '{query}' in subject: {subject}")
430
  return None # لم يتم العثور
431
 
432
  async def download_and_extract_content(self, file_path, subject):
433
  """تحميل الملف من HF واستخراج النص منه"""
434
  if file_path in self.file_cache:
435
+ # logger.info(f"💾 Using cached content for {file_path}") # إلغاء التعليق للتتبع
436
  return self.file_cache[file_path]
437
+
438
  logger.info(f"⏳ Downloading {file_path} from Hugging Face...")
439
  try:
440
+ # استخدام asyncio.to_thread لتشغيل الدالة المتزامنة في thread منفصل
441
+ local_path = await asyncio.to_thread(
442
+ hf_hub_download,
443
  repo_id=REPO_ID,
444
  filename=file_path,
445
  repo_type="dataset"
446
  )
447
+
448
  text_content = ""
449
  logger.info(f"📄 Extracting content from {local_path}")
450
+
451
  if local_path.lower().endswith('.pdf'):
452
  # استخدام PyMuPDF (fitz) لاستخراج أدق
453
+ with fitz.open(local_path) as doc:
454
+ for page in doc:
455
+ text_content += page.get_text("text", sort=True) # محاولة فرز النص
456
+ text_content += "\n\n--- Page Break ---\n\n" # إضافة فاصل صفحات
457
+
458
  elif local_path.lower().endswith('.txt'):
459
+ with open(local_path, 'r', encoding='utf-8', errors='ignore') as f: # تجاهل أخطاء الترميز
460
  text_content = f.read()
461
+
462
  elif local_path.lower().endswith('.docx'):
463
+ doc_obj = docx.Document(local_path)
464
+ full_text = []
465
+ for para in doc_obj.paragraphs:
466
+ full_text.append(para.text)
467
+ text_content = '\n'.join(full_text)
468
+
469
  else:
470
  logger.warning(f"Unsupported file type: {local_path}")
471
+ return f"Error: Unsupported file type ({os.path.basename(file_path)})."
472
+
473
+ # تنظيف أساسي ومتقدم
474
+ text_content = re.sub(r'\s+\n', '\n', text_content) # إزالة المسافات الزائدة قبل سطر جديد
475
+ text_content = re.sub(r'\n{3,}', '\n\n', text_content) # تقليل الأسطر الفارغة المتتالية
476
+ text_content = re.sub(r' +', ' ', text_content).strip() # تقليل المسافات المتتالية
477
 
 
 
 
478
  if len(text_content) < 50:
479
+ logger.warning(f"File {file_path} content is very short or empty after extraction.")
480
+
 
481
  self.file_cache[file_path] = text_content
482
+ # logger.debug(f"Content for {file_path}: {text_content[:500]}...") # إلغاء التعليق للتتبع
483
  return text_content
484
+
485
  except Exception as e:
486
+ logger.error(f"❌ Error downloading/extracting {file_path}: {e}", exc_info=True)
487
+ return f"Error: Could not retrieve or process file {os.path.basename(file_path)}."
488
+
489
+ # ========== (الدوال الرئيسية) ==========
490
 
 
 
491
  async def explain_lecture(self, user_query, subject, user_id):
492
  """شرح محاضرة بناءً على استعلام المستخدم"""
493
  file_info = self._find_file_by_query(user_query, subject)
494
+
495
  if not file_info:
496
+ files_list_text = await self.get_files_list_text(subject)
497
+ return f"❌ لم أتمكن من العثور على الملف المطلوب.\n\n{files_list_text}\n\nيرجى تحديد رقم الملف من القائمة أو كتابة جزء واضح من اسمه."
498
+
499
  file_path = file_info['path']
500
+ file_name = file_info['name']
501
  await bot.application.bot.send_chat_action(chat_id=user_id, action="typing")
502
  content = await self.download_and_extract_content(file_path, subject)
503
+
504
  if content.startswith("Error:"):
505
+ return f"❌ خطأ في معالجة الملف: {file_name}\n{content}"
506
+
507
+ if not content.strip():
508
+ return f"❌ المحتوى فارغ للملف: {file_name}"
509
+
510
  memory = self.get_user_memory(user_id)
511
+ memory['history'].append({"role": "user", "content": f"اشرح لي النقاط الأساسية في هذه المحاضرة: {file_name}"})
512
+
513
+ # قص المحتوى ليلائم نافذة السياق (مع مراعاة التوكنات التقريبية)
514
+ max_content_chars = 7000 # تقليل لترك مساحة للبرومبت والرد
515
+ if len(content) > max_content_chars:
516
+ content_snippet = content[:max_content_chars] + "\n\n[... المحتوى مقطوع ...]"
517
+ logger.warning(f"Content for {file_name} truncated to {max_content_chars} chars.")
518
+ else:
519
+ content_snippet = content
520
+
521
+
522
+ system_prompt = f"أنت مساعد أكاديمي متخصص في مادة {subject}. مهمتك هي شرح النقاط الأساسية في محتوى المحاضرة المقدم لك. ركز على المفاهيم الجوهرية، التعريفات الهامة، النتائج الرئيسية، وأي معلومات ضرورية لفهم الموضوع. قدم الشرح بطريقة منظمة وواضحة باستخدام نقاط Markdown. تجنب التفاصيل الثانوية غير الضرورية."
523
+
524
  messages = [
525
  {"role": "system", "content": system_prompt},
526
+ {"role": "user", "content": f"اسم الملف: {file_name}\n\nالمحتوى:\n```\n{content_snippet}\n```\n\nيرجى شرح النقاط الأساسية في هذا المحتوى."}
527
  ]
528
+
529
  response = await self._call_nvidia_api(messages, 1500)
530
  memory['history'].append({"role": "assistant", "content": response})
531
+ return f"📝 **شرح لأهم نقاط ملف: {file_name}**\n\n{response}"
532
 
533
  async def explain_concept(self, user_query, subject, user_id):
534
  """شرح مفهوم أو مصطلح طبي"""
535
  memory = self.get_user_memory(user_id)
536
  memory['history'].append({"role": "user", "content": user_query})
537
+
538
+ system_prompt = f"أنت خبير أكاديمي في مجال المختبرات الطبية، متخصص حالياً في مادة {subject}. اشرح المفهوم أو المصطلح التالي ({user_query}) بوضوح ودقة. ابدأ بتعريف أساسي، ثم وضح أهميته وتطبيقاته العملية في المختبر، واربطه بمادة {subject} إن أمكن. استخدم لغة سهلة ومباشرة."
539
+
540
  messages = [
541
  {"role": "system", "content": system_prompt},
542
+ # إضافة آخر تفاعلين للسياق إذا كانت ذات ص��ة بالمفهوم
543
+ *memory['history'][-3:-1], # يأخذ العنصرين قبل الأخير (آخر سؤال وجواب)
544
+ {"role": "user", "content": f"اشرح لي المفهوم التالي: {user_query}"}
545
  ]
546
+
547
+ response = await self._call_nvidia_api(messages, 1000) # تقليل التوكنات للردود المركزة
548
  memory['history'].append({"role": "assistant", "content": response})
549
  return f"🧪 **شرح مفهوم: {user_query}**\n\n{response}"
550
 
 
552
  """معالجة استعلام عام من المستخدم"""
553
  memory = self.get_user_memory(user_id)
554
  history = memory.get('history', [])
555
+
556
+ system_prompt = f"أنت مساعد ذكي متخصص في المختبرات الطبية. المادة الدراسية الحالية التي يركز عليها الطالب هي '{subject}'. أجب على سؤال الطالب ({user_message}) إجابة واضحة ومباشرة. استخدم سياق المحادثة السابق إذا كان ضرورياً لفهم السؤال. إذا كان السؤال خارج نطاق المادة أو التخصص، اعتذر بلطف."
557
+
558
  messages = [{"role": "system", "content": system_prompt}]
559
+ # إضافة آخر 4 تفاعلات للسياق (سؤالين وجوابين)
560
+ messages.extend(history[-4:])
561
  messages.append({"role": "user", "content": user_message})
562
+
563
  response = await self._call_nvidia_api(messages)
564
  memory['history'].append({"role": "user", "content": user_message})
565
  memory['history'].append({"role": "assistant", "content": response})
 
567
 
568
  async def generate_questions_for_subject(self, subject, user_id):
569
  """توليد أسئلة من ملف عشوائي في المادة"""
570
+ if subject not in self.available_materials or not self.available_materials[subject]['files']:
 
571
  return "❌ لا توجد ملفات في هذه المادة لتوليد أسئلة منها."
572
+
573
+ files = self.available_materials[subject]['files']
574
+ # تفضيل ملفات المحاضرات أو الملخصات إن وجدت
575
+ preferred_files = [f for f in files if f['type'] in ('lecture', 'summary')]
576
+ if preferred_files:
577
+ file_info = random.choice(preferred_files)
578
+ else:
579
+ file_info = random.choice(files) # اختر أي ملف إذا لم توجد المفضلة
580
+
581
  file_path = file_info['path']
582
+ file_name = file_info['name']
583
+
584
  await bot.application.bot.send_chat_action(chat_id=user_id, action="typing")
585
  content = await self.download_and_extract_content(file_path, subject)
586
+
587
+ if content.startswith("Error:") or not content.strip():
588
+ logger.error(f"Failed to get content for question generation from {file_name}. Content: {content[:100]}")
589
+ # محاولة اختيار ملف آخر مرة واحدة
590
+ if len(files) > 1:
591
+ logger.info("Retrying with another file for question generation...")
592
+ remaining_files = [f for f in files if f['path'] != file_path]
593
+ if remaining_files:
594
+ file_info = random.choice(remaining_files)
595
+ file_path = file_info['path']
596
+ file_name = file_info['name']
597
+ content = await self.download_and_extract_content(file_path, subject)
598
+ if content.startswith("Error:") or not content.strip():
599
+ logger.error(f"Retry failed for question generation from {file_name}.")
600
+ return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة من ملفات المادة."
601
+ else:
602
+ return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة ��ن ملفات المادة."
603
+ else:
604
+ return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة من ملفات المادة."
605
+
606
+
607
+ max_content_chars = 7000
608
+ content_snippet = content[:max_content_chars] if len(content) > max_content_chars else content
609
+
610
+ system_prompt = f"أنت خبير في وضع الأسئلة لمادة {subject}. بناءً على المحتوى التالي من ملف '{file_name}'، قم بإنشاء 5 أسئلة متنوعة لاختبار الفهم. يجب أن تشمل الأسئلة (إذا أمكن): سؤال اختيار من متعدد (MCQ) واحد على الأقل، سؤال إجابة قصيرة واحد على الأقل، وسؤال يتطلب تفكيراً أعمق قليلاً. اجعل الأسئلة واضحة ومباشرة."
611
+
612
  messages = [
613
  {"role": "system", "content": system_prompt},
614
+ {"role": "user", "content": f"المحتوى:\n```\n{content_snippet}\n```\n\nقم بإنشاء 5 أسئلة متنوعة بناءً على هذا المحتوى."}
615
  ]
616
+
617
  response = await self._call_nvidia_api(messages)
618
+ return f"❓ **أسئلة مقترحة من ملف: {file_name}**\n\n{response}"
619
 
620
  async def generate_summary(self, subject, user_id):
621
  """تلخيص ملف مهم من المادة"""
622
+ if subject not in self.available_materials or not self.available_materials[subject]['files']:
 
623
  return "❌ لا توجد ملفات في هذه المادة لتلخيصها."
624
+
625
+ files = self.available_materials[subject]['files']
626
+
627
+ # إعطاء أولوية للملفات المسماة "summary" أو "ملخص"
628
  summary_file = next((f for f in files if f['type'] == 'summary'), None)
629
+ # إذا لم يوجد، اختر أول ملف محاضرة
630
+ if not summary_file:
631
+ summary_file = next((f for f in files if f['type'] == 'lecture'), None)
632
+ # إذا لم يوجد محاضرة أيضاً، اختر أول ملف
633
  if not summary_file:
634
+ summary_file = files[0]
635
+
 
636
  file_path = summary_file['path']
637
+ file_name = summary_file['name']
638
+
639
  await bot.application.bot.send_chat_action(chat_id=user_id, action="typing")
640
  content = await self.download_and_extract_content(file_path, subject)
 
 
 
641
 
642
+ if content.startswith("Error:") or not content.strip():
643
+ logger.error(f"Failed to get content for summary from {file_name}. Content: {content[:100]}")
644
+ return f"❌ لم أتمكن من قراءة ملف ({file_name}) للتلخيص."
645
+
646
+ max_content_chars = 7000
647
+ content_snippet = content[:max_content_chars] if len(content) > max_content_chars else content
648
+
649
+ system_prompt = f"أنت خبير في تلخيص المواد العلمية لمادة {subject}. قم بتلخيص المحتوى التالي من ملف '{file_name}' في 5 نقاط رئيسية وموجزة. يجب أن تغطي النقاط أهم الأفكار والمفاهيم الواردة في النص."
650
+
651
  messages = [
652
  {"role": "system", "content": system_prompt},
653
+ {"role": "user", "content": f"المحتوى:\n```\n{content_snippet}\n```\n\nلخص هذا المحتوى في 5 نقاط رئيسية."}
654
  ]
655
+
656
  response = await self._call_nvidia_api(messages)
657
+ return f"📋 **ملخص ملف: {file_name}**\n\n{response}"
658
 
659
  # ========== (باقي دوال المعالجة الأصلية) ==========
660
+
661
  async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
662
  """بدء المحادثة وعرض القائمة الرئيسية"""
663
  user_id = update.effective_user.id
664
+ logger.info(f"User {user_id} started the bot.")
665
+
666
  welcome_text = """
667
  🏥 **مرحباً بك في بوت المختبرات الطبية الذكي** 🔬
668
 
669
+ أنا هنا لمساعدتك في دراسة موادك! 📚
670
+
671
+ **المواد المتاحة حالياً:**
672
  """
673
+
674
  if not self.available_materials:
675
+ welcome_text += "\n\n⚠️ عذراً، لم أتمكن من تحميل أي مواد دراسية حالياً. حاول تحديث القائمة بالضغط على (🔄)."
676
  else:
677
+ for subject in sorted(self.available_materials.keys()): # فرز المواد أبجدياً
678
  file_count = len(self.available_materials[subject]['files'])
679
  welcome_text += f"\n• {subject} ({file_count} ملف)"
680
+
681
+ welcome_text += "\n\n👇 اختر المادة التي تريد البدء بها:"
682
+
683
  keyboard = self.create_subjects_keyboard()
684
  reply_markup = InlineKeyboardMarkup(keyboard)
685
+
686
+ # إذا كانت استجابة لـ callback query (مثل العودة للقائمة), عدّل الرسالة
687
  if update.callback_query:
688
+ try:
689
+ await update.callback_query.edit_message_text(welcome_text, reply_markup=reply_markup)
690
+ except Exception as e:
691
+ logger.error(f"Error editing message in start: {e}")
692
+ # إذا فشل التعديل (ربما الرسالة قديمة), أرسل رسالة جديدة
693
+ await update.effective_message.reply_text(welcome_text, reply_markup=reply_markup)
694
  else:
695
+ await update.message.reply_text(welcome_text, reply_markup=reply_markup)
696
+
697
  return SELECTING_SUBJECT
698
+
699
  def create_subjects_keyboard(self):
700
  """إنشاء لوحة مفاتيح للمواد المتاحة"""
701
  keyboard = []
702
+ # عرض المواد في عمودين إذا كان عددها أكبر من 4
703
+ subjects = sorted(self.available_materials.keys())
704
+ row = []
705
+ max_cols = 2 if len(subjects) > 4 else 1
706
+ for i, subject in enumerate(subjects):
707
  file_count = len(self.available_materials[subject]['files'])
708
  display_name = f"{subject} ({file_count})"
709
+ row.append(InlineKeyboardButton(display_name, callback_data=f"subject_{subject}"))
710
+ if len(row) == max_cols or i == len(subjects) - 1:
711
+ keyboard.append(row)
712
+ row = []
713
+
714
+
715
  keyboard.append([InlineKeyboardButton("🔄 تحديث قائمة المواد", callback_data="refresh_materials")])
716
  keyboard.append([InlineKeyboardButton("❓ مساعدة", callback_data="general_help")])
717
  return keyboard
718
+
719
  async def handle_subject_selection(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
720
  """معالجة اختيار المادة"""
721
  query = update.callback_query
722
  await query.answer()
723
+
724
  user_id = query.from_user.id
725
  callback_data = query.data
726
+ logger.info(f"User {user_id} selected: {callback_data}")
727
+
728
  if callback_data == "general_help":
729
  return await self.handle_general_help(query, context)
730
  elif callback_data == "refresh_materials":
731
  await query.edit_message_text("🔄 جاري تحديث قائمة المواد...")
732
+ self.load_all_materials() # إعادة تحميل المواد
733
  keyboard = self.create_subjects_keyboard()
734
  reply_markup = InlineKeyboardMarkup(keyboard)
735
+ await query.edit_message_text("✅ تم تحديث قائمة المواد.\nاختر المادة:", reply_markup=reply_markup)
736
  return SELECTING_SUBJECT
737
+
738
  subject = callback_data.replace("subject_", "")
739
  context.user_data['current_subject'] = subject
740
+
741
  memory = self.get_user_memory(user_id)
742
  memory['last_subject'] = subject
743
+
744
  # التأكد من أن المادة موجودة (احتياطي)
745
  if subject not in self.available_materials:
746
+ logger.error(f"Selected subject '{subject}' not found in available materials.")
747
+ await query.edit_message_text("❌ خطأ: المادة المحددة غير موجودة. ربما تحتاج لتحديث القائمة؟")
748
+ return await self.start(update, context) # العودة للقائمة الرئيسية
749
 
750
  subject_files = self.available_materials[subject]['files']
751
  subject_name = subject.replace('_', ' ').title()
752
+
753
  keyboard = [
754
+ [InlineKeyboardButton("📖 شرح محاضرة", callback_data="explain_lecture"), InlineKeyboardButton("🔍 استعراض الملفات", callback_data="browse_files")],
755
+ [InlineKeyboardButton(" توليد أسئلة", callback_data="generate_questions"), InlineKeyboardButton("📝 تلخيص ملف", callback_data="summarize_content")],
 
 
756
  [InlineKeyboardButton("🧪 تفسير مفهوم", callback_data="explain_concept")],
757
+ [InlineKeyboardButton("🏠 العودة للقائمة الرئيسية", callback_data="main_menu")]
758
  ]
759
  reply_markup = InlineKeyboardMarkup(keyboard)
760
+
761
  await query.edit_message_text(
762
  f"📚 **{subject_name}**\n\n"
763
+ f"عدد الملفات المتاحة: {len(subject_files)}\n\n"
764
+ f"ماذا تريد أن تفعل؟",
765
  reply_markup=reply_markup,
766
  parse_mode='Markdown'
767
  )
 
771
  """معالجة العودة إلى قائمة الإجراءات"""
772
  query = update.callback_query
773
  await query.answer()
774
+ logger.info(f"User {query.from_user.id} requested back to actions.")
775
+
776
  subject = context.user_data.get('current_subject')
777
+ if not subject or subject not in self.available_materials:
778
+ logger.warning("Subject context lost or invalid in handle_back_actions. Returning to main menu.")
779
  return await self.start(update, context) # إذا ضاع السياق، عد للبداية
780
 
781
  subject_files = self.available_materials[subject]['files']
782
  subject_name = subject.replace('_', ' ').title()
783
+
784
  keyboard = [
785
+ [InlineKeyboardButton("📖 شرح محاضرة", callback_data="explain_lecture"), InlineKeyboardButton("🔍 استعراض الملفات", callback_data="browse_files")],
786
+ [InlineKeyboardButton(" توليد أسئلة", callback_data="generate_questions"), InlineKeyboardButton("📝 تلخيص ملف", callback_data="summarize_content")],
 
 
787
  [InlineKeyboardButton("🧪 تفسير مفهوم", callback_data="explain_concept")],
788
+ [InlineKeyboardButton("🏠 العودة للقائمة الرئيسية", callback_data="main_menu")]
789
  ]
790
  reply_markup = InlineKeyboardMarkup(keyboard)
791
+
792
  await query.edit_message_text(
793
  f"📚 **{subject_name}**\n\n"
794
+ f"ماذا تريد أن تفعل؟",
795
  reply_markup=reply_markup,
796
  parse_mode='Markdown'
797
  )
798
  return SELECTING_ACTION
799
+
800
  async def handle_main_menu(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
801
  """معالجة العودة للقائمة الرئيسية"""
802
  query = update.callback_query
803
  await query.answer()
804
+ logger.info(f"User {query.from_user.id} requested main menu.")
805
  context.user_data.clear() # تنظيف السياق عند العودة للرئيسية
806
  return await self.start(update, context)
807
+
808
  async def handle_more_questions(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
809
+ """معالجة طلب المزيد من الأسئلة أو العودة"""
810
  query = update.callback_query
811
  await query.answer()
812
+ logger.info(f"User {query.from_user.id} requested more questions / interaction.")
813
+
814
  subject = context.user_data.get('current_subject', 'عام')
815
+
816
  keyboard = [
817
+ [InlineKeyboardButton("🔙 العودة لقائمة الخيارات", callback_data="back_to_actions")],
818
  ]
819
  reply_markup = InlineKeyboardMarkup(keyboard)
820
+
821
+ await query.edit_message_text(f"💬 تفضل، اكتب سؤالك أو طلبك الجديد المتعلق بمادة '{subject}':", reply_markup=reply_markup)
822
+ context.user_data['waiting_for'] = 'general' # ضبط الحالة لانتظار استعلام عام
823
  return WAITING_FOR_QUESTION
824
+
825
  async def handle_change_subject(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
826
  """معالجة تغيير المادة"""
827
  query = update.callback_query
828
  await query.answer()
829
+ logger.info(f"User {query.from_user.id} requested change subject.")
830
  keyboard = self.create_subjects_keyboard()
831
  reply_markup = InlineKeyboardMarkup(keyboard)
832
+ await query.edit_message_text("🔄 اختر المادة الجديدة:", reply_markup=reply_markup)
833
  return SELECTING_SUBJECT
834
 
835
  async def handle_action_selection(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
836
  """معالجة اختيار الإجراء"""
837
  query = update.callback_query
838
  await query.answer()
839
+
840
  action = query.data
841
  user_id = query.from_user.id
842
  subject = context.user_data.get('current_subject')
843
+ logger.info(f"User {user_id} selected action: {action} for subject: {subject}")
844
 
845
+
846
+ if not subject or subject not in self.available_materials:
847
+ logger.error("Subject context lost or invalid in handle_action_selection.")
848
  await query.edit_message_text("❌ حدث خطأ في السياق. يرجى البدء من جديد.")
849
  return await self.start(update, context)
850
 
851
+ # زر العودة للقائمة الرئيسية
852
+ back_button = InlineKeyboardButton("🔙 رجوع", callback_data="back_to_actions")
853
+ keyboard_with_back = [[back_button]]
854
+ reply_markup_back = InlineKeyboardMarkup(keyboard_with_back)
855
+
856
+
857
  if action == "main_menu":
858
  return await self.start(update, context)
859
+
860
  elif action == "browse_files":
861
+ files_text = await self.get_files_list_text(subject)
862
+ keyboard = [
863
+ # يمكن إضافة زر لشرح ملف من هنا مباشرة إذا أردت
864
+ [InlineKeyboardButton("📖 طلب شرح ملف محدد", callback_data="explain_lecture")],
865
+ [back_button]
866
+ ]
867
+ reply_markup = InlineKeyboardMarkup(keyboard)
868
+ await query.edit_message_text(files_text, reply_markup=reply_markup)
869
+ # البقاء في نفس الحالة لعرض الخيارات مرة أخرى
870
+ return SELECTING_ACTION
871
+
872
+
873
  elif action == "explain_lecture":
874
  files_list = await self.get_files_list_text(subject)
875
+ if files_list.startswith(""): # إذا لم توجد ملفات
876
+ await query.edit_message_text(files_list, reply_markup=reply_markup_back)
877
+ return SELECTING_ACTION
878
+
879
  await query.edit_message_text(
880
  f"📖 **شرح محاضرة**\n\n"
881
+ f"{files_list}\n\n"
882
+ f"📝 اكتب رقم الملف من القائمة أعلاه أو جزءاً من اسمه:",
883
+ reply_markup=reply_markup_back
 
884
  )
885
  context.user_data['waiting_for'] = 'lecture_explanation'
886
  return WAITING_FOR_QUESTION
887
+
888
  elif action == "generate_questions":
889
+ await query.edit_message_text("⏳ حسناً، جاري توليد بعض الأسئلة للمراجعة...", reply_markup=reply_markup_back)
890
  questions = await self.generate_questions_for_subject(subject, user_id)
891
+ # await query.edit_message_text(questions, reply_markup=reply_markup_back) # لا تعدل الرسالة، أرسل الأسئلة كرد
892
+ await query.message.reply_text(questions, reply_markup=reply_markup_back) # ارسال رد جديد
893
+ return SELECTING_ACTION # العودة لقائمة الخيارات
894
+
 
895
  elif action == "explain_concept":
 
 
896
  await query.edit_message_text(
897
  "🧪 **تفسير مفهوم**\n\n"
898
+ "ما هو المفهوم أو المصطلح الطبي الذي تود شرحه؟\n"
899
+ "مثال: 'ما هو تحليل CBC؟' أو 'اشرح لي دورة كريبس'",
900
+ reply_markup=reply_markup_back
901
  )
902
  context.user_data['waiting_for'] = 'concept_explanation'
903
  return WAITING_FOR_QUESTION
904
+
905
  elif action == "summarize_content":
906
+ await query.edit_message_text("⏳ تمام، جاري تلخيص ملف مهم من المادة...", reply_markup=reply_markup_back)
907
  summary = await self.generate_summary(subject, user_id)
908
+ # await query.edit_message_text(summary, reply_markup=reply_markup_back) # لا تعدل، أرسل كرد
909
+ await query.message.reply_text(summary, reply_markup=reply_markup_back)
910
+ return SELECTING_ACTION # العودة لقائمة الخيارات
 
911
 
912
+ # إذا لم يتعرف على الإجراء (احتياطي)
913
+ logger.warning(f"Unknown action selected: {action}")
914
+ await query.message.reply_text("عذراً، لم أتعرف على هذا الخيار.")
 
 
 
 
 
 
 
 
 
915
  return SELECTING_ACTION
916
 
917
+
918
+ async def browse_available_files(self, query, context):
919
+ """عرض الملفات المتاحة للمادة - تم دمجه في handle_action_selection"""
920
+ # هذه الدالة لم تعد مستخدمة بشكل مباشر، الكود موجود في handle_action_selection
921
+ pass
922
+
923
+
924
  async def get_files_list_text(self, subject):
925
  """إنشاء نص لقائمة الملفات"""
926
  if subject not in self.available_materials:
927
+ logger.error(f"Subject '{subject}' not found when trying to list files.")
928
  return "❌ خطأ: لم يتم العثور على المادة المحددة."
929
 
930
  files = self.available_materials[subject]['files']
931
+
932
  if not files:
933
+ return "❌ لا توجد ملفات متاحة لهذه المادة بعد."
934
+
935
+ files_text = f"📁 **الملفات المتاحة لمادة {subject}:**\n\n"
936
+ # عرض عدد محدود من الملفات لتجنب رسالة طويلة جداً
937
+ max_files_to_show = 25
938
+ for i, file_info in enumerate(files[:max_files_to_show], 1):
939
  file_name = file_info['name']
940
  lecture_num = file_info['lecture_number']
941
  file_type = file_info['type']
942
+
943
  type_emoji = {
944
  'lecture': '📖',
945
+ 'lab': '🧪',
946
  'exam': '📝',
947
  'summary': '📋',
948
  'unknown': '📄'
949
  }.get(file_type, '📄')
950
+
951
+ num_text = f" (محاضرة {lecture_num})" if lecture_num else ""
952
+ # تنسيق السطر: الرقم. ايموجي اسم_الملف (رقم المحاضرة إن وجد)
953
+ files_text += f"{i}. {type_emoji} `{file_name}`{num_text}\n"
954
+
955
+ if len(files) > max_files_to_show:
956
+ files_text += f"\n... و {len(files) - max_files_to_show} ملفات أخرى."
957
+
958
  return files_text
959
 
960
+
961
  async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
962
  """معالجة الرسائل النصية من المستخدم"""
963
+ if not update.message or not update.message.text:
964
+ return # تجاهل الرسائل غير النصية أو الفارغة
965
+
966
  user_message = update.message.text
967
  user_id = update.effective_user.id
968
  waiting_for = context.user_data.get('waiting_for')
969
  subject = context.user_data.get('current_subject', 'general')
970
+ logger.info(f"User {user_id} sent message: '{user_message}' | waiting_for: {waiting_for} | subject: {subject}")
971
+
972
+
973
  await update.message.chat.send_action(action="typing")
974
+
975
+ response = "عذراً، لم أفهم طلبك. هل يمكنك توضيحه؟" # رد افتراضي
976
+ next_state = WAITING_FOR_QUESTION # البقاء في حالة الانتظار افتراضياً
977
+
978
  try:
979
  if waiting_for == 'lecture_explanation':
980
  response = await self.explain_lecture(user_message, subject, user_id)
981
+ next_state = SELECTING_ACTION # تم الرد، العودة للخيارات
982
  elif waiting_for == 'concept_explanation':
983
  response = await self.explain_concept(user_message, subject, user_id)
984
+ next_state = SELECTING_ACTION # تم الرد، العودة للخيارات
985
+ else: # يتضمن 'general' أو أي شيء آخر (مثل سؤال عام بعد الضغط على "أسئلة أخرى")
986
  response = await self.process_general_query(user_message, subject, user_id)
987
+ next_state = SELECTING_ACTION # تم الرد، العودة للخيارات
988
+
989
+ # إرسال الرد
990
  await update.message.reply_text(response, parse_mode='Markdown')
991
+
992
+ # إذا نجح الرد، أرسل قائمة الخيارات التالية
993
+ if next_state == SELECTING_ACTION:
994
+ context.user_data['waiting_for'] = None # إنهاء حالة الانتظار
995
+ keyboard = [
996
+ [InlineKeyboardButton("🔄 طرح سؤال آخر", callback_data="more_questions")],
997
+ [InlineKeyboardButton("📚 تغيير المادة", callback_data="change_subject")],
998
+ [InlineKeyboardButton("🏠 القائمة الرئيسية", callback_data="main_menu")]
999
+ ]
1000
+ reply_markup = InlineKeyboardMarkup(keyboard)
1001
+ await update.message.reply_text("هل تحتاج مساعدة أخرى؟", reply_markup=reply_markup)
1002
+
1003
+ return next_state # العودة للحالة المناسبة (إما خيارات أو انتظار سؤال آخر)
1004
+
1005
  except Exception as e:
1006
+ logger.error(f"❌ Error processing message from user {user_id}: {e}", exc_info=True)
1007
+ await update.message.reply_text("❌ حدث خطأ غير متوقع أثناء معالجة طلبك. لقد تم تسجيل الخطأ. يرجى المحاولة مرة أخرى أو اختيار خيار آخر.")
1008
+ context.user_data['waiting_for'] = None # إنهاء الانتظار عند حدوث خطأ فادح
1009
+ return SELECTING_ACTION # العودة لقائمة الخيارات كحل احتياطي آمن
1010
+
1011
 
1012
  # ========== إنشاء كائن البوت ==========
1013
  bot = MedicalLabBot()
 
1019
  """الصفحة الرئيسية"""
1020
  materials_count = len(bot.available_materials)
1021
  total_files = sum(len(material['files']) for material in bot.available_materials.values())
1022
+
1023
  # تحديد رسالة الحالة بناءً على حالة التهيئة
1024
  status_message = "⏳ جاري التهيئة (الاتصال بـ Telegram)..."
1025
  status_color = "#ffc107" # أصفر
1026
+ init_details = ""
1027
  if bot.initialization_status == "success":
1028
  status_message = "✅ نشط ومهيأ"
1029
  status_color = "#28a745" # أخضر
1030
+ init_details = "تم الاتصال بـ Telegram وضبط الويب هوك بنجاح."
1031
  elif bot.initialization_status == "failed":
1032
+ status_message = "❌ فشل التهيئة"
1033
  status_color = "#dc3545" # أحمر
1034
+ init_details = "فشل الاتصال بـ Telegram أو ضبط الويب هوك بعد عدة محاولات."
1035
+
1036
+
1037
  return f"""
1038
  <html>
1039
  <head>
1040
+ <title>Medical Lab Bot Status</title>
1041
  <style>
1042
+ body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; margin: 40px; background-color: #f8f9fa; color: #343a40; line-height: 1.6; }}
1043
+ .container {{ max-width: 800px; margin: 20px auto; background-color: #ffffff; padding: 30px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); }}
1044
+ .status-box {{ padding: 25px; margin-bottom: 25px; border-radius: 8px; border-left: 6px solid {status_color}; background-color: #e9ecef; }}
1045
+ .status-box h2 {{ margin-top: 0; color: #212529; font-size: 1.6em; }}
1046
+ .status-box p {{ margin-bottom: 8px; font-size: 1.1em; }}
1047
+ h1 {{ color: #0056b3; text-align: center; margin-bottom: 30px; font-weight: 600; }}
1048
+ h3 {{ color: #0056b3; border-bottom: 2px solid #dee2e6; padding-bottom: 8px; margin-top: 30px; }}
1049
+ ul {{ list-style: none; padding-left: 0; }}
1050
+ li {{ margin-bottom: 12px; padding-left: 25px; position: relative; }}
1051
+ li::before {{ content: '✓'; color: {status_color}; position: absolute; left: 0; font-weight: bold; }}
1052
+ .info-box {{ padding: 20px; background-color: #f1f3f5; border-radius: 8px; margin-top: 30px; font-size: 0.95em; color: #495057; }}
1053
+ strong {{ color: #212529; }}
1054
  </style>
1055
  </head>
1056
  <body>
1057
  <div class="container">
1058
+ <h1>🏥 حالة بوت المختبرات الطبية الذكي</h1>
1059
+ <div class="status-box">
1060
+ <h2>{status_message}</h2>
1061
+ <p><strong>تفاصيل التهيئة:</strong> {init_details}</p>
1062
  <p><strong>المواد المحملة:</strong> {materials_count} مادة</p>
1063
  <p><strong>إجمالي الملفات:</strong> {total_files} ملف</p>
1064
  </div>
1065
+ <h3>🎯 المميزات الرئيسية:</h3>
1066
  <ul>
1067
+ <li>شرح المواد الدراسية من ملفات PDF وWord وTXT.</li>
1068
+ <li>توليد أسئلة متنوعة للمراجعة واختبار الفهم.</li>
1069
+ <li>تلخيص المحتوى الدراسي في نقاط رئيسية.</li>
1070
+ <li>تفسير المفاهيم والمصطلحات العلمية والطبية.</li>
1071
+ <li>استعراض الملفات المتاحة لكل مادة.</li>
1072
+ <li>ذاكرة محادثة لكل مستخدم للحفاظ على السياق.</li>
1073
  </ul>
1074
+ <div class="info-box">
1075
+ <strong>ℹ️ معلومات تقنية:</strong> هذا البوت يستخدم FastAPI للواجهة، Uvicorn كخادم، مكتبة python-telegram-bot للتفاعل مع Telegram، Hugging Face Datasets لتخزين المواد الدراسية، وواجهة برمجة تطبيقات NVIDIA للقدرات الذكية.
1076
  </div>
1077
  </div>
1078
  </body>
 
1087
  # التحقق من أن التطبيق مهيأ
1088
  if not bot.is_initialized or not bot.application:
1089
  logger.error("❌ التطبيق غير مهيأ، لا يمكن معالجة التحديث (ربما لا يزال قيد التهيئة).")
1090
+ # إرجاع 503 Service Unavailable لإخبار Telegram بإعادة المحاولة لاحقًا
1091
  raise HTTPException(status_code=503, detail="Application not initialized or still initializing")
1092
+
1093
  update_data = await request.json()
1094
+ # logger.debug(f"Received update: {update_data}") # إلغاء التعليق للتتبع المكثف
1095
  update = Update.de_json(update_data, bot.application.bot)
1096
+
1097
+ # معالجة التحديث في مهمة منفصلة لتجنب حظر الحلقة الرئيسية
1098
+ asyncio.create_task(bot.application.process_update(update))
1099
+
1100
+ # إرجاع استجابة سريعة لـ Telegram
1101
  return {"status": "ok"}
1102
+
1103
+ except json.JSONDecodeError:
1104
+ logger.error("❌ Received invalid JSON data.")
1105
+ raise HTTPException(status_code=400, detail="Invalid JSON data")
1106
+ except HTTPException as http_exc:
1107
+ # إعادة إرسال الخطأ HTTP كما هو (مثل 503 أعلاه)
1108
+ raise http_exc
1109
  except Exception as e:
1110
+ logger.error(f"❌ Error processing update: {e}", exc_info=True)
1111
+ # إرجاع خطأ عام 500 Internal Server Error
1112
+ raise HTTPException(status_code=500, detail="Internal server error while processing update")
1113
+
1114
 
1115
  @app.get("/health")
1116
  async def health_check():
1117
  """فحص صحة الخدمة"""
1118
  materials_count = len(bot.available_materials)
1119
  total_files = sum(len(material['files']) for material in bot.available_materials.values())
1120
+
1121
+ status_code = 503 # Service Unavailable افتراضياً
1122
+ service_status = "unhealthy"
1123
  if bot.initialization_status == "pending":
1124
+ service_status = "initializing"
1125
+ # يمكن إرجاع 200 هنا إذا أردت أن تعتبره "صحي" أثناء التهيئة
1126
+ # status_code = 200
1127
  elif bot.initialization_status == "success":
1128
+ service_status = "healthy"
1129
+ status_code = 200 # OK
1130
  elif bot.initialization_status == "failed":
1131
+ service_status = "unhealthy_failed_init"
1132
+ status_code = 500 # Internal Server Error
1133
+
1134
+
1135
+ response_payload = {
1136
+ "status": service_status,
1137
  "service": "medical-lab-bot",
1138
  "initialization_status": bot.initialization_status,
1139
  "materials_loaded": materials_count,
 
1141
  "timestamp": datetime.now().isoformat()
1142
  }
1143
 
1144
+ # استخدام JSONResponse لتحديد status_code بشكل صحيح
1145
+ from fastapi.responses import JSONResponse
1146
+ return JSONResponse(content=response_payload, status_code=status_code)
1147
+
1148
  # ========== التشغيل الرئيسي ==========
1149
  if __name__ == "__main__":
1150
  port = int(os.environ.get("PORT", 7860))
1151
+ # يمكن تحديد مستوى السجل من متغير بيئة أيضاً
1152
+ log_level = os.environ.get("LOG_LEVEL", "info").lower()
1153
+ logger.info(f"🚀 Starting Medical Lab Bot on port {port} with log level {log_level}")
1154
+ uvicorn.run(
1155
+ "app:app", # تغيير لاستخدام صيغة uvicorn القياسية
1156
+ host="0.0.0.0",
1157
+ port=port,
1158
+ log_level=log_level,
1159
+ reload=False # تعطيل إعادة التحميل التلقائي في الإنتاج
1160
+ )