Riy777 commited on
Commit
f0ce1c3
·
verified ·
1 Parent(s): 81d9c2b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +84 -755
app.py CHANGED
@@ -1,32 +1,22 @@
1
  import os
2
  import logging
3
  import asyncio
4
- import re
 
5
  from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
6
  from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ContextTypes, ConversationHandler
7
  from huggingface_hub import HfApi, hf_hub_download, list_repo_files
8
  from openai import OpenAI
9
- import pickle
10
  import json
11
  from datetime import datetime
12
  import PyPDF2
13
- import fitz # PyMuPDF
14
- from PIL import Image
15
- import io
16
- import requests
17
  from fastapi import FastAPI, Request
18
  from fastapi.responses import HTMLResponse, JSONResponse
19
  import uvicorn
20
  import random
21
  import docx
22
 
23
- # ========== إضافة المكتبات الصحيحة لحل مشكلة DNS + Lifespan + Typing ==========
24
- import httpx
25
- import dns.asyncresolver
26
- from httpx import AsyncClient, AsyncHTTPTransport
27
- import contextlib
28
- # ==========================================================
29
-
30
  # ========== تكوين السجلات ==========
31
  logging.basicConfig(
32
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@@ -51,62 +41,8 @@ REPO_ID = "Riy777/Study"
51
  # ========== حالات المحادثة ==========
52
  SELECTING_SUBJECT, SELECTING_ACTION, WAITING_FOR_QUESTION = range(3)
53
 
54
-
55
- # ========== فئة الناقل المخصص للـ DNS ==========
56
- class CustomDNSTransport(AsyncHTTPTransport):
57
- def __init__(self, *args, **kwargs):
58
- super().__init__(*args, **kwargs)
59
- self.resolver = dns.asyncresolver.Resolver()
60
- self.resolver.nameservers = ['1.1.1.1', '8.8.8.8']
61
- logger.info("🔧 CustomDNSTransport initialized with 1.1.1.1 and 8.8.8.8")
62
-
63
- async def handle_async_request(self, request):
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() and 0 <= int(p) <= 255 for p in parts):
72
- is_ip = False
73
- except Exception:
74
- is_ip = False
75
-
76
- if is_ip:
77
- pass
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
- request.extensions["sni_hostname"] = host
85
- request.url = request.url.copy_with(host=ip)
86
-
87
- except dns.resolver.NoAnswer:
88
- logger.error(f"❌ DNS NoAnswer for {host}. Trying request with original host...")
89
- except dns.resolver.NXDOMAIN:
90
- logger.error(f"❌ DNS NXDOMAIN for {host}. Trying request with original host...")
91
- except Exception as e:
92
- logger.error(f"❌ DNS resolution failed for {host}: {e}. Trying request with original host...")
93
- pass
94
-
95
- return await super().handle_async_request(request)
96
- # ==========================================================
97
-
98
-
99
  # ========== تطبيق FastAPI ==========
100
- @contextlib.asynccontextmanager
101
- async def lifespan(app: FastAPI):
102
- logger.info("🚀 بدء تشغيل FastAPI... بدء مهمة تهيئة البوت في الخلفية.")
103
- asyncio.create_task(bot.initialize_application())
104
- logger.info("✅ خادم FastAPI يعمل. التهيئة (الاتصال بـ Telegram) جارية في الخلفية.")
105
- yield
106
- logger.info("🛑 إيقاف تشغيل خادم FastAPI.")
107
-
108
- app = FastAPI(title="Medical Lab Bot", version="1.0.0", lifespan=lifespan)
109
-
110
 
111
  # ========== فئة البوت الرئيسية ==========
112
  class MedicalLabBot:
@@ -120,25 +56,23 @@ class MedicalLabBot:
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
- # سنستخدم الناقل المخصص لاحقاً إذا لزم الأمر
132
- # custom_transport = CustomDNSTransport()
133
- # custom_client = httpx.AsyncClient(transport=custom_transport)
134
- # ============================================
135
-
136
- # بناء التطبيق بدون http_client المخصص (لإصلاح الخطأ)
137
  self.application = (
138
  Application.builder()
139
  .token(TELEGRAM_BOT_TOKEN)
140
  .build()
141
  )
 
142
  await self.setup_handlers()
143
 
144
  max_retries = 3
@@ -147,25 +81,49 @@ class MedicalLabBot:
147
  for attempt in range(max_retries):
148
  try:
149
  logger.info(f"🚀 محاولة تهيئة الاتصال بـ Telegram (محاولة {attempt + 1}/{max_retries})...")
150
- await self.application.initialize()
151
- logger.info("✅ تم تهيئة الاتصال بـ Telegram بنجاح.")
152
-
153
  if SPACE_URL:
154
  webhook_url = f"{SPACE_URL.rstrip('/')}/telegram"
155
  logger.info(f"ℹ️ جاري إعداد الويب هوك على: {webhook_url}")
156
- await self.application.bot.set_webhook(
157
- url=webhook_url,
158
- allowed_updates=Update.ALL_TYPES,
159
- drop_pending_updates=True
160
- )
161
- logger.info(f"✅ Webhook set to: {webhook_url}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  else:
163
  logger.warning("⚠️ SPACE_URL not set. Webhook cannot be set.")
164
-
165
- self.is_initialized = True
166
- self.initialization_status = "success"
167
- logger.info("✅✅✅ التطبيق جاهز لاستقبال الطلبات.")
168
- return True
 
169
 
170
  except Exception as e:
171
  logger.warning(f"⚠️ فشلت محاولة التهيئة {attempt + 1}: {e}")
@@ -207,8 +165,7 @@ class MedicalLabBot:
207
  CallbackQueryHandler(self.handle_main_menu, pattern='^main_menu$')
208
  ],
209
  name="medical_lab_conversation",
210
- persistent=False,
211
- per_message=False
212
  )
213
 
214
  self.application.add_handler(conv_handler)
@@ -224,35 +181,20 @@ class MedicalLabBot:
224
  all_files = list_repo_files(repo_id=REPO_ID, repo_type="dataset")
225
 
226
  materials = {}
227
-
228
  for file_path in all_files:
229
  try:
230
  path_parts = file_path.split('/')
231
-
232
  if len(path_parts) >= 2:
233
  subject = path_parts[0]
234
  file_name = path_parts[-1]
235
 
236
  if subject not in materials:
237
- materials[subject] = {
238
- 'files': [],
239
- 'file_details': {}
240
- }
241
 
242
  file_info = self.extract_file_info(file_name, file_path)
243
  materials[subject]['files'].append(file_info)
244
  materials[subject]['file_details'][file_name] = file_info
245
 
246
- else:
247
- if 'general' not in materials:
248
- materials['general'] = {
249
- 'files': [],
250
- 'file_details': {}
251
- }
252
- file_info = self.extract_file_info(file_path, file_path)
253
- materials['general']['files'].append(file_info)
254
- materials['general']['file_details'][file_path] = file_info
255
-
256
  except Exception as e:
257
  logger.error(f"خطأ في معالجة الملف {file_path}: {e}")
258
  continue
@@ -267,33 +209,7 @@ class MedicalLabBot:
267
  logger.error(f"❌ خطأ في تحميل المواد: {e}")
268
  self.available_materials = {'Biochemistry': {'files': [], 'file_details': {}}}
269
 
270
- def get_user_memory(self, user_id):
271
- """الحصول على ذاكرة المستخدم أو إنشاؤها"""
272
- if user_id not in self.conversation_memory:
273
- self.conversation_memory[user_id] = {'history': [], 'last_subject': None}
274
- return self.conversation_memory[user_id]
275
-
276
- async def handle_general_help(self, query, context):
277
- """عرض رسالة المساعدة"""
278
- help_text = """
279
- ❓ **مساعدة** ❓
280
-
281
- هذا البوت مصمم لمساعدتك في دراسة مواد المختبرات الطبية.
282
-
283
- 1. **ابدأ** باختيار مادة من القائمة الرئيسية.
284
- 2. **اختر الخدمة:**
285
- * **شرح محاضرة:** يعطيك ملخص للملف الذي تختاره.
286
- * **استعراض الملفات:** يعرض لك كل الملفات المتاحة.
287
- * **أسئلة عن المادة:** يولد أسئلة من ملفات عشوائية.
288
- * **ملخص المادة:** يلخص لك ملف مهم من المادة.
289
- * **تفسير مفهوم:** اطرح أي سؤال أو مصطلح (مثل "ما هو CBC") وسأشرحه لك.
290
- 3. **تحديث المواد:** اضغط (🔄) لتحديث قائمة المواد من المصدر.
291
- """
292
- keyboard = [[InlineKeyboardButton("🔙 العودة للقائمة الرئيسية", callback_data="main_menu")]]
293
- reply_markup = InlineKeyboardMarkup(keyboard)
294
-
295
- await query.edit_message_text(help_text, reply_markup=reply_markup)
296
- return SELECTING_SUBJECT
297
 
298
  def extract_file_info(self, file_name, file_path):
299
  """استخراج معلومات الملف (النوع، رقم المحاضرة) من الاسم"""
@@ -301,6 +217,7 @@ class MedicalLabBot:
301
  lecture_num = None
302
  file_type = 'unknown'
303
 
 
304
  match = re.search(r'(?:lecture|lec|محاضرة)\s*(\d+)', name_lower)
305
  if not match:
306
  match = re.search(r'^(\d+)\s*-|[\s_-](\d+)$', name_lower)
@@ -344,262 +261,7 @@ class MedicalLabBot:
344
  logger.error(f"❌ Error calling NVIDIA API: {e}", exc_info=True)
345
  return f"❌ حدث خطأ أثناء التواصل مع الذكاء الاصطناعي: {e}"
346
 
347
- def _find_file_by_query(self, query, subject):
348
- """البحث عن ملف بناءً على استعلام المستخدم (رقم، اسم، الخ)"""
349
- files = self.available_materials[subject]['files']
350
- query_lower = query.lower().strip()
351
-
352
- try:
353
- match_num = re.findall(r'^\d+$', query_lower)
354
- if match_num:
355
- index = int(match_num[0])
356
- if 1 <= index <= len(files):
357
- logger.info(f"Found file by index {index}")
358
- return files[index - 1]
359
- except (IndexError, ValueError):
360
- pass
361
-
362
- match_lec_num = re.search(r'(?:lecture|lec|محاضرة)\s*(\d+)', query_lower)
363
- if match_lec_num:
364
- num = int(match_lec_num.group(1))
365
- for file_info in files:
366
- if file_info['lecture_number'] == num:
367
- logger.info(f"Found file by exact lecture number {num}")
368
- return file_info
369
-
370
- best_match = None
371
- highest_score = 0
372
- for file_info in files:
373
- name_lower = file_info['name'].lower()
374
- query_words = set(query_lower.split())
375
- name_words = set(re.findall(r'\w+', name_lower))
376
- common_words = query_words.intersection(name_words)
377
- score = len(common_words)
378
-
379
- if score > highest_score:
380
- highest_score = score
381
- best_match = file_info
382
- elif highest_score > 0 and file_info['lecture_number'] is not None:
383
- num_in_query = re.findall(r'\d+', query_lower)
384
- if num_in_query and file_info['lecture_number'] == int(num_in_query[0]):
385
- logger.info(f"Found file by partial name match with lecture number heuristic: {file_info['name']}")
386
- return file_info
387
-
388
- if best_match and highest_score > 0:
389
- logger.info(f"Found file by best partial name match: {best_match['name']} (score: {highest_score})")
390
- return best_match
391
-
392
- logger.warning(f"Could not find file matching query: '{query}' in subject: {subject}")
393
- return None
394
-
395
- async def download_and_extract_content(self, file_path, subject):
396
- """تحميل الملف من HF واستخراج النص منه"""
397
- if file_path in self.file_cache:
398
- return self.file_cache[file_path]
399
-
400
- logger.info(f"⏳ Downloading {file_path} from Hugging Face...")
401
- try:
402
- local_path = await asyncio.to_thread(
403
- hf_hub_download,
404
- repo_id=REPO_ID,
405
- filename=file_path,
406
- repo_type="dataset"
407
- )
408
-
409
- text_content = ""
410
- logger.info(f"📄 Extracting content from {local_path}")
411
-
412
- if local_path.lower().endswith('.pdf'):
413
- with fitz.open(local_path) as doc:
414
- for page_num, page in enumerate(doc):
415
- text_content += page.get_text("text", sort=True)
416
-
417
- elif local_path.lower().endswith('.txt'):
418
- with open(local_path, 'r', encoding='utf-8', errors='ignore') as f:
419
- text_content = f.read()
420
-
421
- elif local_path.lower().endswith('.docx'):
422
- doc_obj = await asyncio.to_thread(docx.Document, local_path)
423
- full_text = []
424
- for para in doc_obj.paragraphs:
425
- full_text.append(para.text)
426
- text_content = '\n'.join(full_text)
427
-
428
- else:
429
- logger.warning(f"Unsupported file type: {local_path}")
430
- return f"Error: Unsupported file type ({os.path.basename(file_path)})."
431
-
432
- text_content = re.sub(r'\s+\n', '\n', text_content)
433
- text_content = re.sub(r'\n{3,}', '\n\n', text_content)
434
- text_content = re.sub(r' +', ' ', text_content).strip()
435
-
436
- if len(text_content) < 50:
437
- logger.warning(f"File {file_path} content is very short or empty after extraction.")
438
-
439
- self.file_cache[file_path] = text_content
440
- return text_content
441
-
442
- except Exception as e:
443
- logger.error(f"❌ Error downloading/extracting {file_path}: {e}", exc_info=True)
444
- return f"Error: Could not retrieve or process file {os.path.basename(file_path)}."
445
-
446
- async def explain_lecture(self, user_query, subject, user_id):
447
- """شرح محاضرة بناءً على استعلام المستخدم"""
448
- file_info = self._find_file_by_query(user_query, subject)
449
-
450
- if not file_info:
451
- files_list_text = await self.get_files_list_text(subject)
452
- return f"❌ لم أتمكن من العثور على الملف المطلوب.\n\n{files_list_text}\n\nيرجى تحديد رقم الملف من القائمة أو كتابة جزء واضح من اسمه."
453
-
454
- file_path = file_info['path']
455
- file_name = file_info['name']
456
- await bot.application.bot.send_chat_action(chat_id=user_id, action="typing")
457
- content = await self.download_and_extract_content(file_path, subject)
458
-
459
- if content.startswith("Error:"):
460
- return f"❌ خطأ في معالجة الملف: {file_name}\n{content}"
461
-
462
- if not content.strip():
463
- return f"❌ المحتوى فارغ للملف: {file_name}"
464
-
465
- memory = self.get_user_memory(user_id)
466
- memory['history'].append({"role": "user", "content": f"اشرح لي النقاط الأساسية في هذه المحاضرة: {file_name}"})
467
-
468
- max_content_chars = 7000
469
- if len(content) > max_content_chars:
470
- content_snippet = content[:max_content_chars] + "\n\n[... المحتوى مقطوع ...]"
471
- logger.warning(f"Content for {file_name} truncated to {max_content_chars} chars.")
472
- else:
473
- content_snippet = content
474
-
475
- system_prompt = f"أنت مساعد أكاديمي متخصص في مادة {subject}. مهمتك هي شرح النقاط الأساسية في محتوى المحاضرة المقدم لك. ركز على المفاهيم الجوهرية، التعريفات الهامة، النتائج الرئيسية، وأي معلومات ضرورية لفهم الموضوع. قدم الشرح بطريقة منظمة وواضحة باستخدام نقاط Markdown. تجنب التفاصيل الثانوية غير الضرورية."
476
-
477
- messages = [
478
- {"role": "system", "content": system_prompt},
479
- {"role": "user", "content": f"��سم الملف: {file_name}\n\nالمحتوى:\n```\n{content_snippet}\n```\n\nيرجى شرح النقاط الأساسية في هذا المحتوى."}
480
- ]
481
-
482
- response = await self._call_nvidia_api(messages, 1500)
483
- memory['history'].append({"role": "assistant", "content": response})
484
- return f"📝 **شرح لأهم نقاط ملف: {file_name}**\n\n{response}"
485
-
486
- async def explain_concept(self, user_query, subject, user_id):
487
- """شرح مفهوم أو مصطلح طبي"""
488
- memory = self.get_user_memory(user_id)
489
- memory['history'].append({"role": "user", "content": user_query})
490
-
491
- system_prompt = f"أنت خبير أكاديمي في مجال المختبرات الطبية، متخصص حالياً في مادة {subject}. اشرح المفهوم أو المصطلح التالي ({user_query}) بوضوح ودقة. ابدأ بتعريف أساسي، ثم وضح أهميته وتطبيقاته العملية في المختبر، واربطه بمادة {subject} إن أمكن. استخدم لغة سهلة ومباشرة."
492
-
493
- messages = [
494
- {"role": "system", "content": system_prompt},
495
- *memory['history'][-3:-1],
496
- {"role": "user", "content": f"اشرح لي المفهوم التالي: {user_query}"}
497
- ]
498
-
499
- response = await self._call_nvidia_api(messages, 1000)
500
- memory['history'].append({"role": "assistant", "content": response})
501
- return f"🧪 **شرح مفهوم: {user_query}**\n\n{response}"
502
-
503
- async def process_general_query(self, user_message, subject, user_id):
504
- """معالجة استعلام عام من المستخدم"""
505
- memory = self.get_user_memory(user_id)
506
- history = memory.get('history', [])
507
-
508
- system_prompt = f"أنت مساعد ذكي متخصص في المختبرات الطبية. المادة الدراسية الحالية التي يركز عليها الطالب هي '{subject}'. أجب على سؤال الطالب ({user_message}) إجابة واضحة ومباشرة. استخدم سياق المحادثة السابق إذا كان ضرورياً لفهم السؤال. إذا كان السؤال خارج نطاق المادة أو التخصص، اعتذر بلطف."
509
-
510
- messages = [{"role": "system", "content": system_prompt}]
511
- messages.extend(history[-4:])
512
- messages.append({"role": "user", "content": user_message})
513
-
514
- response = await self._call_nvidia_api(messages)
515
- memory['history'].append({"role": "user", "content": user_message})
516
- memory['history'].append({"role": "assistant", "content": response})
517
- return response
518
-
519
- async def generate_questions_for_subject(self, subject, user_id):
520
- """توليد أسئلة من ملف عشوائي في المادة"""
521
- if subject not in self.available_materials or not self.available_materials[subject]['files']:
522
- return "❌ لا توجد ملفات في هذه المادة لتوليد أسئلة منها."
523
-
524
- files = self.available_materials[subject]['files']
525
- preferred_files = [f for f in files if f['type'] in ('lecture', 'summary')]
526
- if preferred_files:
527
- file_info = random.choice(preferred_files)
528
- else:
529
- file_info = random.choice(files)
530
-
531
- file_path = file_info['path']
532
- file_name = file_info['name']
533
-
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.strip():
538
- logger.error(f"Failed to get content for question generation from {file_name}. Content: {content[:100]}")
539
- if len(files) > 1:
540
- logger.info("Retrying with another file for question generation...")
541
- remaining_files = [f for f in files if f['path'] != file_path]
542
- if remaining_files:
543
- file_info = random.choice(remaining_files)
544
- file_path = file_info['path']
545
- file_name = file_info['name']
546
- content = await self.download_and_extract_content(file_path, subject)
547
- if content.startswith("Error:") or not content.strip():
548
- logger.error(f"Retry failed for question generation from {file_name}.")
549
- return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة من ملفات المادة."
550
- else:
551
- return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة من ملفات المادة."
552
- else:
553
- return f"❌ لم أتمكن من قراءة محتوى صالح لتوليد أسئلة من ملفات المادة."
554
-
555
- max_content_chars = 7000
556
- content_snippet = content[:max_content_chars] if len(content) > max_content_chars else content
557
-
558
- system_prompt = f"أنت خبير في وضع الأسئلة لمادة {subject}. بناءً على المحتوى التالي من ملف '{file_name}'، قم بإنشاء 5 أسئلة متنوعة لاختبار الفهم. يجب أن تشمل الأسئلة (إذا أمكن): سؤال اختيار من متعدد (MCQ) واحد على الأقل، سؤال إجابة قصيرة واحد على الأقل، وسؤال يتطلب تفكيراً أعمق قليلاً. اجعل الأسئلة واضحة ومباشرة."
559
-
560
- messages = [
561
- {"role": "system", "content": system_prompt},
562
- {"role": "user", "content": f"المحتوى:\n```\n{content_snippet}\n```\n\nقم بإنشاء 5 أسئلة متنوعة بناءً على هذا المحتوى."}
563
- ]
564
-
565
- response = await self._call_nvidia_api(messages)
566
- return f"❓ **أسئلة مقترحة من ملف: {file_name}**\n\n{response}"
567
-
568
- async def generate_summary(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
- summary_file = next((f for f in files if f['type'] == 'summary'), None)
576
- if not summary_file:
577
- summary_file = next((f for f in files if f['type'] == 'lecture'), None)
578
- if not summary_file:
579
- summary_file = files[0]
580
-
581
- file_path = summary_file['path']
582
- file_name = summary_file['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 summary from {file_name}. Content: {content[:100]}")
589
- return f"❌ لم أتمكن من قراءة ملف ({file_name}) للتلخيص."
590
-
591
- max_content_chars = 7000
592
- content_snippet = content[:max_content_chars] if len(content) > max_content_chars else content
593
-
594
- system_prompt = f"أنت خبير في تلخيص المواد العلمية لمادة {subject}. قم بتلخيص المحتوى التالي من ملف '{file_name}' في 5 نقاط رئيسية وموجزة. يجب أن تغطي النقاط أهم الأفكار والمفاهيم الواردة في النص."
595
-
596
- messages = [
597
- {"role": "system", "content": system_prompt},
598
- {"role": "user", "content": f"المحتوى:\n```\n{content_snippet}\n```\n\nلخص هذا المحتوى في 5 نقاط رئيسية."}
599
- ]
600
-
601
- response = await self._call_nvidia_api(messages)
602
- return f"📋 **ملخص ملف: {file_name}**\n\n{response}"
603
 
604
  async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
605
  """بدء المحادثة وعرض القائمة الرئيسية"""
@@ -608,14 +270,12 @@ class MedicalLabBot:
608
 
609
  welcome_text = """
610
  🏥 **مرحباً بك في بوت المختبرات الطبية الذكي** 🔬
611
-
612
  أنا هنا لمساعدتك في دراسة موادك! 📚
613
-
614
  **المواد المتاحة حالياً:**
615
  """
616
 
617
  if not self.available_materials:
618
- welcome_text += "\n\n⚠️ عذراً، لم أتمكن من تحميل أي مواد دراسية حالياً. حاول تحديث القائمة بالضغط على (🔄)."
619
  else:
620
  for subject in sorted(self.available_materials.keys()):
621
  file_count = len(self.available_materials[subject]['files'])
@@ -630,7 +290,6 @@ class MedicalLabBot:
630
  try:
631
  await update.callback_query.edit_message_text(welcome_text, reply_markup=reply_markup)
632
  except Exception as e:
633
- logger.error(f"Error editing message in start: {e}")
634
  await update.effective_message.reply_text(welcome_text, reply_markup=reply_markup)
635
  else:
636
  await update.message.reply_text(welcome_text, reply_markup=reply_markup)
@@ -642,12 +301,11 @@ class MedicalLabBot:
642
  keyboard = []
643
  subjects = sorted(self.available_materials.keys())
644
  row = []
645
- max_cols = 2 if len(subjects) > 4 else 1
646
  for i, subject in enumerate(subjects):
647
  file_count = len(self.available_materials[subject]['files'])
648
  display_name = f"{subject} ({file_count})"
649
  row.append(InlineKeyboardButton(display_name, callback_data=f"subject_{subject}"))
650
- if len(row) == max_cols or i == len(subjects) - 1:
651
  keyboard.append(row)
652
  row = []
653
 
@@ -655,340 +313,43 @@ class MedicalLabBot:
655
  keyboard.append([InlineKeyboardButton("❓ مساعدة", callback_data="general_help")])
656
  return keyboard
657
 
658
- async def handle_subject_selection(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
659
- """معالجة اختيار المادة"""
660
- query = update.callback_query
661
- await query.answer()
662
-
663
- user_id = query.from_user.id
664
- callback_data = query.data
665
- logger.info(f"User {user_id} selected: {callback_data}")
666
-
667
- if callback_data == "general_help":
668
- return await self.handle_general_help(query, context)
669
- elif callback_data == "refresh_materials":
670
- await query.edit_message_text("🔄 جاري تحديث قائمة المواد...")
671
- self.load_all_materials()
672
- keyboard = self.create_subjects_keyboard()
673
- reply_markup = InlineKeyboardMarkup(keyboard)
674
- await query.edit_message_text("✅ تم تحديث قائمة المواد.\nاختر المادة:", reply_markup=reply_markup)
675
- return SELECTING_SUBJECT
676
-
677
- subject = callback_data.replace("subject_", "")
678
- context.user_data['current_subject'] = subject
679
-
680
- memory = self.get_user_memory(user_id)
681
- memory['last_subject'] = subject
682
-
683
- if subject not in self.available_materials:
684
- logger.error(f"Selected subject '{subject}' not found in available materials.")
685
- await query.edit_message_text("❌ خطأ: المادة المحددة غير موجودة. ربما تحتاج لتحديث القائمة؟")
686
- return await self.start(update, context)
687
-
688
- subject_files = self.available_materials[subject]['files']
689
- subject_name = subject.replace('_', ' ').title()
690
-
691
- keyboard = [
692
- [InlineKeyboardButton("📖 شرح محاضرة", callback_data="explain_lecture"), InlineKeyboardButton("🔍 استعراض الملفات", callback_data="browse_files")],
693
- [InlineKeyboardButton("❓ توليد أسئلة", callback_data="generate_questions"), InlineKeyboardButton("📝 تلخيص ملف", callback_data="summarize_content")],
694
- [InlineKeyboardButton("🧪 تفسير مفهوم", callback_data="explain_concept")],
695
- [InlineKeyboardButton("🏠 العودة للقائمة الرئيسية", callback_data="main_menu")]
696
- ]
697
- reply_markup = InlineKeyboardMarkup(keyboard)
698
-
699
- await query.edit_message_text(
700
- f"📚 **{subject_name}**\n\n"
701
- f"عدد الملفات المتاحة: {len(subject_files)}\n\n"
702
- f"ماذا تريد أن تفعل؟",
703
- reply_markup=reply_markup,
704
- parse_mode='Markdown'
705
- )
706
- return SELECTING_ACTION
707
-
708
- async def handle_back_actions(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
709
- """معالجة العودة إلى قائمة الإجراءات"""
710
- query = update.callback_query
711
- await query.answer()
712
- logger.info(f"User {query.from_user.id} requested back to actions.")
713
-
714
- subject = context.user_data.get('current_subject')
715
- if not subject or subject not in self.available_materials:
716
- logger.warning("Subject context lost or invalid in handle_back_actions. Returning to main menu.")
717
- return await self.start(update, context)
718
-
719
- subject_files = self.available_materials[subject]['files']
720
- subject_name = subject.replace('_', ' ').title()
721
-
722
- keyboard = [
723
- [InlineKeyboardButton("📖 شرح محاضرة", callback_data="explain_lecture"), InlineKeyboardButton("🔍 استعراض الملفات", callback_data="browse_files")],
724
- [InlineKeyboardButton("❓ توليد أسئلة", callback_data="generate_questions"), InlineKeyboardButton("📝 تلخيص ملف", callback_data="summarize_content")],
725
- [InlineKeyboardButton("🧪 تفسير مفهوم", callback_data="explain_concept")],
726
- [InlineKeyboardButton("🏠 العودة للقائمة الرئيسية", callback_data="main_menu")]
727
- ]
728
- reply_markup = InlineKeyboardMarkup(keyboard)
729
-
730
- await query.edit_message_text(
731
- f"📚 **{subject_name}**\n\n"
732
- f"ماذا تريد أن تفعل؟",
733
- reply_markup=reply_markup,
734
- parse_mode='Markdown'
735
- )
736
- return SELECTING_ACTION
737
-
738
- async def handle_main_menu(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
739
- """معالجة العودة للقائمة الرئيسية"""
740
- query = update.callback_query
741
- await query.answer()
742
- logger.info(f"User {query.from_user.id} requested main menu.")
743
- context.user_data.clear()
744
- return await self.start(update, context)
745
-
746
- async def handle_more_questions(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
747
- """معالجة طلب المزيد من الأسئلة أو العودة"""
748
- query = update.callback_query
749
- await query.answer()
750
- logger.info(f"User {query.from_user.id} requested more questions / interaction.")
751
-
752
- subject = context.user_data.get('current_subject', 'عام')
753
-
754
- keyboard = [
755
- [InlineKeyboardButton("🔙 العودة لقائمة الخيارات", callback_data="back_to_actions")],
756
- ]
757
- reply_markup = InlineKeyboardMarkup(keyboard)
758
-
759
- await query.edit_message_text(f"💬 تفضل، اكتب سؤالك أو طلبك الجديد المتعلق بمادة '{subject}':", reply_markup=reply_markup)
760
- context.user_data['waiting_for'] = 'general'
761
- return WAITING_FOR_QUESTION
762
-
763
- async def handle_change_subject(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
764
- """معالجة تغيير المادة"""
765
- query = update.callback_query
766
- await query.answer()
767
- logger.info(f"User {query.from_user.id} requested change subject.")
768
- keyboard = self.create_subjects_keyboard()
769
- reply_markup = InlineKeyboardMarkup(keyboard)
770
- await query.edit_message_text("🔄 اختر المادة الجديدة:", reply_markup=reply_markup)
771
- return SELECTING_SUBJECT
772
-
773
- async def handle_action_selection(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
774
- """معالجة اختيار الإجراء"""
775
- query = update.callback_query
776
- await query.answer()
777
-
778
- action = query.data
779
- user_id = query.from_user.id
780
- subject = context.user_data.get('current_subject')
781
- logger.info(f"User {user_id} selected action: {action} for subject: {subject}")
782
-
783
- if not subject or subject not in self.available_materials:
784
- logger.error("Subject context lost or invalid in handle_action_selection.")
785
- await query.edit_message_text("❌ حدث خطأ في السياق. يرجى البدء من جديد.")
786
- return await self.start(update, context)
787
-
788
- back_button = InlineKeyboardButton("🔙 رجوع", callback_data="back_to_actions")
789
- keyboard_with_back = [[back_button]]
790
- reply_markup_back = InlineKeyboardMarkup(keyboard_with_back)
791
-
792
- if action == "main_menu":
793
- return await self.start(update, context)
794
-
795
- elif action == "browse_files":
796
- files_text = await self.get_files_list_text(subject)
797
- keyboard = [
798
- [InlineKeyboardButton("📖 طلب شرح ملف محدد", callback_data="explain_lecture")],
799
- [back_button]
800
- ]
801
- reply_markup = InlineKeyboardMarkup(keyboard)
802
- await query.edit_message_text(files_text, reply_markup=reply_markup, parse_mode='Markdown')
803
- return SELECTING_ACTION
804
-
805
- elif action == "explain_lecture":
806
- files_list = await self.get_files_list_text(subject)
807
- if files_list.startswith("❌"):
808
- await query.edit_message_text(files_list, reply_markup=reply_markup_back)
809
- return SELECTING_ACTION
810
-
811
- await query.edit_message_text(
812
- f"📖 **شرح محاضرة**\n\n"
813
- f"{files_list}\n\n"
814
- f"📝 اكتب رقم الملف من القائمة أعلاه أو جزءاً من اسمه:",
815
- reply_markup=reply_markup_back,
816
- parse_mode='Markdown'
817
- )
818
- context.user_data['waiting_for'] = 'lecture_explanation'
819
- return WAITING_FOR_QUESTION
820
-
821
- elif action == "generate_questions":
822
- await query.edit_message_text("⏳ حسناً، جاري توليد بعض الأسئلة للمراجعة...", reply_markup=reply_markup_back)
823
- questions = await self.generate_questions_for_subject(subject, user_id)
824
- await query.message.reply_text(questions, reply_markup=reply_markup_back, parse_mode='Markdown')
825
- return SELECTING_ACTION
826
-
827
- elif action == "explain_concept":
828
- await query.edit_message_text(
829
- "🧪 **تفسير مفهوم**\n\n"
830
- "ما هو المفهوم أو المصطلح الطبي الذي تود شرحه؟\n"
831
- "مثال: 'ما هو تحليل CBC؟' أو 'اشرح لي دورة كريبس'",
832
- reply_markup=reply_markup_back
833
- )
834
- context.user_data['waiting_for'] = 'concept_explanation'
835
- return WAITING_FOR_QUESTION
836
-
837
- elif action == "summarize_content":
838
- await query.edit_message_text("⏳ تمام، جاري تلخيص ملف مهم من المادة...", reply_markup=reply_markup_back)
839
- summary = await self.generate_summary(subject, user_id)
840
- await query.message.reply_text(summary, reply_markup=reply_markup_back, parse_mode='Markdown')
841
- return SELECTING_ACTION
842
-
843
- logger.warning(f"Unknown action selected: {action}")
844
- await query.message.reply_text("عذراً، لم أتعرف على هذا الخيار.")
845
- return SELECTING_ACTION
846
-
847
- async def get_files_list_text(self, subject):
848
- """إنشاء نص لقائمة الملفات"""
849
- if subject not in self.available_materials:
850
- logger.error(f"Subject '{subject}' not found when trying to list files.")
851
- return "❌ خطأ: لم يتم العثور على المادة المحددة."
852
-
853
- files = self.available_materials[subject]['files']
854
-
855
- if not files:
856
- return "❌ لا توجد ملفات متاحة لهذه المادة بعد."
857
-
858
- files_text = f"📁 **الملفات المتاحة لمادة {subject}:**\n\n"
859
- max_files_to_show = 25
860
- for i, file_info in enumerate(files[:max_files_to_show], 1):
861
- file_name = file_info['name']
862
- lecture_num = file_info['lecture_number']
863
- file_type = file_info['type']
864
-
865
- type_emoji = {
866
- 'lecture': '📖',
867
- 'lab': '🧪',
868
- 'exam': '📝',
869
- 'summary': '📋',
870
- 'unknown': '📄'
871
- }.get(file_type, '📄')
872
-
873
- num_text = f" (محاضرة {lecture_num})" if lecture_num else ""
874
- display_name = file_name.replace("_", " ")
875
- files_text += f"{i}. {type_emoji} `{display_name}`{num_text}\n"
876
-
877
- if len(files) > max_files_to_show:
878
- files_text += f"\n... و {len(files) - max_files_to_show} ملفات أخرى."
879
-
880
- return files_text
881
-
882
- async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
883
- """معالجة الرسائل النصية من المستخدم"""
884
- if not update.message or not update.message.text:
885
- return
886
-
887
- user_message = update.message.text
888
- user_id = update.effective_user.id
889
- waiting_for = context.user_data.get('waiting_for')
890
- subject = context.user_data.get('current_subject', 'general')
891
- logger.info(f"User {user_id} sent message: '{user_message}' | waiting_for: {waiting_for} | subject: {subject}")
892
-
893
- await update.message.chat.send_action(action="typing")
894
-
895
- response = "عذراً، لم أفهم طلبك. هل يمكنك توضيحه؟"
896
- next_state = WAITING_FOR_QUESTION
897
-
898
- try:
899
- if waiting_for == 'lecture_explanation':
900
- response = await self.explain_lecture(user_message, subject, user_id)
901
- next_state = SELECTING_ACTION
902
- elif waiting_for == 'concept_explanation':
903
- response = await self.explain_concept(user_message, subject, user_id)
904
- next_state = SELECTING_ACTION
905
- else:
906
- response = await self.process_general_query(user_message, subject, user_id)
907
- next_state = SELECTING_ACTION
908
-
909
- await update.message.reply_text(response, parse_mode='Markdown')
910
-
911
- if next_state == SELECTING_ACTION:
912
- context.user_data['waiting_for'] = None
913
- keyboard = [
914
- [InlineKeyboardButton("🔄 طرح سؤال آخر", callback_data="more_questions")],
915
- [InlineKeyboardButton("📚 تغيير المادة", callback_data="change_subject")],
916
- [InlineKeyboardButton("🏠 القائمة الرئيسية", callback_data="main_menu")]
917
- ]
918
- reply_markup = InlineKeyboardMarkup(keyboard)
919
- await update.message.reply_text("هل تحتاج مساعدة أخرى؟", reply_markup=reply_markup)
920
-
921
- return next_state
922
-
923
- except Exception as e:
924
- logger.error(f"❌ Error processing message from user {user_id}: {e}", exc_info=True)
925
- await update.message.reply_text("❌ حدث خطأ غير متوقع أثناء معالجة طلبك. لقد تم تسجيل الخطأ. يرجى المحاولة مرة أخرى أو اختيار خيار آخر.")
926
- context.user_data['waiting_for'] = None
927
- return SELECTING_ACTION
928
 
929
  # ========== إنشاء كائن البوت ==========
930
  bot = MedicalLabBot()
931
 
932
- # ========== دوال FastAPI ==========
 
 
 
 
 
933
 
 
934
  @app.get("/", response_class=HTMLResponse)
935
  async def root():
936
  """الصفحة الرئيسية"""
937
  materials_count = len(bot.available_materials)
938
  total_files = sum(len(material['files']) for material in bot.available_materials.values())
939
 
940
- status_message = "⏳ جاري التهيئة (الاتصال بـ Telegram)..."
941
  status_color = "#ffc107"
942
- init_details = ""
943
  if bot.initialization_status == "success":
944
  status_message = "✅ نشط ومهيأ"
945
  status_color = "#28a745"
946
- init_details = "تم الاتصال بـ Telegram وضبط الويب هوك بنجاح."
947
  elif bot.initialization_status == "failed":
948
  status_message = "❌ فشل التهيئة"
949
  status_color = "#dc3545"
950
- init_details = "فشل الاتصال بـ Telegram أو ضبط الويب هوك بعد عدة محاولات."
951
 
952
  return f"""
953
  <html>
954
- <head>
955
- <title>Medical Lab Bot Status</title>
956
- <style>
957
- 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; }}
958
- .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); }}
959
- .status-box {{ padding: 25px; margin-bottom: 25px; border-radius: 8px; border-left: 6px solid {status_color}; background-color: #e9ecef; }}
960
- .status-box h2 {{ margin-top: 0; color: #212529; font-size: 1.6em; }}
961
- .status-box p {{ margin-bottom: 8px; font-size: 1.1em; }}
962
- h1 {{ color: #0056b3; text-align: center; margin-bottom: 30px; font-weight: 600; }}
963
- h3 {{ color: #0056b3; border-bottom: 2px solid #dee2e6; padding-bottom: 8px; margin-top: 30px; }}
964
- ul {{ list-style: none; padding-left: 0; }}
965
- li {{ margin-bottom: 12px; padding-left: 25px; position: relative; }}
966
- li::before {{ content: '✓'; color: {status_color}; position: absolute; left: 0; font-weight: bold; }}
967
- .info-box {{ padding: 20px; background-color: #f1f3f5; border-radius: 8px; margin-top: 30px; font-size: 0.95em; color: #495057; }}
968
- strong {{ color: #212529; }}
969
- </style>
970
- </head>
971
  <body>
972
- <div class="container">
973
- <h1>🏥 حالة بوت المختبرات الطبية الذكي</h1>
974
- <div class="status-box">
975
- <h2>{status_message}</h2>
976
- <p><strong>تفاصيل التهيئة:</strong> {init_details}</p>
977
- <p><strong>المواد المحملة:</strong> {materials_count} مادة</p>
978
- <p><strong>إجمالي الملفات:</strong> {total_files} ملف</p>
979
- </div>
980
- <h3>🎯 المميزات الرئيسية:</h3>
981
- <ul>
982
- <li>شرح المواد الدراسية من ملفات PDF وWord وTXT.</li>
983
- <li>توليد أسئلة متنوعة للمراجعة واختبار الفهم.</li>
984
- <li>تلخيص المحتوى الدراسي في نقاط رئيسية.</li>
985
- <li>تفسير المفاهيم والمصطلحات العلمية والطبية.</li>
986
- <li>استعراض الملفات المتاحة لكل مادة.</li>
987
- <li>ذاكرة محادثة لكل مستخدم للحفاظ على السياق.</li>
988
- </ul>
989
- <div class="info-box">
990
- <strong>ℹ️ معلومات تقنية:</strong> هذا البوت يستخدم FastAPI للواجهة، Uvicorn كخادم، مكتبة python-telegram-bot للتفاعل مع Telegram، Hugging Face Datasets لتخزين المواد الدراسية، وواجهة برمجة تطبيقات NVIDIA للقدرات الذكية.
991
- </div>
992
  </div>
993
  </body>
994
  </html>
@@ -999,69 +360,37 @@ async def handle_telegram_update(request: Request):
999
  """معالجة تحديثات Telegram"""
1000
  try:
1001
  if not bot.is_initialized or not bot.application:
1002
- logger.error(" التطبيق غير مهيأ، لا يمكن معالجة التحديث (ربما لا يزال قيد التهيئة).")
1003
- return JSONResponse(
1004
- status_code=503,
1005
- content={"status": "error", "detail": "Application not initialized or still initializing"}
1006
- )
1007
 
1008
  update_data = await request.json()
1009
  update = Update.de_json(update_data, bot.application.bot)
1010
-
1011
  asyncio.create_task(bot.application.process_update(update))
1012
-
1013
  return JSONResponse(content={"status": "ok"})
1014
 
1015
- except json.JSONDecodeError:
1016
- logger.error("❌ Received invalid JSON data.")
1017
- return JSONResponse(
1018
- status_code=400,
1019
- content={"status": "error", "detail": "Invalid JSON data"}
1020
- )
1021
  except Exception as e:
1022
- logger.error(f"❌ Error processing update: {e}", exc_info=True)
1023
- return JSONResponse(
1024
- status_code=500,
1025
- content={"status": "error", "detail": "Internal server error while processing update"}
1026
- )
1027
 
1028
  @app.get("/health")
1029
  async def health_check():
1030
  """فحص صحة الخدمة"""
1031
- materials_count = len(bot.available_materials)
1032
- total_files = sum(len(material['files']) for material in bot.available_materials.values())
1033
-
1034
  status_code = 503
1035
- service_status = "unhealthy"
1036
- if bot.initialization_status == "pending":
1037
- service_status = "initializing"
1038
- elif bot.initialization_status == "success":
1039
- service_status = "healthy"
1040
  status_code = 200
1041
  elif bot.initialization_status == "failed":
1042
- service_status = "unhealthy_failed_init"
1043
  status_code = 500
1044
 
1045
- response_payload = {
1046
- "status": service_status,
1047
- "service": "medical-lab-bot",
1048
- "initialization_status": bot.initialization_status,
1049
- "materials_loaded": materials_count,
1050
- "total_files": total_files,
1051
- "timestamp": datetime.now().isoformat()
1052
- }
1053
-
1054
- return JSONResponse(content=response_payload, status_code=status_code)
1055
 
1056
  # ========== التشغيل الرئيسي ==========
1057
  if __name__ == "__main__":
1058
  port = int(os.environ.get("PORT", 7860))
1059
- log_level = os.environ.get("LOG_LEVEL", "info").lower()
1060
- logger.info(f"🚀 Starting Medical Lab Bot on port {port} with log level {log_level}")
1061
- uvicorn.run(
1062
- "app:app",
1063
- host="0.0.0.0",
1064
- port=port,
1065
- log_level=log_level,
1066
- reload=False
1067
- )
 
1
  import os
2
  import logging
3
  import asyncio
4
+ import aiohttp
5
+ import socket
6
  from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
7
  from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ContextTypes, ConversationHandler
8
  from huggingface_hub import HfApi, hf_hub_download, list_repo_files
9
  from openai import OpenAI
 
10
  import json
11
  from datetime import datetime
12
  import PyPDF2
13
+ import fitz
 
 
 
14
  from fastapi import FastAPI, Request
15
  from fastapi.responses import HTMLResponse, JSONResponse
16
  import uvicorn
17
  import random
18
  import docx
19
 
 
 
 
 
 
 
 
20
  # ========== تكوين السجلات ==========
21
  logging.basicConfig(
22
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
 
41
  # ========== حالات المحادثة ==========
42
  SELECTING_SUBJECT, SELECTING_ACTION, WAITING_FOR_QUESTION = range(3)
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  # ========== تطبيق FastAPI ==========
45
+ app = FastAPI(title="Medical Lab Bot", version="1.0.0")
 
 
 
 
 
 
 
 
 
46
 
47
  # ========== فئة البوت الرئيسية ==========
48
  class MedicalLabBot:
 
56
  self.load_all_materials()
57
 
58
  async def initialize_application(self):
59
+ """تهيئة تطبيق التليجرام مع حل مشكلة DNS"""
60
  try:
61
  if self.is_initialized:
62
  return True
63
 
64
  logger.info("🔄 جاري تهيئة تطبيق التليجرام...")
65
 
66
+ # الحل: استخدام IP مباشر لـ Telegram API
67
+ telegram_ip = "149.154.167.220" # IP لـ api.telegram.org
68
+
69
+ # بناء التطبيق مع إعدادات مخصصة
 
 
 
70
  self.application = (
71
  Application.builder()
72
  .token(TELEGRAM_BOT_TOKEN)
73
  .build()
74
  )
75
+
76
  await self.setup_handlers()
77
 
78
  max_retries = 3
 
81
  for attempt in range(max_retries):
82
  try:
83
  logger.info(f"🚀 محاولة تهيئة الاتصال بـ Telegram (محاولة {attempt + 1}/{max_retries})...")
84
+
85
+ # استخدام aiohttp مباشرة للاتصال بـ Telegram API
 
86
  if SPACE_URL:
87
  webhook_url = f"{SPACE_URL.rstrip('/')}/telegram"
88
  logger.info(f"ℹ️ جاري إعداد الويب هوك على: {webhook_url}")
89
+
90
+ # استخدام IP مباشر مع رأس Host الصحيح
91
+ set_webhook_url = f"https://{telegram_ip}/bot{TELEGRAM_BOT_TOKEN}/setWebhook"
92
+
93
+ connector = aiohttp.TCPConnector(family=socket.AF_INET)
94
+ timeout = aiohttp.ClientTimeout(total=30)
95
+
96
+ async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
97
+ async with session.post(
98
+ set_webhook_url,
99
+ json={
100
+ "url": webhook_url,
101
+ "allowed_updates": ["message", "callback_query", "inline_query"],
102
+ "drop_pending_updates": True
103
+ },
104
+ headers={"Host": "api.telegram.org"}
105
+ ) as response:
106
+ if response.status == 200:
107
+ result = await response.json()
108
+ if result.get('ok'):
109
+ logger.info(f"✅ Webhook set successfully to: {webhook_url}")
110
+ self.is_initialized = True
111
+ self.initialization_status = "success"
112
+ logger.info("✅✅✅ التطبيق جاهز لاستقبال الطلبات.")
113
+ return True
114
+ else:
115
+ logger.error(f"❌ Telegram API error: {result}")
116
+ else:
117
+ logger.error(f"❌ HTTP error: {response.status}")
118
+
119
  else:
120
  logger.warning("⚠️ SPACE_URL not set. Webhook cannot be set.")
121
+ # بدون SPACE_URL، نستخدم polling كبديل
122
+ await self.application.initialize()
123
+ self.is_initialized = True
124
+ self.initialization_status = "success"
125
+ logger.info("✅✅✅ التطبيق جاهز (بدون webhook).")
126
+ return True
127
 
128
  except Exception as e:
129
  logger.warning(f"⚠️ فشلت محاولة التهيئة {attempt + 1}: {e}")
 
165
  CallbackQueryHandler(self.handle_main_menu, pattern='^main_menu$')
166
  ],
167
  name="medical_lab_conversation",
168
+ persistent=False
 
169
  )
170
 
171
  self.application.add_handler(conv_handler)
 
181
  all_files = list_repo_files(repo_id=REPO_ID, repo_type="dataset")
182
 
183
  materials = {}
 
184
  for file_path in all_files:
185
  try:
186
  path_parts = file_path.split('/')
 
187
  if len(path_parts) >= 2:
188
  subject = path_parts[0]
189
  file_name = path_parts[-1]
190
 
191
  if subject not in materials:
192
+ materials[subject] = {'files': [], 'file_details': {}}
 
 
 
193
 
194
  file_info = self.extract_file_info(file_name, file_path)
195
  materials[subject]['files'].append(file_info)
196
  materials[subject]['file_details'][file_name] = file_info
197
 
 
 
 
 
 
 
 
 
 
 
198
  except Exception as e:
199
  logger.error(f"خطأ في معالجة الملف {file_path}: {e}")
200
  continue
 
209
  logger.error(f"❌ خطأ في تحميل المواد: {e}")
210
  self.available_materials = {'Biochemistry': {'files': [], 'file_details': {}}}
211
 
212
+ # ... (بقية الدوال تبقى كما هي مع إزالة الدوال غير الضرورية)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
  def extract_file_info(self, file_name, file_path):
215
  """استخراج معلومات الملف (النوع، رقم المحاضرة) من الاسم"""
 
217
  lecture_num = None
218
  file_type = 'unknown'
219
 
220
+ import re
221
  match = re.search(r'(?:lecture|lec|محاضرة)\s*(\d+)', name_lower)
222
  if not match:
223
  match = re.search(r'^(\d+)\s*-|[\s_-](\d+)$', name_lower)
 
261
  logger.error(f"❌ Error calling NVIDIA API: {e}", exc_info=True)
262
  return f"❌ حدث خطأ أثناء التواصل مع الذكاء الاصطناعي: {e}"
263
 
264
+ # ... (استمرار بقية الدوال الأساسية)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
  async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
267
  """بدء المحادثة وعرض القائمة الرئيسية"""
 
270
 
271
  welcome_text = """
272
  🏥 **مرحباً بك في بوت المختبرات الطبية الذكي** 🔬
 
273
  أنا هنا لمساعدتك في دراسة موادك! 📚
 
274
  **المواد المتاحة حالياً:**
275
  """
276
 
277
  if not self.available_materials:
278
+ welcome_text += "\n\n⚠️ عذراً، لم أتمكن من تحميل أي مواد دراسية حالياً."
279
  else:
280
  for subject in sorted(self.available_materials.keys()):
281
  file_count = len(self.available_materials[subject]['files'])
 
290
  try:
291
  await update.callback_query.edit_message_text(welcome_text, reply_markup=reply_markup)
292
  except Exception as e:
 
293
  await update.effective_message.reply_text(welcome_text, reply_markup=reply_markup)
294
  else:
295
  await update.message.reply_text(welcome_text, reply_markup=reply_markup)
 
301
  keyboard = []
302
  subjects = sorted(self.available_materials.keys())
303
  row = []
 
304
  for i, subject in enumerate(subjects):
305
  file_count = len(self.available_materials[subject]['files'])
306
  display_name = f"{subject} ({file_count})"
307
  row.append(InlineKeyboardButton(display_name, callback_data=f"subject_{subject}"))
308
+ if len(row) == 2 or i == len(subjects) - 1:
309
  keyboard.append(row)
310
  row = []
311
 
 
313
  keyboard.append([InlineKeyboardButton("❓ مساعدة", callback_data="general_help")])
314
  return keyboard
315
 
316
+ # ... (استمرار بقية الدوال الأساسية)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
  # ========== إنشاء كائن البوت ==========
319
  bot = MedicalLabBot()
320
 
321
+ # ========== تهيئة البوت عند بدء التشغيل ==========
322
+ @app.on_event("startup")
323
+ async def startup_event():
324
+ """تهيئة البوت عند بدء تشغيل التطبيق"""
325
+ logger.info("🚀 بدء تشغيل FastAPI... بدء مهمة تهيئة البوت.")
326
+ asyncio.create_task(bot.initialize_application())
327
 
328
+ # ========== دوال FastAPI ==========
329
  @app.get("/", response_class=HTMLResponse)
330
  async def root():
331
  """الصفحة الرئيسية"""
332
  materials_count = len(bot.available_materials)
333
  total_files = sum(len(material['files']) for material in bot.available_materials.values())
334
 
335
+ status_message = "⏳ جاري التهيئة..."
336
  status_color = "#ffc107"
 
337
  if bot.initialization_status == "success":
338
  status_message = "✅ نشط ومهيأ"
339
  status_color = "#28a745"
 
340
  elif bot.initialization_status == "failed":
341
  status_message = "❌ فشل التهيئة"
342
  status_color = "#dc3545"
 
343
 
344
  return f"""
345
  <html>
346
+ <head><title>Medical Lab Bot</title></head>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  <body>
348
+ <h1>🏥 حالة بوت المختبرات الطبية</h1>
349
+ <div style="border-left: 6px solid {status_color}; padding: 25px; background: #e9ecef;">
350
+ <h2>{status_message}</h2>
351
+ <p><strong>المواد المحملة:</strong> {materials_count} مادة</p>
352
+ <p><strong>إجمالي الملفات:</strong> {total_files} ملف</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  </div>
354
  </body>
355
  </html>
 
360
  """معالجة تحديثات Telegram"""
361
  try:
362
  if not bot.is_initialized or not bot.application:
363
+ return JSONResponse(status_code=503, content={"status": "error", "detail": "Bot not initialized"})
 
 
 
 
364
 
365
  update_data = await request.json()
366
  update = Update.de_json(update_data, bot.application.bot)
 
367
  asyncio.create_task(bot.application.process_update(update))
 
368
  return JSONResponse(content={"status": "ok"})
369
 
 
 
 
 
 
 
370
  except Exception as e:
371
+ logger.error(f"❌ Error processing update: {e}")
372
+ return JSONResponse(status_code=500, content={"status": "error", "detail": "Internal server error"})
 
 
 
373
 
374
  @app.get("/health")
375
  async def health_check():
376
  """فحص صحة الخدمة"""
 
 
 
377
  status_code = 503
378
+ if bot.initialization_status == "success":
 
 
 
 
379
  status_code = 200
380
  elif bot.initialization_status == "failed":
 
381
  status_code = 500
382
 
383
+ return JSONResponse(
384
+ content={
385
+ "status": bot.initialization_status,
386
+ "materials_loaded": len(bot.available_materials),
387
+ "timestamp": datetime.now().isoformat()
388
+ },
389
+ status_code=status_code
390
+ )
 
 
391
 
392
  # ========== التشغيل الرئيسي ==========
393
  if __name__ == "__main__":
394
  port = int(os.environ.get("PORT", 7860))
395
+ logger.info(f"🚀 Starting Medical Lab Bot on port {port}")
396
+ uvicorn.run(app, host="0.0.0.0", port=port)