Update app.py
Browse files
app.py
CHANGED
|
@@ -1,32 +1,22 @@
|
|
| 1 |
import os
|
| 2 |
import logging
|
| 3 |
import asyncio
|
| 4 |
-
import
|
|
|
|
| 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
|
| 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 |
-
|
| 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 |
-
"""تهيئة تطبيق التليجرام
|
| 124 |
try:
|
| 125 |
if self.is_initialized:
|
| 126 |
return True
|
| 127 |
|
| 128 |
logger.info("🔄 جاري تهيئة تطبيق التليجرام...")
|
| 129 |
|
| 130 |
-
#
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
#
|
| 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 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
if SPACE_URL:
|
| 154 |
webhook_url = f"{SPACE_URL.rstrip('/')}/telegram"
|
| 155 |
logger.info(f"ℹ️ جاري إعداد الويب هوك على: {webhook_url}")
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
)
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
else:
|
| 163 |
logger.warning("⚠️ SPACE_URL not set. Webhook cannot be set.")
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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) ==
|
| 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 |
-
|
| 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 |
-
# ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = "⏳ جاري
|
| 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 |
-
<
|
| 973 |
-
|
| 974 |
-
<
|
| 975 |
-
|
| 976 |
-
|
| 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 |
-
|
| 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}"
|
| 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 |
-
|
| 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 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 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 |
-
|
| 1060 |
-
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|