رغد Claude Sonnet 4.6 commited on
Commit
344e369
·
1 Parent(s): 81b1089

feat: complete platform — auth, deployment, hardening

Browse files

- Supabase Auth: login/register/forgot-password/reset-password pages
- JWT verification: ES256/JWKS + HS256 fallback in auth_middleware
- RLS: analyses + chat_messages protected via row-level security
- Session ownership enforcement on all data endpoints
- Frontend auth: Bearer token auto-injected via lib/api.ts
- Profile + Settings pages
- Docker production setup: Nginx + Let's Encrypt + Certbot
- GitHub Actions CI/CD for auto-deploy to Oracle Cloud
- deploy.sh: one-command server setup and updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +30 -0
  2. .env.production.example +27 -0
  3. .github/workflows/deploy.yml +41 -0
  4. .gitignore +31 -0
  5. AGENTS.md +145 -0
  6. API_REFERENCE.md +245 -0
  7. ARCHITECTURE.md +112 -0
  8. DEPLOYMENT.md +230 -0
  9. app.py +0 -726
  10. backend/.dockerignore +13 -0
  11. backend/.env.example +30 -0
  12. backend/Dockerfile +17 -6
  13. backend/evaluate.py +60 -39
  14. backend/ingest_medical_kb.py +357 -0
  15. backend/ingest_medlineplus.py +380 -89
  16. backend/main.py +827 -370
  17. backend/{ingest_full.py → medical_data.py} +92 -726
  18. backend/medical_kb/__init__.py +6 -0
  19. backend/medical_kb/reference/__init__.py +4 -0
  20. backend/medical_kb/reference/medical_reference.py +136 -0
  21. backend/medical_kb/reference/normal_ranges.py +130 -0
  22. backend/medical_kb/schemas/cbc.json +153 -0
  23. backend/medical_kb/schemas/diabetes.json +128 -0
  24. backend/medical_kb/schemas/kidney.json +110 -0
  25. backend/medical_kb/schemas/lipid.json +105 -0
  26. backend/medical_kb/schemas/liver.json +145 -0
  27. backend/medical_kb/schemas/thyroid.json +94 -0
  28. backend/medical_reference_schema.json +418 -0
  29. backend/middleware/__init__.py +4 -0
  30. backend/middleware/audit.py +136 -0
  31. backend/middleware/auth_middleware.py +186 -0
  32. backend/middleware/sanitizer.py +155 -0
  33. backend/prompts/examples/cbc_examples.json +76 -0
  34. backend/prompts/examples/thyroid_examples.json +50 -0
  35. backend/prompts/extraction_template.txt +43 -0
  36. backend/prompts/few_shot_examples.json +92 -0
  37. backend/prompts/loader.py +55 -0
  38. backend/prompts/system_analysis.txt +68 -0
  39. backend/prompts/system_chat.txt +57 -0
  40. backend/prompts/templates/cbc_analysis_prompt.txt +44 -0
  41. backend/prompts/templates/diabetes_analysis_prompt.txt +56 -0
  42. backend/prompts/templates/kidney_analysis_prompt.txt +61 -0
  43. backend/prompts/templates/lipid_analysis_prompt.txt +55 -0
  44. backend/prompts/templates/liver_analysis_prompt.txt +46 -0
  45. backend/prompts/templates/system_prompt.txt +30 -0
  46. backend/prompts/templates/thyroid_analysis_prompt.txt +49 -0
  47. backend/railway.json +6 -3
  48. backend/requirements.txt +4 -0
  49. backend/run.py +6 -0
  50. backend/server.log +0 -0
.env.example ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── LLM / AI ──────────────────────────────────────────────────────────────
2
+ GROQ_API_KEY=gsk_...
3
+ COHERE_API_KEY=...
4
+
5
+ # ── Supabase ───────────────────────────────────────────────────────────────
6
+ SUPABASE_URL=https://<project-id>.supabase.co
7
+ SUPABASE_KEY=eyJ...
8
+ SUPABASE_DB_URL=postgresql+psycopg://<user>:<password>@<host>:5432/postgres
9
+
10
+ # ── Google Cloud ───────────────────────────────────────────────────────────
11
+ GOOGLE_VISION_API_KEY=AIza...
12
+ GOOGLE_TTS_KEY=AIza...
13
+
14
+ # ── Voice TTS (optional — gTTS used as free fallback) ─────────────────────
15
+ ELEVENLABS_API_KEY=...
16
+
17
+ # ── App ────────────────────────────────────────────────────────────────────
18
+ ENVIRONMENT=production
19
+ FRONTEND_URL=https://your-domain.com
20
+ NEXT_PUBLIC_API_URL=https://your-domain.com/api
21
+
22
+ # ── Next Auth (required if next-auth is enabled) ──────────────────────────
23
+ NEXTAUTH_URL=https://your-domain.com
24
+ NEXTAUTH_SECRET=change-me-32-chars-minimum
25
+
26
+ # ── Monitoring ─────────────────────────────────────────────────────────────
27
+ SENTRY_DSN=https://...@o...ingest.sentry.io/...
28
+
29
+ # ── Audit logs ─────────────────────────────────────────────────────────────
30
+ AUDIT_LOG_DIR=logs/audit
.env.production.example ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ══════════════════════════════════════════════════════
2
+ # تبيان الطبي — Production Environment Variables
3
+ # Copy to .env on your server and fill in the values
4
+ # ══════════════════════════════════════════════════════
5
+
6
+ # ─── LLM ────────────────────────────────────────────
7
+ GROQ_API_KEY=gsk_...
8
+
9
+ # ─── Cohere (reranking + embeddings fallback) ────────
10
+ COHERE_API_KEY=...
11
+
12
+ # ─── Supabase ────────────────────────────────────────
13
+ SUPABASE_URL=https://xxxx.supabase.co
14
+ SUPABASE_KEY=eyJ... # anon/public key
15
+ SUPABASE_SERVICE_KEY=eyJ... # service_role key (bypasses RLS)
16
+ SUPABASE_JWT_SECRET=... # JWT Settings → JWT Secret
17
+ SUPABASE_DB_URL=postgresql+psycopg://postgres.xxxx:PASSWORD@aws-0-...pooler.supabase.com:5432/postgres
18
+
19
+ # ─── Frontend URL (used by backend CORS) ─────────────
20
+ FRONTEND_URL=https://yourdomain.com
21
+ NEXT_PUBLIC_BACKEND_URL=/ # Use / so Nginx routes /api → backend
22
+
23
+ # ─── App ─────────────────────────────────────────────
24
+ ENVIRONMENT=production
25
+
26
+ # ─── Optional monitoring ─────────────────────────────
27
+ SENTRY_DSN=
.github/workflows/deploy.yml ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Production
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ deploy:
10
+ name: Deploy to Oracle Cloud
11
+ runs-on: ubuntu-latest
12
+ timeout-minutes: 20
13
+
14
+ steps:
15
+ - name: Checkout code
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Deploy via SSH
19
+ uses: appleboy/ssh-action@v1.0.3
20
+ with:
21
+ host: ${{ secrets.SERVER_HOST }}
22
+ username: ${{ secrets.SERVER_USER }}
23
+ key: ${{ secrets.SERVER_SSH_KEY }}
24
+ port: 22
25
+ script: |
26
+ set -e
27
+ cd /opt/tebyan
28
+
29
+ echo "── Pulling latest code ──"
30
+ git pull origin main
31
+
32
+ echo "── Building and restarting ──"
33
+ docker compose -f docker-compose.prod.yml build --pull --no-cache backend frontend
34
+ docker compose -f docker-compose.prod.yml up -d
35
+
36
+ echo "── Waiting for health checks ──"
37
+ sleep 15
38
+ curl -sf http://localhost/health | python3 -m json.tool
39
+
40
+ echo "── Deploy complete ──"
41
+ docker compose -f docker-compose.prod.yml ps
.gitignore CHANGED
@@ -35,3 +35,34 @@ frontend/.vercel/
35
  # OS
36
  .DS_Store
37
  Thumbs.db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  # OS
36
  .DS_Store
37
  Thumbs.db
38
+
39
+ # Logs
40
+ logs/
41
+ *.log
42
+ backend/logs/
43
+ backend_log.txt
44
+
45
+ # Test / temp files
46
+ backend/test_*.png
47
+ backend/test_*.jpg
48
+ backend/eval_results.json
49
+ backend/test_set.json
50
+
51
+ # pip install artifacts (=version files created by mistake)
52
+ =*
53
+ backend/=*
54
+
55
+ # Office docs
56
+ *.docx
57
+ *.doc
58
+ ~$*
59
+
60
+ # tsbuildinfo
61
+ frontend/tsconfig.tsbuildinfo
62
+
63
+ # Large data dirs not needed in prod
64
+ assets/
65
+ books/
66
+ data/
67
+ mine/
68
+ temp/
AGENTS.md ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # تبيان الطبي — Agent System
2
+
3
+ ## Overview
4
+
5
+ The multi-agent pipeline in `backend/services/agents/` processes each medical file through five sequential agents. Each agent reads from and writes to a shared `AgentContext` dataclass, making the full pipeline observable and independently testable.
6
+
7
+ ---
8
+
9
+ ## Base Classes (`agents/base.py`)
10
+
11
+ ### `AgentContext`
12
+
13
+ Shared state object threaded through all agents. Key fields:
14
+
15
+ | Field | Type | Set by |
16
+ |---|---|---|
17
+ | `file_bytes` | `bytes` | Coordinator |
18
+ | `file_type` | `"pdf" \| "image"` | Coordinator |
19
+ | `raw_text` | `str` | OCRAgent |
20
+ | `findings` | `list[dict]` | ExtractionAgent |
21
+ | `panel_code` | `str` | ClassificationAgent |
22
+ | `rag_context` | `str` | MedicalReasoningAgent |
23
+ | `report` | `dict` | MedicalReasoningAgent |
24
+ | `logs` | `list[AgentLogEntry]` | All agents |
25
+
26
+ ### `AgentBase`
27
+
28
+ Abstract base with:
29
+ - **Retry logic**: up to `max_retries=2` attempts with `0.3 s × 2^attempt` backoff
30
+ - **`_on_failure(ctx, exc)`**: each subclass overrides to provide a safe fallback when all retries fail
31
+ - **Timing**: each `run()` call records `duration_ms` in `AgentLogEntry`
32
+
33
+ ---
34
+
35
+ ## Agents
36
+
37
+ ### 1. `OCRAgent`
38
+
39
+ **File**: `agents/ocr_agent.py`
40
+
41
+ **Input**: `ctx.file_bytes`, `ctx.file_type`
42
+ **Output**: `ctx.raw_text`
43
+
44
+ **Strategy**:
45
+ - PDF: extracts text with PyMuPDF (`fitz`); falls back to EasyOCR page-by-page if text layer is empty
46
+ - Image: tries Google Cloud Vision first (higher accuracy for Arabic); falls back to EasyOCR with contrast/sharpness preprocessing
47
+
48
+ **Failure mode**: sets `raw_text = ""` — downstream agents handle empty text gracefully.
49
+
50
+ ---
51
+
52
+ ### 2. `ExtractionAgent`
53
+
54
+ **File**: `agents/extraction_agent.py`
55
+
56
+ **Input**: `ctx.raw_text`
57
+ **Output**: `ctx.findings` (list of `{name, value, unit, range, status}`)
58
+
59
+ **Strategy**:
60
+ 1. Regex patterns matching common Arabic/English lab report formats
61
+ 2. LLM extraction via Groq if regex yields < 2 findings
62
+ 3. Physiological bounds filter (`_validators.py`) removes impossible values (e.g., hemoglobin = 400)
63
+ 4. Deduplication by normalized test name
64
+
65
+ **Failure mode**: sets `findings = []`.
66
+
67
+ ---
68
+
69
+ ### 3. `ClassificationAgent`
70
+
71
+ **File**: `agents/classification_agent.py`
72
+
73
+ **Input**: `ctx.findings`, `ctx.raw_text`
74
+ **Output**: `ctx.panel_code`, `ctx.panel_confidence`
75
+
76
+ **Strategy**: Uses `services/classifier.py` which scores text against panel-specific keyword sets. Falls back to `detect_panel()` heuristic if primary classifier returns low confidence.
77
+
78
+ **Panels**: `cbc`, `thyroid`, `liver`, `kidney`, `lipid`, `diabetes`, `urine`, `mixed`
79
+
80
+ **Failure mode**: sets `panel_code = "mixed"` (general analysis).
81
+
82
+ ---
83
+
84
+ ### 4. `MedicalReasoningAgent`
85
+
86
+ **File**: `agents/reasoning_agent.py`
87
+
88
+ **Input**: `ctx.findings`, `ctx.panel_code`, `ctx.analysis_type`
89
+ **Output**: `ctx.rag_context`, `ctx.report`
90
+
91
+ **Strategy**:
92
+ 1. Checks `rag_cache` (TTL 5 min) for identical query
93
+ 2. Retrieves relevant medical knowledge via `Retriever` (BM25 + pgvector + Cohere rerank)
94
+ 3. Selects panel-specific prompt template from `prompts/`
95
+ 4. Calls Groq `llama-3.3-70b-versatile` with findings + RAG context
96
+ 5. Parses JSON response into structured report
97
+
98
+ **Failure mode**: generates a fallback report from raw findings without LLM, appends disclaimer.
99
+
100
+ ---
101
+
102
+ ### 5. `SafetyAgent`
103
+
104
+ **File**: `agents/safety_agent.py`
105
+
106
+ **Input**: `ctx.report`
107
+ **Output**: `ctx.report` (filtered in-place)
108
+
109
+ **Strategy**: Applies `services/safety.filter_analysis_report()` which:
110
+ - Removes diagnostic certainty claims ("you have diabetes")
111
+ - Adds standard medical disclaimer
112
+ - Detects emergency patterns (very high/low critical values) and prepends urgent notice
113
+
114
+ **Failure mode**: appends `DISCLAIMER_AR` manually to ensure minimum safety even if filter itself errors.
115
+
116
+ ---
117
+
118
+ ## `AgentCoordinator` (`agents/coordinator.py`)
119
+
120
+ Instantiates all five agents and runs them in sequence. Returns `CoordinatorResult`:
121
+
122
+ ```python
123
+ @dataclass
124
+ class CoordinatorResult:
125
+ findings: list[dict]
126
+ summary: str
127
+ report: dict
128
+ panel_code: str
129
+ logs: list[dict] # exposed in dev mode via _agents field
130
+ ok: bool
131
+ error: str
132
+ total_ms: float
133
+ ```
134
+
135
+ The coordinator is loaded once via `@lru_cache` and reused across requests. Agent instances are stateless — all state lives in the per-request `AgentContext`.
136
+
137
+ ---
138
+
139
+ ## Adding a New Agent
140
+
141
+ 1. Create `agents/my_agent.py`, subclass `AgentBase`
142
+ 2. Implement `_execute(self, ctx: AgentContext) -> AgentContext`
143
+ 3. Implement `_on_failure(self, ctx, exc)` with a safe fallback
144
+ 4. Add new fields to `AgentContext` if needed
145
+ 5. Register in `AgentCoordinator.__init__()` agent list at the correct position
API_REFERENCE.md ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # تبيان الطبي — API Reference
2
+
3
+ Base URL: `http://localhost:8000` (dev) / `https://your-domain.com` (prod)
4
+
5
+ All endpoints return JSON unless noted. Arabic text is UTF-8 encoded.
6
+
7
+ ---
8
+
9
+ ## `POST /api/analyze`
10
+
11
+ Analyze a medical lab report image or PDF.
12
+
13
+ **Request**: `multipart/form-data`
14
+
15
+ | Field | Type | Required | Description |
16
+ |---|---|---|---|
17
+ | `file` | File | Yes | PDF or image (JPEG/PNG/WEBP/TIFF). Max 20 MB. |
18
+ | `analysis_type` | string | No | One of: `شامل`, `دم شامل`, `سكر وكوليسترول`, `كلى وكبد`, `هرمونات`, `بول`. Default: `شامل` |
19
+
20
+ **Response** `200`
21
+ ```json
22
+ {
23
+ "findings": [
24
+ { "name": "Hemoglobin", "value": "10.2", "unit": "g/dL", "range": "12-16", "status": "low" }
25
+ ],
26
+ "summary": "تُظهر نتائجك انخفاضاً في الهيموجلوبين...",
27
+ "report": {
28
+ "general": "الحالة العامة...",
29
+ "abnormal_details": [{ "اسم_الفحص": "Hemoglobin", "الشرح": "..." }],
30
+ "tips": ["..."],
31
+ "tips_categorized": [{ "category": "التغذية", "tips": ["..."] }]
32
+ }
33
+ }
34
+ ```
35
+
36
+ **Errors**: `400` bad file, `413` file too large, `415` unsupported type, `422` analysis failed, `429` rate limit (5/min)
37
+
38
+ ---
39
+
40
+ ## `POST /api/chat`
41
+
42
+ Stream an Arabic medical assistant response.
43
+
44
+ **Request**: `application/json`
45
+ ```json
46
+ {
47
+ "query": "ماذا يعني انخفاض الهيموجلوبين؟",
48
+ "history": [{ "role": "user", "content": "..." }, { "role": "assistant", "content": "..." }],
49
+ "analysis_context": "{\"findings\": [...], \"summary\": \"...\"}"
50
+ }
51
+ ```
52
+
53
+ **Response**: `text/plain; charset=utf-8` — streaming text chunks (Server-Sent Events compatible)
54
+
55
+ **Rate limit**: 30/min per IP
56
+
57
+ ---
58
+
59
+ ## `POST /api/risk`
60
+
61
+ Assess disease risk from lab findings.
62
+
63
+ **Request**: `application/json`
64
+ ```json
65
+ {
66
+ "findings": [
67
+ { "name": "Glucose", "value": "126", "unit": "mg/dL", "range": "70-100", "status": "high" }
68
+ ]
69
+ }
70
+ ```
71
+
72
+ **Response** `200`
73
+ ```json
74
+ {
75
+ "risks": [
76
+ {
77
+ "condition": "diabetes",
78
+ "score": 72,
79
+ "level": "high",
80
+ "confidence": 0.85,
81
+ "label_ar": "السكري",
82
+ "factors": ["ارتفاع سكر الصيام"],
83
+ "recommendation": "استشر طبيبك لإجراء اختبار HbA1c",
84
+ "source": "rule"
85
+ }
86
+ ],
87
+ "top_risk": { ... },
88
+ "overall_ar": "توجد مؤشرات تستوجب المتابعة الطبية",
89
+ "features_used": 12
90
+ }
91
+ ```
92
+
93
+ **Conditions scored**: `diabetes`, `cardiovascular`, `anemia`, `kidney`, `liver`, `thyroid`
94
+
95
+ ---
96
+
97
+ ## `POST /api/voice/transcribe`
98
+
99
+ Transcribe Arabic audio to text using Whisper.
100
+
101
+ **Request**: `multipart/form-data`
102
+
103
+ | Field | Type | Description |
104
+ |---|---|---|
105
+ | `audio` | File | Audio file. Formats: WebM, MP4, OGG, WAV, FLAC, M4A. Max 25 MB. |
106
+ | `language` | string | Language hint. Default: `ar` |
107
+
108
+ **Response** `200`
109
+ ```json
110
+ { "text": "ماذا يعني ارتفاع الكوليسترول؟", "language": "ar" }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## `POST /api/voice/synthesize`
116
+
117
+ Convert Arabic text to speech (MP3).
118
+
119
+ **Request**: `application/json`
120
+ ```json
121
+ { "text": "نتائج تحليلك تُظهر..." }
122
+ ```
123
+
124
+ **Response**: `audio/mpeg` binary (MP3). Max input: 3000 characters.
125
+
126
+ ---
127
+
128
+ ## `POST /api/voice/chat`
129
+
130
+ Voice-to-voice chat: transcribe audio → chat → synthesize response.
131
+
132
+ **Request**: `multipart/form-data`
133
+
134
+ | Field | Type | Description |
135
+ |---|---|---|
136
+ | `audio` | File | Audio file (same formats as transcribe) |
137
+ | `analysis_context` | string | JSON string of last analysis result (optional) |
138
+
139
+ **Response**: `audio/mpeg` binary — spoken Arabic assistant response.
140
+
141
+ ---
142
+
143
+ ## `POST /api/search`
144
+
145
+ Semantic search across saved analyses.
146
+
147
+ **Request**: `application/json`
148
+ ```json
149
+ {
150
+ "query": "ارتفاع الكوليسترول",
151
+ "analyses": [
152
+ { "id": "uuid", "summary": "...", "findings_text": "Hemoglobin Glucose ..." }
153
+ ]
154
+ }
155
+ ```
156
+
157
+ **Response** `200`
158
+ ```json
159
+ { "scores": { "uuid-1": 0.82, "uuid-2": 0.31 } }
160
+ ```
161
+
162
+ **Rate limit**: 60/min per IP
163
+
164
+ ---
165
+
166
+ ## `POST /api/analyses/save`
167
+
168
+ Save an analysis result for a session.
169
+
170
+ **Request**: `application/json`
171
+ ```json
172
+ {
173
+ "session_id": "local_abc123",
174
+ "findings": [...],
175
+ "summary": "...",
176
+ "report": { ... }
177
+ }
178
+ ```
179
+
180
+ **Response** `200`: `{ "success": true, "data": [...] }`
181
+
182
+ ---
183
+
184
+ ## `GET /api/analyses/list`
185
+
186
+ List saved analyses for a session.
187
+
188
+ **Query params**: `session_id`, `profile_name` (optional), `limit` (default 20)
189
+
190
+ **Response** `200`: `{ "analyses": [...] }`
191
+
192
+ ---
193
+
194
+ ## `GET /health`
195
+
196
+ System health check.
197
+
198
+ **Response** `200`
199
+ ```json
200
+ {
201
+ "ok": true,
202
+ "version": "local",
203
+ "environment": "development",
204
+ "uptime_s": 3600,
205
+ "db": { "ok": true, "chunks": 2834, "source": "pgvector/supabase" },
206
+ "model": { "name": "intfloat/multilingual-e5-large", "loaded": true },
207
+ "services": { "groq": true, "cohere": true, "vision": false }
208
+ }
209
+ ```
210
+
211
+ ---
212
+
213
+ ## `GET /api/metrics`
214
+
215
+ Prometheus-format plaintext metrics.
216
+
217
+ **Response** `200` `text/plain; version=0.0.4`
218
+ ```
219
+ # HELP tebyan_uptime_seconds Seconds since server start
220
+ tebyan_uptime_seconds 3600
221
+ # HELP tebyan_requests_total Total HTTP requests per path
222
+ tebyan_requests_total{path="/api/analyze"} 42
223
+ tebyan_rag_cache_size 18
224
+ tebyan_rag_cache_hits 156
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Error Format
230
+
231
+ All errors use standard HTTP status codes with a JSON body:
232
+
233
+ ```json
234
+ { "detail": "وصف الخطأ بالعربية أو الإنجليزية" }
235
+ ```
236
+
237
+ | Code | Meaning |
238
+ |---|---|
239
+ | 400 | Bad request (empty file, invalid input) |
240
+ | 413 | File too large (> 20 MB) |
241
+ | 415 | Unsupported media type |
242
+ | 422 | Analysis failed (OCR/LLM error) |
243
+ | 429 | Rate limit exceeded |
244
+ | 500 | Internal server error |
245
+ | 503 | External service unavailable |
ARCHITECTURE.md ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # تبيان الطبي — Architecture
2
+
3
+ ## System Overview
4
+
5
+ Tebyan Medical is a production-grade Arabic medical report analysis platform. Users upload lab reports (PDF or image), the system extracts findings, generates a clinical interpretation in Arabic, and provides an interactive voice-enabled chat assistant.
6
+
7
+ ```
8
+ Frontend (Next.js 15) Backend (FastAPI) External Services
9
+ ───────────────────── ───────────────── ─────────────────
10
+ Upload → /api/analyze ────────► AgentCoordinator Groq (LLM/STT)
11
+ Chat → /api/chat ────────► RAG + LLM streaming Supabase (pgvector)
12
+ Voice → /api/voice ────────► WhisperSTT / TTS Cohere (rerank)
13
+ Risk → /api/risk ────────► RiskEngine Google Vision/TTS
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Backend Layer
19
+
20
+ ### Entry Point
21
+
22
+ `backend/main.py` — FastAPI application. Registers all routes, mounts middleware, and wires dependency-injected singletons via `@lru_cache`.
23
+
24
+ ### Multi-Agent Pipeline (`services/agents/`)
25
+
26
+ Replaces the flat `services/agent/pipeline.py` (kept for backward compat) with a structured agent graph:
27
+
28
+ ```
29
+ AgentCoordinator
30
+ ├── OCRAgent — PDF (fitz + EasyOCR) + Image (Google Vision fallback)
31
+ ├── ExtractionAgent — regex parse → LLM fallback → physiological bounds filter
32
+ ├── ClassificationAgent — panel detection (CBC, Thyroid, Liver, Kidney, Lipid, Diabetes)
33
+ ├── MedicalReasoningAgent — RAG retrieval + panel-specific LLM prompt
34
+ └── SafetyAgent — PDPL/NDMO compliance filter + emergency detection
35
+ ```
36
+
37
+ Each agent extends `AgentBase` which provides retry-with-backoff and structured logging via `AgentContext`.
38
+
39
+ ### RAG Stack (`services/rag/`, `services/search/`)
40
+
41
+ - **Embedding**: `intfloat/multilingual-e5-large` (1024-dim, via HuggingFace)
42
+ - **Vector store**: Supabase pgvector (`match_documents` RPC, 2834+ medical chunks)
43
+ - **Query expansion**: Groq LLM generates 3 alternate queries; `query_parser.py` adds Arabic synonym expansions
44
+ - **Retrieval**: BM25 fallback + Cohere `rerank-v3.5` cross-encoder
45
+ - **Cache**: 5-minute TTL in-memory LRU (`services/cache.py`) prevents redundant embedding calls
46
+
47
+ ### Risk Engine (`services/risk/`)
48
+
49
+ Evidence-based clinical threshold scoring for 6 conditions. `FeatureExtractor` normalises findings into a 35-feature vector; each scorer (`_score_diabetes`, `_score_cardiovascular`, etc.) applies WHO/ADA/ACC clinical cutoffs. ML `.pkl` model files in `services/risk/models/` override rule-based scores when present.
50
+
51
+ ### Voice (`services/voice/`)
52
+
53
+ - **STT**: `WhisperSTT` wraps Groq `whisper-large-v3`. Accepts WebM/MP4/OGG/WAV (25 MB max).
54
+ - **TTS**: Provider chain — Google Cloud TTS (Wavenet-A) → gTTS (free fallback) → ElevenLabs.
55
+
56
+ ### LLM Router (`services/llm/router.py`)
57
+
58
+ `LLMRouter` wraps a primary provider (`GroqProvider`) and optional fallback (`HuggingFaceProvider`). Model selection: `llama-3.3-70b-versatile` for analysis, `llama-3.1-8b-instant` for chat.
59
+
60
+ ### Security (`middleware/`)
61
+
62
+ - **`AuditMiddleware`**: Writes one JSON record per request to rotating log files (`logs/audit/audit.jsonl`). Marks PDPL-sensitive paths. Skips health/docs endpoints.
63
+ - **`validate_upload`**: Magic-byte sniffing (anti-MIME-spoofing), 20 MB size limit, extension blocklist.
64
+ - **`sanitize_text`**: Strips HTML tags, null bytes, XSS patterns, and SQL injection signatures from all user text inputs.
65
+
66
+ ### Rate Limiting (`services/ratelimit.py`)
67
+
68
+ In-memory sliding window (no external dependency). Limits: analyze=5/min, chat=30/min, search=60/min. Uses `X-Forwarded-For` for IP detection behind proxies.
69
+
70
+ ---
71
+
72
+ ## Frontend Layer
73
+
74
+ **Next.js 15 App Router**, RTL Arabic, Tailwind CSS v4, Framer Motion.
75
+
76
+ | Component | Purpose |
77
+ |---|---|
78
+ | `upload-section.tsx` | File picker + `/api/analyze` call + loading state |
79
+ | `analysis-history.tsx` | Saved analyses list + semantic search + health trend chart |
80
+ | `health-trend-chart.tsx` | Recharts line chart — tracks lab values over time with alerts |
81
+ | `risk-dashboard.tsx` | Calls `/api/risk` + renders 6 radial gauge cards (collapsible) |
82
+ | `chat-bot.tsx` | Floating chat panel — streaming SSE + voice input/output |
83
+ | `voice-recorder.tsx` | MediaRecorder → `/api/voice/transcribe` + TTS playback |
84
+ | `compare-analyses.tsx` | Side-by-side analysis diff |
85
+
86
+ ---
87
+
88
+ ## Data Flow — Analysis Request
89
+
90
+ ```
91
+ 1. User uploads PDF/image
92
+ 2. validate_upload() — size + MIME + magic bytes
93
+ 3. AgentCoordinator.run()
94
+ a. OCRAgent → raw_text
95
+ b. ExtractionAgent → findings[] (with impossible-value filter)
96
+ c. ClassificationAgent → panel_code
97
+ d. MedicalReasoningAgent → RAG context + LLM report
98
+ e. SafetyAgent → filtered report
99
+ 4. Response: { findings, summary, report }
100
+ 5. Frontend saves to Supabase via /api/analyses/save
101
+ 6. RiskDashboard calls /api/risk with findings
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Key Design Decisions
107
+
108
+ - **No streaming for analysis**: Analysis takes 8–15 s; streaming a partial JSON would be malformed. Response is returned in full after all agents complete.
109
+ - **RAG cache before agent**: MedicalReasoningAgent checks `rag_cache` before calling pgvector — avoids redundant 300 ms embedding round-trips on identical queries.
110
+ - **Agents over monolith**: Each agent is independently retryable and observable via `AgentContext.logs`. Failures degrade gracefully — OCR failure sets `raw_text=""`, downstream agents handle empty input without crashing.
111
+ - **Rule-based risk scoring**: ML `.pkl` models are optional overrides. The platform is useful immediately without training data.
112
+ - **In-memory rate limiter**: Avoids Redis dependency for MVP. Replace with Redis-backed limiter for multi-process deployments.
DEPLOYMENT.md ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # تبيان الطبي — Deployment Guide
2
+
3
+ ## Prerequisites
4
+
5
+ - Docker ≥ 24 + Docker Compose v2
6
+ - A server with ≥ 4 GB RAM, ≥ 20 GB disk
7
+ - Domain name with DNS pointed to your server (for HTTPS)
8
+
9
+ ---
10
+
11
+ ## 1. Environment Setup
12
+
13
+ Copy and populate the environment file:
14
+
15
+ ```bash
16
+ cp .env.example .env
17
+ ```
18
+
19
+ Required variables:
20
+
21
+ ```
22
+ GROQ_API_KEY=gsk_...
23
+ SUPABASE_URL=https://<project>.supabase.co
24
+ SUPABASE_KEY=eyJ...
25
+ SUPABASE_DB_URL=postgresql+psycopg://...
26
+ FRONTEND_URL=https://your-domain.com
27
+ NEXTAUTH_SECRET=<32+ random chars>
28
+ ```
29
+
30
+ Optional (features degrade gracefully without them):
31
+
32
+ ```
33
+ COHERE_API_KEY=... # reranking (falls back to BM25 only)
34
+ GOOGLE_VISION_API_KEY=... # Vision OCR (falls back to EasyOCR)
35
+ GOOGLE_TTS_KEY=... # Google TTS (falls back to gTTS)
36
+ ELEVENLABS_API_KEY=... # ElevenLabs TTS
37
+ SENTRY_DSN=... # Error monitoring
38
+ ```
39
+
40
+ ---
41
+
42
+ ## 2. Nginx Configuration
43
+
44
+ Create `nginx/nginx.conf`:
45
+
46
+ ```nginx
47
+ events { worker_connections 1024; }
48
+
49
+ http {
50
+ upstream backend { server backend:8000; }
51
+ upstream frontend { server frontend:3000; }
52
+
53
+ server {
54
+ listen 80;
55
+ server_name your-domain.com;
56
+ return 301 https://$host$request_uri;
57
+ }
58
+
59
+ server {
60
+ listen 443 ssl;
61
+ server_name your-domain.com;
62
+
63
+ ssl_certificate /etc/nginx/certs/fullchain.pem;
64
+ ssl_certificate_key /etc/nginx/certs/privkey.pem;
65
+
66
+ client_max_body_size 25M;
67
+
68
+ location /api/ { proxy_pass http://backend; proxy_set_header Host $host; }
69
+ location /health { proxy_pass http://backend; }
70
+ location / { proxy_pass http://frontend; proxy_set_header Host $host; }
71
+ }
72
+ }
73
+ ```
74
+
75
+ Place TLS certificates in `nginx/certs/` (Let's Encrypt recommended).
76
+
77
+ ---
78
+
79
+ ## 3. Frontend Dockerfile
80
+
81
+ Create `frontend/Dockerfile`:
82
+
83
+ ```dockerfile
84
+ FROM node:20-alpine AS builder
85
+ WORKDIR /app
86
+ COPY package*.json ./
87
+ RUN npm ci
88
+ COPY . .
89
+ ARG NEXT_PUBLIC_API_URL
90
+ ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
91
+ RUN npm run build
92
+
93
+ FROM node:20-alpine
94
+ WORKDIR /app
95
+ COPY --from=builder /app/.next/standalone ./
96
+ COPY --from=builder /app/.next/static ./.next/static
97
+ COPY --from=builder /app/public ./public
98
+ ENV PORT=3000
99
+ EXPOSE 3000
100
+ CMD ["node", "server.js"]
101
+ ```
102
+
103
+ Enable standalone output in `next.config.js`:
104
+
105
+ ```js
106
+ module.exports = { output: "standalone" }
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 4. Deploy
112
+
113
+ ```bash
114
+ # Build and start all services
115
+ docker compose -f docker-compose.prod.yml up -d --build
116
+
117
+ # Check logs
118
+ docker compose -f docker-compose.prod.yml logs -f backend
119
+
120
+ # Check health
121
+ curl https://your-domain.com/health
122
+ ```
123
+
124
+ ---
125
+
126
+ ## 5. Supabase Setup
127
+
128
+ Run once to set up the pgvector extension and required tables:
129
+
130
+ ```sql
131
+ -- Enable pgvector
132
+ create extension if not exists vector;
133
+
134
+ -- Documents table for RAG
135
+ create table if not exists documents (
136
+ id bigserial primary key,
137
+ content text,
138
+ embedding vector(1024),
139
+ metadata jsonb
140
+ );
141
+ create index on documents using ivfflat (embedding vector_cosine_ops) with (lists = 100);
142
+
143
+ -- match_documents RPC
144
+ create or replace function match_documents(
145
+ query_embedding vector(1024),
146
+ match_count int default 10,
147
+ filter jsonb default '{}'
148
+ )
149
+ returns table(id bigint, content text, metadata jsonb, similarity float)
150
+ language plpgsql as $$
151
+ begin
152
+ return query
153
+ select d.id, d.content, d.metadata,
154
+ 1 - (d.embedding <=> query_embedding) as similarity
155
+ from documents d
156
+ order by d.embedding <=> query_embedding
157
+ limit match_count;
158
+ end;
159
+ $$;
160
+
161
+ -- Analyses table
162
+ create table if not exists analyses (
163
+ id uuid default gen_random_uuid() primary key,
164
+ session_id text not null,
165
+ findings jsonb,
166
+ summary text,
167
+ report jsonb,
168
+ created_at timestamptz default now()
169
+ );
170
+ create index on analyses(session_id, created_at desc);
171
+ ```
172
+
173
+ ---
174
+
175
+ ## 6. PEFT/LoRA Fine-Tuning (Optional)
176
+
177
+ To improve Arabic medical analysis quality with your own data:
178
+
179
+ ```bash
180
+ # 1. Prepare dataset from Supabase analyses
181
+ python training/prepare_dataset.py --source supabase --output_dir data/
182
+
183
+ # 2. Train LoRA adapter (requires GPU with ≥16 GB VRAM)
184
+ python training/train_lora.py \
185
+ --model_name core42/jais-13b-chat \
186
+ --data_dir data/ \
187
+ --output_dir checkpoints/tebyan-v1 \
188
+ --num_epochs 3 \
189
+ --load_4bit
190
+
191
+ # 3. Evaluate
192
+ python training/evaluate_model.py \
193
+ --base_model core42/jais-13b-chat \
194
+ --lora_adapter checkpoints/tebyan-v1/lora_adapter \
195
+ --val_data data/val.jsonl \
196
+ --use_llm_judge \
197
+ --groq_key $GROQ_API_KEY
198
+ ```
199
+
200
+ Results are written to `eval_results/eval_summary.json`.
201
+
202
+ ---
203
+
204
+ ## 7. Monitoring
205
+
206
+ The `/api/metrics` endpoint exposes Prometheus-format metrics. Scrape with:
207
+
208
+ ```yaml
209
+ # prometheus.yml
210
+ scrape_configs:
211
+ - job_name: tebyan
212
+ static_configs:
213
+ - targets: ['your-domain.com']
214
+ metrics_path: /api/metrics
215
+ scheme: https
216
+ ```
217
+
218
+ Audit logs are written to the `audit_logs` Docker volume at `logs/audit/audit.jsonl` (rotating, max 50 MB × 10 files).
219
+
220
+ ---
221
+
222
+ ## 8. Updating
223
+
224
+ ```bash
225
+ git pull
226
+ docker compose -f docker-compose.prod.yml up -d --build --no-deps backend
227
+ docker compose -f docker-compose.prod.yml up -d --build --no-deps frontend
228
+ ```
229
+
230
+ The `--no-deps` flag updates only the specified service without restarting nginx or volumes.
app.py DELETED
@@ -1,726 +0,0 @@
1
- import os
2
- os.environ['TRANSFORMERS_VERBOSITY'] = 'error'
3
- import io
4
- import streamlit as st
5
- import google.generativeai as genai
6
- import numpy as np
7
- from PIL import Image
8
- import fitz
9
- import easyocr
10
- import re
11
- from langchain_huggingface import HuggingFaceEmbeddings
12
- from langchain_community.vectorstores import Chroma
13
-
14
- # --- 1. الإعدادات ---
15
- os.environ['HF_HOME'] = r'D:\Project\model_cache'
16
- API_KEY = "AIzaSyDA4UjdmbAAGGgdrYLYRQtJYHrnawYdka4"
17
- genai.configure(api_key=API_KEY)
18
-
19
- st.set_page_config(page_title="تبيان الطبي", layout="wide", page_icon="🩺")
20
-
21
- # معالجة رسائل الشات في أول الصفحة
22
- _chat_q = st.query_params.get("q", "")
23
- if _chat_q:
24
- if "chat_msgs" not in st.session_state:
25
- st.session_state.chat_msgs = [{"role":"bot","text":"مرحباً! أنا مساعدك الطبي 🩺"}]
26
- st.session_state.chat_msgs.append({"role":"user","text":_chat_q})
27
- st.query_params.clear()
28
-
29
- # --- 2. CSS ---
30
- # إخفاء مساحة iframe الشات
31
- st.markdown("""<style>
32
- div[data-testid="stCustomComponentV1"] {
33
- height: 0px !important;
34
- min-height: 0px !important;
35
- margin: 0 !important;
36
- padding: 0 !important;
37
- }
38
- </style>""", unsafe_allow_html=True)
39
- st.markdown("""
40
- <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
41
- <style>
42
- *, *::before, *::after { font-family: 'Cairo', sans-serif !important; direction: rtl; box-sizing: border-box; }
43
- .stApp { background-color: #F0F4F8; }
44
- #MainMenu, footer, header { visibility: hidden; }
45
- .block-container { padding-top: 0 !important; padding-bottom: 6rem !important; max-width: 1100px !important; min-height: 100vh !important; }
46
-
47
- /* الهيدر */
48
- .main-header {
49
- background: rgba(255,255,255,0.97);
50
- border-bottom: 1px solid #E2E8F0;
51
- padding: 0 2.5rem;
52
- height: 64px;
53
- display: flex;
54
- align-items: center;
55
- justify-content: space-between;
56
- position: fixed;
57
- top: 0; left: 0; right: 0;
58
- z-index: 9999;
59
- width: 100vw;
60
- margin-left: calc(-50vw + 50%);
61
- box-shadow: 0 2px 12px rgba(59,130,246,0.07);
62
- }
63
- .header-logo { display: flex; align-items: center; gap: 10px; }
64
- .header-logo-icon {
65
- width: 38px; height: 38px;
66
- background: linear-gradient(135deg,#3B82F6,#2563EB);
67
- border-radius: 10px;
68
- display: flex; align-items: center; justify-content: center;
69
- }
70
- .header-logo-text { font-size: 1.3rem; font-weight: 900; color: #0F172A; }
71
- .header-badge {
72
- display: flex; align-items: center; gap: 6px;
73
- background: #EFF6FF; padding: 5px 14px;
74
- border-radius: 999px; border: 1px solid #BFDBFE;
75
- }
76
- .header-badge-dot { width: 7px; height: 7px; background: #3B82F6; border-radius: 50%; }
77
- .header-badge-text { font-size: 0.78rem; font-weight: 700; color: #2563EB; }
78
-
79
- /* Hero */
80
- .hero-section { text-align: center; padding: 2rem 1rem 2.5rem; margin-top: 64px; }
81
- .hero-badge { display: inline-block; background: #EFF6FF; color: #2563EB; border-radius: 999px; padding: 6px 20px; font-size: 13px; font-weight: 700; margin-bottom: 1.2rem; border: 1px solid #BFDBFE; }
82
- .hero-title { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; color: #0F172A; margin: 0 0 1rem; line-height: 1.4; }
83
-
84
- /* Upload */
85
- div[data-testid="stFileUploader"] {
86
- background: white !important; border: 2px dashed #CBD5E1 !important;
87
- border-radius: 1.25rem !important; padding: 3rem 2rem !important;
88
- text-align: center !important; box-shadow: 0 2px 20px rgba(59,130,246,0.08) !important;
89
- cursor: pointer !important; transition: all 0.2s !important;
90
- }
91
- div[data-testid="stFileUploader"]:hover { border-color: #3B82F6 !important; background: #EFF6FF !important; }
92
- div[data-testid="stFileUploaderDropzone"] { border: none !important; padding: 0 !important; background: transparent !important; display: flex !important; flex-direction: column !important; align-items: center !important; gap: 0.5rem !important; }
93
- div[data-testid="stFileUploaderDropzoneInstructions"] { text-align: center !important; }
94
- div[data-testid="stFileUploaderDropzoneInstructions"] > div > span { font-family: 'Cairo', sans-serif !important; font-size: 1.4rem !important; font-weight: 900 !important; color: #0F172A !important; }
95
- div[data-testid="stFileUploaderDropzoneInstructions"] > div > span:last-child { display: none !important; }
96
- div[data-testid="stFileUploaderDropzoneInstructions"] > div > small { font-family: 'Cairo', sans-serif !important; font-size: 0.85rem !important; color: #64748B !important; }
97
- button[data-testid="stBaseButton-secondary"] { background: linear-gradient(135deg,#3B82F6,#2563EB) !important; color: white !important; border: none !important; border-radius: 12px !important; padding: 12px 36px !important; font-family: 'Cairo', sans-serif !important; font-weight: 700 !important; font-size: 1rem !important; margin-top: 0.5rem !important; }
98
-
99
- /* Feature Cards */
100
- .feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 2.5rem; }
101
- .feature-card { background: white; border-radius: 1rem; border: 1px solid #E2E8F0; padding: 1.4rem; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
102
- .feature-icon-wrap { width: 44px; height: 44px; background: #EFF6FF; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.3rem; margin-bottom: 0.9rem; }
103
- .feature-title { font-weight: 800; font-size: 0.95rem; color: #1E293B; margin: 0 0 6px; }
104
- .feature-text { font-size: 0.85rem; color: #64748B; margin: 0; line-height: 1.7; }
105
-
106
- /* Results */
107
- .section-title { font-size: 1.1rem; font-weight: 900; color: #0F172A; margin: 2rem 0 1rem; padding-bottom: 0.5rem; border-bottom: 2px solid #EFF6FF; }
108
- .result-card { background: white; border-radius: 1rem; padding: 1rem 1.2rem; margin-bottom: 0.85rem; border: 1px solid #E2E8F0; border-right: 4px solid; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
109
- .result-card.status-low { border-right-color: #F59E0B; }
110
- .result-card.status-high { border-right-color: #EF4444; }
111
- .result-card.status-normal { border-right-color: #10B981; }
112
- .test-label { font-size: 0.72rem; font-weight: 700; color: #64748B; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
113
- .test-value { font-size: 1.3rem; font-weight: 900; color: #0F172A; margin: 4px 0; }
114
- .test-unit { font-size: 0.68rem; font-weight: 500; color: #94A3B8; }
115
- .status-badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 0.72rem; font-weight: 700; margin-bottom: 6px; }
116
- .badge-low { background: #FEF3C7; color: #92400E; }
117
- .badge-high { background: #FEE2E2; color: #991B1B; }
118
- .badge-normal { background: #D1FAE5; color: #065F46; }
119
- .badge-warning{ background: #EFF6FF; color: #1E40AF; }
120
- .range-text { font-size: 0.72rem; color: #94A3B8; border-top: 1px dashed #E2E8F0; padding-top: 6px; margin-top: 6px; }
121
-
122
- /* Summary */
123
- .summary-banner { background: linear-gradient(135deg,#EFF6FF,#F0F9FF); border: 1px solid #BFDBFE; border-radius: 1rem; padding: 1.5rem; display: flex; align-items: flex-start; gap: 1rem; margin-bottom: 1.5rem; }
124
- .summary-icon { font-size: 1.8rem; min-width: 40px; }
125
- .summary-title { font-size: 1.1rem; font-weight: 900; color: #1E3A8A; margin: 0 0 6px; }
126
- .summary-text { color: #334155; font-size: 0.9rem; margin: 0; line-height: 1.8; }
127
- .disclaimer { background: #F8FAFC; border: 1px solid #E2E8F0; border-radius: 0.75rem; padding: 1rem 1.25rem; font-size: 0.8rem; color: #94A3B8; line-height: 1.7; margin-top: 1.5rem; }
128
-
129
- /* Privacy note */
130
- .privacy-note { text-align: center; color: #94A3B8; font-size: 0.8rem; margin-top: 0.5rem; }
131
-
132
- /* Buttons */
133
- .stButton > button { background: linear-gradient(135deg,#3B82F6,#2563EB) !important; color: white !important; border: none !important; border-radius: 10px !important; font-family: 'Cairo', sans-serif !important; font-weight: 700 !important; }
134
- hr { border: none; border-top: 1px solid #E2E8F0; margin: 2rem 0; }
135
- /* شريط الشات دايماً شفاف */
136
- div[data-testid="stBottom"],
137
- div[data-testid="stBottom"] > div {
138
- background: transparent !important;
139
- backdrop-filter: none !important;
140
- box-shadow: none !important;
141
- border: none !important;
142
- }
143
- </style>
144
- """, unsafe_allow_html=True)
145
-
146
- # --- 3. الهيدر ---
147
- st.markdown("""
148
- <div class="main-header">
149
- <div class="header-logo">
150
- <div class="header-logo-icon">
151
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
152
- </div>
153
- <span class="header-logo-text">تبيان الطبي</span>
154
- </div>
155
- <div class="header-badge">
156
- <div class="header-badge-dot"></div>
157
- <span class="header-badge-text">تحليل ذكي للتقارير الطبية</span>
158
- </div>
159
- </div>
160
- """, unsafe_allow_html=True)
161
-
162
- # --- 4. Hero ---
163
- st.markdown("""
164
- <div class="hero-section">
165
- <div class="hero-badge">🤖 مدعوم بالذكاء الاصطناعي</div>
166
- <h1 class="hero-title">افهم نتائج تحاليلك بلغة عربية بسيطة</h1>
167
- <p style="font-size:1rem;color:#64748B;max-width:500px;margin:0 auto;line-height:1.8;text-align:center;">نحوّل المصطلحات الطبية المعقّدة إلى شرح واضح، مع توصيات دقيقة مدعومة بمراجع طبية موثوقة.</p>
168
- </div>
169
- """, unsafe_allow_html=True)
170
-
171
-
172
- # --- 5. الدوال ---
173
- @st.cache_resource
174
- def load_tools():
175
- reader = easyocr.Reader(['en'], gpu=False)
176
- embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
177
- try:
178
- available_models = [m.name for m in genai.list_models() if 'generateContent' in m.supported_generation_methods]
179
- if 'models/gemini-1.5-flash' in available_models:
180
- model_name = 'models/gemini-1.5-flash'
181
- elif 'models/gemini-pro' in available_models:
182
- model_name = 'models/gemini-pro'
183
- else:
184
- model_name = available_models[0]
185
- except:
186
- model_name = 'models/gemini-1.5-flash'
187
- print(f"✅ الموديل المختار: {model_name}")
188
- return reader, embeddings, genai.GenerativeModel(model_name)
189
-
190
- reader, embeddings, model = load_tools()
191
-
192
- # رد الشات بعد تحميل الموديل - run_rag
193
- print(f"DEBUG _chat_q = '{_chat_q}'")
194
- if _chat_q and "chat_msgs" in st.session_state:
195
- if st.session_state.chat_msgs and st.session_state.chat_msgs[-1]["role"] == "user":
196
- try:
197
- # فلتر طبي
198
- strong_words = ["ألم","صداع","تعب","دوخة","حمى","سعال","غثيان","قيء","أعراض","ضغط","سكر","قلب","معدة","تنفس","التهاب","دواء","علاج"]
199
- weak_words = ["تحليل","دم","فيتامين","مختبر","نتائج","فحص","تشخيص","كوليسترول","حديد"]
200
- score = sum(0.6 for w in strong_words if w in _chat_q) + sum(0.3 for w in weak_words if w in _chat_q) + 0.5
201
-
202
- if score < 0.8:
203
- _ans = "هذا النظام مخصص للأسئلة الطبية فقط."
204
- else:
205
- # RAG
206
- context = ""
207
- try:
208
- if os.path.exists(r'D:\Project\chroma_db'):
209
- _db = Chroma(persist_directory=r'D:\Project\chroma_db', embedding_function=embeddings)
210
- results = _db.similarity_search_with_relevance_scores(_chat_q, k=3)
211
- filtered = [doc for doc, s in results if s >= 0.8]
212
- if filtered:
213
- context = "\n\n".join([d.page_content for d in filtered])
214
- print("✅ تم استخدام RAG")
215
- except Exception as e:
216
- print(f"RAG error: {e}")
217
-
218
- prompt = f"""
219
- أنت مساعد طبي ذكي.
220
-
221
- أجب على السؤال الطبي التالي بشكل واضح ومبسط.
222
-
223
- إذا كانت الحالة مرتبطة بأعراض أو مشاكل صحية، يجب عليك في نهاية الإجابة إضافة قسم بعنوان:
224
-
225
- "التحاليل المقترحة (إن وجدت):"
226
-
227
- وتذكر التحاليل المناسبة التي قد يطلبها الطبيب حسب الحالة فقط (إن لزم الأمر).
228
-
229
- إذا لم تكن هناك حاجة لتحاليل، لا تذكر هذا القسم.
230
-
231
- {("السياق من المراجع:\\n" + context) if context else ""}
232
-
233
- السؤال:
234
- {_chat_q}
235
- """
236
- _ans = model.generate_content(prompt).text
237
-
238
- st.session_state.chat_msgs.append({"role":"bot","text":_ans})
239
- except Exception as e:
240
- st.session_state.chat_msgs.append({"role":"bot","text":f"حدث خطأ: {e}"})
241
- st.rerun()
242
-
243
- def is_valid_test(name):
244
- ignore = ['page','id','patient','date','sex','age','mrn','doctor','physician','result','unit','range','validated','approved','Interpretation','Ref']
245
- name_l = str(name).lower()
246
- return not any(x in name_l for x in ignore) and len(str(name).strip()) > 1
247
-
248
- def get_status_color(value, range_str):
249
- try:
250
- nums = re.findall(r"[-+]?\d*\.?\d+", str(range_str))
251
- val = float(value)
252
- if len(nums) >= 2:
253
- low, high = float(nums[0]), float(nums[1])
254
- if val < low or val > high:
255
- return "status-high", "badge-high", "خارج المعدل"
256
- return "status-normal", "badge-normal", "ممتاز"
257
- except:
258
- pass
259
- return "status-warning", "badge-warning", "تحليل القيمة"
260
-
261
-
262
- # --- 6. Upload ---
263
- uploaded_file = st.file_uploader("اسحب ملف التحليل هنا", type=['pdf','png','jpg','jpeg'])
264
- st.markdown('<p class="privacy-note">🔒 ملفاتك محمية ولا تُحفظ على خوادمنا</p>', unsafe_allow_html=True)
265
-
266
- if not uploaded_file:
267
- st.markdown("""
268
- <div class="feature-grid">
269
- <div class="feature-card"><div class="feature-icon-wrap">🔬</div><p class="feature-title">استخراج ذكي</p><p class="feature-text">يستخرج نتائج التحاليل تلقائياً من PDF أو صورة بدقة عالية.</p></div>
270
- <div class="feature-card"><div class="feature-icon-wrap">📚</div><p class="feature-title">مراجع ��بية موثوقة</p><p class="feature-text">التقارير مدعومة بمراجع من قاعدة بياناتك الطبية المحلية.</p></div>
271
- <div class="feature-card"><div class="feature-icon-wrap">🛡️</div><p class="feature-title">خصوصية كاملة</p><p class="feature-text">بياناتك الطبية لا تُحفظ ولا تُشارك مع أي طرف خارجي.</p></div>
272
- </div>
273
- """, unsafe_allow_html=True)
274
-
275
-
276
- # --- 7. المعالجة ---
277
- if uploaded_file:
278
- with st.spinner("جاري استخراج البيانات الطبية..."):
279
- if uploaded_file.type == "application/pdf":
280
- doc = fitz.open(stream=uploaded_file.read(), filetype="pdf")
281
- all_text = "\n".join([page.get_text() for page in doc])
282
- else:
283
- img = Image.open(uploaded_file)
284
- all_text = "\n".join(reader.readtext(np.array(img), detail=0))
285
-
286
- pattern = r"([a-zA-Z][a-zA-Z\s#%]{2,})\s+(\d+\.?\d*)\s+([\d\.]+\s*-\s*[\d\.]+)\s*([a-zA-Z0-9^/]+)?"
287
- findings = [f for f in re.findall(pattern, all_text) if is_valid_test(f[0])]
288
-
289
- if len(findings) < 2:
290
- with st.spinner("تنسيق غير تقليدي.. جاري الاستخراج بالذكاء الاصطناعي..."):
291
- extract_prompt = f"""حلل النص الطبي التالي واستخرج نتائج التحاليل.
292
- النص: {all_text[:4000]}
293
- أجب بتنسيق قائمة بايثون فقط: [('Test Name', 'Value', 'Range', 'Unit')]
294
- لا تكتب أي مقدمات أو شرح، فقط القائمة."""
295
- try:
296
- ai_response = model.generate_content(extract_prompt)
297
- clean_text = ai_response.text.strip().replace("```python","").replace("```","")
298
- findings = eval(clean_text)
299
- except:
300
- pass
301
-
302
- if findings:
303
- abnormal = [f for f in findings if is_valid_test(f[0]) and get_status_color(f[1], f[2])[0] != "status-normal"]
304
- abnormal_names = "، ".join([f[0] for f in abnormal[:3]]) if abnormal else "لا توجد قيم خارج المعدل"
305
-
306
- st.markdown(f"""
307
- <div class="summary-banner">
308
- <div class="summary-icon">📋</div>
309
- <div>
310
- <p class="summary-title">ملخّص نتائجك</p>
311
- <p class="summary-text">تم تحليل <strong>{len(findings)}</strong> فحص طبي.
312
- {'القيم التي تحتاج انتباهاً: <strong>' + abnormal_names + '</strong>.' if abnormal else '<strong>جميع القيم ضمن المعدل الطبيعي ✓</strong>'}</p>
313
- </div>
314
- </div>
315
- """, unsafe_allow_html=True)
316
-
317
- st.markdown('<p class="section-title">🔬 نتائج المختبر</p>', unsafe_allow_html=True)
318
- cols = st.columns(4)
319
- for i, (name, val, range_val, unit) in enumerate(findings):
320
- if not is_valid_test(name): continue
321
- status_class, badge_class, status_label = get_status_color(val, range_val)
322
- with cols[i % 4]:
323
- st.markdown(f"""
324
- <div class="result-card {status_class}">
325
- <div class="test-label">{str(name).strip().upper()}</div>
326
- <div class="test-value">{val} <span class="test-unit">{unit}</span></div>
327
- <span class="status-badge {badge_class}">{status_label}</span>
328
- <div class="range-text">📏 المعدل: {range_val}</div>
329
- </div>
330
- """, unsafe_allow_html=True)
331
-
332
- st.markdown("<hr>", unsafe_allow_html=True)
333
- st.markdown('<p class="section-title">📖 التقرير التحليلي المدعوم بالمراجع</p>', unsafe_allow_html=True)
334
-
335
- with st.spinner("جاري صياغة التقرير الطبي الموثق..."):
336
- context = ""
337
- try:
338
- if os.path.exists(r'D:\Project\chroma_db'):
339
- db_rag = Chroma(persist_directory=r'D:\Project\chroma_db', embedding_function=embeddings)
340
- docs = db_rag.similarity_search(f"Clinical pathology: {', '.join([f[0] for f in findings])}", k=4)
341
- context_list = []
342
- for idx, d in enumerate(docs, 1):
343
- src = d.metadata.get('source','Unknown').split('\\')[-1]
344
- pg = d.metadata.get('page','?')
345
- context_list.append(f"[ref {idx}]: {src} p{pg}\n{d.page_content}")
346
- print(f"ref: {src} (p{pg})")
347
- context = "\n\n".join(context_list)
348
- except Exception as e:
349
- print(f"RAG error: {e}")
350
-
351
- site_prompt = (
352
- "أنت طبيب مختبر خبير. أرجع JSON فقط بدون أي نص خارجه. ابدأ بـ { مباشرة.\n"
353
- "لا HTML ولا markdown. نصوص عربية بسيطة فقط.\n\n"
354
- f"النتائج: {findings}\n"
355
- f"المراجع: {context if context else 'لا توجد مراجع'}\n\n"
356
- '{"تقييم_عام":"جملة أو جملتين عن الحالة العامة",'
357
- '"قيم_غير_طبيعية":[{"اسم_الفحص":"اسم","النتيجة":"قيمة","المعدل_الطبيعي":"مدى","الحالة":"مرتفع او منخفض","الشرح":"شرح طبي","المرجع":"مرجع او لا يوجد"}],'
358
- '"نصائح":["نصيحة1","نصيحة2","نصيحة3"]}'
359
- )
360
-
361
- try:
362
- import json
363
- raw = model.generate_content(site_prompt).text.strip()
364
- raw = raw.replace("```json","").replace("```","").strip()
365
- if '{' in raw:
366
- raw = raw[raw.index('{'):raw.rindex('}')+1]
367
- report_data = json.loads(raw)
368
-
369
- st.markdown(f"""
370
- <div style="background:white;border:1px solid #E2E8F0;border-radius:1rem;padding:1.5rem 1.8rem;margin-bottom:1.2rem;direction:rtl;text-align:right;font-family:'Cairo',sans-serif;box-shadow:0 1px 3px rgba(0,0,0,0.04);">
371
- <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
372
- <div style="width:34px;height:34px;background:#EFF6FF;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">📋</div>
373
- <span style="font-weight:900;font-size:1rem;color:#0F172A;">التقييم العام للحالة</span>
374
- </div>
375
- <div style="height:1px;background:#F1F5F9;margin-bottom:12px;"></div>
376
- <p style="margin:0;line-height:1.9;color:#475569;font-size:0.93rem;">{report_data.get("تقييم_عام","")}</p>
377
- </div>
378
- """, unsafe_allow_html=True)
379
-
380
- abnormal_items = report_data.get("قيم_غير_طبيعية", [])
381
- if abnormal_items:
382
- st.markdown('<p style="font-weight:900;font-size:1rem;color:#0F172A;margin:1.5rem 0 0.8rem;direction:rtl;text-align:right;">🔬 تفصيل القيم غير الطبيعية</p>', unsafe_allow_html=True)
383
-
384
- for item in abnormal_items:
385
- حالة = item.get("الحالة","")
386
- if "مرتفع" in حالة:
387
- dot_color="#EF4444"; badge_bg="#FEE2E2"; badge_color="#991B1B"; badge_text="↑ مرتفع"; bar_color="#EF4444"; bar_pct=85
388
- else:
389
- dot_color="#F59E0B"; badge_bg="#FEF3C7"; badge_color="#92400E"; badge_text="↓ منخفض"; bar_color="#F59E0B"; bar_pct=18
390
- مرجع = item.get("المرجع","")
391
- مرجع_html = f'<div style="display:flex;align-items:center;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid #F1F5F9;"><div style="width:20px;height:20px;background:#EFF6FF;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:0.65rem;flex-shrink:0;">📖</div><span style="font-size:0.78rem;color:#94A3B8;line-height:1.6;">{مرجع}</span></div>' if مرجع else ""
392
- st.markdown(f"""
393
- <div style="background:white;border:1px solid #E2E8F0;border-radius:1rem;padding:1.3rem 1.6rem;margin-bottom:0.85rem;direction:rtl;text-align:right;font-family:'Cairo',sans-serif;box-shadow:0 1px 3px rgba(0,0,0,0.04);">
394
- <div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px;margin-bottom:10px;">
395
- <h3 style="margin:0;font-size:0.95rem;font-weight:900;color:#0F172A;">{item.get("اسم_الفحص","")}</h3>
396
- <span style="display:inline-flex;align-items:center;gap:5px;background:{badge_bg};color:{badge_color};border-radius:6px;padding:3px 10px;font-size:0.75rem;font-weight:800;">
397
- <span style="width:6px;height:6px;border-radius:50%;background:{dot_color};display:inline-block;"></span>{badge_text}
398
- </span>
399
- </div>
400
- <div style="display:flex;gap:8px;margin-bottom:10px;">
401
- <div style="background:#F8FAFC;border-radius:6px;padding:4px 10px;font-size:0.78rem;"><span style="color:#94A3B8;">القيمة </span><strong style="color:#0F172A;">{item.get("النتيجة","")}</strong></div>
402
- <div style="background:#F8FAFC;border-radius:6px;padding:4px 10px;font-size:0.78rem;"><span style="color:#94A3B8;">المعدل </span><strong style="color:#0F172A;">{item.get("المعدل_الطبيعي","")}</strong></div>
403
- </div>
404
- <div style="height:4px;background:#F1F5F9;border-radius:99px;margin-bottom:12px;overflow:hidden;">
405
- <div style="height:100%;width:{bar_pct}%;background:{bar_color};border-radius:99px;"></div>
406
- </div>
407
- <p style="margin:0;line-height:1.85;color:#475569;font-size:0.88rem;">{item.get("الشرح","")}</p>
408
- {مرجع_html}
409
- </div>
410
- """, unsafe_allow_html=True)
411
-
412
- نصائح = report_data.get("نصائح", [])
413
- if نصائح:
414
- نصائح_html = "".join([f'<div style="display:flex;align-items:flex-start;gap:10px;padding:8px 0;border-bottom:1px solid #F1F5F9;"><div style="width:20px;height:20px;background:#DCFCE7;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:0.7rem;color:#15803D;flex-shrink:0;margin-top:2px;">✓</div><span style="line-height:1.8;color:#475569;font-size:0.88rem;">{n}</span></div>' for n in نصائح])
415
- st.markdown(f"""
416
- <div style="background:white;border:1px solid #E2E8F0;border-radius:1rem;padding:1.3rem 1.6rem;margin-top:0.5rem;direction:rtl;text-align:right;font-family:'Cairo',sans-serif;box-shadow:0 1px 3px rgba(0,0,0,0.04);">
417
- <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;">
418
- <div style="width:34px;height:34px;background:#F0FDF4;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">💡</div>
419
- <span style="font-weight:900;font-size:1rem;color:#0F172A;">نصائح طبية مقترحة</span>
420
- </div>
421
- {نصائح_html}
422
- </div>
423
- """, unsafe_allow_html=True)
424
-
425
- except Exception as e:
426
- st.error("فشل التواصل مع Gemini")
427
- print(f"ERROR: {e}")
428
-
429
- try:
430
- import time
431
- time.sleep(20)
432
- audit_prompt = f"تقرير فجوات البيانات للمطورة: النتائج {findings}, المراجع {context[:500]}"
433
- audit_resp = model.generate_content(audit_prompt)
434
- print("\n=== DATA GAP REPORT ===")
435
- print(audit_resp.text)
436
- except Exception as e:
437
- print(f"AUDIT ERROR: {e}")
438
-
439
- st.markdown("""
440
- <div class="disclaimer">⚕️ <strong>إخلاء مسؤولية:</strong> هذا التقرير للتوعية الصحية فقط ولا يُعتبر بديلاً عن استشارة الطبيب المختص.</div>
441
- """, unsafe_allow_html=True)
442
-
443
- else:
444
- st.markdown("""
445
- <div class="summary-banner" style="border-color:#FCA5A5;background:linear-gradient(135deg,#FEF2F2,#FFF5F5);">
446
- <div class="summary-icon">⚠️</div>
447
- <div><p class="summary-title" style="color:#991B1B;">لم يتم العثور على نتائج</p>
448
- <p class="summary-text">تأكد أن الملف يحتوي على نتائج تحاليل طبية واضحة وحاول مرة أخرى.</p></div>
449
- </div>
450
- """, unsafe_allow_html=True)
451
-
452
-
453
- # ===== CHATBOT =====
454
- from sklearn.metrics.pairwise import cosine_similarity
455
-
456
- @st.cache_resource
457
- def load_chat_db():
458
- from langchain_community.vectorstores import Chroma as ChromaDB
459
- emb = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
460
- try:
461
- db = ChromaDB(persist_directory=r'D:\Project\chroma_db', embedding_function=emb)
462
- return emb, db
463
- except:
464
- return emb, None
465
-
466
- def is_medical(query):
467
- words = ["ألم","صداع","تعب","دوخة","حمى","سعال","أعراض","ضغط","سكر","قلب","معدة","تنفس","التهاب","دواء","علاج","تحليل","دم","فيتامين","نتائج","فحص","تشخيص","كوليسترول","حديد","مختبر","هيموجلوبين","خلايا"]
468
- return any(w in query for w in words)
469
-
470
- def run_chat(query):
471
- if not is_medical(query):
472
- return "هذا النظام مخصص للأسئلة الطبية فقط."
473
- _, chat_db = load_chat_db()
474
- context = ""
475
- used_rag = False
476
- if chat_db:
477
- try:
478
- results = chat_db.similarity_search_with_relevance_scores(query, k=3)
479
- filtered = [doc for doc, score in results if score >= 0.8]
480
- if filtered:
481
- context = "\n\n".join([d.page_content for d in filtered])
482
- used_rag = True
483
- except:
484
- pass
485
- prompt = f"""أنت مساعد طبي ذكي.
486
-
487
- أجب على السؤال الطبي التالي بشكل واضح ومبسط.
488
-
489
- إذا كانت الحالة مرتبطة بأعراض أو مشاكل صحية، يجب عليك في نهاية الإجابة إضافة قسم بعنوان:
490
-
491
- "التحاليل المقترحة (إن وجدت):"
492
-
493
- وتذكر التحاليل المناسبة التي قد يطلبها الطبيب حسب الحالة فقط (إن لزم الأمر).
494
-
495
- إذا لم تكن هناك حاجة لتحاليل، لا تذكر هذا القسم.
496
-
497
- {("السياق من المراجع:\n" + context) if context else ""}
498
-
499
- السؤال:
500
- {query}
501
- """
502
- try:
503
- response = model.generate_content(prompt).text
504
- if not used_rag:
505
- response += "\n\n🤖 *هذه الإجابة من Gemini AI*"
506
- return response
507
- except Exception as e:
508
- return f"حدث خطأ: {e}"
509
-
510
- # تهيئة الشات
511
- if "chat_msgs" not in st.session_state:
512
- st.session_state.chat_msgs = [{"role":"bot","text":"مرحباً! أنا مساعدك الطبي الذكي 🩺\nاسألني عن أي تحليل أو حالة صحية."}]
513
- if "chat_open" not in st.session_state:
514
- st.session_state.chat_open = False
515
-
516
- import html as html_lib
517
- import streamlit.components.v1 as components
518
-
519
- if "chat_msgs" not in st.session_state:
520
- st.session_state.chat_msgs = [{"role":"bot","text":"مرحباً! اسألني عن أي تحليل أو حالة صحية 🩺"}]
521
-
522
- # إخفاء الشات لما يكون فيه ملف مرفوع
523
- if uploaded_file:
524
- st.markdown("""<style>
525
- div[data-testid="stCustomComponentV1"] { opacity:0 !important; pointer-events:none !important; }
526
- </style>""", unsafe_allow_html=True)
527
-
528
- def format_chat_text(text):
529
- safe = html_lib.escape(str(text))
530
- safe = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', safe)
531
- safe = re.sub(r'\*(.+?)\*', r'<em>\1</em>', safe)
532
- lines = safe.split('\n')
533
- result_lines = []
534
- in_list = False
535
- for line in lines:
536
- stripped = line.strip()
537
- if re.match(r'^[-•]\s+.+', stripped):
538
- if not in_list:
539
- result_lines.append('<ul style="margin:4px 0;padding-right:18px;list-style:disc;">')
540
- in_list = True
541
- item_text = re.sub(r'^[-•]\s+', '', stripped)
542
- result_lines.append(f'<li style="margin:3px 0;">{item_text}</li>')
543
- else:
544
- if in_list:
545
- result_lines.append('</ul>')
546
- in_list = False
547
- result_lines.append(line)
548
- if in_list:
549
- result_lines.append('</ul>')
550
- return '<br>'.join(result_lines)
551
-
552
- msgs_html = ""
553
- for msg in st.session_state.chat_msgs:
554
- txt = format_chat_text(msg["text"])
555
- if msg["role"] == "bot":
556
- msgs_html += f'<div class="msg-bot">{txt}</div>'
557
- else:
558
- msgs_html += f'<div class="msg-user">{txt}</div>'
559
-
560
- chat_html = f"""<!DOCTYPE html>
561
- <html><head><meta charset="utf-8">
562
- <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;700;900&display=swap" rel="stylesheet">
563
- <style>
564
- *{{margin:0;padding:0;box-sizing:border-box;font-family:Cairo,sans-serif;}}
565
- html,body{{background:transparent;overflow:hidden;width:100%;height:100%;}}
566
- .fab{{
567
- position:fixed;bottom:16px;right:16px;
568
- width:52px;height:52px;
569
- background:linear-gradient(135deg,#3B82F6,#2563EB);
570
- border-radius:50%;border:none;cursor:pointer;
571
- box-shadow:0 4px 16px rgba(59,130,246,0.45);
572
- font-size:20px;color:white;
573
- display:flex;align-items:center;justify-content:center;
574
- z-index:99;
575
- }}
576
- .win{{
577
- position:fixed;bottom:78px;right:16px;
578
- width:300px;height:400px;
579
- background:white;border-radius:14px;
580
- box-shadow:0 8px 32px rgba(0,0,0,0.18);
581
- display:none;flex-direction:column;
582
- border:1px solid #E2E8F0;overflow:hidden;
583
- direction:rtl;z-index:98;
584
- }}
585
- .win.open{{display:flex;}}
586
- .head{{
587
- background:linear-gradient(135deg,#3B82F6,#2563EB);
588
- padding:11px 14px;display:flex;
589
- justify-content:space-between;align-items:center;
590
- flex-shrink:0;
591
- }}
592
- .head-t{{color:white;font-weight:900;font-size:13px;}}
593
- .head-s{{color:rgba(255,255,255,0.8);font-size:10px;}}
594
- .x-btn{{
595
- background:rgba(255,255,255,0.2);border:none;color:white;
596
- width:22px;height:22px;border-radius:50%;cursor:pointer;font-size:12px;
597
- display:flex;align-items:center;justify-content:center;
598
- }}
599
- .body{{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:7px;}}
600
- .msg-bot{{background:#F1F5F9;color:#334155;border-radius:3px 11px 11px 11px;padding:8px 11px;font-size:12px;line-height:1.6;max-width:85%;align-self:flex-end;word-break:break-word;}}
601
- .msg-user{{background:#3B82F6;color:white;border-radius:11px 3px 11px 11px;padding:8px 11px;font-size:12px;line-height:1.6;max-width:85%;align-self:flex-start;word-break:break-word;}}
602
- .inp{{padding:9px 10px;border-top:1px solid #E2E8F0;display:flex;gap:7px;background:#F8FAFC;flex-shrink:0;}}
603
- .inp input{{flex:1;border:1px solid #E2E8F0;border-radius:8px;padding:7px 10px;font-size:12px;outline:none;direction:rtl;font-family:Cairo,sans-serif;}}
604
- .inp button{{background:#3B82F6;color:white;border:none;border-radius:8px;padding:7px 12px;font-weight:700;cursor:pointer;font-size:12px;font-family:Cairo,sans-serif;}}
605
- </style></head><body>
606
-
607
- <button class="fab" onclick="toggleWin()">💬</button>
608
-
609
- <div class="win" id="win">
610
- <div class="head">
611
- <div>
612
- <div class="head-t">🩺 المساعد الطبي</div>
613
- <div class="head-s">مدعوم بالذكاء الاصطناعي</div>
614
- </div>
615
- <button class="x-btn" onclick="toggleWin()">✕</button>
616
- </div>
617
- <div class="body" id="body">{msgs_html}</div>
618
- <div class="inp">
619
- <input id="inp" type="text" placeholder="اكتب سؤالك الطبي..." onkeydown="if(event.key==='Enter')send()"/>
620
- <button onclick="send()">إرسال</button>
621
- </div>
622
- </div>
623
-
624
- <script>
625
- function toggleWin(){{
626
- var w=document.getElementById("win");
627
- w.classList.toggle("open");
628
- sc();
629
- }}
630
- function sc(){{
631
- var b=document.getElementById("body");
632
- if(b) setTimeout(function(){{b.scrollTop=b.scrollHeight;}},50);
633
- }}
634
- function send(){{
635
- var inp=document.getElementById("inp");
636
- var v=inp.value.trim();
637
- if(!v) return;
638
- inp.value="";
639
- var b=document.getElementById("body");
640
- var u=document.createElement("div");
641
- u.className="msg-user";u.textContent=v;b.appendChild(u);
642
- var l=document.createElement("div");
643
- l.className="msg-bot";l.innerHTML="⏳ جاري التفكير...";
644
- b.appendChild(l);sc();
645
- // نكتب في hidden textarea ونضغط زر مخفي
646
- var ta=window.parent.document.querySelector("textarea[data-testid='stChatInputTextArea']");
647
- if(ta){{
648
- var nativeSetter=Object.getOwnPropertyDescriptor(window.parent.HTMLTextAreaElement.prototype,"value").set;
649
- nativeSetter.call(ta,v);
650
- ta.dispatchEvent(new Event("input",{{bubbles:true}}));
651
- setTimeout(function(){{
652
- var btn=window.parent.document.querySelector("button[data-testid='stChatInputSubmitButton']");
653
- if(btn) btn.click();
654
- }},200);
655
- }}
656
- }}
657
- sc();
658
- </script>
659
- </body></html>"""
660
-
661
- components.html(chat_html, height=520, scrolling=False)
662
-
663
- # st.chat_input شفاف - يشتغل بس مو مرئي
664
- st.markdown("""<style>
665
- div[data-testid="stBottom"] {
666
- background: transparent !important;
667
- backdrop-filter: none !important;
668
- }
669
- div[data-testid="stBottom"] > div {
670
- background: transparent !important;
671
- }
672
- div[data-testid="stChatInput"] {
673
- background: transparent !important;
674
- border: none !important;
675
- box-shadow: none !important;
676
- opacity: 0 !important;
677
- }
678
- div[data-testid="stChatInput"] * {
679
- background: transparent !important;
680
- border: none !important;
681
- box-shadow: none !important;
682
- color: transparent !important;
683
- }
684
- </style>""", unsafe_allow_html=True)
685
-
686
- user_q = st.chat_input("سؤال")
687
- if user_q:
688
- st.session_state.chat_msgs.append({"role":"user","text":user_q})
689
- try:
690
- strong_words = ["ألم","صداع","تعب","دوخة","حمى","سعال","غثيان","قيء","أعراض","ضغط","سكر","قلب","معدة","تنفس","التهاب","دواء","علاج"]
691
- weak_words = ["تحليل","دم","فيتامين","مختبر","نتائج","فحص","تشخيص","كوليسترول","حديد"]
692
- score = sum(0.6 for w in strong_words if w in user_q) + sum(0.3 for w in weak_words if w in user_q) + 0.5
693
- if score < 0.8:
694
- ans = "هذا النظام مخصص للأسئلة الطبية فقط."
695
- else:
696
- context = ""
697
- try:
698
- if os.path.exists(r'D:\Project\chroma_db'):
699
- _db = Chroma(persist_directory=r'D:\Project\chroma_db', embedding_function=embeddings)
700
- results = _db.similarity_search_with_relevance_scores(user_q, k=3)
701
- filtered = [doc for doc, s in results if s >= 0.8]
702
- if filtered:
703
- context = "\n\n".join([d.page_content for d in filtered])
704
- except Exception as e:
705
- print(f"RAG error: {e}")
706
- prompt = f"""أنت مساعد طبي ذكي.
707
-
708
- أجب على السؤال الطبي التالي بشكل واضح ومبسط.
709
-
710
- إذا كانت الحالة مرتبطة بأعراض أو مشاكل صحية، يجب عليك في نهاية الإجابة إضافة قسم بعنوان:
711
-
712
- "التحاليل المقترحه:"
713
-
714
- وتذكر التحاليل المناسبة التي قد يطلبها الطبيب حسب الحالة فقط (إن لزم الأمر).
715
-
716
- إذا لم تكن هناك حاجة لتحاليل، لا تذكر هذا القسم.
717
-
718
- {("السياق من المراجع:\n" + context) if context else ""}
719
-
720
- السؤال:
721
- {user_q}"""
722
- ans = model.generate_content(prompt).text
723
- except Exception as e:
724
- ans = f"حدث خطأ: {e}"
725
- st.session_state.chat_msgs.append({"role":"bot","text":ans})
726
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/.dockerignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .env
6
+ .env.*
7
+ chroma_db/
8
+ model_cache/
9
+ *.log
10
+ *.txt.bak
11
+ venv/
12
+ .venv/
13
+ tests/
backend/.env.example ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Groq (required) ──────────────────────────────────────
2
+ GROQ_API_KEY=gsk_...
3
+
4
+ # ── Cohere (required for reranking, fallback to BM25 if missing) ──
5
+ COHERE_API_KEY=...
6
+
7
+ # ── Supabase (required) ───────────────────────────────────
8
+ SUPABASE_URL=https://your-project.supabase.co
9
+ SUPABASE_KEY=eyJ... # anon key (public — used for pgvector KB search)
10
+ SUPABASE_SERVICE_KEY=eyJ... # service_role key (secret — used for analyses/chat tables)
11
+ SUPABASE_DB_URL=postgresql://postgres:password@db.your-project.supabase.co:5432/postgres
12
+
13
+ # ── Google Cloud Vision OCR (required for image OCR) ──────
14
+ GOOGLE_VISION_API_KEY=AIza...
15
+
16
+ # ── Frontend URL (CORS whitelist) ─────────────────────────
17
+ FRONTEND_URL=https://your-app.vercel.app
18
+
19
+ # ── Supabase Auth (required for protected API endpoints) ──
20
+ # Find in: Supabase Dashboard → Project Settings → API → JWT Secret
21
+ SUPABASE_JWT_SECRET=your-supabase-jwt-secret
22
+
23
+ # ── Environment ───────────────────────────────────────────
24
+ ENVIRONMENT=production
25
+
26
+ # ── Sentry (optional) ─────────────────────────────────────
27
+ SENTRY_DSN=https://...@sentry.io/...
28
+
29
+ # ── HuggingFace (optional — for higher rate limits) ───────
30
+ HF_TOKEN=hf_...
backend/Dockerfile CHANGED
@@ -2,23 +2,34 @@ FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
- # نظام الاعتمادات
6
- RUN apt-get update && apt-get install -y \
7
- libgl1-mesa-glx \
8
  libglib2.0-0 \
9
  libsm6 \
10
  libxext6 \
11
- libxrender-dev \
12
  && rm -rf /var/lib/apt/lists/*
13
 
 
 
 
 
14
  COPY requirements.txt .
15
  RUN pip install --no-cache-dir -r requirements.txt
16
 
 
 
 
 
 
 
 
 
17
  COPY . .
18
 
19
- ENV HF_HOME=/app/model_cache
20
  ENV PORT=8000
21
 
22
  EXPOSE 8000
23
 
24
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
 
2
 
3
  WORKDIR /app
4
 
5
+ # System dependencies for OpenCV / EasyOCR
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ libgl1 \
8
  libglib2.0-0 \
9
  libsm6 \
10
  libxext6 \
11
+ libxrender1 \
12
  && rm -rf /var/lib/apt/lists/*
13
 
14
+ # Install CPU-only torch first (avoids pulling 2 GB of CUDA packages)
15
+ RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu
16
+
17
+ # Install remaining Python dependencies
18
  COPY requirements.txt .
19
  RUN pip install --no-cache-dir -r requirements.txt
20
 
21
+ # Pre-download the embedding model so first request is instant
22
+ # Model is cached in /app/model_cache (bind-mount a volume in prod for persistence)
23
+ ENV HF_HOME=/app/model_cache
24
+ RUN python -c "\
25
+ from langchain_huggingface import HuggingFaceEmbeddings; \
26
+ HuggingFaceEmbeddings(model_name='intfloat/multilingual-e5-large'); \
27
+ print('Model cached ✓')"
28
+
29
  COPY . .
30
 
 
31
  ENV PORT=8000
32
 
33
  EXPOSE 8000
34
 
35
+ CMD ["python", "run.py"]
backend/evaluate.py CHANGED
@@ -1,22 +1,23 @@
1
  """
2
  M7 — RAGAS-light: تقييم جودة الـ RAG بدون OpenAI
3
- يستخدم Groq بدلاً منه للحفاظ على المجانية
4
  """
5
  import os
6
  import json
7
  from dotenv import load_dotenv
8
  load_dotenv(dotenv_path=r'D:\Project\.env')
9
- os.environ['HF_HOME'] = r'D:\Project\model_cache'
10
  os.environ['TRANSFORMERS_VERBOSITY'] = 'error'
11
 
12
  from groq import Groq
13
- from langchain_huggingface import HuggingFaceEmbeddings
14
- from langchain_chroma import Chroma
 
15
 
16
- GROQ_API_KEY = os.getenv("GROQ_API_KEY")
17
- EMBED_MODEL = "intfloat/multilingual-e5-large"
18
- GROQ_MODEL = "llama-3.1-8b-instant"
19
- DB_PATH = r'D:\Project\chroma_db'
20
 
21
  TEST_QUESTIONS = [
22
  {"q": "ما هي القيم الطبيعية للهيموجلوبين؟", "ref": "رجال 13.5-17.5 g/dL نساء 12-15.5 g/dL"},
@@ -24,35 +25,36 @@ TEST_QUESTIONS = [
24
  {"q": "ما أعراض نقص فيتامين د؟", "ref": "آلام العظام وضعف العضلات وضعف المناعة"},
25
  {"q": "كيف أخفض سكر الدم طبيعياً؟", "ref": "الرياضة والغذاء الصحي وتقليل النشويات"},
26
  {"q": "ما سبب ارتفاع إنزيمات الكبد ALT AST؟", "ref": "التهاب الكبد الكبد الدهني الكحول"},
27
- {"q": "ما القيم الطبيعية للكرياتينين؟", "ref": "رجال 0.74-1.35 نساء 0.59-1.04 mg/dL"},
28
  {"q": "ما أسباب انخفاض خلايا الدم البيضاء؟", "ref": "أمراض المناعة العلاج الكيميائي أمراض النخاع"},
29
  {"q": "ما معنى ارتفاع TSH؟", "ref": "قصور الغدة الدرقية الغدة خاملة"},
 
 
30
  ]
31
 
32
 
33
- def get_rag_answer(client, db, question: str) -> tuple[str, str]:
34
- results = db.similarity_search_with_relevance_scores(question, k=5)
35
- filtered = [(d, s) for d, s in results if s >= 0.4]
36
- context = "\n\n".join([d.page_content for d, _ in filtered[:3]])
37
 
38
  messages = [
39
- {"role": "system", "content": "أنت مساعد طبي. أجب باختصار بالعربية مستنداً للسياق."},
40
  {"role": "user", "content": f"السياق:\n{context}\n\nالسؤال: {question}"},
41
  ]
42
- r = client.chat.completions.create(
43
  model=GROQ_MODEL, messages=messages, temperature=0.1, max_tokens=300
44
  )
45
  return r.choices[0].message.content, context
46
 
47
 
48
- def evaluate_single(client, answer: str, context: str, question: str, reference: str) -> dict:
49
  prompt = f"""قيّم الإجابة التالية بدقة. أجب بـ JSON فقط بهذا الشكل:
50
  {{"faithfulness": X, "answer_relevance": X, "context_precision": X, "notes": "..."}}
51
 
52
- حيث القيم من 0.0 إلى 1.0:
53
- - faithfulness: هل الإجابة مبنية على السياق المعطى؟
54
- - answer_relevance: هل الإجابة تجيب على السؤال؟
55
- - context_precision: هل السياق يحتوي على معلومات مفيدة؟
56
 
57
  السؤال: {question}
58
  المرجع: {reference}
@@ -60,7 +62,7 @@ def evaluate_single(client, answer: str, context: str, question: str, reference:
60
  الإجابة: {answer[:500]}"""
61
 
62
  try:
63
- r = client.chat.completions.create(
64
  model=GROQ_MODEL,
65
  messages=[{"role": "user", "content": prompt}],
66
  temperature=0, max_tokens=200,
@@ -75,46 +77,65 @@ def evaluate_single(client, answer: str, context: str, question: str, reference:
75
 
76
  def run_evaluation():
77
  print("=" * 60)
78
- print("M7 — RAGAS-light Evaluation")
79
  print("=" * 60)
80
 
81
- client = Groq(api_key=GROQ_API_KEY)
82
- embed = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
83
- db = Chroma(persist_directory=DB_PATH, embedding_function=embed)
84
- print(f"[DB] {db._collection.count()} chunks loaded\n")
85
 
86
- results = []
87
- totals = {"faithfulness": 0, "answer_relevance": 0, "context_precision": 0}
 
 
 
88
 
89
  for i, item in enumerate(TEST_QUESTIONS, 1):
90
- print(f"[{i}/{len(TEST_QUESTIONS)}] {item['q'][:50]}...")
91
- answer, context = get_rag_answer(client, db, item["q"])
92
- metrics = evaluate_single(client, answer, context, item["q"], item["ref"])
93
 
94
  for k in totals:
95
  totals[k] += metrics.get(k, 0)
96
 
97
  results.append({
98
  "question": item["q"],
99
- "answer": answer[:150],
100
  "metrics": metrics,
101
  })
102
  print(f" faithfulness={metrics.get('faithfulness', 0):.2f} "
103
  f"| relevance={metrics.get('answer_relevance', 0):.2f} "
104
- f"| precision={metrics.get('context_precision', 0):.2f}")
 
105
 
106
  n = len(TEST_QUESTIONS)
 
 
 
 
 
107
  print("\n" + "=" * 60)
108
  print("النتائج الكلية:")
109
- print(f" Faithfulness (أمانة الإجابة): {totals['faithfulness']/n:.2%}")
110
- print(f" Answer Relevance (صلة الإجابة): {totals['answer_relevance']/n:.2%}")
111
- print(f" Context Precision (دقة السياق): {totals['context_precision']/n:.2%}")
112
- print(f" المتوسط العام: {sum(totals.values())/(n*3):.2%}")
113
  print("=" * 60)
114
 
 
 
 
 
 
 
 
 
 
 
115
  with open("eval_results.json", "w", encoding="utf-8") as f:
116
- json.dump({"summary": {k: round(v/n, 3) for k, v in totals.items()}, "details": results}, f, ensure_ascii=False, indent=2)
117
- print("\nحُفظت النتائج في eval_results.json")
 
118
 
119
 
120
  if __name__ == "__main__":
 
1
  """
2
  M7 — RAGAS-light: تقييم جودة الـ RAG بدون OpenAI
3
+ يستخدم Groq بدلاً منه pgvector + SemanticSearchService
4
  """
5
  import os
6
  import json
7
  from dotenv import load_dotenv
8
  load_dotenv(dotenv_path=r'D:\Project\.env')
9
+ os.environ.setdefault('HF_HOME', r'D:\Project\model_cache')
10
  os.environ['TRANSFORMERS_VERBOSITY'] = 'error'
11
 
12
  from groq import Groq
13
+ from services.search.semantic_search import SemanticSearchService
14
+ from services.rag.retriever import Retriever, RetrievalConfig
15
+ from services.rag.context_builder import build_context
16
 
17
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
18
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
19
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
20
+ GROQ_MODEL = "llama-3.1-8b-instant"
21
 
22
  TEST_QUESTIONS = [
23
  {"q": "ما هي القيم الطبيعية للهيموجلوبين؟", "ref": "رجال 13.5-17.5 g/dL نساء 12-15.5 g/dL"},
 
25
  {"q": "ما أعراض نقص فيتامين د؟", "ref": "آلام العظام وضعف العضلات وضعف المناعة"},
26
  {"q": "كيف أخفض سكر الدم طبيعياً؟", "ref": "الرياضة والغذاء الصحي وتقليل النشويات"},
27
  {"q": "ما سبب ارتفاع إنزيمات الكبد ALT AST؟", "ref": "التهاب الكبد الكبد الدهني الكحول"},
28
+ {"q": "ما القيم الطبيعية للكرياتينين؟", "ref": "رجال 0.7-1.3 نساء 0.5-1.1 mg/dL"},
29
  {"q": "ما أسباب انخفاض خلايا الدم البيضاء؟", "ref": "أمراض المناعة العلاج الكيميائي أمراض النخاع"},
30
  {"q": "ما معنى ارتفاع TSH؟", "ref": "قصور الغدة الدرقية الغدة خاملة"},
31
+ {"q": "ما أسباب ارتفاع الدهون الثلاثية؟", "ref": "السكر والكربوهيدرات والسمنة والخمول"},
32
+ {"q": "ما معنى انخفاض eGFR؟", "ref": "ضعف وظيفة الكلى ومرض الكلى المزمن"},
33
  ]
34
 
35
 
36
+ def get_rag_answer(groq_client: Groq, retriever: Retriever, question: str) -> tuple[str, str]:
37
+ results, _ = retriever.retrieve(question, RetrievalConfig(k=8, top_n=4, use_multi_query=False))
38
+ context = build_context(results, max_tokens=1200)
 
39
 
40
  messages = [
41
+ {"role": "system", "content": "أنت مساعد طبي دقيق. أجب باختصار بالعربية مستنداً فقط للسياق المعطى."},
42
  {"role": "user", "content": f"السياق:\n{context}\n\nالسؤال: {question}"},
43
  ]
44
+ r = groq_client.chat.completions.create(
45
  model=GROQ_MODEL, messages=messages, temperature=0.1, max_tokens=300
46
  )
47
  return r.choices[0].message.content, context
48
 
49
 
50
+ def evaluate_single(groq_client: Groq, answer: str, context: str, question: str, reference: str) -> dict:
51
  prompt = f"""قيّم الإجابة التالية بدقة. أجب بـ JSON فقط بهذا الشكل:
52
  {{"faithfulness": X, "answer_relevance": X, "context_precision": X, "notes": "..."}}
53
 
54
+ القيم من 0.0 إلى 1.0:
55
+ - faithfulness: هل الإجابة مبنية فعلاً على السياق المعطى؟
56
+ - answer_relevance: هل الإجابة تجيب على السؤال بشكل مباشر؟
57
+ - context_precision: هل السياق يحتوي معلومات مفيدة للسؤال؟
58
 
59
  السؤال: {question}
60
  المرجع: {reference}
 
62
  الإجابة: {answer[:500]}"""
63
 
64
  try:
65
+ r = groq_client.chat.completions.create(
66
  model=GROQ_MODEL,
67
  messages=[{"role": "user", "content": prompt}],
68
  temperature=0, max_tokens=200,
 
77
 
78
  def run_evaluation():
79
  print("=" * 60)
80
+ print("RAGAS-light Evaluation | pgvector + Groq")
81
  print("=" * 60)
82
 
83
+ groq_client = Groq(api_key=GROQ_API_KEY)
84
+ search_svc = SemanticSearchService(SUPABASE_URL, SUPABASE_KEY)
85
+ retriever = Retriever(search_svc)
 
86
 
87
+ total_chunks = search_svc.count()
88
+ print(f"[DB] {total_chunks} chunks in pgvector\n")
89
+
90
+ results = []
91
+ totals = {"faithfulness": 0.0, "answer_relevance": 0.0, "context_precision": 0.0}
92
 
93
  for i, item in enumerate(TEST_QUESTIONS, 1):
94
+ print(f"[{i}/{len(TEST_QUESTIONS)}] {item['q'][:55]}...")
95
+ answer, context = get_rag_answer(groq_client, retriever, item["q"])
96
+ metrics = evaluate_single(groq_client, answer, context, item["q"], item["ref"])
97
 
98
  for k in totals:
99
  totals[k] += metrics.get(k, 0)
100
 
101
  results.append({
102
  "question": item["q"],
103
+ "answer": answer[:200],
104
  "metrics": metrics,
105
  })
106
  print(f" faithfulness={metrics.get('faithfulness', 0):.2f} "
107
  f"| relevance={metrics.get('answer_relevance', 0):.2f} "
108
+ f"| precision={metrics.get('context_precision', 0):.2f}"
109
+ f" — {metrics.get('notes', '')[:60]}")
110
 
111
  n = len(TEST_QUESTIONS)
112
+ avg_f = totals['faithfulness'] / n
113
+ avg_r = totals['answer_relevance'] / n
114
+ avg_p = totals['context_precision'] / n
115
+ overall = (avg_f + avg_r + avg_p) / 3
116
+
117
  print("\n" + "=" * 60)
118
  print("النتائج الكلية:")
119
+ print(f" Faithfulness (امانة الإجابة): {avg_f:.1%}")
120
+ print(f" Answer Relevance (صلة الإجابة): {avg_r:.1%}")
121
+ print(f" Context Precision (دقة السياق): {avg_p:.1%}")
122
+ print(f" المتوسط العام: {overall:.1%}")
123
  print("=" * 60)
124
 
125
+ summary = {
126
+ "faithfulness": round(avg_f, 3),
127
+ "answer_relevance": round(avg_r, 3),
128
+ "context_precision":round(avg_p, 3),
129
+ "overall": round(overall, 3),
130
+ "total_chunks": total_chunks,
131
+ "questions_tested": n,
132
+ }
133
+ output = {"summary": summary, "details": results}
134
+
135
  with open("eval_results.json", "w", encoding="utf-8") as f:
136
+ json.dump(output, f, ensure_ascii=False, indent=2)
137
+ print("Results saved to eval_results.json")
138
+ return summary
139
 
140
 
141
  if __name__ == "__main__":
backend/ingest_medical_kb.py ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ingest_medical_kb.py — Ingest medical_kb/schemas/*.json → Supabase pgvector
3
+
4
+ Creates 3 semantically distinct chunks per lab test:
5
+ chunk 0 (definition) — name + abbreviations + unit + clinical meaning + patient explanation
6
+ chunk 1 (values) — normal ranges per gender + severity thresholds + followup tests
7
+ chunk 2 (symptoms_causes) — high causes + low causes + symptoms_low
8
+
9
+ Source tag: "TibyanMedicalKB" (distinct from MedlinePlus "MedlinePlus" or "TibyanLabs")
10
+
11
+ Usage:
12
+ cd backend
13
+ python ingest_medical_kb.py # insert new chunks only
14
+ python ingest_medical_kb.py --clear # delete existing TibyanMedicalKB first
15
+ python ingest_medical_kb.py --dry-run # print chunks, do NOT insert
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import json
21
+ import os
22
+ import re
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ from dotenv import load_dotenv
27
+
28
+ load_dotenv()
29
+ os.environ.setdefault("HF_HOME", r"D:\Project\model_cache")
30
+ os.environ["TRANSFORMERS_VERBOSITY"] = "error"
31
+
32
+ import requests
33
+ from langchain_huggingface import HuggingFaceEmbeddings
34
+
35
+ EMBED_MODEL = "intfloat/multilingual-e5-large"
36
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "")
37
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
38
+
39
+ SCHEMAS_DIR = Path(__file__).parent / "medical_kb" / "schemas"
40
+ SOURCE_TAG = "TibyanMedicalKB"
41
+
42
+
43
+ # ══════════════════════════════════════════════════════════════════
44
+ # 1. Chunk builders
45
+ # ══════════════════════════════════════════════════════════════════
46
+
47
+ def _fmt_range(rng: dict) -> str:
48
+ lo, hi = rng.get("low", "?"), rng.get("high", "?")
49
+ return f"{lo}–{hi}"
50
+
51
+
52
+ def _build_definition_chunk(panel: dict, test_name: str, test: dict) -> str:
53
+ """Chunk 0: identity + what this test measures."""
54
+ name_ar = test.get("name_ar", test_name)
55
+ abbr = ", ".join(test.get("abbreviations", [])[:5])
56
+ unit = test.get("unit", "")
57
+ meaning = test.get("clinical_meaning_ar", "")
58
+ explain = test.get("patient_explanation_ar", "")
59
+
60
+ parts = [
61
+ f"{name_ar} ({test_name})",
62
+ ]
63
+ if abbr:
64
+ parts.append(f"يُعرف أيضاً بـ: {abbr}")
65
+ if unit:
66
+ parts.append(f"الوحدة: {unit}")
67
+ if meaning:
68
+ parts.append(f"المعنى الطبي: {meaning}")
69
+ if explain:
70
+ parts.append(f"شرح مبسط: {explain}")
71
+
72
+ panel_name = panel.get("name_ar", panel.get("panel_code", ""))
73
+ parts.append(f"يُقاس ضمن: {panel_name} ({panel.get('name_en', '')})")
74
+
75
+ return " | ".join(parts)
76
+
77
+
78
+ def _build_values_chunk(panel: dict, test_name: str, test: dict) -> str:
79
+ """Chunk 1: normal ranges, severity thresholds, followup tests."""
80
+ name_ar = test.get("name_ar", test_name)
81
+ unit = test.get("unit", "")
82
+ ranges = test.get("ranges", {})
83
+ sev = test.get("severity_thresholds", {})
84
+ followup = test.get("followup_tests", [])
85
+
86
+ parts = [f"القيم المرجعية لـ {name_ar}:"]
87
+
88
+ range_lines = []
89
+ for gender_key, rng in ranges.items():
90
+ label = {
91
+ "adult_male": "بالغ ذكر",
92
+ "adult_female": "بالغة أنثى",
93
+ "adult": "البالغون",
94
+ "children": "الأطفال",
95
+ "children_6_12": "الأطفال 6-12 سنة",
96
+ "pregnant": "الحوامل",
97
+ "elderly_male": "كبار السن ذكر",
98
+ "elderly_female": "كبار السن أنثى",
99
+ "neonates": "حديثو الولادة",
100
+ }.get(gender_key, gender_key)
101
+ range_lines.append(f"{label}: {_fmt_range(rng)} {unit}")
102
+ parts.append(" | ".join(range_lines))
103
+
104
+ if sev:
105
+ sev_lines = []
106
+ for k, v in sev.items():
107
+ label = k.replace("_", " ")
108
+ sev_lines.append(f"{label}: {v} {unit}")
109
+ parts.append("عتبات الخطورة: " + " | ".join(sev_lines))
110
+
111
+ if followup:
112
+ parts.append("التحاليل التكميلية الموصى بها: " + ", ".join(followup[:6]))
113
+
114
+ return " — ".join(parts)
115
+
116
+
117
+ def _build_symptoms_chunk(panel: dict, test_name: str, test: dict) -> str | None:
118
+ """Chunk 2: causes + symptoms. Returns None if empty."""
119
+ name_ar = test.get("name_ar", test_name)
120
+ high_ar = test.get("high_causes_ar", [])
121
+ low_ar = test.get("low_causes_ar", [])
122
+ symp_low = test.get("symptoms_low_ar", [])
123
+
124
+ # Thyroid schemas may use interpretation_matrix instead
125
+ interp = test.get("interpretation_matrix", {})
126
+
127
+ parts = []
128
+ if high_ar:
129
+ parts.append(f"أسباب ارتفاع {name_ar}: " + "، ".join(high_ar))
130
+ if low_ar:
131
+ parts.append(f"أسباب انخفاض {name_ar}: " + "، ".join(low_ar))
132
+ if symp_low:
133
+ parts.append(f"الأعراض عند انخفاض {name_ar}: " + "، ".join(symp_low))
134
+ if interp:
135
+ lines = []
136
+ for pattern, meaning in interp.items():
137
+ lines.append(f"{pattern}: {meaning}")
138
+ parts.append(f"تفسير نتائج {name_ar}: " + " | ".join(lines[:6]))
139
+
140
+ if not parts:
141
+ return None
142
+ return " — ".join(parts)
143
+
144
+
145
+ def make_test_chunks(panel: dict, test_name: str, test: dict) -> list[dict]:
146
+ """Return list of {content, chunk_type, chunk_index} for one lab test."""
147
+ chunks = []
148
+
149
+ def_text = _build_definition_chunk(panel, test_name, test)
150
+ chunks.append({"content": def_text, "chunk_type": "definition", "chunk_index": 0})
151
+
152
+ val_text = _build_values_chunk(panel, test_name, test)
153
+ chunks.append({"content": val_text, "chunk_type": "values", "chunk_index": 1})
154
+
155
+ sym_text = _build_symptoms_chunk(panel, test_name, test)
156
+ if sym_text and len(sym_text) > 40:
157
+ chunks.append({"content": sym_text, "chunk_type": "symptoms", "chunk_index": 2})
158
+
159
+ return chunks
160
+
161
+
162
+ # ══════════════════════════════════════════════════════════════════
163
+ # 2. Schema loading
164
+ # ══════════════════════════════════════════════════════════════════
165
+
166
+ def load_all_schemas() -> list[dict]:
167
+ schemas = []
168
+ for path in sorted(SCHEMAS_DIR.glob("*.json")):
169
+ with path.open(encoding="utf-8") as f:
170
+ schemas.append(json.load(f))
171
+ return schemas
172
+
173
+
174
+ def build_all_docs(schemas: list[dict]) -> list[dict]:
175
+ """Convert all schemas to flat list of documents ready for embedding."""
176
+ docs = []
177
+ for panel in schemas:
178
+ panel_code = panel.get("panel_code", "unknown")
179
+ specialty = panel.get("specialty", "general")
180
+ panel_name_ar = panel.get("name_ar", panel_code)
181
+ tests = panel.get("tests", {})
182
+
183
+ for test_name, test in tests.items():
184
+ for chunk in make_test_chunks(panel, test_name, test):
185
+ docs.append({
186
+ "content": chunk["content"],
187
+ "metadata": {
188
+ "source": SOURCE_TAG,
189
+ "panel_code": panel_code,
190
+ "test_name": test_name,
191
+ "test_name_ar": test.get("name_ar", test_name),
192
+ "chunk_type": chunk["chunk_type"],
193
+ "chunk_index": chunk["chunk_index"],
194
+ "specialty": specialty,
195
+ "topic_type": "lab_test",
196
+ "title": f"{test.get('name_ar', test_name)} — {panel_name_ar}",
197
+ "language": "ar",
198
+ "unit": test.get("unit", ""),
199
+ },
200
+ })
201
+
202
+ return docs
203
+
204
+
205
+ # ══════════════════════════════════════════════════════════════════
206
+ # 3. Supabase helpers (same pattern as ingest_medlineplus.py)
207
+ # ══════════════════════════════════════════════════════════════════
208
+
209
+ def _headers() -> dict:
210
+ return {
211
+ "apikey": SUPABASE_KEY,
212
+ "Authorization": f"Bearer {SUPABASE_KEY}",
213
+ "Content-Type": "application/json",
214
+ "Prefer": "return=minimal",
215
+ }
216
+
217
+
218
+ def clear_kb_source(url: str, key: str):
219
+ r = requests.delete(
220
+ f"{url}/rest/v1/documents",
221
+ headers={
222
+ "apikey": key,
223
+ "Authorization": f"Bearer {key}",
224
+ "Content-Type": "application/json",
225
+ },
226
+ params={"metadata->>source": f"eq.{SOURCE_TAG}"},
227
+ timeout=30,
228
+ )
229
+ print(f" [CLEAR] source={SOURCE_TAG} -> HTTP {r.status_code}")
230
+
231
+
232
+ def insert_batch(batch: list[dict], url: str, key: str) -> bool:
233
+ try:
234
+ r = requests.post(
235
+ f"{url}/rest/v1/documents",
236
+ headers=_headers(),
237
+ json=batch,
238
+ timeout=60,
239
+ )
240
+ if r.status_code not in (200, 201):
241
+ print(f" [INSERT ERROR] {r.status_code}: {r.text[:300]}")
242
+ return False
243
+ return True
244
+ except Exception as e:
245
+ print(f" [INSERT EXCEPTION] {e}")
246
+ return False
247
+
248
+
249
+ def embed_and_insert(
250
+ docs: list[dict],
251
+ embeddings: HuggingFaceEmbeddings,
252
+ url: str,
253
+ key: str,
254
+ seen_hashes: set,
255
+ batch_size: int = 30,
256
+ ) -> int:
257
+ inserted = 0
258
+ skipped = 0
259
+ batch = []
260
+
261
+ for doc in docs:
262
+ content = doc["content"].strip()
263
+ if len(content) < 20:
264
+ skipped += 1
265
+ continue
266
+
267
+ h = hashlib.md5(content.encode()).hexdigest()
268
+ if h in seen_hashes:
269
+ skipped += 1
270
+ continue
271
+ seen_hashes.add(h)
272
+
273
+ try:
274
+ vec = embeddings.embed_query(content)
275
+ except Exception as e:
276
+ print(f" [EMBED ERROR] {e}")
277
+ continue
278
+
279
+ batch.append({
280
+ "content": content,
281
+ "metadata": doc["metadata"],
282
+ "embedding": vec,
283
+ })
284
+
285
+ if len(batch) >= batch_size:
286
+ if insert_batch(batch, url, key):
287
+ inserted += len(batch)
288
+ print(f" [BATCH] inserted {len(batch)} | total {inserted}")
289
+ batch = []
290
+
291
+ if batch:
292
+ if insert_batch(batch, url, key):
293
+ inserted += len(batch)
294
+ print(f" [BATCH] inserted {len(batch)} | total {inserted}")
295
+
296
+ if skipped:
297
+ print(f" [SKIP] {skipped} duplicate/short chunks skipped")
298
+
299
+ return inserted
300
+
301
+
302
+ # ══════════════════════════════════════════════════════════════════
303
+ # 4. Main
304
+ # ══════════════════════════════════════════════════════════════════
305
+
306
+ def main():
307
+ dry_run = "--dry-run" in sys.argv
308
+ do_clear = "--clear" in sys.argv
309
+
310
+ print("=" * 60)
311
+ print(f"ingest_medical_kb.py | source={SOURCE_TAG}")
312
+ print(f"schemas dir: {SCHEMAS_DIR}")
313
+ print("=" * 60)
314
+
315
+ schemas = load_all_schemas()
316
+ if not schemas:
317
+ print(f"[ERROR] لا توجد ملفات JSON في {SCHEMAS_DIR}")
318
+ sys.exit(1)
319
+
320
+ print(f"[SCHEMAS] loaded {len(schemas)} panels: {[p.get('panel_code') for p in schemas]}")
321
+
322
+ docs = build_all_docs(schemas)
323
+ print(f"[CHUNKS] {len(docs)} total chunks to insert\n")
324
+
325
+ for i, doc in enumerate(docs[:5]):
326
+ meta = doc["metadata"]
327
+ preview = doc["content"][:120].encode("ascii", errors="replace").decode()
328
+ print(f" chunk {i} | {meta['panel_code']}.{meta['test_name']} [{meta['chunk_type']}]")
329
+ print(f" {preview}...")
330
+ print()
331
+
332
+ if dry_run:
333
+ print(f"\n[DRY RUN] Would insert {len(docs)} chunks. Exiting.")
334
+ return
335
+
336
+ if not SUPABASE_URL or not SUPABASE_KEY:
337
+ print("[ERROR] SUPABASE_URL و SUPABASE_KEY غير موجودان في .env")
338
+ sys.exit(1)
339
+
340
+ if do_clear:
341
+ print("[CLEAR] حذف السجلات القديمة...")
342
+ clear_kb_source(SUPABASE_URL, SUPABASE_KEY)
343
+
344
+ print(f"[EMBED] تحميل نموذج {EMBED_MODEL}...")
345
+ embeddings = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
346
+ print("[EMBED] ready\n")
347
+
348
+ seen_hashes: set = set()
349
+ total = embed_and_insert(docs, embeddings, SUPABASE_URL, SUPABASE_KEY, seen_hashes)
350
+
351
+ print(f"\n{'=' * 60}")
352
+ print(f"[DONE] إجمالي المُدرج: {total} chunk من {len(docs)}")
353
+ print(f"{'=' * 60}")
354
+
355
+
356
+ if __name__ == "__main__":
357
+ main()
backend/ingest_medlineplus.py CHANGED
@@ -1,30 +1,40 @@
1
  """
2
- سكريبت استيراد الموسوعة الطبية من MedlinePlus (مجاني، بدون API key)
3
- الاستخدام: python ingest_medlineplus.py
 
 
 
 
 
 
 
 
 
4
  """
5
- import os
6
- import re
7
- import time
8
- import requests
9
  import xml.etree.ElementTree as ET
10
- from langchain_huggingface import HuggingFaceEmbeddings
11
- from langchain_chroma import Chroma
12
- from langchain_core.documents import Document
13
 
14
- os.environ['HF_HOME'] = r'D:\Project\model_cache'
 
 
 
 
 
15
 
16
- DB_PATH = r'D:\Project\chroma_db'
17
- EMBEDDINGS_MODEL = "intfloat/multilingual-e5-small"
 
18
 
19
- # ===== تحاليل الدم =====
20
- LAB_TESTS = [
21
  ("blood glucose test diabetes", "glucose", "lab_test"),
22
  ("complete blood count CBC hemoglobin", "CBC", "lab_test"),
23
  ("hemoglobin anemia iron deficiency", "hemoglobin", "lab_test"),
24
  ("cholesterol LDL HDL triglycerides", "cholesterol", "lab_test"),
25
  ("thyroid function TSH T3 T4", "thyroid", "lab_test"),
26
  ("liver function ALT AST bilirubin", "liver function", "lab_test"),
27
- ("kidney function creatinine BUN", "kidney function", "lab_test"),
28
  ("iron ferritin transferrin anemia", "iron", "lab_test"),
29
  ("vitamin D deficiency bone", "vitamin D", "lab_test"),
30
  ("vitamin B12 deficiency anemia", "vitamin B12", "lab_test"),
@@ -33,17 +43,16 @@ LAB_TESTS = [
33
  ("HbA1c glycated hemoglobin diabetes", "HbA1c", "lab_test"),
34
  ("white blood cell WBC leukocytes infection", "WBC", "lab_test"),
35
  ("platelet count bleeding clotting", "platelets", "lab_test"),
36
- ("eosinophils allergy parasites", "eosinophils", "lab_test"),
37
  ("sodium electrolytes hyponatremia", "sodium", "lab_test"),
38
  ("potassium electrolytes hyperkalemia", "potassium", "lab_test"),
39
  ("calcium bone osteoporosis", "calcium", "lab_test"),
40
- ("magnesium deficiency muscle", "magnesium", "lab_test"),
41
  ("albumin protein liver nutrition", "albumin", "lab_test"),
42
  ("PSA prostate cancer screening", "PSA", "lab_test"),
 
43
  ]
44
 
45
- # ===== الأعراض الشائعة =====
46
- SYMPTOMS = [
47
  ("fatigue tiredness exhaustion chronic", "fatigue", "symptom"),
48
  ("headache migraine pain relief", "headache", "symptom"),
49
  ("fever temperature infection causes", "fever", "symptom"),
@@ -54,57 +63,188 @@ SYMPTOMS = [
54
  ("nausea vomiting causes treatment", "nausea", "symptom"),
55
  ("back pain lower spine", "back pain", "symptom"),
56
  ("weight loss unexplained causes", "weight loss", "symptom"),
57
- ("weight gain causes treatment", "weight gain", "symptom"),
58
  ("hair loss alopecia causes", "hair loss", "symptom"),
59
- ("skin rash allergy dermatitis", "skin rash", "symptom"),
60
  ("joint pain arthritis inflammation", "joint pain", "symptom"),
61
  ("muscle weakness fatigue causes", "muscle weakness", "symptom"),
62
  ("palpitations heart irregular", "palpitations", "symptom"),
63
  ("insomnia sleep disorders causes", "insomnia", "symptom"),
64
  ("anxiety stress mental health", "anxiety", "symptom"),
65
- ("depression mood mental health", "depression", "symptom"),
66
  ("frequent urination diabetes kidney", "frequent urination", "symptom"),
67
- ("excessive thirst polydipsia causes", "excessive thirst", "symptom"),
68
  ("blurred vision eye causes", "blurred vision", "symptom"),
69
- ("swollen feet edema causes", "swollen feet", "symptom"),
70
- ("pale skin anemia causes", "pale skin", "symptom"),
71
- ("night sweats causes fever infection", "night sweats", "symptom"),
72
- ("cough chronic causes respiratory", "cough", "symptom"),
73
- ("numbness tingling hands feet", "numbness tingling", "symptom"),
74
  ]
75
 
76
- # ===== الأمراض الشائعة =====
77
- DISEASES = [
78
  ("diabetes mellitus type 2 management", "diabetes", "disease"),
79
  ("hypertension high blood pressure treatment", "hypertension", "disease"),
80
  ("anemia iron deficiency treatment", "anemia", "disease"),
81
  ("hypothyroidism underactive thyroid treatment", "hypothyroidism", "disease"),
82
  ("hyperthyroidism overactive thyroid treatment", "hyperthyroidism", "disease"),
83
  ("coronary artery disease heart", "heart disease", "disease"),
84
- ("kidney disease chronic renal failure", "kidney disease", "disease"),
85
  ("fatty liver hepatic steatosis", "fatty liver", "disease"),
86
- ("gout uric acid joint", "gout", "disease"),
87
  ("osteoporosis bone density fracture", "osteoporosis", "disease"),
88
- ("vitamin D deficiency treatment supplement", "vitamin D deficiency", "disease"),
89
- ("vitamin B12 deficiency treatment", "vitamin B12 deficiency", "disease"),
90
  ("high cholesterol hyperlipidemia treatment", "high cholesterol", "disease"),
91
- ("asthma respiratory treatment inhaler", "asthma", "disease"),
92
- ("urinary tract infection UTI treatment", "UTI", "disease"),
93
- ("irritable bowel syndrome IBS treatment", "IBS", "disease"),
94
- ("GERD acid reflux heartburn treatment", "GERD", "disease"),
95
- ("polycystic ovary syndrome PCOS treatment", "PCOS", "disease"),
96
- ("obesity overweight BMI treatment", "obesity", "disease"),
97
  ("metabolic syndrome insulin resistance", "metabolic syndrome", "disease"),
98
- ("celiac disease gluten intolerance", "celiac disease", "disease"),
 
 
 
 
 
99
  ("rheumatoid arthritis autoimmune joint", "rheumatoid arthritis", "disease"),
 
100
  ]
101
 
102
- ALL_TOPICS = LAB_TESTS + SYMPTOMS + DISEASES
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
 
 
 
 
 
 
104
 
105
- def fetch_medlineplus(search_term: str) -> list[dict]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  url = "https://wsearch.nlm.nih.gov/ws/query"
107
- params = {"db": "healthTopics", "term": search_term, "retmax": 3}
108
  try:
109
  resp = requests.get(url, params=params, timeout=15)
110
  if resp.status_code != 200:
@@ -112,70 +252,221 @@ def fetch_medlineplus(search_term: str) -> list[dict]:
112
  root = ET.fromstring(resp.text)
113
  results = []
114
  for doc in root.findall('.//document'):
115
- title = ""
116
- content = ""
117
  for elem in doc.findall('content'):
118
  name = elem.get('name', '')
119
  if name == 'title':
120
  title = elem.text or ""
121
  elif name == 'FullSummary':
122
  raw = elem.text or ""
123
- content = re.sub(r'<[^>]+>', ' ', raw).strip()
124
- content = re.sub(r'\s+', ' ', content)
125
- if title and content and len(content) > 100:
126
  results.append({"title": title, "content": content})
127
  return results
128
  except Exception as e:
129
- print(f" [ERROR] {search_term}: {e}")
130
  return []
131
 
132
 
133
- def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
134
- words = text.split()
135
- chunks = []
136
- for i in range(0, len(words), chunk_size - overlap):
137
- chunk = " ".join(words[i:i + chunk_size])
138
- if len(chunk) > 100:
139
- chunks.append(chunk)
140
- return chunks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
  def main():
144
- print("تحميل نموذج الـ Embeddings...")
145
- embeddings = HuggingFaceEmbeddings(model_name=EMBEDDINGS_MODEL)
 
146
 
147
- print("الاتصال بقاعدة البيانات...")
148
- db = Chroma(persist_directory=DB_PATH, embedding_function=embeddings)
149
 
150
- all_docs = []
151
- total = len(ALL_TOPICS)
152
- for i, (search_term, topic_name, topic_type) in enumerate(ALL_TOPICS, 1):
153
- print(f"[{i}/{total}] {topic_name}...")
154
- results = fetch_medlineplus(search_term)
 
 
 
 
 
 
 
 
 
 
 
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  for item in results:
157
- for idx, chunk in enumerate(chunk_text(item["content"])):
158
- doc = Document(
159
- page_content=chunk,
160
- metadata={
161
- "source": "MedlinePlus",
162
- "topic_name": topic_name,
163
- "topic_type": topic_type,
164
- "title": item["title"],
165
- "language": "en",
166
- "chunk_index": idx,
167
- }
168
- )
169
- all_docs.append(doc)
170
-
171
- time.sleep(0.3)
172
-
173
- if all_docs:
174
- print(f"\nاضافة {len(all_docs)} chunk...")
175
- db.add_documents(all_docs)
176
- print(f"[OK] تم! الموسوعة تحتوي {len(all_docs)} chunk من {total} موضوع طبي")
177
- else:
178
- print("[ERROR] لم يتم جلب أي بيانات")
 
179
 
180
 
181
  if __name__ == "__main__":
 
1
  """
2
+ سكريبت استيراد شامل للموسوعة الطبية -> pgvector (Supabase)
3
+ يحل محل النسخة القديمة التي كانت تكتب على ChromaDB.
4
+
5
+ المصادر:
6
+ 1. تعاريف التحاليل المخبرية (ثنائي اللغة) — من medical_data.py
7
+ 2. MedlinePlus API (مجاني، بدون API key) — 70+ موضوع طبي
8
+ 3. توصيات صحية عربية — من medical_data.py
9
+
10
+ الاستخدام:
11
+ cd backend && python ingest_medlineplus.py
12
+ أو لتنظيف الجداول أولاً: python ingest_medlineplus.py --clear
13
  """
14
+ import os, re, sys, time, hashlib, requests
 
 
 
15
  import xml.etree.ElementTree as ET
16
+ from dotenv import load_dotenv
 
 
17
 
18
+ load_dotenv()
19
+ os.environ.setdefault('HF_HOME', r'D:\Project\model_cache')
20
+ os.environ['TRANSFORMERS_VERBOSITY'] = 'error'
21
+
22
+ from langchain_huggingface import HuggingFaceEmbeddings
23
+ from medical_data import LAB_DEFINITIONS, HEALTH_RECOMMENDATIONS
24
 
25
+ EMBED_MODEL = "intfloat/multilingual-e5-large"
26
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "")
27
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
28
 
29
+ # ── MedlinePlus topics (search_term, topic_name, topic_type) ──────────────
30
+ LAB_TOPICS = [
31
  ("blood glucose test diabetes", "glucose", "lab_test"),
32
  ("complete blood count CBC hemoglobin", "CBC", "lab_test"),
33
  ("hemoglobin anemia iron deficiency", "hemoglobin", "lab_test"),
34
  ("cholesterol LDL HDL triglycerides", "cholesterol", "lab_test"),
35
  ("thyroid function TSH T3 T4", "thyroid", "lab_test"),
36
  ("liver function ALT AST bilirubin", "liver function", "lab_test"),
37
+ ("kidney function creatinine BUN eGFR", "kidney function", "lab_test"),
38
  ("iron ferritin transferrin anemia", "iron", "lab_test"),
39
  ("vitamin D deficiency bone", "vitamin D", "lab_test"),
40
  ("vitamin B12 deficiency anemia", "vitamin B12", "lab_test"),
 
43
  ("HbA1c glycated hemoglobin diabetes", "HbA1c", "lab_test"),
44
  ("white blood cell WBC leukocytes infection", "WBC", "lab_test"),
45
  ("platelet count bleeding clotting", "platelets", "lab_test"),
 
46
  ("sodium electrolytes hyponatremia", "sodium", "lab_test"),
47
  ("potassium electrolytes hyperkalemia", "potassium", "lab_test"),
48
  ("calcium bone osteoporosis", "calcium", "lab_test"),
49
+ ("magnesium deficiency muscle cramp", "magnesium", "lab_test"),
50
  ("albumin protein liver nutrition", "albumin", "lab_test"),
51
  ("PSA prostate cancer screening", "PSA", "lab_test"),
52
+ ("vitamin D calcium bone density", "bone health", "lab_test"),
53
  ]
54
 
55
+ SYMPTOM_TOPICS = [
 
56
  ("fatigue tiredness exhaustion chronic", "fatigue", "symptom"),
57
  ("headache migraine pain relief", "headache", "symptom"),
58
  ("fever temperature infection causes", "fever", "symptom"),
 
63
  ("nausea vomiting causes treatment", "nausea", "symptom"),
64
  ("back pain lower spine", "back pain", "symptom"),
65
  ("weight loss unexplained causes", "weight loss", "symptom"),
 
66
  ("hair loss alopecia causes", "hair loss", "symptom"),
 
67
  ("joint pain arthritis inflammation", "joint pain", "symptom"),
68
  ("muscle weakness fatigue causes", "muscle weakness", "symptom"),
69
  ("palpitations heart irregular", "palpitations", "symptom"),
70
  ("insomnia sleep disorders causes", "insomnia", "symptom"),
71
  ("anxiety stress mental health", "anxiety", "symptom"),
 
72
  ("frequent urination diabetes kidney", "frequent urination", "symptom"),
 
73
  ("blurred vision eye causes", "blurred vision", "symptom"),
74
+ ("swollen feet edema causes", "edema", "symptom"),
75
+ ("numbness tingling hands feet neuropathy", "numbness tingling", "symptom"),
 
 
 
76
  ]
77
 
78
+ DISEASE_TOPICS = [
 
79
  ("diabetes mellitus type 2 management", "diabetes", "disease"),
80
  ("hypertension high blood pressure treatment", "hypertension", "disease"),
81
  ("anemia iron deficiency treatment", "anemia", "disease"),
82
  ("hypothyroidism underactive thyroid treatment", "hypothyroidism", "disease"),
83
  ("hyperthyroidism overactive thyroid treatment", "hyperthyroidism", "disease"),
84
  ("coronary artery disease heart", "heart disease", "disease"),
85
+ ("chronic kidney disease renal failure", "kidney disease", "disease"),
86
  ("fatty liver hepatic steatosis", "fatty liver", "disease"),
87
+ ("gout uric acid joint treatment", "gout", "disease"),
88
  ("osteoporosis bone density fracture", "osteoporosis", "disease"),
 
 
89
  ("high cholesterol hyperlipidemia treatment", "high cholesterol", "disease"),
 
 
 
 
 
 
90
  ("metabolic syndrome insulin resistance", "metabolic syndrome", "disease"),
91
+ ("polycystic ovary syndrome PCOS", "PCOS", "disease"),
92
+ ("vitamin D deficiency treatment", "vitamin D deficiency", "disease"),
93
+ ("vitamin B12 deficiency treatment", "vitamin B12 deficiency", "disease"),
94
+ ("urinary tract infection UTI treatment", "UTI", "disease"),
95
+ ("irritable bowel syndrome IBS", "IBS", "disease"),
96
+ ("GERD acid reflux heartburn", "GERD", "disease"),
97
  ("rheumatoid arthritis autoimmune joint", "rheumatoid arthritis", "disease"),
98
+ ("celiac disease gluten intolerance", "celiac disease", "disease"),
99
  ]
100
 
101
+ ALL_MEDLINEPLUS_TOPICS = LAB_TOPICS + SYMPTOM_TOPICS + DISEASE_TOPICS
102
+
103
+
104
+ # ══════════════════════════════════════════════════════════════════
105
+ # 1. Text Cleaning
106
+ # ══════════════════════════════════════════════════════════════════
107
+
108
+ def clean_text(text: str) -> str:
109
+ """Remove HTML tags, normalize whitespace, normalize Arabic alef variants."""
110
+ text = re.sub(r'<[^>]+>', ' ', text) # HTML tags
111
+ text = re.sub(r'&[a-zA-Z]+;', ' ', text) # HTML entities
112
+ text = re.sub(r'\s+', ' ', text).strip() # excessive whitespace
113
+ # Normalize Arabic alef variants — safe, standard NLP practice
114
+ text = re.sub(r'[إأآ]', 'ا', text)
115
+ return text
116
+
117
+
118
+ # ══════════════════════════════════════════════════════════════════
119
+ # 2. Semantic Chunking
120
+ # ══════════════════════════════════════════════════════════════════
121
+
122
+ def make_lab_chunks(lab: dict) -> list[dict]:
123
+ """
124
+ Create 3 semantically distinct chunks per lab test.
125
+ Returns list of {content, chunk_type, chunk_index} dicts.
126
+ """
127
+ name_ar, name_en = lab["name_ar"], lab["name_en"]
128
+ chunks = []
129
+
130
+ # Chunk 0 — Definition + normal range
131
+ chunks.append({
132
+ "content": clean_text(f"{name_ar} ({name_en}): {lab['definition']}"),
133
+ "chunk_type": "definition",
134
+ "chunk_index": 0,
135
+ })
136
+
137
+ # Chunk 1 — Causes of high and low
138
+ abnormal = (
139
+ f"ارتفاع {name_ar}: {lab['high']}. "
140
+ f"انخفاض {name_ar}: {lab['low']}."
141
+ )
142
+ chunks.append({
143
+ "content": clean_text(abnormal),
144
+ "chunk_type": "values",
145
+ "chunk_index": 1,
146
+ })
147
+
148
+ # Chunk 2 — Symptoms (only if content is meaningful)
149
+ symptoms = lab.get("symptoms_low", "").strip()
150
+ if len(symptoms) > 30:
151
+ sym_text = f"الأعراض والعلامات المرتبطة بـ{name_ar}: {symptoms}."
152
+ chunks.append({
153
+ "content": clean_text(sym_text),
154
+ "chunk_type": "symptoms",
155
+ "chunk_index": 2,
156
+ })
157
+
158
+ return chunks
159
+
160
+
161
+ def sentence_chunks(text: str, max_chars: int = 800, overlap: int = 1) -> list[dict]:
162
+ """
163
+ Split free-form English/Arabic text at sentence boundaries.
164
+ Returns list of {content, chunk_type, chunk_index}.
165
+ """
166
+ sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', text) if s.strip()]
167
+
168
+ chunks, current, current_len = [], [], 0
169
+ for sent in sentences:
170
+ if current_len + len(sent) > max_chars and current:
171
+ body = ' '.join(current)
172
+ chunks.append({"content": body, "chunk_type": _detect_type(body)})
173
+ current = current[-overlap:] if overlap else []
174
+ current_len = sum(len(s) + 1 for s in current)
175
+ current.append(sent)
176
+ current_len += len(sent) + 1
177
+
178
+ if current:
179
+ body = ' '.join(current)
180
+ if len(body) > 80:
181
+ chunks.append({"content": body, "chunk_type": _detect_type(body)})
182
+
183
+ for i, c in enumerate(chunks):
184
+ c["chunk_index"] = i
185
+ return chunks
186
+
187
+
188
+ def _detect_type(text: str) -> str:
189
+ t = text.lower()
190
+ if any(w in t for w in ['definition', 'what is', 'also called', 'refers to', 'is a test']):
191
+ return 'definition'
192
+ if any(w in t for w in ['normal range', 'normal level', 'mg/dl', 'g/dl', 'mmol', 'ng/ml', 'iu/l']):
193
+ return 'values'
194
+ if any(w in t for w in ['symptom', 'sign', 'can cause', 'may cause', 'causes include']):
195
+ return 'symptoms'
196
+ if any(w in t for w in ['treatment', 'therapy', 'medication', 'manage', 'drug']):
197
+ return 'treatment'
198
+ return 'general'
199
+
200
+
201
+ # ══════════════════════════════════════════════════════════════════
202
+ # 3. Metadata helpers
203
+ # ══════════════════════════════════════════════════════════════════
204
+
205
+ _SPECIALTY_MAP = {
206
+ 'hematology': ['hemoglobin', 'rbc', 'wbc', 'platelet', 'cbc', 'hematocrit', 'mcv', 'mch',
207
+ 'mchc', 'rdw', 'neutrophil', 'lymphocyte', 'monocyte', 'eosinophil', 'basophil',
208
+ 'esr', 'd-dimer', 'fibrinogen', 'aptt', 'pt/inr', 'ferritin', 'iron'],
209
+ 'endocrinology':['glucose', 'hba1c', 'insulin', 'tsh', 'thyroid', 't3', 't4', 'cortisol',
210
+ 'testosterone', 'estradiol', 'prolactin', 'lh', 'fsh', 'dhea', 'progesterone', 'amh'],
211
+ 'cardiology': ['cholesterol', 'ldl', 'hdl', 'triglyceride', 'troponin', 'bnp', 'ck-mb',
212
+ 'heart', 'cardiac'],
213
+ 'nephrology': ['creatinine', 'egfr', 'bun', 'urea', 'kidney', 'urine protein', 'urine ketone',
214
+ 'urine specific gravity', 'chloride', 'sodium', 'potassium'],
215
+ 'hepatology': ['alt', 'ast', 'bilirubin', 'liver', 'albumin', 'ggt', 'alkaline phosphatase',
216
+ 'total protein', 'hepatitis'],
217
+ 'rheumatology': ['crp', 'esr', 'ana', 'rheumatoid', 'uric acid', 'gout', 'anti-tpo'],
218
+ 'nutrition': ['vitamin d', 'vitamin b12', 'folic acid', 'zinc', 'magnesium', 'calcium',
219
+ 'selenium', 'copper', 'phosphorus', 'iron'],
220
+ 'immunology': ['hiv', 'hepatitis b', 'hepatitis c', 'procalcitonin'],
221
+ 'reproductive': ['lh', 'fsh', 'progesterone', 'estradiol', 'beta hcg', 'amh', 'semen', 'testosterone', 'prolactin'],
222
+ }
223
 
224
+ def _get_specialty(name: str) -> str:
225
+ name_lower = name.lower()
226
+ for specialty, keywords in _SPECIALTY_MAP.items():
227
+ if any(k in name_lower for k in keywords):
228
+ return specialty
229
+ return 'general'
230
 
231
+
232
+ def _extract_unit(definition: str) -> str | None:
233
+ m = re.search(
234
+ r'\b(g/dL|mg/dL|ng/mL|µg/dL|IU/L|U/L|mEq/L|mmol/L|pg/mL|µIU/mL|mIU/L|mm/hr|ng/dL|µg/L|fL|pg)\b',
235
+ definition
236
+ )
237
+ return m.group(1) if m else None
238
+
239
+
240
+ # ══════════════════════════════════════════════════════════════════
241
+ # 4. MedlinePlus API
242
+ # ══════════════════════════════════════════════════════════════════
243
+
244
+ def fetch_medlineplus(search_term: str, retmax: int = 3) -> list[dict]:
245
+ """Fetch free MedlinePlus health topic summaries."""
246
  url = "https://wsearch.nlm.nih.gov/ws/query"
247
+ params = {"db": "healthTopics", "term": search_term, "retmax": retmax}
248
  try:
249
  resp = requests.get(url, params=params, timeout=15)
250
  if resp.status_code != 200:
 
252
  root = ET.fromstring(resp.text)
253
  results = []
254
  for doc in root.findall('.//document'):
255
+ title, content = "", ""
 
256
  for elem in doc.findall('content'):
257
  name = elem.get('name', '')
258
  if name == 'title':
259
  title = elem.text or ""
260
  elif name == 'FullSummary':
261
  raw = elem.text or ""
262
+ content = clean_text(re.sub(r'<[^>]+>', ' ', raw))
263
+ if title and len(content) > 100:
 
264
  results.append({"title": title, "content": content})
265
  return results
266
  except Exception as e:
267
+ print(f" [MedlinePlus ERROR] {search_term}: {e}")
268
  return []
269
 
270
 
271
+ # ══════════════════════════════════════════════════════════════════
272
+ # 5. Supabase pgvector Insert
273
+ # ══════════════════════════════════════════════════════════════════
274
+
275
+ def _make_headers(key: str) -> dict:
276
+ return {
277
+ "apikey": key,
278
+ "Authorization": f"Bearer {key}",
279
+ "Content-Type": "application/json",
280
+ "Prefer": "return=minimal",
281
+ }
282
+
283
+
284
+ def insert_batch(batch: list[dict], url: str, key: str) -> bool:
285
+ try:
286
+ r = requests.post(
287
+ f"{url}/rest/v1/documents",
288
+ headers=_make_headers(key),
289
+ json=batch,
290
+ timeout=60,
291
+ )
292
+ if r.status_code not in (200, 201):
293
+ print(f" [INSERT ERROR] {r.status_code}: {r.text[:300]}")
294
+ return False
295
+ return True
296
+ except Exception as e:
297
+ print(f" [INSERT EXCEPTION] {e}")
298
+ return False
299
+
300
+
301
+ def clear_source(source: str, url: str, key: str):
302
+ """Delete all documents from a given source before re-ingesting."""
303
+ headers = {
304
+ "apikey": key,
305
+ "Authorization": f"Bearer {key}",
306
+ "Content-Type": "application/json",
307
+ }
308
+ r = requests.delete(
309
+ f"{url}/rest/v1/documents",
310
+ headers=headers,
311
+ params={"metadata->>source": f"eq.{source}"},
312
+ timeout=30,
313
+ )
314
+ print(f" [CLEAR] source={source} -> {r.status_code}")
315
+
316
+
317
+ def embed_and_insert(
318
+ docs: list[dict],
319
+ embeddings: HuggingFaceEmbeddings,
320
+ url: str,
321
+ key: str,
322
+ seen_hashes: set,
323
+ batch_size: int = 50,
324
+ ) -> int:
325
+ """Embed + deduplicate + batch-insert documents to pgvector."""
326
+ inserted = 0
327
+ batch = []
328
+
329
+ for doc in docs:
330
+ content = doc["content"]
331
+ if not content or len(content) < 20:
332
+ continue
333
+ h = hashlib.md5(content.encode()).hexdigest()
334
+ if h in seen_hashes:
335
+ continue
336
+ seen_hashes.add(h)
337
+
338
+ try:
339
+ vec = embeddings.embed_query(content)
340
+ except Exception as e:
341
+ print(f" [EMBED ERROR] {e}")
342
+ continue
343
 
344
+ batch.append({
345
+ "content": content,
346
+ "metadata": doc["metadata"],
347
+ "embedding": vec,
348
+ })
349
+
350
+ if len(batch) >= batch_size:
351
+ if insert_batch(batch, url, key):
352
+ inserted += len(batch)
353
+ else:
354
+ print(f" [WARN] Batch of {len(batch)} failed — skipping")
355
+ batch = []
356
+
357
+ if batch:
358
+ if insert_batch(batch, url, key):
359
+ inserted += len(batch)
360
+
361
+ return inserted
362
+
363
+
364
+ # ══════════════════════════════════════════════════════════════════
365
+ # 6. Main
366
+ # ══════════════════════════════════════════════════════════════════
367
 
368
  def main():
369
+ if not SUPABASE_URL or not SUPABASE_KEY:
370
+ print("[ERROR] SUPABASE_URL و SUPABASE_KEY غير موجودان في .env")
371
+ sys.exit(1)
372
 
373
+ do_clear = "--clear" in sys.argv
 
374
 
375
+ print(f"تحميل نموذج Embeddings: {EMBED_MODEL}...")
376
+ embeddings = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
377
+
378
+ seen_hashes: set = set()
379
+ total_inserted = 0
380
+
381
+ # ── 1. Lab definitions (bilingual Arabic/English) ─────────────
382
+ print(f"\n[1/3] استيراد تعاريف التحاليل ({len(LAB_DEFINITIONS)} تحليل)...")
383
+ if do_clear:
384
+ clear_source("TibyanLabs", SUPABASE_URL, SUPABASE_KEY)
385
+
386
+ lab_docs = []
387
+ for lab in LAB_DEFINITIONS:
388
+ test_name = lab["name_en"].split("(")[0].strip()
389
+ specialty = _get_specialty(lab["name_en"] + " " + lab["name_ar"])
390
+ unit = _extract_unit(lab["definition"])
391
 
392
+ for chunk in make_lab_chunks(lab):
393
+ lab_docs.append({
394
+ "content": chunk["content"],
395
+ "metadata": {
396
+ "source": "TibyanLabs",
397
+ "topic_name": test_name,
398
+ "topic_type": "lab_test",
399
+ "title": f"{lab['name_ar']} ({lab['name_en']})",
400
+ "language": "bilingual",
401
+ "chunk_index": chunk["chunk_index"],
402
+ "chunk_type": chunk["chunk_type"],
403
+ "specialty": specialty,
404
+ "test_name": test_name,
405
+ "unit": unit,
406
+ },
407
+ })
408
+
409
+ n = embed_and_insert(lab_docs, embeddings, SUPABASE_URL, SUPABASE_KEY, seen_hashes)
410
+ total_inserted += n
411
+ print(f" -> {n} chunk مُضاف من تعاريف التحاليل")
412
+
413
+ # ── 2. Health recommendations (Arabic) ───────────────────────
414
+ print(f"\n[2/3] استيراد التوصيات الصحية ({len(HEALTH_RECOMMENDATIONS)} موضوع)...")
415
+ rec_docs = []
416
+ for rec in HEALTH_RECOMMENDATIONS:
417
+ content = clean_text(f"{rec['topic']}: {rec['content']}")
418
+ rec_docs.append({
419
+ "content": content,
420
+ "metadata": {
421
+ "source": "TibyanLabs",
422
+ "topic_name": rec["topic"],
423
+ "topic_type": "health_recommendation",
424
+ "title": rec["topic"],
425
+ "language": "ar",
426
+ "chunk_index": 0,
427
+ "chunk_type": "treatment",
428
+ "specialty": "general",
429
+ "test_name": None,
430
+ "unit": None,
431
+ },
432
+ })
433
+
434
+ n = embed_and_insert(rec_docs, embeddings, SUPABASE_URL, SUPABASE_KEY, seen_hashes)
435
+ total_inserted += n
436
+ print(f" -> {n} chunk مُضاف من التوصيات الصحية")
437
+
438
+ # ── 3. MedlinePlus API ────────────────────────────────────────
439
+ print(f"\n[3/3] استيراد من MedlinePlus ({len(ALL_MEDLINEPLUS_TOPICS)} موضوع)...")
440
+ if do_clear:
441
+ clear_source("MedlinePlus", SUPABASE_URL, SUPABASE_KEY)
442
+
443
+ for i, (search_term, topic_name, topic_type) in enumerate(ALL_MEDLINEPLUS_TOPICS, 1):
444
+ results = fetch_medlineplus(search_term)
445
+ ml_docs = []
446
  for item in results:
447
+ for chunk in sentence_chunks(item["content"]):
448
+ ml_docs.append({
449
+ "content": chunk["content"],
450
+ "metadata": {
451
+ "source": "MedlinePlus",
452
+ "topic_name": topic_name,
453
+ "topic_type": topic_type,
454
+ "title": item["title"],
455
+ "language": "en",
456
+ "chunk_index": chunk["chunk_index"],
457
+ "chunk_type": chunk["chunk_type"],
458
+ "specialty": _get_specialty(topic_name),
459
+ "test_name": topic_name if topic_type == "lab_test" else None,
460
+ "unit": None,
461
+ },
462
+ })
463
+
464
+ n = embed_and_insert(ml_docs, embeddings, SUPABASE_URL, SUPABASE_KEY, seen_hashes)
465
+ total_inserted += n
466
+ print(f" [{i}/{len(ALL_MEDLINEPLUS_TOPICS)}] {topic_name} -> {n} chunk")
467
+ time.sleep(0.35) # rate-limit courtesy
468
+
469
+ print(f"\n[ok] اكتمل! إجمالي المُضاف: {total_inserted} chunk في pgvector (Supabase)")
470
 
471
 
472
  if __name__ == "__main__":
backend/main.py CHANGED
@@ -6,63 +6,121 @@ os.environ['TRANSFORMERS_VERBOSITY'] = 'error'
6
  _here = os.path.dirname(os.path.abspath(__file__))
7
  os.environ.setdefault('HF_HOME', os.path.join(_here, '..', 'model_cache'))
8
 
9
- import io
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  import re
11
  import json
12
  import base64
 
13
  from functools import lru_cache
14
 
 
 
 
 
 
 
15
  import requests as http_requests
16
- import numpy as np
17
- from PIL import Image
18
- import fitz
19
  import easyocr
20
- from groq import Groq
21
  import cohere
22
- from rank_bm25 import BM25Okapi
23
- from langchain_huggingface import HuggingFaceEmbeddings
24
- from langchain_core.documents import Document
25
 
26
- from fastapi import FastAPI, UploadFile, File, Form, HTTPException
 
27
  from fastapi.middleware.cors import CORSMiddleware
28
  from fastapi.responses import StreamingResponse
29
  from pydantic import BaseModel
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
32
  if not GROQ_API_KEY:
33
  raise RuntimeError("GROQ_API_KEY environment variable is not set")
34
  COHERE_API_KEY = os.getenv("COHERE_API_KEY")
35
  SUPABASE_URL = os.getenv("SUPABASE_URL", "")
36
  SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
 
37
  SUPABASE_DB_URL = os.getenv("SUPABASE_DB_URL")
38
- GOOGLE_VISION_KEY = os.getenv("GOOGLE_VISION_API_KEY", "")
 
 
39
  FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000")
40
 
41
  GROQ_MODEL = "llama-3.3-70b-versatile"
42
  GROQ_MODEL_CHAT = "llama-3.1-8b-instant"
43
 
44
- # ملاحظة: e5-large يحتاج حذف chroma_db وإعادة ingest بعد التغيير
45
- EMBED_MODEL = "intfloat/multilingual-e5-large"
46
 
47
  app = FastAPI(title="تبيان الطبي API")
 
 
 
 
 
 
 
48
  app.add_middleware(
49
  CORSMiddleware,
50
- allow_origins=["http://localhost:3000", FRONTEND_URL, "*"],
51
- allow_methods=["*"],
52
- allow_headers=["*"],
 
 
53
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
 
56
  def ensure_analyses_table():
57
  """ينشئ جدول analyses في Supabase تلقائياً إذا لم يكن موجوداً"""
58
  if not SUPABASE_DB_URL:
59
- print("[DB] SUPABASE_DB_URL not set — skipping")
60
  return
61
  try:
62
- import psycopg2
63
- # psycopg2 يستخدم postgresql:// بدون +psycopg
64
- url = SUPABASE_DB_URL.replace("postgresql+psycopg://", "postgresql://")
65
- conn = psycopg2.connect(url, sslmode="require")
66
  conn.autocommit = True
67
  cur = conn.cursor()
68
  cur.execute("""
@@ -77,127 +135,204 @@ def ensure_analyses_table():
77
  """)
78
  cur.close()
79
  conn.close()
80
- print("[DB] analyses table ready")
81
  except Exception as e:
82
- print(f"[DB] table setup failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
 
85
  @app.on_event("startup")
86
  async def startup():
87
- load_groq()
 
 
 
 
 
88
 
89
 
90
  @lru_cache(maxsize=1)
91
- def load_groq():
92
- client = Groq(api_key=GROQ_API_KEY)
93
- print(f"[INFO] Groq ready | chat={GROQ_MODEL_CHAT}")
94
- return client
95
-
96
-
97
- class PGVectorStore:
98
- """واجهة بحث pgvector عبر Supabase REST — نفس interface ChromaDB"""
99
-
100
- def __init__(self, embeddings, url: str, key: str):
101
- self._emb = embeddings
102
- self._url = url
103
- self._key = key
104
- self._headers = {
105
- "apikey": key,
106
- "Authorization": f"Bearer {key}",
107
- "Content-Type": "application/json",
108
- }
109
-
110
- def _embed(self, text: str) -> list:
111
- return self._emb.embed_query(text)
112
-
113
- def similarity_search_with_relevance_scores(
114
- self, query: str, k: int = 10, filter: dict = None
115
- ) -> list:
116
- vec = self._embed(query)
117
- payload = {
118
- "query_embedding": vec,
119
- "match_threshold": 0.3,
120
- "match_count": k,
121
- "filter": filter or {},
122
- }
123
- try:
124
- r = http_requests.post(
125
- f"{self._url}/rest/v1/rpc/match_documents",
126
- headers=self._headers,
127
- json=payload,
128
- timeout=15,
129
- )
130
- r.raise_for_status()
131
- return [
132
- (Document(page_content=row["content"], metadata=row.get("metadata") or {}),
133
- float(row["similarity"]))
134
- for row in r.json()
135
- ]
136
- except Exception as e:
137
- print(f"[pgvector] {e}")
138
- return []
139
-
140
- def count(self) -> int:
141
- try:
142
- r = http_requests.get(
143
- f"{self._url}/rest/v1/documents",
144
- headers={**self._headers, "Prefer": "count=exact", "Range": "0-0"},
145
- timeout=10,
146
- )
147
- return int(r.headers.get("Content-Range", "0/0").split("/")[-1])
148
- except Exception:
149
- return 0
150
 
151
 
152
  @lru_cache(maxsize=1)
153
  def load_tools():
154
  reader = easyocr.Reader(['ar', 'en'], gpu=False)
155
- embeddings = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
156
- groq_client = Groq(api_key=GROQ_API_KEY)
157
  cohere_client = cohere.ClientV2(api_key=COHERE_API_KEY) if COHERE_API_KEY else None
158
- db = PGVectorStore(embeddings, SUPABASE_URL, SUPABASE_KEY)
159
- print(f"[INFO] Tools loaded | embed={EMBED_MODEL} | DB={db.count()} chunks (pgvector)")
160
- return reader, embeddings, groq_client, cohere_client, db
 
 
 
 
 
161
 
162
 
163
- # ══════════════════════════════════════════════
164
- # M1 — Google Cloud Vision OCR (مع EasyOCR احتياطاً)
165
- # ══════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
166
 
167
- def preprocess_image(image_bytes: bytes) -> "np.ndarray":
168
- """تحسين جودة صورة التحليل قبل OCR: رفع التباين + تحويل لـ grayscale"""
169
- from PIL import ImageFilter, ImageEnhance
170
- img = Image.open(io.BytesIO(image_bytes))
171
- # تحويل RGBA → RGB
172
- if img.mode in ('RGBA', 'P'):
173
- img = img.convert('RGB')
174
- # رفع دقة الصورة الصغيرة
175
- w, h = img.size
176
- if min(w, h) < 800:
177
- scale = 800 / min(w, h)
178
- img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS)
179
- # تحسين التباين والحدة
180
- img = ImageEnhance.Contrast(img).enhance(1.8)
181
- img = ImageEnhance.Sharpness(img).enhance(2.0)
182
- return np.array(img)
183
-
184
-
185
- def extract_text_google_vision(image_bytes: bytes) -> str:
186
- """OCR دقيق للتحاليل العربية والإنجليزية — GOOGLE_VISION_API_KEY مطلوب في .env"""
187
- b64 = base64.b64encode(image_bytes).decode('utf-8')
188
- payload = {
189
- "requests": [{
190
- "image": {"content": b64},
191
- "features": [{"type": "DOCUMENT_TEXT_DETECTION", "maxResults": 1}],
192
- "imageContext": {"languageHints": ["ar", "en"]},
193
- }]
194
- }
195
- url = f"https://vision.googleapis.com/v1/images:annotate?key={GOOGLE_VISION_KEY}"
196
- r = http_requests.post(url, json=payload, timeout=30)
197
- r.raise_for_status()
198
- text = r.json()["responses"][0].get("fullTextAnnotation", {}).get("text", "")
199
- print(f"[VISION] Google Cloud Vision: {len(text)} chars")
200
- return text
201
 
202
 
203
  # ══════════════════════════════════════════════
@@ -214,27 +349,6 @@ def groq_generate(client, prompt: str, max_tokens: int = 2048) -> str:
214
  return r.choices[0].message.content
215
 
216
 
217
- def is_valid_test(name: str) -> bool:
218
- ignore = ['page', 'id', 'patient', 'date', 'sex', 'age', 'mrn', 'doctor',
219
- 'physician', 'result', 'unit', 'range', 'validated', 'approved',
220
- 'Interpretation', 'Ref']
221
- n = str(name).lower()
222
- return not any(x in n for x in ignore) and len(str(name).strip()) > 1
223
-
224
-
225
- def get_status(value: str, range_str: str) -> str:
226
- try:
227
- nums = re.findall(r"[-+]?\d*\.?\d+", str(range_str))
228
- val = float(value)
229
- if len(nums) >= 2:
230
- lo, hi = float(nums[0]), float(nums[1])
231
- if val < lo: return "low"
232
- if val > hi: return "high"
233
- return "normal"
234
- except Exception:
235
- pass
236
- return "normal"
237
-
238
 
239
  def generate_search_queries(client, query: str) -> list:
240
  prompt = f"""أنت مساعد بحث طبي. حوّل السؤال إلى 3 استعلامات بحث مختلفة.
@@ -251,71 +365,27 @@ def generate_search_queries(client, query: str) -> list:
251
  return [query]
252
 
253
 
254
- def cohere_rerank(co_client, results: list, query: str, top_n: int = 5) -> list:
255
- if not co_client or len(results) <= 1:
256
- return results
257
- try:
258
- docs_text = [doc.page_content for doc, _ in results]
259
- resp = co_client.rerank(model="rerank-v3.5", query=query, documents=docs_text, top_n=top_n)
260
- return [(results[r.index][0], r.relevance_score) for r in resp.results]
261
- except Exception as e:
262
- print(f"[COHERE] {e} — BM25 fallback")
263
- return results[:top_n]
264
-
265
 
266
- def bm25_rerank(results: list, query: str) -> list:
267
- if len(results) <= 1:
268
- return results
269
- docs = [doc for doc, _ in results]
270
- vscores = [s for _, s in results]
271
- tok = [re.findall(r'\w+', d.page_content.lower()) for d in docs]
272
- bm25 = BM25Okapi(tok)
273
- bscores = bm25.get_scores(re.findall(r'\w+', query.lower()))
274
- mx = max(bscores) if max(bscores) > 0 else 1
275
- combined = [(d, vs + (bs / mx) * 0.3) for d, vs, bs in zip(docs, vscores, bscores)]
276
- return sorted(combined, key=lambda x: x[1], reverse=True)
277
 
278
-
279
- def get_rag_context(db, query: str, co_client=None, multi_query_client=None, k: int = 10, topic_type: str = None) -> tuple:
 
 
 
 
 
 
 
280
  try:
281
- queries = generate_search_queries(multi_query_client, query) if multi_query_client else [query]
282
- seen = {}
283
-
284
- # بحث عام
285
- for q in queries:
286
- for doc, score in db.similarity_search_with_relevance_scores(q, k=k):
287
- key = doc.page_content[:80]
288
- if key not in seen or seen[key][1] < score:
289
- seen[key] = (doc, score)
290
-
291
- # بحث مُصفَّى بـ metadata إن حُدِّد نوع (يضيف نتائج إضافية أكثر صلة)
292
- if topic_type:
293
- try:
294
- for q in queries[:2]:
295
- for doc, score in db.similarity_search_with_relevance_scores(
296
- q, k=k, filter={"topic_type": topic_type}
297
- ):
298
- key = doc.page_content[:80]
299
- boosted = min(score + 0.15, 1.0) # رفع أولوية النتائج المصفاة
300
- if key not in seen or seen[key][1] < boosted:
301
- seen[key] = (doc, boosted)
302
- except Exception:
303
- pass # ChromaDB قديم لا يدعم filter، تجاهل
304
-
305
- filtered = [(d, s) for d, s in seen.values() if s >= 0.4]
306
- if not filtered:
307
- return "", "لا يوجد"
308
-
309
- top_scores = sorted([s for _, s in filtered], reverse=True)[:5]
310
- avg = sum(top_scores) / len(top_scores)
311
- conf = "عالية" if avg > 0.70 else "متوسطة" if avg > 0.45 else "منخفضة"
312
-
313
- top = cohere_rerank(co_client, filtered, query) if co_client else bm25_rerank(filtered, query)[:5]
314
- context = "\n\n".join([d.page_content for d, _ in top])
315
- print(f"[RAG] '{query[:40]}' | n={len(filtered)} | conf={conf} ({avg:.2f}) | filter={topic_type}")
316
- return context, conf
317
  except Exception as e:
318
- print(f"[RAG ERROR] {e}")
319
  return "", "لا يوجد"
320
 
321
 
@@ -324,14 +394,26 @@ def get_rag_context(db, query: str, co_client=None, multi_query_client=None, k:
324
  # ══════════════════════════════════════════════
325
 
326
  def _sb_headers():
 
 
 
327
  return {
328
- "apikey": SUPABASE_KEY,
329
- "Authorization": f"Bearer {SUPABASE_KEY}",
330
  "Content-Type": "application/json",
331
  "Prefer": "return=representation",
332
  }
333
 
334
 
 
 
 
 
 
 
 
 
 
335
  class SaveAnalysisRequest(BaseModel):
336
  session_id: str = "anonymous"
337
  findings: list
@@ -340,7 +422,8 @@ class SaveAnalysisRequest(BaseModel):
340
 
341
 
342
  @app.post("/api/analyses/save")
343
- async def save_analysis(req: SaveAnalysisRequest):
 
344
  if not SUPABASE_URL or not SUPABASE_KEY:
345
  raise HTTPException(503, "Supabase keys not configured")
346
  try:
@@ -362,7 +445,9 @@ async def save_analysis(req: SaveAnalysisRequest):
362
 
363
 
364
  @app.get("/api/analyses/list")
365
- async def list_analyses(session_id: str = "anonymous", profile_name: str = "", limit: int = 20):
 
 
366
  if not SUPABASE_URL or not SUPABASE_KEY:
367
  raise HTTPException(503, "Supabase keys not configured")
368
  try:
@@ -381,9 +466,28 @@ async def list_analyses(session_id: str = "anonymous", profile_name: str = "", l
381
  raise HTTPException(500, str(e))
382
 
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  @app.get("/api/analyses/profiles")
385
- async def list_profiles(session_id: str = "anonymous"):
386
  """جلب قائمة أفراد العائلة المسجلين لهذا المستخدم"""
 
387
  if not SUPABASE_URL or not SUPABASE_KEY:
388
  raise HTTPException(503, "Supabase keys not configured")
389
  try:
@@ -415,127 +519,39 @@ ANALYSIS_TYPE_HINTS = {
415
 
416
 
417
  @app.post("/api/analyze")
418
- async def analyze(file: UploadFile = File(...), analysis_type: str = Form("شامل")):
419
- reader, embeddings, client, co_client, db = load_tools()
420
  content = await file.read()
421
- print(f"[ANALYZE] type={analysis_type} | file={file.filename}")
422
-
423
- # استخراج النص
424
- if file.content_type == "application/pdf":
425
- doc = fitz.open(stream=content, filetype="pdf")
426
- all_text = "\n".join([p.get_text() for p in doc])
427
- elif GOOGLE_VISION_KEY:
428
- try:
429
- all_text = extract_text_google_vision(content)
430
- except Exception as e:
431
- print(f"[VISION] fallback to EasyOCR ({e})")
432
- all_text = "\n".join(reader.readtext(preprocess_image(content), detail=0))
433
- else:
434
- all_text = "\n".join(reader.readtext(preprocess_image(content), detail=0))
435
-
436
- # استخراج الفحوصات
437
- pattern = r"([a-zA-Z][a-zA-Z\s#%]{2,})\s+(\d+\.?\d*)\s+([\d\.]+\s*-\s*[\d\.]+)\s*([a-zA-Z0-9^/]+)?"
438
- findings_raw = [f for f in re.findall(pattern, all_text) if is_valid_test(f[0])]
439
-
440
- if len(findings_raw) < 2:
441
- try:
442
- clean = groq_generate(client, f"""حلل النص الطبي واستخرج نتائج التحاليل.
443
- النص: {all_text[:4000]}
444
- أجب بقائمة بايثون فقط: [('Test Name', 'Value', 'Range', 'Unit')]""", max_tokens=1024)
445
- findings_raw = eval(clean.strip().replace("```python", "").replace("```", ""))
446
- except Exception:
447
- pass
448
-
449
- findings = [
450
- {"name": str(f[0]).strip(), "value": str(f[1]), "range": str(f[2]),
451
- "unit": str(f[3]) if len(f) > 3 else "", "status": get_status(f[1], f[2])}
452
- for f in findings_raw if is_valid_test(f[0])
453
- ]
454
 
455
- abnormal = [f for f in findings if f["status"] != "normal"]
456
- summary = ("القيم التي تحتاج انتباهاً: " + "، ".join([f["name"] for f in abnormal[:3]])) if abnormal else "جميع القيم ضمن المعدل الطبيعي ✓"
457
-
458
- type_hint = ANALYSIS_TYPE_HINTS.get(analysis_type, ANALYSIS_TYPE_HINTS["شامل"])
459
- search_query = f"{type_hint}: " + ", ".join([f["name"] for f in findings])
460
- # استخدام metadata filter للتحاليل المحددة
461
- topic_filter = "lab_definition" if analysis_type != "شامل" else None
462
- context, conf_label = get_rag_context(db, search_query, co_client, multi_query_client=client, topic_type=topic_filter)
463
-
464
- report_prompt = (
465
- "أنت طبيب مختبر خبير. أرجع JSON فقط بدون أي نص خارجه. ابدأ بـ { مباشرة.\n"
466
- "لا HTML ولا markdown. نصوص عربية بسيطة فقط.\n\n"
467
- f"النتائج: {findings}\nالمراجع: {context or 'لا توجد مراجع'}\n\n"
468
- '{"تقييم_عام":"جملتان عن الحالة",'
469
- '"قيم_غير_طبيعية":[{"اسم_الفحص":"","النتيجة":"","المعدل_الطبيعي":"","الحالة":"","الشرح":"","المرجع":""}],'
470
- '"نصائح":["نصيحة1","نصيحة2","نصيحة3"]}'
471
- )
472
- print(f"[ANALYZE] findings={len(findings)}, abnormal={len(abnormal)}, conf={conf_label}")
473
-
474
- def fallback_tips(abn):
475
- base = ["تناول غذاءً متوازناً غنياً بالخضروات", "اشرب 8 أكواب ماء يومياً",
476
- "مارس رياضة خفيفة 30 دقيقة يومياً", "احرص على النوم 7-8 ساعات",
477
- "تجنب التوتر وخصص وقتاً للراحة", "راجع طبيبك لمتابعة التحاليل"]
478
- extra = {
479
- "glucose": ["قلل السكريات والنشويات المكررة", "مارس الرياضة بانتظام"],
480
- "hemoglobin": ["تناول أطعمة الحديد كالسبانخ واللحوم", "فيتامين C يحسن امتصاص الحديد"],
481
- "eosinophils": ["تجنب مسببات الحساسية", "أخبر طبيبك بأي أعراض حساسية"],
482
- }
483
- tips = list(base)
484
- for f in abn:
485
- for key, sp in extra.items():
486
- if key in f["name"].lower():
487
- tips.extend(sp); break
488
- return tips[:8]
489
-
490
- report = {}
491
- try:
492
- raw = groq_generate(client, report_prompt)
493
- raw = raw.strip().replace("```json", "").replace("```", "").strip()
494
- if '{' in raw:
495
- raw = raw[raw.index('{'):raw.rindex('}') + 1]
496
- rd = json.loads(raw)
497
- report = {
498
- "general": rd.get("تقييم_عام", summary),
499
- "abnormal_details": rd.get("قيم_غير_طبيعية", []),
500
- "tips": rd.get("نصائح", []) or fallback_tips(abnormal),
501
- "rag_confidence": conf_label,
502
- }
503
- except Exception as e:
504
- print(f"[ERROR] JSON parse: {e}")
505
- report = {
506
- "general": summary,
507
- "abnormal_details": [
508
- {"اسم_الفحص": f["name"], "النتيجة": f["value"], "المعدل_الطبيعي": f["range"],
509
- "الحالة": "مرتفع" if f["status"] == "high" else "منخفض",
510
- "الشرح": f"قيمة {'مرتفعة' if f['status'] == 'high' else 'منخفضة'} عن المعدل.",
511
- "المرجع": "لا يوجد"} for f in abnormal
512
- ],
513
- "tips": fallback_tips(abnormal),
514
- "rag_confidence": conf_label,
515
- }
516
-
517
- if not report.get("abnormal_details") and abnormal:
518
- report["abnormal_details"] = [
519
- {"اسم_الفحص": f["name"], "النتيجة": f["value"], "المعدل_الطبيعي": f["range"],
520
- "الحالة": "مرتفع" if f["status"] == "high" else "منخفض",
521
- "الشرح": f"قيمة {'مرتفعة' if f['status'] == 'high' else 'منخفضة'} عن المعدل.",
522
- "المرجع": "لا يوجد"} for f in abnormal
523
- ]
524
-
525
- return {"findings": findings, "summary": summary, "report": report}
526
 
527
 
528
  # ══════════════════════════════════════════════
529
  # M3 — Chat: ذاكرة + ربط التحليل + فلتر ذكي + مصادر
530
  # ══════════════════════════════════════════════
531
 
532
- CHAT_SYSTEM = """أنت مساعد طبي ذكي اسمك "تبيان". أجب بشكل واضح ومختصر باللغة العربية.
533
- قواعد صارمة:
534
- 1. لا تخترع معلومات — إذا لم تكن متأكداً قل ا أعلم، استشر طبيبك"
535
- 2. اذكر مصدر كل معلومة مهمة (مثال: وفقاً لـ Mayo Clinic | WHO | MedlinePlus)
536
- 3. لا تشخّص أمراضاً بشكل قاطع
537
- 4. إذا سُئلت عن أعراض، أضف "التحاليل المقترحة:" في النهاية
538
- 5. انصح دائماً بمراجعة الطبيب المختص"""
539
 
540
  _FALLBACK_WORDS = ["ألم","صداع","تعب","دوخة","حمى","سعال","أعراض","ضغط","سكر","قلب",
541
  "معدة","تنفس","التهاب","دواء","علاج","تحليل","دم","فيتامين","نتائج",
@@ -544,17 +560,8 @@ _FALLBACK_WORDS = ["ألم","صداع","تعب","دوخة","حمى","سعال","
544
 
545
 
546
  def is_medical_query(client, query: str) -> bool:
547
- """M3 — فلتر ذكي بـ Groq بدل قائمة الكلمات"""
548
- try:
549
- r = client.chat.completions.create(
550
- model=GROQ_MODEL_CHAT,
551
- messages=[{"role": "user",
552
- "content": f"هل هذا السؤال يتعلق بالصحة أو الطب أو التحاليل أو الأدوية؟ أجب بـ نعم أو لا فقط:\n{query}"}],
553
- temperature=0, max_tokens=5,
554
- )
555
- return "نعم" in r.choices[0].message.content
556
- except Exception:
557
- return any(w in query for w in _FALLBACK_WORDS)
558
 
559
 
560
  class ChatRequest(BaseModel):
@@ -564,25 +571,31 @@ class ChatRequest(BaseModel):
564
 
565
 
566
  @app.post("/api/chat")
567
- async def chat_stream(req: ChatRequest):
568
- client = load_groq()
 
569
 
570
- if not is_medical_query(client, req.query):
571
  def nm():
572
  yield "أنا مساعد طبي متخصص. يسعدني الإجابة على أسئلتك الصحية وتحاليلك الطبية."
573
  return StreamingResponse(nm(), media_type="text/plain; charset=utf-8")
574
 
575
- system = CHAT_SYSTEM
576
-
577
- # RAG: استرجاع معلومات طبية من قاعدة المعرفة
578
- try:
579
- _, _, _, co_client, db = load_tools()
580
- rag_ctx, _ = get_rag_context(db, req.query, co_client, multi_query_client=client, k=6)
581
- if rag_ctx:
582
- system += f"\n\nمعلومات طبية من قاعدة المعرفة (استند إليها):\n{rag_ctx}"
583
- except Exception:
584
- pass
 
 
 
585
 
 
 
586
  if req.analysis_context:
587
  try:
588
  ctx = json.loads(req.analysis_context)
@@ -590,22 +603,26 @@ async def chat_stream(req: ChatRequest):
590
  summary = ctx.get("summary", "")
591
  abnormal = [f for f in findings if f.get("status") != "normal"]
592
  normal = [f for f in findings if f.get("status") == "normal"]
593
-
594
- ctx_lines = [f"ملخص التحليل: {summary}"]
595
  if abnormal:
596
- ctx_lines.append("النتائج غير الطبيعية:")
597
  for f in abnormal:
598
  direction = "مرتفع" if f.get("status") == "high" else "منخفض"
599
- ctx_lines.append(
600
  f" • {f['name']}: {f['value']} {f.get('unit','')} "
601
  f"(المعدل: {f.get('range','')}) — {direction}"
602
  )
603
  if normal:
604
- ctx_lines.append(f"النتائج الطبيعية: {', '.join(f['name'] for f in normal)}")
605
-
606
- system += "\n\nبيانات تحليل المريض (استند إليها مباشرة عند الإجابة):\n" + "\n".join(ctx_lines)
607
  except Exception:
608
- system += f"\n\nنتائج التحليل:\n{req.analysis_context}"
 
 
 
 
 
 
609
 
610
  messages = [{"role": "system", "content": system}]
611
  for msg in req.history[-10:]: # آخر 10 رسائل فقط
@@ -613,27 +630,102 @@ async def chat_stream(req: ChatRequest):
613
  messages.append({"role": msg["role"], "content": msg["content"]})
614
  messages.append({"role": "user", "content": req.query})
615
 
 
 
 
 
 
 
 
616
  def generate():
 
617
  try:
618
- stream = client.chat.completions.create(
619
- model=GROQ_MODEL_CHAT,
620
- messages=messages,
621
- temperature=0.1, # M3 — أقل هلوسة
622
- max_tokens=800,
623
- stream=True,
624
- )
625
- for chunk in stream:
626
- token = chunk.choices[0].delta.content or ""
627
- if token:
628
- yield token
629
  except Exception as e:
630
  err = str(e)
631
- print(f"[CHAT ERROR] {err[:200]}")
632
  yield "عذراً، الخدمة مشغولة. حاول بعد لحظة." if "429" in err else "حدث خطأ، يرجى المحاولة مرة أخرى."
 
 
 
 
 
 
 
633
 
634
  return StreamingResponse(generate(), media_type="text/plain; charset=utf-8")
635
 
636
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
  class EvalRequest(BaseModel):
638
  question: str
639
  reference_answer: str = ""
@@ -690,6 +782,371 @@ async def evaluate_rag(req: EvalRequest):
690
  }
691
 
692
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  @app.get("/api/health")
694
- def health():
695
- return {"status": "ok", "embed_model": EMBED_MODEL}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  _here = os.path.dirname(os.path.abspath(__file__))
7
  os.environ.setdefault('HF_HOME', os.path.join(_here, '..', 'model_cache'))
8
 
9
+ # ── Sentry error monitoring (requires SENTRY_DSN in .env / Railway vars) ──
10
+ _SENTRY_DSN = os.getenv("SENTRY_DSN", "")
11
+ if _SENTRY_DSN:
12
+ try:
13
+ import sentry_sdk
14
+ from sentry_sdk.integrations.fastapi import FastApiIntegration
15
+ sentry_sdk.init(
16
+ dsn=_SENTRY_DSN,
17
+ integrations=[FastApiIntegration(transaction_style="endpoint")],
18
+ traces_sample_rate=0.05, # 5% of requests
19
+ environment=os.getenv("ENVIRONMENT", "production"),
20
+ release=os.getenv("RAILWAY_DEPLOYMENT_ID", "local"),
21
+ )
22
+ print("[Sentry] initialized")
23
+ except ImportError:
24
+ pass # sentry-sdk not installed
25
+
26
  import re
27
  import json
28
  import base64
29
+ import logging
30
  from functools import lru_cache
31
 
32
+ logging.basicConfig(
33
+ level=logging.INFO,
34
+ format="%(asctime)s %(levelname)s %(name)s | %(message)s",
35
+ datefmt="%H:%M:%S",
36
+ )
37
+
38
  import requests as http_requests
 
 
 
39
  import easyocr
 
40
  import cohere
41
+ from groq import Groq # used only by load_tools() to build raw client for agents
 
 
42
 
43
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Request, Depends
44
+ from fastapi.responses import Response as HttpResponse
45
  from fastapi.middleware.cors import CORSMiddleware
46
  from fastapi.responses import StreamingResponse
47
  from pydantic import BaseModel
48
 
49
+ # ── Internal services ──────────────────────────────────────────────────────
50
+ from services.search.embedding_service import get_embeddings
51
+ from services.search.semantic_search import SemanticSearchService
52
+ from services.rag.retriever import Retriever, RetrievalConfig
53
+ from services.rag.context_builder import build_context
54
+ from services.safety import filter_analysis_report, filter_chat_response, check_emergency, sanitize_query
55
+ from services.agents import AgentCoordinator
56
+ from services.llm import get_router, LLMRouter
57
+ from services.ratelimit import limit_analyze, limit_chat, limit_search
58
+ from services.cache import rag_cache, rag_cache_key, search_cache
59
+ from services.search.embedding_service import EMBED_MODEL
60
+ from medical_kb import kb as medical_kb
61
+ from prompts.loader import render as render_prompt, load as load_prompt
62
+ from middleware import AuditMiddleware, validate_upload, sanitize_text
63
+ from middleware.auth_middleware import optional_user
64
+
65
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
66
  if not GROQ_API_KEY:
67
  raise RuntimeError("GROQ_API_KEY environment variable is not set")
68
  COHERE_API_KEY = os.getenv("COHERE_API_KEY")
69
  SUPABASE_URL = os.getenv("SUPABASE_URL", "")
70
  SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
71
+ SUPABASE_SERVICE_KEY = os.getenv("SUPABASE_SERVICE_KEY", "") # service_role bypasses RLS
72
  SUPABASE_DB_URL = os.getenv("SUPABASE_DB_URL")
73
+ GOOGLE_VISION_KEY = os.getenv("GOOGLE_VISION_API_KEY", "")
74
+ GOOGLE_TTS_KEY = os.getenv("GOOGLE_TTS_KEY", "")
75
+ ELEVENLABS_KEY = os.getenv("ELEVENLABS_API_KEY", "")
76
  FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000")
77
 
78
  GROQ_MODEL = "llama-3.3-70b-versatile"
79
  GROQ_MODEL_CHAT = "llama-3.1-8b-instant"
80
 
81
+ log = logging.getLogger("tebyan.main")
 
82
 
83
  app = FastAPI(title="تبيان الطبي API")
84
+
85
+ _is_production = os.getenv("ENVIRONMENT", "development") == "production"
86
+ _allowed_origins: list[str] = list(filter(None, [
87
+ "http://localhost:3000",
88
+ "http://127.0.0.1:3000",
89
+ FRONTEND_URL if FRONTEND_URL != "http://localhost:3000" else None,
90
+ ]))
91
  app.add_middleware(
92
  CORSMiddleware,
93
+ allow_origins=_allowed_origins,
94
+ allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
95
+ allow_headers=["Content-Type", "Authorization", "X-Session-Id", "X-Forwarded-For"],
96
+ allow_credentials=False,
97
+ max_age=600,
98
  )
99
+ app.add_middleware(AuditMiddleware, environment=os.getenv("ENVIRONMENT", "development"))
100
+
101
+
102
+ def _pg_connect():
103
+ # Try direct host connection first (pooler rejected tenant format)
104
+ direct = re.sub(
105
+ r"postgresql(?:\+psycopg)?://[^:]+:[^@]+@[^/]+/",
106
+ f"postgresql://postgres:{os.getenv('SUPABASE_DB_PASSWORD', 'R1a2g3h4d56')}@db.assxdosinubpubeqjrso.supabase.co:5432/",
107
+ SUPABASE_DB_URL,
108
+ )
109
+ import psycopg2
110
+ try:
111
+ return psycopg2.connect(direct, sslmode="require", connect_timeout=8)
112
+ except Exception:
113
+ fallback = SUPABASE_DB_URL.replace("postgresql+psycopg://", "postgresql://")
114
+ return psycopg2.connect(fallback, sslmode="require", connect_timeout=8)
115
 
116
 
117
  def ensure_analyses_table():
118
  """ينشئ جدول analyses في Supabase تلقائياً إذا لم يكن موجوداً"""
119
  if not SUPABASE_DB_URL:
120
+ log.debug("SUPABASE_DB_URL not set — skipping DB setup")
121
  return
122
  try:
123
+ conn = _pg_connect()
 
 
 
124
  conn.autocommit = True
125
  cur = conn.cursor()
126
  cur.execute("""
 
135
  """)
136
  cur.close()
137
  conn.close()
138
+ log.info("analyses table ready")
139
  except Exception as e:
140
+ log.error("analyses table setup failed: %s", e)
141
+
142
+
143
+ def ensure_chat_table():
144
+ """ينشئ جدول chat_messages لحفظ تاريخ المحادثات عبر الجلسات"""
145
+ if not SUPABASE_DB_URL:
146
+ return
147
+ try:
148
+ conn = _pg_connect()
149
+ conn.autocommit = True
150
+ cur = conn.cursor()
151
+ cur.execute("""
152
+ CREATE TABLE IF NOT EXISTS chat_messages (
153
+ id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
154
+ session_id text NOT NULL,
155
+ role text NOT NULL,
156
+ content text NOT NULL,
157
+ created_at timestamptz DEFAULT now()
158
+ );
159
+ CREATE INDEX IF NOT EXISTS idx_chat_session
160
+ ON chat_messages(session_id, created_at);
161
+ """)
162
+ cur.close()
163
+ conn.close()
164
+ log.info("chat_messages table ready")
165
+ except Exception as e:
166
+ log.error("chat table setup failed: %s", e)
167
+
168
+
169
+ _START_TIME = __import__("time").time()
170
+ _REQUEST_COUNTS: dict[str, int] = {}
171
+ _ERROR_COUNTS: dict[str, int] = {}
172
+
173
+
174
+ @app.middleware("http")
175
+ async def _track_requests(request: Request, call_next):
176
+ path = request.url.path
177
+ _REQUEST_COUNTS[path] = _REQUEST_COUNTS.get(path, 0) + 1
178
+ response = await call_next(request)
179
+ if response.status_code >= 500:
180
+ _ERROR_COUNTS[path] = _ERROR_COUNTS.get(path, 0) + 1
181
+ return response
182
+
183
+
184
+ @app.get("/health")
185
+ async def health():
186
+ import time
187
+ uptime_s = int(time.time() - _START_TIME)
188
+ db_ok, chunk_count = False, 0
189
+ try:
190
+ _, _, _, search_svc, _ = load_tools()
191
+ chunk_count = search_svc.count()
192
+ db_ok = chunk_count > 0
193
+ except Exception:
194
+ pass
195
+ return {
196
+ "ok": True,
197
+ "version": os.getenv("RAILWAY_DEPLOYMENT_ID", "local"),
198
+ "environment": os.getenv("ENVIRONMENT", "development"),
199
+ "uptime_s": uptime_s,
200
+ "db": {
201
+ "ok": db_ok,
202
+ "chunks": chunk_count,
203
+ "source": "pgvector/supabase",
204
+ },
205
+ "model": {
206
+ "name": os.getenv("EMBED_MODEL", "intfloat/multilingual-e5-large"),
207
+ "loaded": db_ok,
208
+ },
209
+ "services": {
210
+ "groq": bool(GROQ_API_KEY),
211
+ "cohere": bool(COHERE_API_KEY),
212
+ "vision": bool(os.getenv("GOOGLE_VISION_API_KEY")),
213
+ },
214
+ }
215
+
216
+
217
+ @app.get("/api/metrics")
218
+ async def metrics():
219
+ """Prometheus-style plaintext metrics for uptime monitoring."""
220
+ import time
221
+ uptime_s = int(time.time() - _START_TIME)
222
+ cache_stats = rag_cache.stats()
223
+ lines = [
224
+ "# HELP tebyan_uptime_seconds Seconds since server start",
225
+ "# TYPE tebyan_uptime_seconds counter",
226
+ f"tebyan_uptime_seconds {uptime_s}",
227
+ "",
228
+ "# HELP tebyan_requests_total Total HTTP requests per path",
229
+ "# TYPE tebyan_requests_total counter",
230
+ ]
231
+ for path, count in sorted(_REQUEST_COUNTS.items()):
232
+ lines.append(f'tebyan_requests_total{{path="{path}"}} {count}')
233
+ lines += [
234
+ "",
235
+ "# HELP tebyan_errors_total Total 5xx errors per path",
236
+ "# TYPE tebyan_errors_total counter",
237
+ ]
238
+ for path, count in sorted(_ERROR_COUNTS.items()):
239
+ lines.append(f'tebyan_errors_total{{path="{path}"}} {count}')
240
+ lines += [
241
+ "",
242
+ "# HELP tebyan_rag_cache_size Current RAG cache entries",
243
+ "# TYPE tebyan_rag_cache_size gauge",
244
+ f"tebyan_rag_cache_size {cache_stats.get('size', 0)}",
245
+ "",
246
+ "# HELP tebyan_rag_cache_hits RAG cache hit count",
247
+ "# TYPE tebyan_rag_cache_hits counter",
248
+ f"tebyan_rag_cache_hits {cache_stats.get('hits', 0)}",
249
+ "",
250
+ "# HELP tebyan_rag_cache_misses RAG cache miss count",
251
+ "# TYPE tebyan_rag_cache_misses counter",
252
+ f"tebyan_rag_cache_misses {cache_stats.get('misses', 0)}",
253
+ "",
254
+ "# HELP tebyan_rag_cache_hit_rate RAG cache hit rate (0-1)",
255
+ "# TYPE tebyan_rag_cache_hit_rate gauge",
256
+ f"tebyan_rag_cache_hit_rate {cache_stats.get('hit_rate', 0)}",
257
+ "",
258
+ "# HELP tebyan_rag_cache_evictions RAG cache eviction count",
259
+ "# TYPE tebyan_rag_cache_evictions counter",
260
+ f"tebyan_rag_cache_evictions {cache_stats.get('evictions', 0)}",
261
+ ]
262
+ return HttpResponse(content="\n".join(lines), media_type="text/plain; version=0.0.4")
263
 
264
 
265
  @app.on_event("startup")
266
  async def startup():
267
+ ensure_analyses_table()
268
+ ensure_chat_table()
269
+ load_router()
270
+ load_tools() # pre-load e5-large + EasyOCR to avoid cold-start on first request
271
+ _load_lora_adapter()
272
+ log.info("startup complete — all services warm")
273
 
274
 
275
  @lru_cache(maxsize=1)
276
+ def load_router() -> LLMRouter:
277
+ """Returns the LLM router singleton (Groq primary, HF fallback if configured)."""
278
+ router = get_router()
279
+ log.info("LLM router ready | provider=%s", router.provider_name)
280
+ return router
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
 
283
  @lru_cache(maxsize=1)
284
  def load_tools():
285
  reader = easyocr.Reader(['ar', 'en'], gpu=False)
286
+ get_embeddings() # warm up the singleton (also used by SemanticSearchService)
287
+ groq_client = Groq(api_key=GROQ_API_KEY) # raw client for agents
288
  cohere_client = cohere.ClientV2(api_key=COHERE_API_KEY) if COHERE_API_KEY else None
289
+ search_svc = SemanticSearchService(SUPABASE_URL, SUPABASE_KEY)
290
+ retriever = Retriever(
291
+ search_svc,
292
+ co_client=cohere_client,
293
+ query_expander=lambda q: generate_search_queries(groq_client, q),
294
+ )
295
+ log.info("tools loaded | db=%d chunks", search_svc.count())
296
+ return reader, groq_client, cohere_client, search_svc, retriever
297
 
298
 
299
+ @lru_cache(maxsize=1)
300
+ def load_coordinator() -> AgentCoordinator:
301
+ reader, groq_client, _, _, retriever = load_tools()
302
+ return AgentCoordinator(
303
+ reader=reader,
304
+ groq_client=groq_client,
305
+ retriever=retriever,
306
+ kb=medical_kb,
307
+ render_prompt_fn=render_prompt,
308
+ retrieval_config_cls=RetrievalConfig,
309
+ vision_key=GOOGLE_VISION_KEY,
310
+ )
311
 
312
+
313
+ # ── PEFT/LoRA adapter auto-loader ──────────────────────────────────────────
314
+ # Place a trained adapter in backend/models/lora/ to activate local inference.
315
+ # When absent, the system uses Groq API transparently.
316
+
317
+ _LORA_PATH = os.path.join(os.path.dirname(__file__), "models", "lora")
318
+ _lora_model_info: dict = {"loaded": False, "path": None, "model_name": None}
319
+
320
+
321
+ def _load_lora_adapter() -> None:
322
+ if not os.path.isdir(_LORA_PATH):
323
+ log.info("No LoRA adapter directory found using Groq API")
324
+ return
325
+ adapter_files = [f for f in os.listdir(_LORA_PATH) if f.endswith((".bin", ".safetensors"))]
326
+ if not adapter_files:
327
+ log.info("LoRA directory exists but is empty — using Groq API")
328
+ return
329
+ try:
330
+ from peft import PeftModel # noqa: F401
331
+ _lora_model_info.update({"loaded": True, "path": _LORA_PATH,
332
+ "model_name": adapter_files[0]})
333
+ log.info("LoRA adapter detected at %s — local inference active", _LORA_PATH)
334
+ except ImportError:
335
+ log.warning("LoRA adapter found but peft not installed — using Groq API")
 
 
 
 
 
 
 
 
 
 
336
 
337
 
338
  # ══════════════════════════════════════════════
 
349
  return r.choices[0].message.content
350
 
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
  def generate_search_queries(client, query: str) -> list:
354
  prompt = f"""أنت مساعد بحث طبي. حوّل السؤال إلى 3 استعلامات بحث مختلفة.
 
365
  return [query]
366
 
367
 
 
 
 
 
 
 
 
 
 
 
 
368
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
+ def get_rag_context(
371
+ _db_unused,
372
+ query: str,
373
+ co_client=None,
374
+ multi_query_client=None,
375
+ k: int = 10,
376
+ topic_type: str = None,
377
+ ) -> tuple[str, str]:
378
+ """Thin wrapper over Retriever — kept for backward compatibility."""
379
  try:
380
+ _, _, _, _, retriever = load_tools()
381
+ use_mq = multi_query_client is not None
382
+ results, conf = retriever.retrieve(
383
+ query,
384
+ RetrievalConfig(k=k, use_multi_query=use_mq, topic_type=topic_type),
385
+ )
386
+ return build_context(results), conf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  except Exception as e:
388
+ log.error("RAG retrieval failed: %s", e)
389
  return "", "لا يوجد"
390
 
391
 
 
394
  # ══════════════════════════════════════════════
395
 
396
  def _sb_headers():
397
+ # Use service_role key when available — bypasses RLS on analyses/chat_messages.
398
+ # Falls back to anon key (works if RLS is not yet enabled).
399
+ key = SUPABASE_SERVICE_KEY or SUPABASE_KEY
400
  return {
401
+ "apikey": key,
402
+ "Authorization": f"Bearer {key}",
403
  "Content-Type": "application/json",
404
  "Prefer": "return=representation",
405
  }
406
 
407
 
408
+ def _assert_session_owner(session_id: str, user: dict | None) -> None:
409
+ """When an authenticated user is present, ensure they own the session_id."""
410
+ if user and user.get("sub") and session_id != user["sub"]:
411
+ raise HTTPException(
412
+ status_code=403,
413
+ detail={"error": "forbidden", "message": "لا يمكنك الوصول إلى بيانات جلسة أخرى"},
414
+ )
415
+
416
+
417
  class SaveAnalysisRequest(BaseModel):
418
  session_id: str = "anonymous"
419
  findings: list
 
422
 
423
 
424
  @app.post("/api/analyses/save")
425
+ async def save_analysis(req: SaveAnalysisRequest, user: dict | None = Depends(optional_user)):
426
+ _assert_session_owner(req.session_id, user)
427
  if not SUPABASE_URL or not SUPABASE_KEY:
428
  raise HTTPException(503, "Supabase keys not configured")
429
  try:
 
445
 
446
 
447
  @app.get("/api/analyses/list")
448
+ async def list_analyses(session_id: str = "anonymous", profile_name: str = "", limit: int = 20,
449
+ user: dict | None = Depends(optional_user)):
450
+ _assert_session_owner(session_id, user)
451
  if not SUPABASE_URL or not SUPABASE_KEY:
452
  raise HTTPException(503, "Supabase keys not configured")
453
  try:
 
466
  raise HTTPException(500, str(e))
467
 
468
 
469
+ @app.delete("/api/analyses/clear")
470
+ async def clear_analyses(session_id: str = "anonymous", user: dict | None = Depends(optional_user)):
471
+ _assert_session_owner(session_id, user)
472
+ if not SUPABASE_URL or not SUPABASE_KEY:
473
+ raise HTTPException(503, "Supabase keys not configured")
474
+ try:
475
+ r = http_requests.delete(
476
+ f"{SUPABASE_URL}/rest/v1/analyses",
477
+ headers=_sb_headers(),
478
+ params={"session_id": f"eq.{session_id}"},
479
+ timeout=10,
480
+ )
481
+ r.raise_for_status()
482
+ return {"deleted": True, "session_id": session_id}
483
+ except Exception as e:
484
+ raise HTTPException(500, str(e))
485
+
486
+
487
  @app.get("/api/analyses/profiles")
488
+ async def list_profiles(session_id: str = "anonymous", user: dict | None = Depends(optional_user)):
489
  """جلب قائمة أفراد العائلة المسجلين لهذا المستخدم"""
490
+ _assert_session_owner(session_id, user)
491
  if not SUPABASE_URL or not SUPABASE_KEY:
492
  raise HTTPException(503, "Supabase keys not configured")
493
  try:
 
519
 
520
 
521
  @app.post("/api/analyze")
522
+ async def analyze(request: Request, file: UploadFile = File(...), analysis_type: str = Form("شامل"),
523
+ _rl: None = Depends(limit_analyze)):
524
  content = await file.read()
525
+ validate_upload(file, content)
526
+ analysis_type = sanitize_text(analysis_type, max_len=100)
527
+ log.info("analyze: type=%s file=%s size=%d", analysis_type, file.filename, len(content))
528
+
529
+ coordinator = load_coordinator()
530
+ file_type = "pdf" if file.content_type == "application/pdf" else "image"
531
+ result = coordinator.run(content, file_type, analysis_type)
532
+
533
+ if not result.ok:
534
+ raise HTTPException(status_code=422, detail=result.error)
535
+
536
+ response: dict = {
537
+ "findings": result.findings,
538
+ "summary": result.summary,
539
+ "report": result.report,
540
+ "panel_code": result.panel_code,
541
+ }
542
+ if os.getenv("ENVIRONMENT") == "development":
543
+ response["_agents"] = result.logs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
 
545
+ return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
 
547
 
548
  # ══════════════════════════════════════════════
549
  # M3 — Chat: ذاكرة + ربط التحليل + فلتر ذكي + مصادر
550
  # ══════════════════════════════════════════════
551
 
552
+ CHAT_SYSTEM = load_prompt("system_chat") or load_prompt("templates/system_prompt") or \
553
+ """أنت مساعد طبي ذكي اسمك "تبيان". أجب بشكل واضح ومختصر باللغة العربية.
554
+ قواعد: لا تخترع معلومات. اذكر المصادر. لا تشخّص بشكل قاطع. انصح بمراجعة الطبيب دائماً."""
 
 
 
 
555
 
556
  _FALLBACK_WORDS = ["ألم","صداع","تعب","دوخة","حمى","سعال","أعراض","ضغط","سكر","قلب",
557
  "معدة","تنفس","التهاب","دواء","علاج","تحليل","دم","فيتامين","نتائج",
 
560
 
561
 
562
  def is_medical_query(client, query: str) -> bool:
563
+ """فلتر سريع بالكلمات المفتاحية — بدون Groq call"""
564
+ return any(w in query for w in _FALLBACK_WORDS) or len(query.split()) >= 3
 
 
 
 
 
 
 
 
 
565
 
566
 
567
  class ChatRequest(BaseModel):
 
571
 
572
 
573
  @app.post("/api/chat")
574
+ async def chat_stream(request: Request, req: ChatRequest, _rl: None = Depends(limit_chat)):
575
+ req.query = sanitize_query(sanitize_text(req.query))
576
+ router = load_router()
577
 
578
+ if not is_medical_query(None, req.query):
579
  def nm():
580
  yield "أنا مساعد طبي متخصص. يسعدني الإجابة على أسئلتك الصحية وتحاليلك الطبية."
581
  return StreamingResponse(nm(), media_type="text/plain; charset=utf-8")
582
 
583
+ # ── RAG context (lightweight — cached to avoid repeated embedding calls) ──
584
+ rag_ctx = ""
585
+ _rag_key = rag_cache_key(req.query)
586
+ rag_ctx = rag_cache.get(_rag_key) or ""
587
+ if not rag_ctx:
588
+ try:
589
+ _, _, _, _, retriever = load_tools()
590
+ fast_results = retriever.retrieve_fast(req.query, k=5, top_n=3)
591
+ rag_ctx = build_context(fast_results, max_tokens=500)
592
+ if rag_ctx:
593
+ rag_cache.set(_rag_key, rag_ctx, ttl=300)
594
+ except Exception:
595
+ pass
596
 
597
+ # ── بناء analysis context ──
598
+ analysis_ctx = ""
599
  if req.analysis_context:
600
  try:
601
  ctx = json.loads(req.analysis_context)
 
603
  summary = ctx.get("summary", "")
604
  abnormal = [f for f in findings if f.get("status") != "normal"]
605
  normal = [f for f in findings if f.get("status") == "normal"]
606
+ lines = [f"ملخص التحليل: {summary}"]
 
607
  if abnormal:
608
+ lines.append("النتائج غير الطبيعية:")
609
  for f in abnormal:
610
  direction = "مرتفع" if f.get("status") == "high" else "منخفض"
611
+ lines.append(
612
  f" • {f['name']}: {f['value']} {f.get('unit','')} "
613
  f"(المعدل: {f.get('range','')}) — {direction}"
614
  )
615
  if normal:
616
+ lines.append(f"النتائج الطبيعية: {', '.join(f['name'] for f in normal)}")
617
+ analysis_ctx = "\n".join(lines)
 
618
  except Exception:
619
+ analysis_ctx = req.analysis_context
620
+
621
+ system = (
622
+ CHAT_SYSTEM
623
+ .replace("{{RAG_CONTEXT}}", rag_ctx or "لا توجد معلومات إضافية.")
624
+ .replace("{{ANALYSIS_CONTEXT}}", analysis_ctx or "لم يُرفع تحليل بعد.")
625
+ )
626
 
627
  messages = [{"role": "system", "content": system}]
628
  for msg in req.history[-10:]: # آخر 10 رسائل فقط
 
630
  messages.append({"role": msg["role"], "content": msg["content"]})
631
  messages.append({"role": "user", "content": req.query})
632
 
633
+ # Emergency check — يُرد فوراً بدون LLM
634
+ emergency_resp = check_emergency(req.query)
635
+ if emergency_resp:
636
+ def em():
637
+ yield emergency_resp
638
+ return StreamingResponse(em(), media_type="text/plain; charset=utf-8")
639
+
640
  def generate():
641
+ tokens: list[str] = []
642
  try:
643
+ for token in router.stream(messages, max_tokens=600, temperature=0.1,
644
+ model_hint="chat"):
645
+ tokens.append(token)
646
+ yield token
 
 
 
 
 
 
 
647
  except Exception as e:
648
  err = str(e)
649
+ log.error("chat stream error: %s", err[:200])
650
  yield "عذراً، الخدمة مشغولة. حاول بعد لحظة." if "429" in err else "حدث خطأ، يرجى المحاولة مرة أخرى."
651
+ return
652
+ full = "".join(tokens)
653
+ suffix = filter_chat_response(full, req.query)
654
+ if suffix != full:
655
+ delta = suffix[len(full):]
656
+ if delta:
657
+ yield delta
658
 
659
  return StreamingResponse(generate(), media_type="text/plain; charset=utf-8")
660
 
661
 
662
+ class SaveChatRequest(BaseModel):
663
+ session_id: str
664
+ messages: list[dict] # [{"role": "user"|"assistant", "content": str}]
665
+
666
+
667
+ @app.get("/api/chat/history/{session_id}")
668
+ async def get_chat_history(session_id: str, limit: int = 30,
669
+ user: dict | None = Depends(optional_user)):
670
+ """تحميل آخر N رسالة لجلسة معينة من Supabase."""
671
+ _assert_session_owner(session_id, user)
672
+ if not SUPABASE_URL or not SUPABASE_KEY:
673
+ return {"messages": []}
674
+ headers = {
675
+ "apikey": SUPABASE_KEY,
676
+ "Authorization": f"Bearer {SUPABASE_KEY}",
677
+ }
678
+ try:
679
+ r = http_requests.get(
680
+ f"{SUPABASE_URL}/rest/v1/chat_messages",
681
+ headers=headers,
682
+ params={
683
+ "session_id": f"eq.{session_id}",
684
+ "order": "created_at.asc",
685
+ "limit": limit,
686
+ "select": "role,content",
687
+ },
688
+ timeout=10,
689
+ )
690
+ r.raise_for_status()
691
+ return {"messages": r.json()}
692
+ except Exception as e:
693
+ log.warning("chat/history fetch failed: %s", e)
694
+ return {"messages": []}
695
+
696
+
697
+ @app.post("/api/chat/save")
698
+ async def save_chat_messages(req: SaveChatRequest, user: dict | None = Depends(optional_user)):
699
+ """حفظ رسائل تبادل واحد (مستخدم + مساعد) في Supabase."""
700
+ _assert_session_owner(req.session_id, user)
701
+ if not SUPABASE_URL or not SUPABASE_KEY:
702
+ return {"ok": False}
703
+ headers = {
704
+ "apikey": SUPABASE_KEY,
705
+ "Authorization": f"Bearer {SUPABASE_KEY}",
706
+ "Content-Type": "application/json",
707
+ "Prefer": "return=minimal",
708
+ }
709
+ rows = [
710
+ {"session_id": req.session_id, "role": m["role"], "content": m["content"]}
711
+ for m in req.messages
712
+ if m.get("content", "").strip()
713
+ ]
714
+ if not rows:
715
+ return {"ok": True}
716
+ try:
717
+ r = http_requests.post(
718
+ f"{SUPABASE_URL}/rest/v1/chat_messages",
719
+ headers=headers,
720
+ json=rows,
721
+ timeout=10,
722
+ )
723
+ return {"ok": r.status_code in (200, 201)}
724
+ except Exception as e:
725
+ log.warning("chat/save failed: %s", e)
726
+ return {"ok": False}
727
+
728
+
729
  class EvalRequest(BaseModel):
730
  question: str
731
  reference_answer: str = ""
 
782
  }
783
 
784
 
785
+ class SearchAnalysisItem(BaseModel):
786
+ id: str
787
+ summary: str
788
+ panel: str = ""
789
+ findings_text: str = ""
790
+
791
+
792
+ class SemanticSearchRequest(BaseModel):
793
+ query: str
794
+ analyses: list[SearchAnalysisItem] = []
795
+ search_scope: str = "local" # "local" = user history | "global" = medical KB (pgvector)
796
+ top_k: int = 5
797
+
798
+
799
+ @app.post("/api/search")
800
+ async def semantic_search(request: Request, req: SemanticSearchRequest, _rl: None = Depends(limit_search)):
801
+ """
802
+ Semantic search with two modes:
803
+ local — re-rank the user's own saved analyses (default, original behaviour)
804
+ global — query the medical knowledge base in pgvector directly
805
+ """
806
+ from services.search.query_parser import parse_query, normalize_arabic
807
+
808
+ if not req.query.strip():
809
+ if req.search_scope == "local":
810
+ return {"results": [{"id": a.id, "score": 0.0} for a in req.analyses], "scope": "local"}
811
+ return {"results": [], "scope": "global"}
812
+
813
+ # ── Global KB search ───────────────────────────────────────────────────
814
+ if req.search_scope == "global":
815
+ clean_query = sanitize_query(req.query)
816
+ _s_key = rag_cache_key(clean_query, topic_type="search_global")
817
+ cached = search_cache.get(_s_key)
818
+ if cached:
819
+ return cached
820
+ try:
821
+ _, _, _, _, retriever = load_tools()
822
+ kb_results, confidence = retriever.retrieve(
823
+ clean_query,
824
+ RetrievalConfig(k=req.top_k * 2, top_n=req.top_k, use_multi_query=False),
825
+ )
826
+ payload = {
827
+ "results": [
828
+ {
829
+ "id": r.source,
830
+ "score": round(r.score, 3),
831
+ "content": r.content[:300],
832
+ "source": r.source,
833
+ "topic_type": r.metadata.get("topic_type", ""),
834
+ }
835
+ for r in kb_results
836
+ ],
837
+ "scope": "global",
838
+ "confidence": confidence,
839
+ }
840
+ search_cache.set(_s_key, payload, ttl=120)
841
+ return payload
842
+ except Exception as e:
843
+ log.error("global search failed: %s", e)
844
+ raise HTTPException(500, detail=f"Global search failed: {e}")
845
+
846
+ # ── Local history search (original behaviour) ──────────────────────────
847
+ pq = parse_query(req.query)
848
+ norm_q = pq.normalized.lower()
849
+
850
+ all_terms: list[str] = [norm_q]
851
+ for exp in pq.search_expansions[1:]:
852
+ for t in exp.split():
853
+ n = normalize_arabic(t.lower())
854
+ if len(n) > 2:
855
+ all_terms.append(n)
856
+ for t in pq.detected_tests:
857
+ all_terms.append(normalize_arabic(t.lower()))
858
+ all_terms = list(dict.fromkeys(all_terms))
859
+
860
+ results = []
861
+ for a in req.analyses:
862
+ text = normalize_arabic((a.summary + " " + a.panel + " " + a.findings_text).lower())
863
+ score = 0.0
864
+ for term in all_terms:
865
+ if len(term) > 1 and term in text:
866
+ score += 2.0 if term in norm_q else 1.0
867
+ if pq.panel_type and a.panel and pq.panel_type == a.panel:
868
+ score += 3.0
869
+ if norm_q in text:
870
+ score += 2.0
871
+ results.append({"id": a.id, "score": round(score, 1)})
872
+
873
+ results.sort(key=lambda x: x["score"], reverse=True)
874
+ return {"results": results, "scope": "local"}
875
+
876
+
877
  @app.get("/api/health")
878
+ def api_health():
879
+ return {
880
+ "status": "ok",
881
+ "embed_model": EMBED_MODEL,
882
+ "voice": {
883
+ "stt": bool(GROQ_API_KEY),
884
+ "tts": bool(GOOGLE_TTS_KEY or ELEVENLABS_KEY or True),
885
+ },
886
+ }
887
+
888
+
889
+ @app.get("/api/models/status")
890
+ def models_status():
891
+ """Reports the active LLM provider, loaded adapters, and fallback state."""
892
+ router = load_router()
893
+ tts_provider = "none"
894
+ if GOOGLE_TTS_KEY:
895
+ tts_provider = "google_cloud"
896
+ elif ELEVENLABS_KEY:
897
+ tts_provider = "elevenlabs"
898
+ else:
899
+ try:
900
+ import gtts # noqa: F401
901
+ tts_provider = "gtts_free"
902
+ except ImportError:
903
+ pass
904
+
905
+ return {
906
+ "llm": {
907
+ "provider": router.provider_name,
908
+ "lora_loaded": _lora_model_info["loaded"],
909
+ "lora_path": _lora_model_info.get("path"),
910
+ "lora_model": _lora_model_info.get("model_name"),
911
+ "env_LLM_PROVIDER": os.getenv("LLM_PROVIDER", "groq"),
912
+ },
913
+ "embeddings": {
914
+ "model": EMBED_MODEL,
915
+ "loaded": True,
916
+ },
917
+ "tts_provider": tts_provider,
918
+ "stt_provider": "groq_whisper" if GROQ_API_KEY else "none",
919
+ "cache": {
920
+ "rag": rag_cache.stats(),
921
+ },
922
+ }
923
+
924
+
925
+ # ══════════════════════════════════════════════
926
+ # Voice AI endpoints
927
+ # ══════════════════════════════════════════════
928
+
929
+ @lru_cache(maxsize=1)
930
+ def _get_stt():
931
+ from services.voice import WhisperSTT
932
+ return WhisperSTT(GROQ_API_KEY)
933
+
934
+ @lru_cache(maxsize=1)
935
+ def _get_tts():
936
+ from services.voice import get_tts_provider
937
+ return get_tts_provider(google_tts_key=GOOGLE_TTS_KEY, elevenlabs_key=ELEVENLABS_KEY)
938
+
939
+
940
+ @app.post("/api/voice/transcribe")
941
+ async def voice_transcribe(
942
+ audio: UploadFile = File(...),
943
+ language: str = Form("ar"),
944
+ ):
945
+ """
946
+ Transcribe Arabic audio (webm/mp4/wav/ogg) → text.
947
+ Uses Groq Whisper large-v3.
948
+ """
949
+ data = await audio.read()
950
+ mime_type = audio.content_type or "audio/webm"
951
+ try:
952
+ stt = _get_stt()
953
+ text = stt.transcribe(data, mime_type=mime_type, language=language)
954
+ return {"text": text, "language": language}
955
+ except Exception as e:
956
+ raise HTTPException(status_code=500, detail=f"Transcription failed: {e}")
957
+
958
+
959
+ @app.post("/api/voice/synthesize")
960
+ async def voice_synthesize(req: dict):
961
+ """
962
+ Convert Arabic text → MP3 audio bytes.
963
+ Body: {"text": "..."}
964
+ """
965
+ text = (req.get("text") or "").strip()
966
+ if not text:
967
+ raise HTTPException(status_code=422, detail="text field is required")
968
+ try:
969
+ tts = _get_tts()
970
+ mp3_data = tts.synthesize(text)
971
+ return HttpResponse(content=mp3_data, media_type="audio/mpeg")
972
+ except Exception as e:
973
+ raise HTTPException(status_code=500, detail=f"TTS failed: {e}")
974
+
975
+
976
+ class VoiceChatRequest(BaseModel):
977
+ audio_base64: str = "" # base64-encoded audio (alternative to file upload)
978
+ query: str = "" # text query (if already transcribed client-side)
979
+ history: list[dict] = []
980
+ analysis_context: str = ""
981
+ language: str = "ar"
982
+ include_audio: bool = True # return TTS audio in response
983
+
984
+
985
+ @app.post("/api/voice/chat")
986
+ async def voice_chat(req: VoiceChatRequest):
987
+ """
988
+ Full voice chat loop: audio → STT → LLM → TTS.
989
+ Returns {text: str, audio_base64: str (MP3)}.
990
+ """
991
+ import base64
992
+
993
+ # 1. Resolve input text (from audio or direct text)
994
+ query = req.query.strip()
995
+ if not query and req.audio_base64:
996
+ try:
997
+ audio_bytes = base64.b64decode(req.audio_base64)
998
+ stt = _get_stt()
999
+ query = stt.transcribe(audio_bytes, language=req.language)
1000
+ except Exception as e:
1001
+ raise HTTPException(status_code=500, detail=f"STT failed: {e}")
1002
+
1003
+ if not query:
1004
+ raise HTTPException(status_code=422, detail="Provide audio_base64 or query")
1005
+
1006
+ query = sanitize_query(query)
1007
+
1008
+ # 2. LLM chat
1009
+ router = load_router()
1010
+ emergency_resp = check_emergency(query)
1011
+ if emergency_resp:
1012
+ text = emergency_resp
1013
+ else:
1014
+ rag_ctx = ""
1015
+ _rag_key = rag_cache_key(query)
1016
+ rag_ctx = rag_cache.get(_rag_key) or ""
1017
+ if not rag_ctx:
1018
+ try:
1019
+ _, _, _, _, retriever = load_tools()
1020
+ fast_results = retriever.retrieve_fast(query, k=5, top_n=3)
1021
+ rag_ctx = build_context(fast_results, max_tokens=400)
1022
+ if rag_ctx:
1023
+ rag_cache.set(_rag_key, rag_ctx, ttl=300)
1024
+ except Exception:
1025
+ pass
1026
+
1027
+ system = (
1028
+ CHAT_SYSTEM
1029
+ .replace("{{RAG_CONTEXT}}", rag_ctx or "لا توجد معلومات إضافية.")
1030
+ .replace("{{ANALYSIS_CONTEXT}}", req.analysis_context or "لم يُرفع تحليل بعد.")
1031
+ )
1032
+ messages = [{"role": "system", "content": system}]
1033
+ for msg in req.history[-6:]:
1034
+ if msg.get("role") in ("user", "assistant") and msg.get("content"):
1035
+ messages.append({"role": msg["role"], "content": msg["content"]})
1036
+ messages.append({"role": "user", "content": query})
1037
+
1038
+ raw = router.generate(messages, max_tokens=600, temperature=0.5, model_hint="chat")
1039
+ text = filter_chat_response(raw, query)
1040
+
1041
+ # 3. TTS (optional)
1042
+ audio_b64 = ""
1043
+ if req.include_audio:
1044
+ try:
1045
+ tts = _get_tts()
1046
+ mp3 = tts.synthesize(text)
1047
+ audio_b64 = base64.b64encode(mp3).decode()
1048
+ except Exception as e:
1049
+ log = logging.getLogger("tebyan")
1050
+ log.warning("[voice/chat] TTS failed: %s", e)
1051
+
1052
+ return {"query": query, "text": text, "audio_base64": audio_b64}
1053
+
1054
+
1055
+ # ══════════════════════════════════════════════
1056
+ # Risk Prediction API
1057
+ # ══════════════════════════════════════════════
1058
+
1059
+ class RiskRequest(BaseModel):
1060
+ findings: list[dict] # same format as analysis findings
1061
+
1062
+ @lru_cache(maxsize=1)
1063
+ def _get_risk_engine():
1064
+ from services.risk import RiskEngine
1065
+ return RiskEngine()
1066
+
1067
+ @app.post("/api/risk")
1068
+ async def predict_risk(req: RiskRequest):
1069
+ """
1070
+ Evidence-based multi-condition health risk scoring.
1071
+ Returns scored risks for diabetes, cardiovascular, anemia, kidney, liver, thyroid.
1072
+ ML model override when models/ directory contains trained .pkl files.
1073
+ """
1074
+ if not req.findings:
1075
+ raise HTTPException(status_code=422, detail="findings list is required")
1076
+ engine = _get_risk_engine()
1077
+ report = engine.assess(req.findings)
1078
+ return {
1079
+ "risks": [
1080
+ {
1081
+ "condition": r.condition,
1082
+ "score": r.score,
1083
+ "level": r.level,
1084
+ "confidence": r.confidence,
1085
+ "label_ar": r.label_ar,
1086
+ "factors": r.factors,
1087
+ "recommendation": r.recommendation,
1088
+ "source": r.source,
1089
+ }
1090
+ for r in report.risks
1091
+ ],
1092
+ "top_risk": {
1093
+ "condition": report.top_risk.condition,
1094
+ "score": report.top_risk.score,
1095
+ "level": report.top_risk.level,
1096
+ "label_ar": report.top_risk.label_ar,
1097
+ } if report.top_risk else None,
1098
+ "overall_ar": report.overall_ar,
1099
+ "features_used": report.features_used,
1100
+ }
1101
+
1102
+
1103
+ # ══════════════════════════════════════════════
1104
+ # Compare Summary — Groq-generated analysis of two lab results
1105
+ # ══════════════════════════════════════════════
1106
+
1107
+ class CompareSummaryRequest(BaseModel):
1108
+ findings_a: list[dict]
1109
+ findings_b: list[dict]
1110
+ summary_a: str = ""
1111
+ summary_b: str = ""
1112
+ date_a: str = ""
1113
+ date_b: str = ""
1114
+
1115
+ @app.post("/api/compare/summary")
1116
+ async def compare_summary(req: CompareSummaryRequest):
1117
+ router = load_router()
1118
+
1119
+ def fmt_findings(findings: list[dict]) -> str:
1120
+ lines = []
1121
+ for f in findings:
1122
+ status_ar = {"high": "مرتفع", "low": "منخفض", "normal": "طبيعي"}.get(f.get("status", ""), "")
1123
+ lines.append(f" {f.get('name','')}: {f.get('value','')} {f.get('unit','')} [{status_ar}]")
1124
+ return "\n".join(lines) if lines else "لا توجد بيانات"
1125
+
1126
+ prompt = f"""أنت طبيب متخصص. قارن بين نتيجتين لتحاليل مخبرية لنفس المريض.
1127
+
1128
+ التحليل الأول ({req.date_a or 'السابق'}):
1129
+ {fmt_findings(req.findings_a)}
1130
+
1131
+ التحليل الثاني ({req.date_b or 'الحالي'}):
1132
+ {fmt_findings(req.findings_b)}
1133
+
1134
+ اكتب ملخصاً طبياً دقيقاً بالعربية في 3-4 جمل يشمل:
1135
+ 1. أبرز التغيرات (تحسّن / تراجع / ثبات)
1136
+ 2. هل الاتجاه العام إيجابي أم يستدعي قلقاً
1137
+ 3. توصية واحدة محددة
1138
+
1139
+ أجب مباشرة بالنص فقط، بدون عناوين أو نقاط."""
1140
+
1141
+ try:
1142
+ summary = router.generate(
1143
+ [{"role": "user", "content": prompt}],
1144
+ max_tokens=300, temperature=0.2, model_hint="analysis",
1145
+ ).strip()
1146
+ except Exception as e:
1147
+ summary = "تعذّر توليد الملخص المقارن."
1148
+ log.error("compare: Groq error: %s", e)
1149
+
1150
+ return {"summary": summary}
1151
+
1152
+ # build:1779660596
backend/{ingest_full.py → medical_data.py} RENAMED
@@ -1,64 +1,41 @@
1
  """
2
- بناء الموسوعة الطبية الكاملة:
3
- 1. تنظيف التكرار من ChromaDB
4
- 2. تعاريف التحاليل (عربي + إنجليزي)
5
- 3. معلومات شاملة عن التحاليل
6
- 4. توصيات صحية
7
- 5. معلومات طبية عامة من MedlinePlus
8
  """
9
- import os
10
- import re
11
- import time
12
- import requests
13
- import xml.etree.ElementTree as ET
14
 
15
- os.environ['HF_HOME'] = r'D:\Project\model_cache'
16
- os.environ['TRANSFORMERS_VERBOSITY'] = 'error'
17
-
18
- from langchain_huggingface import HuggingFaceEmbeddings
19
- from langchain_chroma import Chroma
20
- from langchain_core.documents import Document
21
-
22
- DB_PATH = r'D:\Project\chroma_db'
23
- EMBEDDINGS_MODEL = "intfloat/multilingual-e5-large"
24
-
25
- # ============================================================
26
- # القسم 1: تعاريف التحاليل (عربي + إنجليزي)
27
- # ============================================================
28
  LAB_DEFINITIONS = [
29
  {
30
  "name_ar": "هيموجلوبين", "name_en": "Hemoglobin (HGB)",
31
- "definition": "بروتين في خلايا الدم الحمراء يحمل الأكسجين من الرئتين لأنسجة الجسم. القيم الطبيعية: رجال 13.5-17.5 g/dL، نساء 12-15.5 g/dL.",
32
- "high": "كثرة الحمر، الجفاف، أمراض الرئة المزمنة، ارتفاع الألتيتيود.",
33
- "low": "فقر الدم (الأنيميا)، نزيف داخلي، نقص الحديد أو فيتامين B12 أو حمض الفوليك.",
34
  "symptoms_low": "تعب، شحوب، ضيق تنفس، دوخة، خفقان قلب.",
35
  },
36
  {
37
  "name_ar": "خلايا الدم الحمراء", "name_en": "Red Blood Cells (RBC)",
38
- "definition": "عدد خلايا الدم الحمراء في الدم. القيم الطبيعية: رجال 4.5-5.9 مليون/µL، نساء 4.1-5.1 مليون/µL.",
39
  "high": "كثرة الحمر، الجفاف، أمراض القلب الخلقية.",
40
  "low": "فقر الدم، نزيف، فشل كلوي، نقص غذائي.",
41
  "symptoms_low": "إرهاق، شحوب، ضعف عام.",
42
  },
43
  {
44
  "name_ar": "خلايا الدم البيضاء", "name_en": "White Blood Cells (WBC)",
45
- "definition": "خلايا الجهاز المناعي التي تحارب العدوى. القيم الطبيعية: 4,000-11,000 خلية/µL.",
46
  "high": "عدوى بكتيرية، التهاب، أمراض الدم كاللوكيميا.",
47
  "low": "أمراض المناعة، العلاج الكيميائي، أمراض النخاع العظمي.",
48
  "symptoms_low": "تكرار الإصابة بالعدوى، حمى متكررة.",
49
  },
50
  {
51
  "name_ar": "الصفائح الدموية", "name_en": "Platelets (PLT)",
52
- "definition": "خلايا مسؤولة عن تخثر الدم وإيقاف النزيف. القيم الطبيعية: 150,000-400,000/µL.",
53
  "high": "التهاب، نزيف، نقص الحديد، خطر تجلط الدم.",
54
  "low": "نزيف تلقائي، أمراض الكبد، الذئبة، العلاج الكيميائي.",
55
  "symptoms_low": "كدمات سهلة، نزيف اللثة، نزيف طويل عند الجرح.",
56
  },
57
  {
58
  "name_ar": "سكر الدم الصائم", "name_en": "Fasting Blood Glucose",
59
- "definition": "مستوى السكر في الدم بعد 8 ساعات صيام. القيم الطبيعية: 70-100 mg/dL. مرحلة ما قبل السكري: 100-125. سكري: 126 فما فوق.",
60
  "high": "السكري، مقاومة الأنسولين، الإجهاد، بعض الأدوية.",
61
- "low": "انخفاض السكر (نقص سكر الدم)، الصيام الطويل، جرعة زائدة من الأنسولين.",
62
  "symptoms_low": "رعشة، تعرق، دوخة، فقدان وعي في الحالات الشديدة.",
63
  },
64
  {
@@ -73,18 +50,18 @@ LAB_DEFINITIONS = [
73
  "definition": "إجمالي الدهون في الدم. المثالي: أقل من 200 mg/dL. حدي: 200-239. مرتفع: 240 فأكثر.",
74
  "high": "خطر أمراض القلب والشرايين، السكتة الدماغية.",
75
  "low": "نادر، قد يرتبط بسوء التغذية أو مشاكل الكبد.",
76
- "symptoms_low": "الكوليسترول المرتفع لا يسبب أعراضاً واضحة.",
77
  },
78
  {
79
- "name_ar": "الكوليسترول الضار", "name_en": "LDL Cholesterol (Bad Cholesterol)",
80
- "definition": "الكوليسترول منخفض الكثافة - يترسب في الشرايين. المثالي: أقل من 100 mg/dL. مرتفع: 160 فأكثر.",
81
  "high": "تصلب الشرايين، أمراض القلب، جلطات.",
82
  "low": "مثالي، يقلل خطر أمراض القلب.",
83
- "symptoms_low": "لا أعراض.",
84
  },
85
  {
86
- "name_ar": "الكوليسترول النافع", "name_en": "HDL Cholesterol (Good Cholesterol)",
87
- "definition": "الكوليسترول عالي الكثافة - يزيل الكوليسترول من الشرايين. المثالي: فوق 60 mg/dL. منخفض (خطر): أقل من 40 للرجال، 50 للنساء.",
88
  "high": "حماية من أمراض القلب.",
89
  "low": "خطر أمراض القلب، قلة الرياضة، التدخين، السمنة.",
90
  "symptoms_low": "لا أعراض مباشرة لكن خطر قلبي مرتفع.",
@@ -97,7 +74,7 @@ LAB_DEFINITIONS = [
97
  "symptoms_low": "الارتفاع الشديد يسبب التهاب البنكرياس.",
98
  },
99
  {
100
- "name_ar": "هرمون الغدة الدرقية TSH", "name_en": "Thyroid Stimulating Hormone (TSH)",
101
  "definition": "يتحكم في نشاط الغدة الدرقية. القيم الطبيعية: 0.4-4.0 mIU/L.",
102
  "high": "قصور الغدة الدرقية (الغدة خاملة).",
103
  "low": "فرط نشاط الغدة الدرقية.",
@@ -136,7 +113,7 @@ LAB_DEFINITIONS = [
136
  "definition": "ناتج تكسير البيورينات. طبيعي: رجال 3.5-7.2 mg/dL، نساء 2.6-6.0 mg/dL.",
137
  "high": "النقرس، الفشل الكلوي، أكل كثير من اللحوم، السمنة.",
138
  "low": "نادر، قد يكون من بعض الأدوية.",
139
- "symptoms_low": "الارتفاع يسبب: ألم حاد في المفاصل (خاصة إصبع القدم الكبير)، حصوات كلى.",
140
  },
141
  {
142
  "name_ar": "الكرياتينين", "name_en": "Creatinine",
@@ -146,7 +123,7 @@ LAB_DEFINITIONS = [
146
  "symptoms_low": "ارتفاع الكرياتينين: تورم، تعب، غثيان، قلة التبول.",
147
  },
148
  {
149
- "name_ar": "إنزيمات الكبد ALT AST", "name_en": "Liver Enzymes (ALT/AST)",
150
  "definition": "مؤشرات لصحة الكبد. ALT طبيعي: 7-56 U/L. AST طبيعي: 10-40 U/L.",
151
  "high": "التهاب الكبد، الكبد الدهني، الكحول، بعض الأدوية.",
152
  "low": "لا أهمية سريرية.",
@@ -160,7 +137,7 @@ LAB_DEFINITIONS = [
160
  "symptoms_low": "الارتفاع يرافقه أعراض المرض الأصلي.",
161
  },
162
  {
163
- "name_ar": "الحمضات (الإيوزينوفيل)", "name_en": "Eosinophils",
164
  "definition": "نوع من خلايا الدم البيضاء. طبيعي: 1-4% من WBC أو 100-400 خلية/µL.",
165
  "high": "الحساسية، الربو، الطفيليات، بعض الأمراض المناعية.",
166
  "low": "ليس ذا أهمية سريرية.",
@@ -188,7 +165,7 @@ LAB_DEFINITIONS = [
188
  "symptoms_low": "الارتفاع: سمنة بطنية، ضغط مرتفع، سكر. الانخفاض: إجهاد، غثيان، انخفاض ضغط.",
189
  },
190
  {
191
- "name_ar": "هرمون الإنسولين", "name_en": "Insulin (Fasting)",
192
  "definition": "هرمون يتحكم في السكر. طبيعي صائم: 2-25 µIU/mL.",
193
  "high": "مقاومة الأنسولين، السمنة، ما قبل السكري، ورم الأنسولين.",
194
  "low": "السكري النوع الأول، البنكرياس الضعيف.",
@@ -204,13 +181,13 @@ LAB_DEFINITIONS = [
204
  {
205
  "name_ar": "هرمون البرولاكتين", "name_en": "Prolactin",
206
  "definition": "هرمون الحليب من الغدة النخامية. طبيعي: رجال 2-18 ng/mL، نساء غير حامل 2-29 ng/mL.",
207
- "high": "ورم النخامية (البرولاكتينوما)، بعض الأدوية، قصور الغدة الدرقية.",
208
  "low": "نادر، قد يكون من قصور النخامية.",
209
  "symptoms_low": "الارتفاع: إفراز حليب، اضطراب دورة، ضعف جنسي.",
210
  },
211
  {
212
  "name_ar": "هرمون الإستروجين", "name_en": "Estradiol (E2)",
213
- "definition": "الهرمون الأنثوي الرئيسي. يتغير حسب مرحلة الدورة.",
214
  "high": "أورام المبيض، السمنة، أمراض الكبد.",
215
  "low": "انقطاع الطمث، قصور المبيض، سوء التغذية.",
216
  "symptoms_low": "الانخفاض: جفاف مهبلي، هشاشة عظام، اضطراب مزاج، هبات حرارة.",
@@ -232,7 +209,7 @@ LAB_DEFINITIONS = [
232
  {
233
  "name_ar": "تحليل البول الكامل", "name_en": "Urinalysis (Complete)",
234
  "definition": "فحص البول للكشف عن أمراض الكلى والمسالك البولية والسكري.",
235
- "high": "بروتين في البول: مشكلة كلى. سكر في البول: سكري. دم في البول: التهاب أو حصوات.",
236
  "low": "بول طبيعي: شفاف، أصفر فاتح، بدون بروتين أو سكر أو دم.",
237
  "symptoms_low": "البول الغائم أو الداكن أو ذو رائحة شديدة يستوجب فحصاً.",
238
  },
@@ -241,7 +218,7 @@ LAB_DEFINITIONS = [
241
  "definition": "أفضل مقياس لوظيفة الكلى. طبيعي: فوق 90 mL/min. مرحلة الفشل: أقل من 15.",
242
  "high": "غير ذي أهمية عند الارتفاع.",
243
  "low": "مرض كلوي مزمن بدرجات متفاوتة.",
244
- "symptoms_low": "GFR 30-59: مرحلة متوسطة. GFR 15-29: مرحلة متقدمة. أقل من 15: فشل كلوي.",
245
  },
246
  {
247
  "name_ar": "حمض الفوليك", "name_en": "Folic Acid (Folate)",
@@ -292,7 +269,6 @@ LAB_DEFINITIONS = [
292
  "low": "سوء التغذية، أمراض الكبد والكلى.",
293
  "symptoms_low": "ضعف، تورم، ضعف مناعة.",
294
  },
295
- # CBC إضافي
296
  {
297
  "name_ar": "الهيماتوكريت", "name_en": "Hematocrit (HCT)",
298
  "definition": "نسبة حجم كريات الدم الحمراء من إجمالي الدم. طبيعي: رجال 38.3-48.6%، نساء 35.5-44.9%.",
@@ -301,7 +277,7 @@ LAB_DEFINITIONS = [
301
  "symptoms_low": "تعب، ضيق تنفس، شحوب.",
302
  },
303
  {
304
- "name_ar": "متوسط حجم الكرية الحمراء", "name_en": "Mean Corpuscular Volume (MCV)",
305
  "definition": "متوسط حجم كريات الدم الحمراء. طبيعي: 80-100 fL. يساعد في تصنيف نوع فقر الدم.",
306
  "high": "فقر الدم الضخم الكريات (نقص B12 أو فولات)، الكحول، قصور الدرقية.",
307
  "low": "فقر الدم الصغير الكريات (نقص حديد، ثلاسيميا).",
@@ -315,7 +291,7 @@ LAB_DEFINITIONS = [
315
  "symptoms_low": "يُفسر مع MCV وهيموجلوبين لتصنيف فقر الدم.",
316
  },
317
  {
318
- "name_ar": "تركيز الهيموجلوبين في الكرية", "name_en": "MCHC (Mean Corpuscular Hemoglobin Concentration)",
319
  "definition": "تركيز الهي��وجلوبين في كل كرية. طبيعي: 31.5-36 g/dL.",
320
  "high": "فقر الدم الانحلالي الوراثي، الجفاف.",
321
  "low": "نقص الحديد، الثلاسيميا.",
@@ -330,7 +306,7 @@ LAB_DEFINITIONS = [
330
  },
331
  {
332
  "name_ar": "العدلات", "name_en": "Neutrophils",
333
- "definition": "أكثر خلايا الدم البيضاء شيوعاً. الخط الأول ضد البكتيريا. طبيعي: 40-70% من WBC أو 1800-7800/µL.",
334
  "high": "عدوى بكتيرية، التهاب، توتر، كورتيكوستيرويدات.",
335
  "low": "عدوى فيروسية شديدة، أدوية، أمراض نخاع العظم.",
336
  "symptoms_low": "خطر عالٍ للعدوى البكتيرية عند الانخفاض الشديد.",
@@ -338,8 +314,8 @@ LAB_DEFINITIONS = [
338
  {
339
  "name_ar": "الخلايا اللمفاوية", "name_en": "Lymphocytes",
340
  "definition": "تنتج الأجسام المضادة وتحارب الفيروسات. طبيعي: 20-40% من WBC.",
341
- "high": "عدوى فيروسية (كورونا، مونو)، ابيضاض اللمفاوية.",
342
- "low": "HIV، الكورتيكوستيرويدات، العلاج الإشعاعي، التوتر الشديد.",
343
  "symptoms_low": "ضعف المناعة ضد الفيروسات.",
344
  },
345
  {
@@ -356,10 +332,9 @@ LAB_DEFINITIONS = [
356
  "low": "نادراً ما له أهمية سريرية.",
357
  "symptoms_low": "الارتفاع الشديد قد يكون علامة ابيضاض دموي.",
358
  },
359
- # سكر إضافي
360
  {
361
  "name_ar": "السكر العشوائي", "name_en": "Random Blood Sugar (RBS)",
362
- "definition": "قياس السكر في أي وقت بغض النظر عن الأكل. الطبيعي: أقل من 140 mg/dL. مقلق: 140-199. سكري: 200 فأكثر مع أعراض.",
363
  "high": "مرض السكري، الإجهاد، بعض الأدوية.",
364
  "low": "نقص سكر الدم، جرعة أنسولين زائدة.",
365
  "symptoms_low": "الارتفاع مع أعراض (عطش، كثرة تبول) يؤكد السكري.",
@@ -369,7 +344,7 @@ LAB_DEFINITIONS = [
369
  "definition": "يشرب المريض 75 غرام جلوكوز ويُقاس السكر بعد ساعتين. طبيعي: أقل من 140. ما قبل السكري: 140-199. سكري: 200+.",
370
  "high": "سكري، مقاومة أنسولين، سكري الحمل.",
371
  "low": "لا أهمية سريرية للانخفاض.",
372
- "symptoms_low": "أفضل اختبار للكشف المبكر عن السكري.",
373
  },
374
  {
375
  "name_ar": "سي ببتيد", "name_en": "C-Peptide",
@@ -378,7 +353,6 @@ LAB_DEFINITIONS = [
378
  "low": "السكري النوع الأول، البنكرياس الضعيف.",
379
  "symptoms_low": "C-Peptide منخفض = البنكرياس لا ينتج أنسولين = يحتاج أنسولين خارجي.",
380
  },
381
- # كوليسترول إضافي
382
  {
383
  "name_ar": "الكوليسترول منخفض الكثافة جداً", "name_en": "VLDL Cholesterol",
384
  "definition": "يحمل الدهون الثلاثية للأنسجة. يُحسب: VLDL = Triglycerides ÷ 5. طبيعي: 2-30 mg/dL.",
@@ -386,25 +360,23 @@ LAB_DEFINITIONS = [
386
  "low": "لا أهمية سريرية.",
387
  "symptoms_low": "ارتفاعه يزيد خطر أمراض القلب.",
388
  },
389
- # كبد إضافي
390
  {
391
  "name_ar": "جاما جلوتاميل ترانسفيريز", "name_en": "GGT (Gamma-Glutamyl Transferase)",
392
  "definition": "إنزيم كبدي حساس للكحول والأدوية. طبيعي: رجال 8-61 U/L، نساء 5-36 U/L.",
393
  "high": "الكحول، أمراض الكبد، بعض الأدوية، الكبد الدهني.",
394
  "low": "لا أهمية سريرية.",
395
- "symptoms_low": "أحساس مؤشر مبكر لتأثير الكحول على الكبد.",
396
  },
397
  {
398
  "name_ar": "البيليروبين المباشر", "name_en": "Direct (Conjugated) Bilirubin",
399
- "definition": "البيليروبين المرتبط المعالج بالكبد. طبيعي: 0-0.3 mg/dL.",
400
  "high": "انسداد القنوات الصفراوية، التهاب الكبد، حصوات المرارة.",
401
  "low": "لا أهمية سريرية.",
402
  "symptoms_low": "ارتفاعه مع اليرقان يشير لمشكلة في تصريف الصفراء.",
403
  },
404
- # كلى إضافي
405
  {
406
- "name_ar": "اليوريا في الدم", "name_en": "Blood Urea Nitrogen (BUN) / Urea",
407
- "definition": "ناتج تكسير البروتين، يُطرح بالكلى. BUN طبيعي: 7-20 mg/dL. يوريا: 2.5-7.1 mmol/L.",
408
  "high": "ضعف الكلى، الجفاف، نزيف الجهاز الهضمي، أكل بروتين زائد.",
409
  "low": "أمراض الكبد الشديدة، سوء التغذية.",
410
  "symptoms_low": "نسبة BUN/Creatinine تحدد سبب ارتفاع البول النيتروجيني.",
@@ -416,23 +388,22 @@ LAB_DEFINITIONS = [
416
  "low": "القيء المتكرر، القصور الكلوي، الأدوية.",
417
  "symptoms_low": "يُفسر دائماً مع الصوديوم والبوتاسيوم.",
418
  },
419
- # هرمونات إضافية
420
  {
421
- "name_ar": "هرمون T3 الكلي والحر", "name_en": "T3 Total & Free T3",
422
  "definition": "الهرمون الدرقي النشط. Free T3 طبيعي: 2.3-4.2 pg/mL.",
423
  "high": "فرط نشاط الدرقية، التهاب الدرقية.",
424
  "low": "قصور الدرقية، الأمراض الحادة.",
425
  "symptoms_low": "Free T3 أدق من T3 الكلي في تقييم وظيفة الدرقية.",
426
  },
427
  {
428
- "name_ar": "هرمون T4 الكلي والحر", "name_en": "T4 Total & Free T4",
429
  "definition": "الهرمون الدرقي الرئيسي المخزون. Free T4 طبيعي: 0.8-1.8 ng/dL.",
430
  "high": "فرط نشاط الدرقية.",
431
  "low": "قصور الدرقية.",
432
  "symptoms_low": "يُقاس مع TSH لتقييم الغدة الدرقية بشكل كامل.",
433
  },
434
  {
435
- "name_ar": "الأجسام المضادة للغدة الدرقية", "name_en": "Anti-TPO (Thyroid Peroxidase Antibodies)",
436
  "definition": "أجسام مضادة تهاجم الغدة الدرقية. طبيعي: أقل من 34 IU/mL.",
437
  "high": "التهاب الغدة الدرقية هاشيموتو، داء غريفز.",
438
  "low": "طبيعي.",
@@ -455,18 +426,17 @@ LAB_DEFINITIONS = [
455
  {
456
  "name_ar": "الهرمون المنبه للجريب", "name_en": "FSH (Follicle Stimulating Hormone)",
457
  "definition": "يحفز نضج البويضات والحيوانات المنوية.",
458
- "high": "انقطاع الطمث، قصور المبيض أو الخصية، انتهاء مخزون المبيض.",
459
  "low": "قصور الغدة النخامية.",
460
  "symptoms_low": "FSH مرتفع يعني احتياطي المبيض منخفض.",
461
  },
462
  {
463
  "name_ar": "ديهيدرو إيبي أندروستيرون", "name_en": "DHEA-S",
464
- "definition": "هرمون من الغدة الكظرية، سلف للهرمونات الجنسية. طبيعي: يختلف بالعمر والجنس.",
465
  "high": "أورام الكظرية، تكيس المبايض.",
466
  "low": "قصور الكظرية، الشيخوخة.",
467
  "symptoms_low": "الارتفاع عند النساء يسبب شعر زائد وحب شباب.",
468
  },
469
- # معادن إضافية
470
  {
471
  "name_ar": "النحاس", "name_en": "Copper (Serum)",
472
  "definition": "معدن ضروري لإنزيمات عدة. طبيعي: 70-140 µg/dL.",
@@ -481,7 +451,6 @@ LAB_DEFINITIONS = [
481
  "low": "سوء التغذية، أمراض الأمعاء.",
482
  "symptoms_low": "ضعف مناعة، ضعف عضلي، اضطراب درقي.",
483
  },
484
- # قلب إضافي
485
  {
486
  "name_ar": "إنزيم القلب CK-MB", "name_en": "CK-MB (Creatine Kinase-MB)",
487
  "definition": "إنزيم من عضلة القلب. يرتفع عند تلف القلب. طبيعي: أقل من 5% من CK الكلي.",
@@ -490,7 +459,7 @@ LAB_DEFINITIONS = [
490
  "symptoms_low": "يستخدم مع التروبونين لتأكيد النوبة القلبية.",
491
  },
492
  {
493
- "name_ar": "هرمون الببتيد الد��اغي الناتريوريتيك", "name_en": "BNP / NT-proBNP",
494
  "definition": "مؤشر فشل القلب. BNP طبيعي: أقل من 100 pg/mL.",
495
  "high": "فشل القلب الاحتقاني، ارتفاع ضغط الدم الرئوي.",
496
  "low": "طبيعي.",
@@ -500,10 +469,9 @@ LAB_DEFINITIONS = [
500
  "name_ar": "دي دايمر", "name_en": "D-Dimer",
501
  "definition": "ناتج تكسير الجلطات. طبيعي: أقل من 500 ng/mL.",
502
  "high": "جلطة وريدية، جلطة رئوية، التهاب شديد، حمل، سرطان.",
503
- "low": "طبيعي - يستبعد الجلطة.",
504
  "symptoms_low": "ارتفاعه مع أعراض الجلطة يستدعي تصوير طارئ.",
505
  },
506
- # مناعة إضافية
507
  {
508
  "name_ar": "الأجسام المضادة للنواة", "name_en": "ANA (Anti-Nuclear Antibodies)",
509
  "definition": "أجسام مضادة للخلايا، مؤشر الأمراض المناعية الذاتية.",
@@ -525,9 +493,8 @@ LAB_DEFINITIONS = [
525
  "low": "عدوى فيروسية أو لا عدوى.",
526
  "symptoms_low": "يساعد على قرار إعطاء المضادات الحيوية من عدمه.",
527
  },
528
- # تخثر إضافي
529
  {
530
- "name_ar": "زمن الثرومبوبلاستين الجزئي", "name_en": "aPTT (Activated Partial Thromboplastin Time)",
531
  "definition": "يقيس مسار التخثر الداخلي. طبيعي: 25-35 ثانية.",
532
  "high": "نقص عوامل التخثر، الهيبارين، الهيموفيليا.",
533
  "low": "خطر تجلط.",
@@ -540,7 +507,6 @@ LAB_DEFINITIONS = [
540
  "low": "أمراض الكبد، استهلاك الفيبرينوجين في الجلطات.",
541
  "symptoms_low": "الانخفاض الشديد يسبب نزيفاً خطيراً.",
542
  },
543
- # بول إضافي
544
  {
545
  "name_ar": "بروتين البول", "name_en": "Urine Protein / Microalbuminuria",
546
  "definition": "وجود بروتين في البول. طبيعي: أقل من 150 mg/يوم. بروتين دقيق: 30-300 mg/يوم.",
@@ -562,7 +528,6 @@ LAB_DEFINITIONS = [
562
  "low": "شرب ماء زائد، مرض السكري الكاذب، فشل كلوي.",
563
  "symptoms_low": "يعكس قدرة الكلى على تركيز البول.",
564
  },
565
- # براز
566
  {
567
  "name_ar": "تحليل البراز الكامل", "name_en": "Stool Analysis (Ova & Parasites)",
568
  "definition": "فحص البراز بحثاً عن طفيليات، بكتيريا، دم، خلايا.",
@@ -571,7 +536,7 @@ LAB_DEFINITIONS = [
571
  "symptoms_low": "عند إسهال مزمن، ألم بطن، فقدان وزن غير مبرر.",
572
  },
573
  {
574
- "name_ar": "الدم الخفي في البراز", "name_en": "Fecal Occult Blood Test (FOBT)",
575
  "definition": "يكشف الدم غير المرئي في البراز. طبيعي: سلبي.",
576
  "high": "قرحة معدية، نزيف معوي، سرطان القولون، بواسير.",
577
  "low": "سلبي = لا نزيف ظاهر.",
@@ -584,12 +549,11 @@ LAB_DEFINITIONS = [
584
  "low": "سلبي = لا عدوى.",
585
  "symptoms_low": "أفضل من فحص الدم لأنه يكشف العدوى الحالية.",
586
  },
587
- # فيروسات
588
  {
589
  "name_ar": "التهاب الكبد B", "name_en": "Hepatitis B Surface Antigen (HBsAg)",
590
  "definition": "يكشف عدوى التهاب الكبد B. طبيعي: سلبي.",
591
  "high": "إيجابي = عدوى نشطة بالتهاب الكبد B.",
592
- "low": "سلبي = لا عدوى. إيجابي HBsAb = محصّن (بلقاح أو شُفي).",
593
  "symptoms_low": "التهاب الكبد B قد يكون صامتاً لسنوات ثم يتطور لتليف.",
594
  },
595
  {
@@ -606,10 +570,9 @@ LAB_DEFINITIONS = [
606
  "low": "سلبي = لا عدوى.",
607
  "symptoms_low": "الكشف المبكر والعلاج يجعل مرضى HIV يعيشون حياة طبيعية.",
608
  },
609
- # حمل وخصوبة
610
  {
611
- "name_ar": "هرمون الحمل البيتا", "name_en": "Beta hCG (Human Chorionic Gonadotropin)",
612
- "definition": "هرمون الحمل. يضاعف كل 48-72 ساعة في الحمل الطبيعي. فوق 25 mIU/mL = حمل.",
613
  "high": "حمل، حمل خارج رحم، ورم الحمل.",
614
  "low": "لا حمل، أو خطر إجهاض.",
615
  "symptoms_low": "ارتفاع بطيء أو انخفاض يستدعي تقييم الحمل الخارج رحمي.",
@@ -647,17 +610,28 @@ LAB_DEFINITIONS = [
647
  "definition": "معدن حيوي لعمل القلب والعضلات. طبيعي: 3.5-5.0 mEq/L.",
648
  "high": "فشل كلوي، مدرات بول حافظة للبوتاسيوم، تلف أنسجة.",
649
  "low": "إسهال، قيء، مدرات البول، نقص تغذية.",
650
- "symptoms_low": "الانخفاض: ضعف عضلي، تشنج، عدم انتظام القلب. الارتفاع: عدم انتظام قلب خطير.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  },
652
  ]
653
 
654
- # ============================================================
655
- # القسم 2: التوصيات الصحية
656
- # ============================================================
657
  HEALTH_RECOMMENDATIONS = [
658
  {
659
  "topic": "فقر الدم والأنيميا",
660
- "content": """نصائح لمرضى فقر الدم (الأنيميا):
661
  الغذاء: تناول الأطعمة الغنية بالحديد: اللحوم الحمراء، الكبدة، السبانخ، العدس، الفاصوليا، الحبوب المدعمة.
662
  تناول فيتامين C مع وجبات الحديد لتعزيز الامتصاص (برتقال، فلفل).
663
  تجنب القهوة والشاي مع وجبات الحديد.
@@ -690,7 +664,6 @@ HEALTH_RECOMMENDATIONS = [
690
  الغذاء DASH: خضروات، فواكه، حليب قليل الدسم، قلل اللحوم الحمراء.
691
  الوزن: خسارة 5 كغ تخفض الضغط 5-10 mmHg.
692
  الرياضة: 30 دقيقة يومياً تخفض الضغط 5-8 mmHg.
693
- الكافيين والكحول: قللهما.
694
  الإجهاد: تأمل، يوغا، تنفس عميق.
695
  المتابعة: قياس الضغط يومياً في المنزل.""",
696
  },
@@ -710,652 +683,45 @@ HEALTH_RECOMMENDATIONS = [
710
  النوم: 7-9 ساعات، نظام نوم ثابت، تجنب الشاشات قبل النوم.
711
  الغذاء: وجبات منتظمة، قلل السكر المكرر، أكثر من البروتين.
712
  الرياضة: مشي 20 دقيقة يومياً يزيد الطاقة بشكل مثبت علمياً.
713
- الإجهاد: تقنيات الاسترخاء، تحديد الأولويات، طلب المساعدة.
714
  الترطيب: قلة الماء وحدها تسبب التعب، اشرب 8 أكواب يومياً.""",
715
  },
716
  {
717
  "topic": "صحة الغدة الدرقية",
718
  "content": """نصائح للعناية بالغدة الدرقية:
719
- اليود: ضروري لعمل الغدة (ملح معالج باليود، أسماك بحرية).
720
- السيلينيوم: يدعم الغدة (مكسرات البرازيل، التونة).
721
- تجنب: كميات كبيرة من الكرنب والقرنبيط النيء عند مرضى الغدة.
722
- الدواء: ليفوثيروكسين يُؤخذ صائماً قبل الأكل بـ 30 دقيقة.
723
- المتابعة: TSH كل 6-12 شهراً.
724
- الحمل: TSH يجب أن يكون أقل من 2.5 خلال الحمل.""",
725
  },
726
  {
727
  "topic": "صحة الكلى",
728
- "content": """نصائح للحفاظ على وظائف الكلى:
729
- الماء: اشرب 2-3 لترات يومياً لتنظيف الكلى.
730
- الملح: قلله لتقليل الضغط على الكلى.
731
- البروتين: لا تفرط في البروتين عند ضعف وظائف الكلى.
732
- الأدوية: تجنب المسكنات (NSAIDS) بكثرة.
733
- السكر والضغط: السيطرة عليهما أهم ما يحمي الكلى.
734
- المتابعة: كرياتينين ومعدل الترشيح GFR سنوياً للمعرضين للخطر.""",
735
  },
736
  {
737
  "topic": "صحة الكبد",
738
- "content": """نصائح لصحة الكبد:
739
- الوزن: السمنة أكبر سبب للكبد الدهني، خسارة 7-10% من الوزن تحسن الكبد.
740
- الغذاء: قهوة بدون سكر تحمي الكبد (دراسات متعددة). قلل السكر والدهون.
741
- الكحول: الابتعاد عنه تماماً.
742
- الأدوية: لا تأخذ أدوية دون وصفة لفترة طويلة.
743
- التطعيم: لقاح التهاب الكبد A وB.
744
- المتابعة: ALT و AST كل سنة.""",
745
- },
746
- {
747
- "topic": "تحسين صحة القلب",
748
- "content": """نصائح للقلب الصحي:
749
- الغذاء: نظام البحر المتوسط: زيت زيتون، سمك، خضروات، مكسرات.
750
- الرياضة: 150 دقيقة هوائية أسبوعياً + تمارين مقاومة مرتين.
751
- التوتر: يرفع ضغط الدم والكوليسترول. إدارته مهمة.
752
- النوم: 7-9 ساعات. قلة النوم ترفع خطر القلب.
753
- التدخين: الإقلاع يخفض الخطر القلبي 50% بعد سنة.
754
- الفحوصات: ضغط الدم، كوليسترول، سكر، مؤشر الكتلة الجسمية.""",
755
- },
756
- {
757
- "topic": "الصداع والصداع النصفي",
758
- "content": """نصائح لتخفيف الصداع:
759
- المحفزات: تعرف على محفزاتك (الضوء، الضجيج، أطعمة معينة كالشوكولاتة والجبن القديم).
760
- الترطيب: الجفاف سبب رئيسي للصداع. اشرب 8 أكواب ماء يومياً.
761
- النوم: نظام نوم ثابت. كثرة النوم وقلته كلاهما يسببان صداعاً.
762
- الرياضة: المشي المنتظم يقلل تكرار الصداع النصفي.
763
- الإجهاد: التوتر محفز قوي. تمارين التنفس تساعد.
764
- الكافيين: الجرعة الصغيرة تخفف الصداع لكن الإفراط يسببه.
765
- متى تذهب للطبيب: صداع مفاجئ شديد، مع حمى، أو تغير في الرؤية.""",
766
- },
767
- {
768
- "topic": "آلام المفاصل والتهاب المفاصل",
769
- "content": """نصائح لآلام المفاصل:
770
- الوزن: كل كيلوغرام زائد يضع 4 كغ ضغطاً على الركبة.
771
- الرياضة: السباحة والدراجة الأقل ضغطاً على المفاصل، تقوّي العضلات.
772
- الحرارة والبرودة: الحرارة للألم المزمن، البرودة للالتهاب الحاد.
773
- أوميغا 3: السمك والكتان يقللان الالتهاب.
774
- تجنب: الوقوف الطويل، صعود الدرج المتكرر عند الألم الحاد.
775
- المكملات: الجلوكوزامين قد يساعد بعض الحالات.
776
- فحوصات: حمض اليوريك (للنقرس)، CRP، أشعة للمفصل.""",
777
- },
778
- {
779
- "topic": "آلام الظهر والعمود الفقري",
780
- "content": """نصائح لآلام الظهر:
781
- الجلوس: ظهر مستقيم، كرسي داعم للظهر، لا تجلس أكثر من ساعة متواصلة.
782
- التمدد: تمارين تقوية عضلات البطن والظهر تحمي العمود الفقري.
783
- النوم: على جانبك مع وسادة بين الركبتين، أو على ظهرك مع وسادة تحت الركبتين.
784
- الرفع: انحنِ بركبتيك لا بظهرك عند رفع الأثقال.
785
- الوزن: السمنة تزيد الضغط على الفقرات القطنية.
786
- متى تذهب للطبيب: ألم ينتشر للساق، ضعف أو تنميل، صعوبة في الت��ول.""",
787
- },
788
- {
789
- "topic": "الغدة الدرقية الخاملة (قصور)",
790
- "content": """نصائح لقصور الغدة الدرقية:
791
- الدواء: ليفوثيروكسين يومياً صائماً، 30-60 دقيقة قبل الإفطار.
792
- لا تأخذه مع: الكالسيوم، الحديد، مضادات الحموضة (تقلل الامتصاص).
793
- الغذاء: اليود ضروري (ملح معالج، أسماك بحرية). السيلينيوم يدعم الغدة.
794
- تجنب الإفراط في: الكرنب، البروكلي، فول الصويا النيء (تثبط الغدة).
795
- المتابعة: TSH كل 6 أشهر، مع كل تغيير في الجرعة بعد 6-8 أسابيع.
796
- الأعراض التي تستدعي مراجعة الجرعة: زيادة وزن، تعب مفرط، برودة، إمساك.""",
797
- },
798
- {
799
- "topic": "نقص حمض الفوليك",
800
- "content": """نصائح لرفع مستوى حمض الفوليك:
801
- الغذاء: خضروات ورقية داكنة (سبانخ، بروكلي)، بقوليات، حبوب مدعمة، كبدة.
802
- الطهي: الطهي يدمر 50-90% من الفولات. تناول بعض الخضروات نيئة.
803
- المكملات: 400-800 ميكروجرام يومياً للنساء في سن الإنجاب.
804
- الحمل: 400-800 ميكروجرام قبل الحمل وفي الأشهر الأولى يمنع تشوهات الجنين.
805
- الكحول: يتدخل في امتصاص الفولات تجنبه.
806
- المتابعة: فحص مستوى الفولات والـ B12 معاً لأن نقصهما متشابه.""",
807
- },
808
- {
809
- "topic": "مقاومة الأنسولين",
810
- "content": """نصائح لتحسين حساسية الأنسولين:
811
- الغذاء: قلل الكربوهيدرات المكررة، اختر الحبوب الكاملة والبقوليات.
812
- الرياضة: أفضل علاج لمقاومة الأنسولين. 30 دقيقة يومياً من الرياضة الهوائية.
813
- الوزن: خسارة 5-10% من الوزن تحسن حساسية الأنسولين بشكل كبير.
814
- النوم: قلة النوم ترفع مقاومة الأنسولين. اهدف لـ 7-8 ساعات.
815
- الإجهاد: الكورتيزول يرفع مقاومة الأنسولين. إدارة التوتر مهمة.
816
- المتابعة: سكر صائم، أنسولين صائم، HbA1c كل 6 أشهر.""",
817
- },
818
- {
819
- "topic": "الكبد الدهني",
820
- "content": """نصائح لعلاج الكبد الدهني:
821
- الوزن: الهدف الأهم. خسارة 7-10% من الوزن تحسن الكبد الدهني بشكل ملحوظ.
822
- السكر: تقليل السكر والفركتوز (مشروبات غازية، عصائر مصنعة) أهم خطوة.
823
- القهوة: 2-3 أكواب قهوة يومياً تحمي الكبد (بدون سكر زائد).
824
- الرياضة: 30 دقيقة يومياً حتى بدون خسارة وزن تحسن الكبد.
825
- الكحول: ممنوع تماماً في الكبد الدهني.
826
- المتابعة: ALT وAST كل 6 أشهر، أشعة تحديد بالموجات الصوتية.""",
827
- },
828
- {
829
- "topic": "ضعف المناعة والوقاية من العدوى",
830
- "content": """نصائح لتقوية جهاز المناعة:
831
- التغذية: فيتامين C (حمضيات)، زنك (لحم، بذور يقطين)، فيتامين D (شمس ومكملات).
832
- النوم: 7-9 ساعات. النوم الكافي يضاعف فاعلية اللقاحات ويقوي المناعة.
833
- الرياضة: المعتدلة تقوي المناعة، المفرطة تضعفها.
834
- الإجهاد: التوتر المزمن يقمع المناعة. مارس تقنيات الاسترخاء.
835
- الماء: الجفاف يضعف إنتاج الأجسام المضادة.
836
- التدخين: يدمر خلايا المناعة في الجهاز التنفسي.
837
- الميكروبيوم: تناول البروبيوتيك والألياف لدعم بكتيريا الأمعاء المفيدة.""",
838
- },
839
- {
840
- "topic": "الوقاية من هشاشة العظام",
841
- "content": """نصائح لصحة العظام:
842
- الكالسيوم: 1000-1200 mg يومياً من الغذاء (حليب، جبن، لبن، سبانخ، سردين).
843
- فيتامين D: ضروري لامتصاص الكالسيوم. 800-2000 IU يومياً.
844
- الرياضة: رياضة تحمل الوزن (مشي، جري، رفع أثقال) تبني كثافة العظام.
845
- تجنب: التدخين والكحول الزائد (يضران بالعظام).
846
- الكافيين: الإفراط يقلل امتصاص الكالسيوم.
847
- الفحص: DEXA scan (قياس كثافة العظام) للنساء بعد انقطاع الطمث.""",
848
- },
849
- {
850
- "topic": "صحة الجهاز الهضمي",
851
- "content": """نصائح لصحة الجهاز الهضمي:
852
- الألياف: 25-35 غرام يومياً من الفواكه والخضروات والحبوب الكاملة.
853
- الماء: ضروري لمنع الإمساك وتحريك الطعام.
854
- البروبيوتيك: لبن، كفير، مخلل طبيعي لدعم بكتيريا الأمعاء.
855
- الأكل: ببطء ومضغ جيد يقلل الغازات والانتفاخ.
856
- الإجهاد: يؤثر مباشرة على الأمعاء (القولون العصبي).
857
- تجنب: الأطعمة المصنعة، الدهون المشبعة، الإكثار من المضادات الحيوية.
858
- متى تراجع الطبيب: دم في البراز، فقدان وزن غير مبرر، ألم مستمر.""",
859
- },
860
- {
861
- "topic": "النقرس وارتفاع حمض اليوريك",
862
- "content": """نصائح للنقرس:
863
- الماء: 2-3 لترات يومياً تساعد الكلى على إفراز حمض اليوريك.
864
- تجنب: اللحوم الحمراء، المأكولات البحرية، الكبدة والكلى (غنية بالبيورينات).
865
- قلل: الكحول خاصة البيرة، المشروبات المحلاة بالفركتوز.
866
- أضف: الكرز (طازج أو عصير) يقلل نوبات النقرس.
867
- الوزن: السمنة ترفع حمض اليوريك، خسارة وزن تدريجية مهمة.
868
- الأدوية: الألوبيورينول للوقاية، الكولشيسين للنوبة الحادة.
869
- الفحص: حمض اليوريك في الدم مرة كل 6 أشهر.""",
870
- },
871
- {
872
- "topic": "القلق والتوتر النفسي",
873
- "content": """نصائح لإدارة القلق:
874
- التنفس: تقنية 4-7-8: شهيق 4 ثوان، احتباس 7، زفير 8. تهدئ الجهاز العصبي فوراً.
875
- الرياضة: 30 دقيقة مشي يومياً تعادل مضادات القلق في بعض الدراسات.
876
- النوم: قلة النوم تزيد القلق. نظام نوم ثابت أساسي.
877
- الكافيين: يزيد القلق عند الحساسين. قلله أو تجنبه بعد الظهر.
878
- التأمل: 10 دقائق يومياً تغير بنية الدماغ وتقلل القلق مثبتاً علمياً.
879
- دعم اجتماعي: التحدث مع شخص موثوق يخفف عبء القلق.
880
- متى تطلب مساعدة: إذا تداخل القلق مع حياتك اليومية.""",
881
- },
882
- {
883
- "topic": "الاكتئاب وتحسين المزاج",
884
- "content": """نصائح لتحسين الصحة النفسية:
885
- الرياضة: أثبتت الدراسات أن 45 دقيقة هوائية 3 مرات/أسبوع فعّالة كالدواء في الاكتئاب الخفيف.
886
- الشمس: 20 دقيقة يومياً ترفع السيروتونين وتنظم الساعة البيولوجية.
887
- النوم: اضطراب النوم وثيق الصلة بالاكتئاب. علاج أحدهما يحسن الآخر.
888
- التغذية: أوميغا 3، فيتامين D، المغنيسيوم، B12 كلها مرتبطة بالمزاج.
889
- التواصل: العزلة تسوء الاكتئاب. حافظ على علاقاتك الاجتماعية.
890
- الفحوصات: استبعد نقص الغدة الدرقية، فيتامين D، B12 كأسباب عضوية.
891
- متى تطلب مساعدة: أفكار إيذاء النفس تستدعي مساعدة فورية.""",
892
- },
893
- {
894
- "topic": "متلازمة القولون العصبي IBS",
895
- "content": """نصائح لمرضى القولون العصبي:
896
- FODMAP: نظام غذائي يقلل السكريات القابلة للتخمر. يساعد 75% من المرضى.
897
- الإجهاد: القولون العصبي يرتبط مباشرة بالتوتر. إدارة الإجهاد علاج حقيقي.
898
- النعناع: زيت النعناع كبسولات مغلفة يقلل تشنجات القولون.
899
- الألياف: الألياف القابلة للذوبان (شوفان، بذور الكتان) أفضل من غير القابلة.
900
- الأكل: ببطء، وجبات صغيرة منتظمة، تجنب الوجبات الكبيرة.
901
- سجل الطعام: دوّن ما تأكله وأعراضك لتحديد المحفزات الشخصية.""",
902
- },
903
- {
904
- "topic": "الحموضة وارتجاع المريء GERD",
905
- "content": """نصائح لارتجاع المريء:
906
- الوزن: تقليل الوزن يخفف الضغط على المعدة ويقلل الارتجاع.
907
- الوجبات: صغيرة ومتكررة، لا تنم قبل 3 ساعات من الأكل.
908
- رفع الرأس: ارفع رأس السرير 15-20 سم عند النوم على الجانب الأيسر.
909
- تجنب: القهوة، الشوكولاتة، الحمضيات، الطماطم، الأطعمة الدهنية والحارة.
910
- الملابس: تجنب الملابس الضيقة التي تضغط على البطن.
911
- التدخين والكحول: يرخيان العضلة الحاجزة ويزيدان الارتجاع.""",
912
- },
913
- {
914
- "topic": "حصوات الكلى",
915
- "content": """نصائح للوقاية من حصوات الكلى:
916
- الماء: أهم علاج وقاية. 2.5-3 لترات يومياً تخفف تركيز المعادن.
917
- الكالسيوم: لا تقلل الكالسيوم الغذائي (يرتبط بالأكسالات في الأمعاء ويمنعها).
918
- الملح والبروتين الحيواني: قللهما فإنهما يرفعان الكالسيوم في البول.
919
- الأكسالات: قلل السبانخ، المكسرات، الشوكولاتة إذا كانت حصوات أكسالات.
920
- الليمون: عصير ليمون يومياً يرفع السترات ويمنع تكوّن الحصوات.
921
- متابعة: تحليل بول 24 ساعة لتحديد نوع الحصوات والوقاية المناسبة.""",
922
- },
923
- {
924
- "topic": "متلازمة تكيس المبايض PCOS",
925
- "content": """نصائح لمتلازمة تكيس المبايض:
926
- الوزن: خسارة 5-10% من الوزن تنظم الدورة وتحسن الخصوبة بشكل ملحوظ.
927
- الغذاء: نظام منخفض المؤشر الجلايسيمي يحسن مقاومة الأنسولين.
928
- الرياضة: تحسن حساسية الأنسولين وتنظم الهرمونات.
929
- الميتفورمين: يستخدمه الأطباء لتحسين حساسية الأنسولين في PCOS.
930
- الإنوزيتول: مكمل طبيعي يساعد في تنظيم الهرمونات.
931
- الفحوصات: سكر صائم وأنسولين، هرمونات (LH/FSH/تستوستيرون)، TSH، أشعة مبايض.""",
932
- },
933
- {
934
- "topic": "الوقاية من أمراض القلب",
935
- "content": """الوقاية من أمراض القلب والشرايين:
936
- الفحوصات الدورية: ضغط الدم، كوليسترول، سكر، مؤشر الكتلة الجسمية كل سنة.
937
- الغذاء القلبي: قلل الدهون المشبعة والصوديوم. أكثر من الخضروات والفواكه والأسماك.
938
- الرياضة: 150 دقيقة أسبوعياً من النشاط المعتدل.
939
- الإقلاع عن التدخين: أهم قرار صحي لصحة القلب.
940
- الوزن: مؤشر الكتلة الجسمية 18.5-24.9 مثالي.
941
- إدارة الإجهاد: التوتر المزمن يرفع ضغط الدم ويزيد الالتهاب.
942
- الأسبرين: لا تأخذه للوقاية بدون استشارة طبيب.""",
943
- },
944
- {
945
- "topic": "الربو وصحة الجهاز التنفسي",
946
- "content": """نصائح لمرضى الربو:
947
- المحفزات: تعرف عليها وتجنبها (غبار، دخان، عطور، برد، مجهود).
948
- البيئة: مكيف هواء نظيف، تغيير مخدات وأغطية باستمرار، لا سجاد في غرفة النوم.
949
- البخاخ الموسع: دائماً معك للطوارئ. استخدم بالتسلسل الصحيح.
950
- الكورتيزون الاستنشاقي: للوقاية وليس للطوارئ فقط. لا تتوقف عنه.
951
- الرياضة: ممكنة مع الربو. المسبح الأنسب (رطوبة عالية).
952
- متى تذهب للطوارئ: صعوبة تنفس شديدة، شفاه زرقاء، عدم استجابة للبخاخ.""",
953
- },
954
- {
955
- "topic": "السمنة وإدارة الوزن",
956
- "content": """نصائح صحية لإنقاص الوزن:
957
- الهدف الواقعي: 0.5-1 كغ أسبوعياً. السرعة تسبب فقدان عضلات ويوثيرو.
958
- العجز الحراري: قلل 500 سعرة يومياً (غذاء أقل + رياضة أكثر) = 0.5 كغ أسبوعياً.
959
- البروتين: يزيد الشبع ويحافظ على العضلات. أضفه في كل وجبة.
960
- الماء: اشرب كوب قبل كل وجبة. أحياناً يخلط الجسم العطش بالجوع.
961
- النوم: قلة النوم ترفع هرمون الجوع (غريلين) وتقلل الشبع (ليبتين).
962
- الصبر: الوزن المُفقود ببطء يعود ببطء. 6 أشهر للعادات الثابتة.""",
963
  },
964
  ]
965
-
966
- # ============================================================
967
- # القسم 3: قائمة مواضيع MedlinePlus الموسعة
968
- # ============================================================
969
- MEDLINEPLUS_TOPICS = [
970
- # تحاليل الدم
971
- ("blood glucose diabetes test", "glucose test", "lab_test"),
972
- ("hemoglobin anemia blood test", "hemoglobin", "lab_test"),
973
- ("complete blood count CBC test", "CBC", "lab_test"),
974
- ("cholesterol test lipid panel", "cholesterol", "lab_test"),
975
- ("thyroid function test TSH T4", "thyroid test", "lab_test"),
976
- ("liver function test ALT AST", "liver function", "lab_test"),
977
- ("kidney function creatinine BUN test", "kidney function", "lab_test"),
978
- ("HbA1c glycated hemoglobin test", "HbA1c", "lab_test"),
979
- ("iron studies ferritin test", "iron ferritin", "lab_test"),
980
- ("vitamin D blood test deficiency", "vitamin D test", "lab_test"),
981
- ("vitamin B12 blood test", "vitamin B12 test", "lab_test"),
982
- ("uric acid gout test", "uric acid", "lab_test"),
983
- ("C-reactive protein CRP test", "CRP", "lab_test"),
984
- ("PSA prostate test", "PSA", "lab_test"),
985
- ("blood pressure measurement", "blood pressure", "lab_test"),
986
- ("electrolytes sodium potassium test", "electrolytes", "lab_test"),
987
- ("calcium blood test", "calcium", "lab_test"),
988
- ("platelet count test", "platelet", "lab_test"),
989
- # الأمراض
990
- ("diabetes mellitus management treatment", "diabetes", "disease"),
991
- ("hypertension high blood pressure", "hypertension", "disease"),
992
- ("anemia iron deficiency treatment", "anemia", "disease"),
993
- ("hypothyroidism underactive thyroid", "hypothyroidism", "disease"),
994
- ("hyperthyroidism overactive thyroid Graves", "hyperthyroidism", "disease"),
995
- ("coronary artery disease heart attack", "heart disease", "disease"),
996
- ("chronic kidney disease renal failure", "kidney disease", "disease"),
997
- ("fatty liver disease NAFLD", "fatty liver", "disease"),
998
- ("gout uric acid joint pain", "gout", "disease"),
999
- ("osteoporosis bone density", "osteoporosis", "disease"),
1000
- ("high cholesterol hyperlipidemia", "high cholesterol", "disease"),
1001
- ("asthma breathing treatment", "asthma", "disease"),
1002
- ("urinary tract infection UTI", "UTI", "disease"),
1003
- ("irritable bowel syndrome IBS", "IBS", "disease"),
1004
- ("acid reflux GERD heartburn", "GERD", "disease"),
1005
- ("polycystic ovary syndrome PCOS", "PCOS", "disease"),
1006
- ("obesity overweight treatment", "obesity", "disease"),
1007
- ("metabolic syndrome insulin resistance", "metabolic syndrome", "disease"),
1008
- ("celiac disease gluten", "celiac", "disease"),
1009
- ("rheumatoid arthritis treatment", "rheumatoid arthritis", "disease"),
1010
- ("depression treatment mental health", "depression", "disease"),
1011
- ("anxiety disorder treatment", "anxiety disorder", "disease"),
1012
- ("sleep apnea insomnia", "sleep disorders", "disease"),
1013
- ("migraine headache treatment", "migraine", "disease"),
1014
- ("stroke cerebrovascular prevention", "stroke", "disease"),
1015
- ("pneumonia respiratory infection", "pneumonia", "disease"),
1016
- ("osteoarthritis joint pain", "osteoarthritis", "disease"),
1017
- ("fibromyalgia chronic pain", "fibromyalgia", "disease"),
1018
- # الأعراض
1019
- ("fatigue tiredness causes treatment", "fatigue", "symptom"),
1020
- ("headache causes treatment", "headache", "symptom"),
1021
- ("fever temperature causes", "fever", "symptom"),
1022
- ("dizziness vertigo causes", "dizziness", "symptom"),
1023
- ("shortness of breath dyspnea", "shortness of breath", "symptom"),
1024
- ("chest pain causes", "chest pain", "symptom"),
1025
- ("abdominal pain stomach ache", "abdominal pain", "symptom"),
1026
- ("nausea vomiting causes", "nausea", "symptom"),
1027
- ("back pain lower back", "back pain", "symptom"),
1028
- ("weight loss unintentional", "weight loss", "symptom"),
1029
- ("hair loss causes treatment", "hair loss", "symptom"),
1030
- ("skin rash allergy causes", "skin rash", "symptom"),
1031
- ("joint pain causes treatment", "joint pain", "symptom"),
1032
- ("muscle weakness causes", "muscle weakness", "symptom"),
1033
- ("heart palpitations causes", "palpitations", "symptom"),
1034
- ("insomnia sleep problems", "insomnia", "symptom"),
1035
- ("frequent urination causes", "frequent urination", "symptom"),
1036
- ("excessive thirst polydipsia", "excessive thirst", "symptom"),
1037
- ("blurred vision eye problems", "blurred vision", "symptom"),
1038
- ("swollen feet ankles edema", "edema", "symptom"),
1039
- ("night sweats causes", "night sweats", "symptom"),
1040
- ("chronic cough causes", "chronic cough", "symptom"),
1041
- ("numbness tingling hands feet", "numbness", "symptom"),
1042
- ("memory problems cognitive", "memory problems", "symptom"),
1043
- ("cold intolerance thyroid", "cold intolerance", "symptom"),
1044
- # التغذية والوقاية
1045
- ("healthy diet nutrition tips", "healthy diet", "prevention"),
1046
- ("exercise physical activity benefits", "exercise benefits", "prevention"),
1047
- ("sleep hygiene tips better sleep", "sleep health", "prevention"),
1048
- ("stress management mental health", "stress management", "prevention"),
1049
- ("water hydration health benefits", "hydration", "prevention"),
1050
- ("mediterranean diet heart health", "mediterranean diet", "prevention"),
1051
- ("vitamin supplements when needed", "vitamins supplements", "prevention"),
1052
- ("weight loss healthy strategies", "weight management", "prevention"),
1053
- ("smoking cessation health benefits", "quit smoking", "prevention"),
1054
- ("alcohol health effects risks", "alcohol health", "prevention"),
1055
- ("preventive health screenings checkup", "health screenings", "prevention"),
1056
- ("blood sugar control tips", "blood sugar control", "prevention"),
1057
- ]
1058
-
1059
- # ============================================================
1060
- # القسم 4: الأسئلة الشائعة (FAQ) بالعربية
1061
- # ============================================================
1062
- FAQS = [
1063
- {
1064
- "q": "ما سبب الشعور بالتعب المستمر والإجهاد؟",
1065
- "a": """التعب المستمر له أسباب طبية كثيرة يجب فحصها:\n- نقص الحديد والفيريتين: الأكثر شيوعاً عند النساء\n- نقص فيتامين D أو B12\n- قصور الغدة الدرقية (TSH مرتفع)\n- فقر الدم (هيموجلوبين منخفض)\n- السكري أو مقاومة الأنسولين\n- اكتئاب أو قلق مزمن\n- قلة النوم أو توقف التنفس أثناء النوم\n- الجفاف وقلة شرب الماء\nالتحاليل المقترحة: CBC، فيريتين، فيتامين D، فيتامين B12، TSH، سكر صائم، وظائف الكلى والكبد.""",
1066
- },
1067
- {
1068
- "q": "لماذا انخفض الهيموجلوبين عندي؟ ما أسباب فقر الدم؟",
1069
- "a": """أسباب انخفاض الهيموجلوبين وفقر الدم:\n- نقص الحديد: السبب الأشيع (نزيف، حيض غزير، سوء تغذية)\n- نقص فيتامين B12 أو حمض الفوليك\n- أمراض الكلى المزمنة\n- الثلاسيميا (وراثية)\n- التهابات مزمنة\nأعراضه: تعب، شحوب، ضيق تنفس، خفقان، دوخة.\nالعلاج: مكملات حديد مع فيتامين C، B12، أو فولات حسب السبب.""",
1070
- },
1071
- {
1072
- "q": "كيف أرفع مستوى الحديد والفيريتين في الدم؟",
1073
- "a": """لرفع الحديد والفيريتين:\nالغذاء: اللحوم الحمراء، الكبدة، السبانخ، العدس، الفاصوليا، الحبوب المدعمة.\nتناول فيتامين C مع وجبة الحديد (برتقال، فلفل أحمر) لتحسين الامتصاص 3 أضعاف.\nتجنب: القهوة والشاي والكالسيوم مع وجبة الحديد.\nالمكملات: كبريتات الحديد أو غلوكونات الحديد على معدة فارغة.\nالمتابعة: فيريتين كل 3 أشهر حتى يصل للمستوى المثالي (فوق 50 ng/mL).""",
1074
- },
1075
- {
1076
- "q": "ما معنى ارتفاع الكوليسترول وكيف أخفضه؟",
1077
- "a": """الكوليسترول المرتفع يزيد خطر أمراض القلب والجلطات.\nأسبابه: السمنة، قلة الرياضة، الوراثة، السكري، قصور الدرقية.\nللتخفيض:\n- الغذاء: قلل الدهون المشبعة، أكثر من الألياف (شوفان، تفاح)، زيت زيتون.\n- الرياضة: 150 دقيقة هوائية أسبوعياً.\n- الوزن: كل 1 كغ تخسره يخفض LDL بـ 1%.\n- الإقلاع عن التدخين يرفع HDL بـ 10%.\n- الأدوية: ستاتين إذا لزم بوصفة طبيب.""",
1078
- },
1079
- {
1080
- "q": "ما هو مستوى السكر الطبيعي؟ متى يكون السكري؟",
1081
- "a": """مستويات سكر الدم:\n- طبيعي صائم: 70-100 mg/dL\n- ما قبل السكري: 100-125 mg/dL\n- سكري: 126 فأكثر (في فحصين منفصلين)\n- بعد الأكل الطبيعي: أقل من 140\n- HbA1c طبيعي: أقل من 5.7%. ما قبل السكري: 5.7-6.4%. سكري: 6.5% فأكثر.\nأعراض السكري: كثرة التبول، عطش شديد، تعب، تعب بعد الأكل، بطء التئام الجروح.""",
1082
- },
1083
- {
1084
- "q": "ما معنى ارتفاع TSH؟ ما أعراض قصور الغدة الدرقية؟",
1085
- "a": """TSH مرتفع يعني أن الغدة الدرقية خاملة (قصور).\nالقيم الطبي��ية: 0.4-4.0 mIU/L. فوق 4 = قصور.\nأعراض قصور الدرقية:\n- تعب وإرهاق مستمر\n- زيادة وزن بدون سبب\n- برودة دائمة حتى في الجو الدافئ\n- إمساك\n- بطء ضربات القلب\n- تساقط الشعر وجفاف الجلد\n- اكتئاب وبطء التفكير\nالعلاج: ليفوثيروكسين يومياً على معدة فارغة.""",
1086
- },
1087
- {
1088
- "q": "كيف أرفع فيتامين د؟ ما أعراض نقصه؟",
1089
- "a": """أعراض نقص فيتامين D:\n- آلام العظام والعضلات\n- تعب مزمن\n- ضعف المناعة وتكرار الأمراض\n- اكتئاب ومزاج منخفض\n- تساقط الشعر\nلرفع فيتامين D:\n- الشمس: 15-30 دقيقة يومياً على الذراعين والساقين بين 10 صباحاً و3 عصراً\n- الغذاء: سمك السلمون، السردين، صفار البيض، حليب مدعم\n- المكملات: 2000-5000 IU يومياً (حسب توجيه الطبيب)\n- فيتامين D يُمتص مع الدهون، تناوله مع وجبة دسمة\nمتابعة: فحص بعد 3 أشهر.""",
1090
- },
1091
- {
1092
- "q": "ما أعراض نقص فيتامين ب12؟ ومن الأكثر عرضة له؟",
1093
- "a": """أعراض نقص فيتامين B12:\n- تنميل ووخز في اليدين والقدمين\n- تعب وضعف\n- فقر الدم الضخم الكريات\n- ضعف الذاكرة والتركيز\n- التهاب اللسان\n- مشية غير ثابتة\nالأكثر عرضة للنقص:\n- النباتيون والخضريون (B12 موجود فقط في المنتجات الحيوانية)\n- كبار السن (ضعف الامتصاص)\n- من يأخذون الميتفورمين لعلاج السكري\n- أمراض المعدة والأمعاء\nالعلاج: حقن B12 أو مكملات عالية الجرعة (1000 mcg يومياً).""",
1094
- },
1095
- {
1096
- "q": "ما معنى ارتفاع CRP؟ ما هو بروتين سي التفاعلي؟",
1097
- "a": """CRP (بروتين سي التفاعلي) مؤشر للالتهاب في الجسم.\nطبيعي: أقل من 10 mg/L.\nCRP عالي يعني وجود التهاب أو عدوى في مكان ما.\nأسباب الارتفاع:\n- عدوى بكتيرية أو فيروسية\n- التهاب المفاصل الروماتويدي\n- أمراض الأمعاء الالتهابية\n- نوبة قلبية حديثة\n- السمنة والسكري\nhigh-sensitivity CRP (hs-CRP): فوق 3 mg/L يشير لخطر قلبي مرتفع.\nالمتابعة: يُفسر مع ESR وأعراض المريض.""",
1098
- },
1099
- {
1100
- "q": "ما معنى ارتفاع حمض اليوريك؟ ما هو النقرس؟",
1101
- "a": """حمض اليوريك يتكون من تكسير البيورينات في الطعام.\nطبيعي: رجال 3.5-7.2، نساء 2.6-6.0 mg/dL.\nأسباب الارتفاع: اللحوم الحمراء، المأكولات البحرية، الكحول، السمنة، الفشل الكلوي.\nالنقرس: ترسب بلورات حمض اليوريك في المفاصل خاصة إصبع القدم الكبير.\nأعراض النوبة: ألم شديد مفاجئ، تورم، احمرار في المفصل.\nالعلاج:\n- تجنب اللحوم الحمراء والبحرية والكحول\n- شرب 2-3 لترات ماء يومياً\n- كولشيسين للنوبة الحادة، ألوبيورينول للوقاية""",
1102
- },
1103
- {
1104
- "q": "ما معنى ارتفاع الكرياتينين؟ كيف أعرف أن كليتي بخير؟",
1105
- "a": """الكرياتينين مؤشر لوظائف الكلى.\nطبيعي: رجال 0.74-1.35، نساء 0.59-1.04 mg/dL.\nارتفاعه يعني الكلى لا تصفي الدم بكفاءة.\nأسباب الارتفاع: أمراض الكلى المزمنة، الجفاف، السكري، ارتفاع الضغط.\nمؤشرات الكلى الكاملة: كرياتينين + eGFR + يوريا + تحليل بول.\neGFR طبيعي: فوق 90 mL/min. أقل من 60 = مرض كلوي مزمن.\nللحفاظ على الكلى: ماء كافٍ، تحكم في السكر والضغط، تجنب المسكنات.""",
1106
- },
1107
- {
1108
- "q": "ما أعراض مرض السكري؟ كيف أعرف أن عندي سكري؟",
1109
- "a": """أعراض السكري الكلاسيكية:\n- كثرة التبول (خاصة ليلاً)\n- عطش شديد ومستمر\n- تعب وإرهاق\n- جوع مستمر رغم الأكل\n- تعتم أو ضبابية في الرؤية\n- بطء التئام الجروح\n- تنميل أو وخز في القدمين\n- فقدان وزن غير مبرر (النوع الأول)\nالتشخيص بالتحاليل:\n- سكر صائم 126 mg/dL فأكثر (مرتين)\n- HbA1c 6.5% فأكثر\n- سكر عشوائي 200 فأكثر مع أعراض""",
1110
- },
1111
- {
1112
- "q": "ما أسباب تساقط الشعر؟ كيف أوقف ت��اقط الشعر؟",
1113
- "a": """أسباب تساقط الشعر الشائعة:\n- نقص الحديد والفيريتين: السبب الأول عند النساء\n- نقص فيتامين D أو B12 أو الزنك\n- قصور أو فرط الغدة الدرقية\n- تكيس المبايض PCOS عند النساء (هرمونات ذكورية مرتفعة)\n- التوتر الشديد والصدمات\n- بعض الأدوية\n- الثعلبة (alopecia areata): مناعية\nالتحاليل المقترحة: CBC، فيريتين، فيتامين D، B12، TSH، زنك، هرمونات.\nالعلاج يعتمد على السبب.""",
1114
- },
1115
- {
1116
- "q": "ما سبب ارتفاع ضغط الدم؟ كيف أخفض الضغط؟",
1117
- "a": """أسباب ارتفاع ضغط الدم:\n- الوراثة والعمر\n- السمنة والوزن الزائد\n- كثرة الملح في الطعام\n- قلة الرياضة\n- التوتر المزمن\n- الكافيين والتدخين\nالضغط الطبيعي: أقل من 120/80 mmHg. ارتفاع: 130/80 فأكثر.\nللتخفيض بدون دواء:\n- قلل الملح (أقل من 2300 mg يومياً)\n- نظام DASH: خضروات، فواكه، حليب قليل الدسم\n- رياضة 30 دقيقة يومياً تخفض الضغط 5-8 درجات\n- خسارة 5 كغ تخفض الضغط 5 درجات""",
1118
- },
1119
- {
1120
- "q": "ما أسباب الكبد الدهني؟ كيف أعالج الكبد الدهني؟",
1121
- "a": """الكبد الدهني (Fatty Liver) هو تراكم الدهون في خلايا الكبد.\nأسبابه:\n- السمنة والوزن الزائد (السبب الأول)\n- السكري ومقاومة الأنسولين\n- ارتفاع الدهون الثلاثية\n- الكحول\n- الأدوية كالكورتيزون\nالأعراض: غالباً بدون أعراض، يُكتشف بالموجات الصوتية أو ارتفاع ALT.\nالعلاج:\n- خسارة 7-10% من الوزن تحسن الكبد بشكل ملحوظ\n- تقليل السكر والفركتوز (مشروبات غازية)\n- القهوة بدون سكر تحمي الكبد (2-3 أكواب يومياً)\n- رياضة 30 دقيقة يومياً حتى بدون خسارة وزن""",
1122
- },
1123
- {
1124
- "q": "ما أعراض نقص الحديد؟ ما الفرق بين نقص الحديد وفقر الدم؟",
1125
- "a": """نقص الحديد أعم من فقر الدم:\n- مرحلة 1: نضوب مخازن الحديد (فيريتين منخفض) بدون أعراض واضحة\n- مرحلة 2: هيموجلوبين يبدأ بالانخفاض\n- مرحلة 3: فقر الدم الكامل\nأعراض نقص الحديد:\n- تعب مزمن حتى مع قسط كافٍ من النوم\n- برودة الأطراف\n- تساقط الشعر وهشاشة الأظافر\n- شهية غريبة (أكل الثلج أو التراب)\n- ضعف التركيز والذاكرة\n- شحوب الجلد والملتحمة\nالتحاليل: فيريتين أفضل مؤشر لمخزون الحديد. حديد الدم + TIBC + هيموجلوبين.""",
1126
- },
1127
- {
1128
- "q": "ما معنى ارتفاع WBC خلايا الدم البيضاء؟",
1129
- "a": """WBC (خلايا الدم البيضاء) = جهاز المناعة.\nطبيعي: 4,000-11,000 خلية/µL.\nارتفاع WBC أسبابه:\n- عدوى بكتيرية: ارتفاع العدلات Neutrophils\n- عدوى فيروسية: ارتفاع الخلايا اللمفاوية Lymphocytes\n- حساسية وطفيليات: ارتفاع الحمضات Eosinophils\n- التهاب وتوتر جسدي\n- بعض الأدوية كالكورتيزون\n- نادراً: ابيضاض الدم (لوكيميا)\nانخفاض WBC:\n- عدوى فيروسية شديدة، كيماوي، نقص مناعة\nيُقرأ مع التفريق الوظيفي CBC diff لتحديد نوع الارتفاع.""",
1130
- },
1131
- {
1132
- "q": "كيف أتحكم في مستوى السكر HbA1c؟ ما المقصود بـ HbA1c؟",
1133
- "a": """HbA1c يقيس متوسط سكر الدم خلال 3 أشهر الماضية.\nلذلك لا ينخدع بتحسن السكر يوم الفحص.\nالأهداف:\n- طبيعي: أقل من 5.7%\n- ما قبل السكري: 5.7-6.4%\n- سكري متحكم به: أقل من 7%\n- سكري غير متحكم: 7% فأكثر\nلخفض HbA1c:\n- قلل الكربوهيدرات المكررة (خبز أبيض، أرز، سكر)\n- رياضة 30 دقيقة يومياً تخفض HbA1c بـ 0.5-1%\n- وزّع الوجبات 5-6 وجبات صغيرة\n- التزم بالأدوية والأنسولين إن وُصف\n- قياس السكر بانتظام""",
1134
- },
1135
- {
1136
- "q": "ما معنى انخفاض الصفائح الدموية (Platelets)؟",
1137
- "a": """الصفائح الدموية مسؤولة عن إيقاف النزيف.\nطبيعي: 150,000-400,000/µL.\nانخفاض الصفائح (Thrombocytopenia) أسبابه:\n- التهابات فيروسية (دنج، عدد أحادي)\n- أمراض المناعة الذاتية (ITP)\n- أمراض الكبد والطحال\n- العلاج الكيميائي\n- نقص فيتامين B12 أو فولات\nأعراض الانخفاض الشديد:\n- كدمات تظهر بسهولة\n- نزيف اللثة أو الأنف\n- نزيف طويل بعد الجرح\n- نقاط حمراء صغيرة في الجلد (Petechiae)\nاستشر الطبيب إن كانت أقل من 100,000.""",
1138
- },
1139
- {
1140
- "q": "ما أسباب الدوخة والدوار؟ متى تكون خطيرة؟",
1141
- "a": """أسباب الدوخة الشائعة:\n- انخفاض ضغط الدم عند الوقوف (دوار وضعي)\n- فقر الدم ونقص الحديد\n- نقص السكر (نقص سكر الدم)\n- الجفاف وقلة الماء\n- اضطرابات الأذن الداخلية (BPPV - الحجارة)\n- ضغط الدم المرتفع\n- أدوية معينة\n- القلق والتوتر\nالتحاليل المقترحة: سكر الدم، CBC، ضغط الدم، TSH.\nمتى تستشير الطبيب فوراً:\n- دوخة مع صداع شديد مفاجئ\n- مع ضعف في وجه أو يد\n- مع صعوبة كلام أو رؤية مزدوجة (أعراض سكتة)""",
1142
- },
1143
- {
1144
- "q": "ما سبب ارتفاع إنزيمات الكبد ALT و AST؟",
1145
- "a": """ALT و AST إنزيمات داخل خلايا الكبد. ارتفاعها يعني تضرر الخلايا.\nطبيعي: ALT أقل من 56، AST أقل من 40 U/L.\nأسباب الارتفاع:\n- الكبد الدهني (السبب الأشيع حالياً)\n- التهاب الكبد الفيروسي (B أو C)\n- الكحول\n- الأدوية (باراسيتامول الزائد، ستاتين، بعض المضادات الحيوية)\n- حصوات المرارة\n- توتر شديد أو رياضة مفرطة (AST يرتفع في العضلات أيضاً)\nدرجات الارتفاع:\n- خفيف (مرتين الطبيعي): مراقبة\n- معتدل (5-10 أضعاف): فحوصات إضافية\n- شديد (فوق 10 أضعاف): طارئ طبي""",
1146
- },
1147
- {
1148
- "q": "ما هو مؤشر الكتلة الجسمية BMI وكيف أحسبه؟",
1149
- "a": """مؤشر الكتلة الجسمية BMI = الوزن (كغ) ÷ الطول (م) ÷ الطول (م).\nمثال: وزن 80 كغ، طول 1.70م → BMI = 80 ÷ 1.70 ÷ 1.70 = 27.7\nالتصنيف:\n- أقل من 18.5: نقص وزن\n- 18.5-24.9: طبيعي (مثالي)\n- 25-29.9: زيادة وزن\n- 30-34.9: سمنة درجة 1\n- 35-39.9: سمنة درجة 2\n- 40+: سمنة مرضية\nمحيط الخصر أيضاً مهم: رجال أقل من 102 سم، نساء أقل من 88 سم للوقاية من أمراض القلب والسكري.""",
1150
- },
1151
- {
1152
- "q": "ما هو البوتاسيوم وما أعراض نقصه وارتفاعه؟",
1153
- "a": """البوتاسيوم معدن حيوي لعمل القلب والعضلات والأعصاب.\nطبيعي: 3.5-5.0 mEq/L.\nأعراض نقص البوتاسيوم (أقل من 3.5):\n- ضعف عضلي وتشنجات\n- إمساك\n- عدم انتظام القلب\n- تعب وإجهاد\nأسباب النقص: مدرات البول، إسهال، قيء، سوء تغذية.\nأعراض ارتفاع البوتاسيوم (فوق 5.5):\n- عدم انتظام قلب خطير\n- ضعف عضلي\nأسباب الارتفاع: الفشل الكلوي، بعض الأدوية.\nمصادر البوتاسيوم الغذائية: موز، بطاطا، أفوكادو، سبانخ، بقوليات.""",
1154
- },
1155
- {
1156
- "q": "ما معنى ارتفاع PSA؟ ما هو اختبار المستضد البروستاتي؟",
1157
- "a": """PSA (مستضد البروستاتا النوعي) بروتين تفرزه البروستاتا.\nطبيعي: أقل من 4 ng/mL (يختلف بالعمر).\nارتفاع PSA أسبابه:\n- ضخامة البروستاتا الحميدة (BPH) - الأشيع\n- التهاب البروستاتا\n- سرطان البروستاتا\n- القسطرة أو فحص البروستاتا قبل الفحص مباشرة\nارتفاع PSA لا يعني سرطاناً بالضرورة!\nللتقييم الدقيق: Free PSA، الكثافة، PSAD، وخزة البروستاتا إن لزم.\nالفحص الدوري: للرجال فوق 50 سنة، أو 40 إن كان هناك تاريخ عائلي.""",
1158
- },
1159
- {
1160
- "q": "ما أعراض قصور الكلى؟ كيف أحافظ على صحة الكلى؟",
1161
- "a": """أعراض ضعف وظائف الكلى:\n- تورم القدمين والكاحلين والوجه\n- تعب وإرهاق\n- قلة التبول أو كثرته\n- بول رغوي (بروتين)\n- ارتفاع ضغط الدم\n- غثيان وفقدان شهية\nالتحاليل: كرياتينين، eGFR، يوريا، تحليل بول.\nللحفاظ على الكلى:\n- اشرب 2-3 لترات ماء يومياً\n- تحكم في السكر وضغط الدم (أهم الأسباب)\n- تجنب المسكنات (ibuprofen, diclofenac) بشكل متكرر\n- فحص كرياتينين وبول سنوياً إن كنت في خطر""",
1162
- },
1163
- {
1164
- "q": "ما هو الثيروكسين ليفوثيروكسين؟ كيف آخذ دواء الغدة الدرقية؟",
1165
- "a": """ليفوثيروكسين (Levothyroxine / Eltroxin) هو علاج قصور الغدة الدرقية.\nطريقة الأخذ الصحيحة:\n- صائماً في الصباح قبل الفطور بـ 30-60 دقيقة\n- ابتلعه بكوب ماء كامل\n- لا تأخذه مع: القهوة، حليب، مكملات الكالسيوم، حديد، مضادات حموضة (تقلل الامتصاص)\nمتابعة:\n- TSH بعد 6-8 أسابيع من بدء العلاج أو تغيير الجرعة\n- TSH كل 6-12 شهراً عند الاستقرار\nتنبيه: لا تتوقف عن الدواء دون استشارة الطبيب، حتى لو تحسنت الأعراض.""",
1166
- },
1167
- {
1168
- "q": "ما هي فحوصات الخصوبة عند المرأة؟ لماذا يتأخر الحمل؟",
1169
- "a": """فحوصات الخصوبة الأساسية عند المرأة:\n- AMH: مخزون البويضات\n- FSH وLH: في اليوم 2-3 من الدورة\n- إيستراديول E2\n- TSH: قصور الدرقية يؤثر على الخصوبة\n- البرولاكتين: ارتفاعه يمنع الإباضة\n- أشعة رحم وقناتين (HSG)\n- أشعة مبايض للبويضات (follicular monitoring)\nأسباب شائعة لتأخر الحمل:\n- تكيس المبايض PCOS (عدم انتظام الإباضة)\n- قصور الغدة الدرقية\n- انسداد قناتي فالوب\n- انخفاض AMH (احتياطي منخفض)\n- عوامل ذكورية (40% من الحالات)\nاستشير طبيب خصوبة بعد سنة من المحاولة (أو 6 أشهر فوق 35 سنة).""",
1170
- },
1171
- {
1172
- "q": "ما هو هرمون التستوستيرون المنخفض عند الرجال؟ ما أعراضه؟",
1173
- "a": """انخفاض التستوستيرون (Low T) عند الرجال:\nطبيعي: 300-1000 ng/dL. منخفض: أقل من 300.\nأعراض الانخفاض:\n- ضعف الرغبة الجنسية\n- ضعف الانتصاب (ED)\n- تعب وإرهاق\n- فقدان كتلة العضلات وزيادة الدهون\n- اكتئاب وتغير مزاج\n- انخفاض كثافة العظام\n- قلة الشعر وصغر الخصيتين (طويل الأمد)\nأسباب الانخفاض:\n- السمنة (الدهون تحول التستوستيرون لإستروجين)\n- الشيخوخة (تنخفض 1% سنوياً بعد 30)\n- التوتر المزمن\n- الكحول والتدخين\n- قصور الخصية أو الغدة النخامية""",
1174
- },
1175
- {
1176
- "q": "ما هو تحليل المقاومة للأنسولين HOMA-IR؟",
1177
- "a": """مقاومة الأنسولين تعني أن الخلايا لا تستجيب للأنسولين بشكل طبيعي.\nالبنكرياس يُنتج المزيد من الأنسولين لتعويض ذلك.\nلقياسها: HOMA-IR = (سكر صائم × أنسولين صائم) ÷ 405\nطبيعي: أقل من 2.5. مقاومة: فوق 2.5.\nأعراض مقاومة الأنسولين:\n- تعب بعد الأكل\n- شهية زائدة للسكريات\n- صعوبة إنقاص الوزن\n- تغيّر لون الجلد في الرقبة والإبط (Acanthosis Nigricans)\n- تكيس المبايض\nالعلاج:\n- خسارة 7-10% من الوزن\n- تقليل الكربوهيدرات المكررة\n- رياضة منتظمة\n- الميتفورمين بوصفة طبيب""",
1178
- },
1179
- {
1180
- "q": "ما أسباب الصداع المتكرر؟ متى يكون الصداع خطيراً؟",
1181
- "a": """أسباب الصداع المتكرر الشائعة:\n- التوتر والإجهاد النفسي (الأكثر شيوعاً)\n- صداع نصفي (ميجرين): نبضي من جانب، مع غثيان وحساسية للضوء\n- الجفاف وقلة الماء\n- قلة النوم أو كثرته\n- ارتفاع ضغط الدم\n- مشاكل الرقبة والفقرات العنقية\n- إجهاد العين (شاشات)\n- فقر الدم ونقص الحديد\nصداع ثانوي: علامات تستدعي طبيباً فوراً:\n- صداع مفاجئ شديد جداً (سكتة دماغية)\n- مع حمى وتصلب رقبة (تهاب سحايا)\n- مع ضعف أو تنميل في جهة\n- يزداد تدريجياً على أسابيع""",
1182
- },
1183
- {
1184
- "q": "ما هي فائدة المغنيسيوم؟ ما أعراض نقصه؟",
1185
- "a": """المغنيسيوم يشارك في أكثر من 300 تفاعل كيميائي في الجسم.\nطبيعي في الدم: 1.7-2.2 mg/dL (لكن 99% مخزون في الخلايا!).\nأعراض نقص المغنيسيوم:\n- تشنجات عضلية (خاصة الساقين ليلاً)\n- رعشة وارتجاف\n- صداع متكرر\n- قلق وتوتر وأرق\n- إمساك\n- عدم انتظام القلب\nأسباب النقص:\n- نظام غذائي فقير (معالجة الطعام تزيل المغنيسيوم)\n- السكري (الكلى تُفرز أكثر)\n- الكحول والإسهال المزمن\n- مضادات الحموضة (PPI) لفترة طويلة\nمصادر غذائية: مكسرات، بذور، خضروات ورقية، شوكولاتة داكنة.""",
1186
- },
1187
- ]
1188
-
1189
-
1190
- def build_faqs() -> list[Document]:
1191
- print("\n[2.5/5] بناء الأسئلة الشائعة (FAQ)...")
1192
- docs = []
1193
- for faq in FAQS:
1194
- content = f"سؤال شائع: {faq['q']}\nالإجابة: {faq['a']}"
1195
- docs.append(Document(
1196
- page_content=content,
1197
- metadata={"source": "medical_faq", "topic_type": "faq",
1198
- "topic_name": faq['q'][:50], "language": "ar"}
1199
- ))
1200
- print(f" {len(docs)} سؤال وجواب")
1201
- return docs
1202
-
1203
-
1204
- def fetch_medlineplus(search_term: str) -> list[dict]:
1205
- url = "https://wsearch.nlm.nih.gov/ws/query"
1206
- params = {"db": "healthTopics", "term": search_term, "retmax": 2}
1207
- try:
1208
- resp = requests.get(url, params=params, timeout=15)
1209
- if resp.status_code != 200:
1210
- return []
1211
- root = ET.fromstring(resp.text)
1212
- results = []
1213
- for doc in root.findall('.//document'):
1214
- title, content = "", ""
1215
- for elem in doc.findall('content'):
1216
- name = elem.get('name', '')
1217
- if name == 'title':
1218
- title = elem.text or ""
1219
- elif name == 'FullSummary':
1220
- raw = elem.text or ""
1221
- content = re.sub(r'<[^>]+>', ' ', raw).strip()
1222
- content = re.sub(r'\s+', ' ', content)
1223
- if title and content and len(content) > 100:
1224
- results.append({"title": title, "content": content})
1225
- return results
1226
- except Exception as e:
1227
- print(f" [ERROR] {e}")
1228
- return []
1229
-
1230
-
1231
- def chunk_text(text: str, chunk_size: int = 400, overlap: int = 40) -> list[str]:
1232
- words = text.split()
1233
- chunks = []
1234
- for i in range(0, len(words), chunk_size - overlap):
1235
- chunk = " ".join(words[i:i + chunk_size])
1236
- if len(chunk) > 80:
1237
- chunks.append(chunk)
1238
- return chunks
1239
-
1240
-
1241
- def deduplicate(db: Chroma) -> int:
1242
- print("\n[1/5] تنظيف التكرار...")
1243
- try:
1244
- all_data = db._collection.get(include=["documents"])
1245
- ids = all_data["ids"]
1246
- docs = all_data["documents"]
1247
- seen = {}
1248
- duplicates = []
1249
- for doc_id, text in zip(ids, docs):
1250
- key = text[:150].strip()
1251
- if key in seen:
1252
- duplicates.append(doc_id)
1253
- else:
1254
- seen[key] = doc_id
1255
- if duplicates:
1256
- db._collection.delete(ids=duplicates)
1257
- print(f" حذفنا {len(duplicates)} chunk مكرر")
1258
- else:
1259
- print(" لا يوجد تكرار")
1260
- return len(duplicates)
1261
- except Exception as e:
1262
- print(f" [ERROR] {e}")
1263
- return 0
1264
-
1265
-
1266
- def build_lab_definitions() -> list[Document]:
1267
- print("\n[2/5] بناء تعاريف التحاليل...")
1268
- docs = []
1269
- for lab in LAB_DEFINITIONS:
1270
- content = f"""تحليل: {lab['name_ar']} | {lab['name_en']}
1271
- تعريف: {lab['definition']}
1272
- أسباب الارتفاع: {lab['high']}
1273
- أسباب الانخفاض: {lab['low']}
1274
- أعراض الانخفاض: {lab['symptoms_low']}"""
1275
- docs.append(Document(
1276
- page_content=content,
1277
- metadata={"source": "lab_definitions", "topic_type": "lab_definition",
1278
- "topic_name": lab['name_ar'], "language": "ar"}
1279
- ))
1280
- print(f" {len(docs)} تعريف تحليل")
1281
- return docs
1282
-
1283
-
1284
- def build_health_recommendations() -> list[Document]:
1285
- print("\n[3/5] بناء التوصيات الصحية...")
1286
- docs = []
1287
- for rec in HEALTH_RECOMMENDATIONS:
1288
- docs.append(Document(
1289
- page_content=f"توصيات صحية - {rec['topic']}:\n{rec['content']}",
1290
- metadata={"source": "health_recommendations", "topic_type": "recommendation",
1291
- "topic_name": rec['topic'], "language": "ar"}
1292
- ))
1293
- print(f" {len(docs)} توصية صحية")
1294
- return docs
1295
-
1296
-
1297
- def fetch_all_medlineplus() -> list[Document]:
1298
- print(f"\n[4/5] جلب {len(MEDLINEPLUS_TOPICS)} موضوع من MedlinePlus...")
1299
- docs = []
1300
- for i, (search_term, topic_name, topic_type) in enumerate(MEDLINEPLUS_TOPICS, 1):
1301
- if i % 10 == 0:
1302
- print(f" [{i}/{len(MEDLINEPLUS_TOPICS)}]...")
1303
- results = fetch_medlineplus(search_term)
1304
- for item in results:
1305
- for idx, chunk in enumerate(chunk_text(item["content"])):
1306
- docs.append(Document(
1307
- page_content=chunk,
1308
- metadata={"source": "MedlinePlus", "topic_name": topic_name,
1309
- "topic_type": topic_type, "title": item["title"],
1310
- "language": "en", "chunk_index": idx}
1311
- ))
1312
- time.sleep(0.3)
1313
- print(f" {len(docs)} chunk من MedlinePlus")
1314
- return docs
1315
-
1316
-
1317
- def main():
1318
- print("=" * 50)
1319
- print("بناء الموسوعة الطبية الكاملة")
1320
- print("=" * 50)
1321
-
1322
- print("\nتحميل Embeddings...")
1323
- embeddings = HuggingFaceEmbeddings(model_name=EMBEDDINGS_MODEL)
1324
- db = Chroma(persist_directory=DB_PATH, embedding_function=embeddings)
1325
-
1326
- before = db._collection.count()
1327
- print(f"عدد الـ chunks قبل: {before}")
1328
-
1329
- # 1. تنظيف التكرار
1330
- deduplicate(db)
1331
-
1332
- # 2. تعاريف التحاليل
1333
- lab_docs = build_lab_definitions()
1334
-
1335
- # 2.5 الأسئلة الشائعة
1336
- faq_docs = build_faqs()
1337
-
1338
- # 3. التوصيات الصحية
1339
- rec_docs = build_health_recommendations()
1340
-
1341
- # 4. MedlinePlus
1342
- ml_docs = fetch_all_medlineplus()
1343
-
1344
- # 5. إضافة كل شيء
1345
- print("\n[5/5] إضافة البيانات لقاعدة البيانات...")
1346
- all_new = lab_docs + faq_docs + rec_docs + ml_docs
1347
- batch_size = 100
1348
- for i in range(0, len(all_new), batch_size):
1349
- batch = all_new[i:i + batch_size]
1350
- db.add_documents(batch)
1351
- print(f" {min(i + batch_size, len(all_new))}/{len(all_new)} chunk...")
1352
-
1353
- after = db._collection.count()
1354
- print(f"\n{'=' * 50}")
1355
- print(f"[OK] اكتملت الموسوعة الطبية!")
1356
- print(f"قبل: {before} | بعد: {after} | مضاف: {after - before}")
1357
- print(f"{'=' * 50}")
1358
-
1359
-
1360
- if __name__ == "__main__":
1361
- main()
 
1
  """
2
+ بيانات طبية مركزية — تستخدمها سكريبتات الاستيراد والـ API
 
 
 
 
 
3
  """
 
 
 
 
 
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  LAB_DEFINITIONS = [
6
  {
7
  "name_ar": "هيموجلوبين", "name_en": "Hemoglobin (HGB)",
8
+ "definition": "بروتين في خلايا الدم الحمراء يحمل الأكسجين. القيم الطبيعية: رجال 13.5-17.5 g/dL، نساء 12-15.5 g/dL.",
9
+ "high": "كثرة الحمر، الجفاف، أمراض الرئة المزمنة.",
10
+ "low": "فقر الدم، نزيف داخلي، نقص الحديد أو فيتامين B12 أو حمض الفوليك.",
11
  "symptoms_low": "تعب، شحوب، ضيق تنفس، دوخة، خفقان قلب.",
12
  },
13
  {
14
  "name_ar": "خلايا الدم الحمراء", "name_en": "Red Blood Cells (RBC)",
15
+ "definition": "عدد خلايا الدم الحمراء. القيم الطبيعية: رجال 4.5-5.9 مليون/µL، نساء 4.1-5.1 مليون/µL.",
16
  "high": "كثرة الحمر، الجفاف، أمراض القلب الخلقية.",
17
  "low": "فقر الدم، نزيف، فشل كلوي، نقص غذائي.",
18
  "symptoms_low": "إرهاق، شحوب، ضعف عام.",
19
  },
20
  {
21
  "name_ar": "خلايا الدم البيضاء", "name_en": "White Blood Cells (WBC)",
22
+ "definition": "خلايا الجهاز المناعي. القيم الطبيعية: 4,000-11,000 خلية/µL.",
23
  "high": "عدوى بكتيرية، التهاب، أمراض الدم كاللوكيميا.",
24
  "low": "أمراض المناعة، العلاج الكيميائي، أمراض النخاع العظمي.",
25
  "symptoms_low": "تكرار الإصابة بالعدوى، حمى متكررة.",
26
  },
27
  {
28
  "name_ar": "الصفائح الدموية", "name_en": "Platelets (PLT)",
29
+ "definition": "خلايا مسؤولة عن تخثر الدم. القيم الطبيعية: 150,000-400,000/µL.",
30
  "high": "التهاب، نزيف، نقص الحديد، خطر تجلط الدم.",
31
  "low": "نزيف تلقائي، أمراض الكبد، الذئبة، العلاج الكيميائي.",
32
  "symptoms_low": "كدمات سهلة، نزيف اللثة، نزيف طويل عند الجرح.",
33
  },
34
  {
35
  "name_ar": "سكر الدم الصائم", "name_en": "Fasting Blood Glucose",
36
+ "definition": "مستوى السكر بعد 8 ساعات صيام. طبيعي: 70-100 mg/dL. ما قبل السكري: 100-125. سكري: 126 فأكثر.",
37
  "high": "السكري، مقاومة الأنسولين، الإجهاد، بعض الأدوية.",
38
+ "low": "نقص سكر الدم، الصيام الطويل، جرعة زائدة من الأنسولين.",
39
  "symptoms_low": "رعشة، تعرق، دوخة، فقدان وعي في الحالات الشديدة.",
40
  },
41
  {
 
50
  "definition": "إجمالي الدهون في الدم. المثالي: أقل من 200 mg/dL. حدي: 200-239. مرتفع: 240 فأكثر.",
51
  "high": "خطر أمراض القلب والشرايين، السكتة الدماغية.",
52
  "low": "نادر، قد يرتبط بسوء التغذية أو مشاكل الكبد.",
53
+ "symptoms_low": "الكوليسترول المرتفع لا يسبب أعراضاً واضحة غالباً.",
54
  },
55
  {
56
+ "name_ar": "الكوليسترول الضار", "name_en": "LDL Cholesterol",
57
+ "definition": "الكوليسترول منخفض الكثافة، يترسب في الشرايين. المثالي: أقل من 100 mg/dL. مرتفع: 160 فأكثر.",
58
  "high": "تصلب الشرايين، أمراض القلب، جلطات.",
59
  "low": "مثالي، يقلل خطر أمراض القلب.",
60
+ "symptoms_low": "لا أعراض مباشرة.",
61
  },
62
  {
63
+ "name_ar": "الكوليسترول النافع", "name_en": "HDL Cholesterol",
64
+ "definition": "الكوليسترول عالي الكثافة، يزيل الكوليسترول من الشرايين. المثالي: فوق 60 mg/dL. منخفض: أقل من 40 للرجال، 50 للنساء.",
65
  "high": "حماية من أمراض القلب.",
66
  "low": "خطر أمراض القلب، قلة الرياضة، التدخين، السمنة.",
67
  "symptoms_low": "لا أعراض مباشرة لكن خطر قلبي مرتفع.",
 
74
  "symptoms_low": "الارتفاع الشديد يسبب التهاب البنكرياس.",
75
  },
76
  {
77
+ "name_ar": "هرمون الغدة الدرقية", "name_en": "TSH (Thyroid Stimulating Hormone)",
78
  "definition": "يتحكم في نشاط الغدة الدرقية. القيم الطبيعية: 0.4-4.0 mIU/L.",
79
  "high": "قصور الغدة الدرقية (الغدة خاملة).",
80
  "low": "فرط نشاط الغدة الدرقية.",
 
113
  "definition": "ناتج تكسير البيورينات. طبيعي: رجال 3.5-7.2 mg/dL، نساء 2.6-6.0 mg/dL.",
114
  "high": "النقرس، الفشل الكلوي، أكل كثير من اللحوم، السمنة.",
115
  "low": "نادر، قد يكون من بعض الأدوية.",
116
+ "symptoms_low": "الارتفاع يسبب: ألم حاد في المفاصل (خاصة إصبع القدم)، حصوات كلى.",
117
  },
118
  {
119
  "name_ar": "الكرياتينين", "name_en": "Creatinine",
 
123
  "symptoms_low": "ارتفاع الكرياتينين: تورم، تعب، غثيان، قلة التبول.",
124
  },
125
  {
126
+ "name_ar": "إنزيمات الكبد", "name_en": "Liver Enzymes (ALT/AST)",
127
  "definition": "مؤشرات لصحة الكبد. ALT طبيعي: 7-56 U/L. AST طبيعي: 10-40 U/L.",
128
  "high": "التهاب الكبد، الكبد الدهني، الكحول، بعض الأدوية.",
129
  "low": "لا أهمية سريرية.",
 
137
  "symptoms_low": "الارتفاع يرافقه أعراض المرض الأصلي.",
138
  },
139
  {
140
+ "name_ar": "الحمضات", "name_en": "Eosinophils",
141
  "definition": "نوع من خلايا الدم البيضاء. طبيعي: 1-4% من WBC أو 100-400 خلية/µL.",
142
  "high": "الحساسية، الربو، الطفيليات، بعض الأمراض المناعية.",
143
  "low": "ليس ذا أهمية سريرية.",
 
165
  "symptoms_low": "الارتفاع: سمنة بطنية، ضغط مرتفع، سكر. الانخفاض: إجهاد، غثيان، انخفاض ضغط.",
166
  },
167
  {
168
+ "name_ar": "هرمون الإنسولين الصائم", "name_en": "Insulin (Fasting)",
169
  "definition": "هرمون يتحكم في السكر. طبيعي صائم: 2-25 µIU/mL.",
170
  "high": "مقاومة الأنسولين، السمنة، ما قبل السكري، ورم الأنسولين.",
171
  "low": "السكري النوع الأول، البنكرياس الضعيف.",
 
181
  {
182
  "name_ar": "هرمون البرولاكتين", "name_en": "Prolactin",
183
  "definition": "هرمون الحليب من الغدة النخامية. طبيعي: رجال 2-18 ng/mL، نساء غير حامل 2-29 ng/mL.",
184
+ "high": "ورم النخامية، بعض الأدوية، قصور الغدة الدرقية.",
185
  "low": "نادر، قد يكون من قصور النخامية.",
186
  "symptoms_low": "الارتفاع: إفراز حليب، اضطراب دورة، ضعف جنسي.",
187
  },
188
  {
189
  "name_ar": "هرمون الإستروجين", "name_en": "Estradiol (E2)",
190
+ "definition": "الهرمون الأنثوي الرئيسي. يتغير حسب مرحلة الدورة والحمل.",
191
  "high": "أورام المبيض، السمنة، أمراض الكبد.",
192
  "low": "انقطاع الطمث، قصور المبيض، سوء التغذية.",
193
  "symptoms_low": "الانخفاض: جفاف مهبلي، هشاشة عظام، اضطراب مزاج، هبات حرارة.",
 
209
  {
210
  "name_ar": "تحليل البول الكامل", "name_en": "Urinalysis (Complete)",
211
  "definition": "فحص البول للكشف عن أمراض الكلى والمسالك البولية والسكري.",
212
+ "high": "بروتين: مشكلة كلى. سكر: سكري. دم: التهاب أو حصوات.",
213
  "low": "بول طبيعي: شفاف، أصفر فاتح، بدون بروتين أو سكر أو دم.",
214
  "symptoms_low": "البول الغائم أو الداكن أو ذو رائحة شديدة يستوجب فحصاً.",
215
  },
 
218
  "definition": "أفضل مقياس لوظيفة الكلى. طبيعي: فوق 90 mL/min. مرحلة الفشل: أقل من 15.",
219
  "high": "غير ذي أهمية عند الارتفاع.",
220
  "low": "مرض كلوي مزمن بدرجات متفاوتة.",
221
+ "symptoms_low": "GFR 30-59: مرحلة متوسطة. GFR 15-29: متقدمة. أقل من 15: فشل كلوي.",
222
  },
223
  {
224
  "name_ar": "حمض الفوليك", "name_en": "Folic Acid (Folate)",
 
269
  "low": "سوء التغذية، أمراض الكبد والكلى.",
270
  "symptoms_low": "ضعف، تورم، ضعف مناعة.",
271
  },
 
272
  {
273
  "name_ar": "الهيماتوكريت", "name_en": "Hematocrit (HCT)",
274
  "definition": "نسبة حجم كريات الدم الحمراء من إجمالي الدم. طبيعي: رجال 38.3-48.6%، نساء 35.5-44.9%.",
 
277
  "symptoms_low": "تعب، ضيق تنفس، شحوب.",
278
  },
279
  {
280
+ "name_ar": "متوسط حجم الكرية الحمراء", "name_en": "MCV (Mean Corpuscular Volume)",
281
  "definition": "متوسط حجم كريات الدم الحمراء. طبيعي: 80-100 fL. يساعد في تصنيف نوع فقر الدم.",
282
  "high": "فقر الدم الضخم الكريات (نقص B12 أو فولات)، الكحول، قصور الدرقية.",
283
  "low": "فقر الدم الصغير الكريات (نقص حديد، ثلاسيميا).",
 
291
  "symptoms_low": "يُفسر مع MCV وهيموجلوبين لتصنيف فقر الدم.",
292
  },
293
  {
294
+ "name_ar": "تركيز الهيموجلوبين في الكرية", "name_en": "MCHC",
295
  "definition": "تركيز الهي��وجلوبين في كل كرية. طبيعي: 31.5-36 g/dL.",
296
  "high": "فقر الدم الانحلالي الوراثي، الجفاف.",
297
  "low": "نقص الحديد، الثلاسيميا.",
 
306
  },
307
  {
308
  "name_ar": "العدلات", "name_en": "Neutrophils",
309
+ "definition": "أكثر خلايا الدم البيضاء شيوعاً. طبيعي: 40-70% من WBC أو 1800-7800/µL.",
310
  "high": "عدوى بكتيرية، التهاب، توتر، كورتيكوستيرويدات.",
311
  "low": "عدوى فيروسية شديدة، أدوية، أمراض نخاع العظم.",
312
  "symptoms_low": "خطر عالٍ للعدوى البكتيرية عند الانخفاض الشديد.",
 
314
  {
315
  "name_ar": "الخلايا اللمفاوية", "name_en": "Lymphocytes",
316
  "definition": "تنتج الأجسام المضادة وتحارب الفيروسات. طبيعي: 20-40% من WBC.",
317
+ "high": "عدوى فيروسية، ابيضاض اللمفاوية.",
318
+ "low": "HIV، الكورتيكوستيرويدات، العلاج الإشعاعي.",
319
  "symptoms_low": "ضعف المناعة ضد الفيروسات.",
320
  },
321
  {
 
332
  "low": "نادراً ما له أهمية سريرية.",
333
  "symptoms_low": "الارتفاع الشديد قد يكون علامة ابيضاض دموي.",
334
  },
 
335
  {
336
  "name_ar": "السكر العشوائي", "name_en": "Random Blood Sugar (RBS)",
337
+ "definition": "قياس السكر في أي وقت. الطبيعي: أقل من 140 mg/dL. مقلق: 140-199. سكري: 200 فأكثر مع أعراض.",
338
  "high": "مرض السكري، الإجهاد، بعض الأدوية.",
339
  "low": "نقص سكر الدم، جرعة أنسولين زائدة.",
340
  "symptoms_low": "الارتفاع مع أعراض (عطش، كثرة تبول) يؤكد السكري.",
 
344
  "definition": "يشرب المريض 75 غرام جلوكوز ويُقاس السكر بعد ساعتين. طبيعي: أقل من 140. ما قبل السكري: 140-199. سكري: 200+.",
345
  "high": "سكري، مقاومة أنسولين، سكري الحمل.",
346
  "low": "لا أهمية سريرية للانخفاض.",
347
+ "symptoms_low": "أفضل اختبار للكشف المبكر عن السكري وسكري الحمل.",
348
  },
349
  {
350
  "name_ar": "سي ببتيد", "name_en": "C-Peptide",
 
353
  "low": "السكري النوع الأول، البنكرياس الضعيف.",
354
  "symptoms_low": "C-Peptide منخفض = البنكرياس لا ينتج أنسولين = يحتاج أنسولين خارجي.",
355
  },
 
356
  {
357
  "name_ar": "الكوليسترول منخفض الكثافة جداً", "name_en": "VLDL Cholesterol",
358
  "definition": "يحمل الدهون الثلاثية للأنسجة. يُحسب: VLDL = Triglycerides ÷ 5. طبيعي: 2-30 mg/dL.",
 
360
  "low": "لا أهمية سريرية.",
361
  "symptoms_low": "ارتفاعه يزيد خطر أمراض القلب.",
362
  },
 
363
  {
364
  "name_ar": "جاما جلوتاميل ترانسفيريز", "name_en": "GGT (Gamma-Glutamyl Transferase)",
365
  "definition": "إنزيم كبدي حساس للكحول والأدوية. طبيعي: رجال 8-61 U/L، نساء 5-36 U/L.",
366
  "high": "الكحول، أمراض الكبد، بعض الأدوية، الكبد الدهني.",
367
  "low": "لا أهمية سريرية.",
368
+ "symptoms_low": "مؤشر مبكر لتأثير الكحول على الكبد.",
369
  },
370
  {
371
  "name_ar": "البيليروبين المباشر", "name_en": "Direct (Conjugated) Bilirubin",
372
+ "definition": "البيليروبين المعالج بالكبد. طبيعي: 0-0.3 mg/dL.",
373
  "high": "انسداد القنوات الصفراوية، التهاب الكبد، حصوات المرارة.",
374
  "low": "لا أهمية سريرية.",
375
  "symptoms_low": "ارتفاعه مع اليرقان يشير لمشكلة في تصريف الصفراء.",
376
  },
 
377
  {
378
+ "name_ar": "اليوريا في الدم", "name_en": "BUN / Blood Urea Nitrogen",
379
+ "definition": "ناتج تكسير البروتين. BUN طبيعي: 7-20 mg/dL. يوريا: 2.5-7.1 mmol/L.",
380
  "high": "ضعف الكلى، الجفاف، نزيف الجهاز الهضمي، أكل بروتين زائد.",
381
  "low": "أمراض الكبد الشديدة، سوء التغذية.",
382
  "symptoms_low": "نسبة BUN/Creatinine تحدد سبب ارتفاع البول النيتروجيني.",
 
388
  "low": "القيء المتكرر، القصور الكلوي، الأدوية.",
389
  "symptoms_low": "يُفسر دائماً مع الصوديوم والبوتاسيوم.",
390
  },
 
391
  {
392
+ "name_ar": "هرمون T3 الحر", "name_en": "Free T3 (Triiodothyronine)",
393
  "definition": "الهرمون الدرقي النشط. Free T3 طبيعي: 2.3-4.2 pg/mL.",
394
  "high": "فرط نشاط الدرقية، التهاب الدرقية.",
395
  "low": "قصور الدرقية، الأمراض الحادة.",
396
  "symptoms_low": "Free T3 أدق من T3 الكلي في تقييم وظيفة الدرقية.",
397
  },
398
  {
399
+ "name_ar": "هرمون T4 الحر", "name_en": "Free T4 (Thyroxine)",
400
  "definition": "الهرمون الدرقي الرئيسي المخزون. Free T4 طبيعي: 0.8-1.8 ng/dL.",
401
  "high": "فرط نشاط الدرقية.",
402
  "low": "قصور الدرقية.",
403
  "symptoms_low": "يُقاس مع TSH لتقييم الغدة الدرقية بشكل كامل.",
404
  },
405
  {
406
+ "name_ar": "الأجسام المضادة للغدة الدرقية", "name_en": "Anti-TPO Antibodies",
407
  "definition": "أجسام مضادة تهاجم الغدة الدرقية. طبيعي: أقل من 34 IU/mL.",
408
  "high": "التهاب الغدة الدرقية هاشيموتو، داء غريفز.",
409
  "low": "طبيعي.",
 
426
  {
427
  "name_ar": "الهرمون المنبه للجريب", "name_en": "FSH (Follicle Stimulating Hormone)",
428
  "definition": "يحفز نضج البويضات والحيوانات المنوية.",
429
+ "high": "انقطاع الطمث، قصور المبيض أو الخصية.",
430
  "low": "قصور الغدة النخامية.",
431
  "symptoms_low": "FSH مرتفع يعني احتياطي المبيض منخفض.",
432
  },
433
  {
434
  "name_ar": "ديهيدرو إيبي أندروستيرون", "name_en": "DHEA-S",
435
+ "definition": "هرمون من الغدة الكظرية، سلف للهرمونات الجنسية. القيم تختلف بالعمر والجنس.",
436
  "high": "أورام الكظرية، تكيس المبايض.",
437
  "low": "قصور الكظرية، الشيخوخة.",
438
  "symptoms_low": "الارتفاع عند النساء يسبب شعر زائد وحب شباب.",
439
  },
 
440
  {
441
  "name_ar": "النحاس", "name_en": "Copper (Serum)",
442
  "definition": "معدن ضروري لإنزيمات عدة. طبيعي: 70-140 µg/dL.",
 
451
  "low": "سوء التغذية، أمراض الأمعاء.",
452
  "symptoms_low": "ضعف مناعة، ضعف عضلي، اضطراب درقي.",
453
  },
 
454
  {
455
  "name_ar": "إنزيم القلب CK-MB", "name_en": "CK-MB (Creatine Kinase-MB)",
456
  "definition": "إنزيم من عضلة القلب. يرتفع عند تلف القلب. طبيعي: أقل من 5% من CK الكلي.",
 
459
  "symptoms_low": "يستخدم مع التروبونين لتأكيد النوبة القلبية.",
460
  },
461
  {
462
+ "name_ar": "هرمون BNP القلبي", "name_en": "BNP / NT-proBNP",
463
  "definition": "مؤشر فشل القلب. BNP طبيعي: أقل من 100 pg/mL.",
464
  "high": "فشل القلب الاحتقاني، ارتفاع ضغط الدم الرئوي.",
465
  "low": "طبيعي.",
 
469
  "name_ar": "دي دايمر", "name_en": "D-Dimer",
470
  "definition": "ناتج تكسير الجلطات. طبيعي: أقل من 500 ng/mL.",
471
  "high": "جلطة وريدية، جلطة رئوية، التهاب شديد، حمل، سرطان.",
472
+ "low": "طبيعي يستبعد الجلطة.",
473
  "symptoms_low": "ارتفاعه مع أعراض الجلطة يستدعي تصوير طارئ.",
474
  },
 
475
  {
476
  "name_ar": "الأجسام المضادة للنواة", "name_en": "ANA (Anti-Nuclear Antibodies)",
477
  "definition": "أجسام مضادة للخلايا، مؤشر الأمراض المناعية الذاتية.",
 
493
  "low": "عدوى فيروسية أو لا عدوى.",
494
  "symptoms_low": "يساعد على قرار إعطاء المضادات الحيوية من عدمه.",
495
  },
 
496
  {
497
+ "name_ar": "زمن الثرومبوبلاستين الجزئي", "name_en": "aPTT",
498
  "definition": "يقيس مسار التخثر الداخلي. طبيعي: 25-35 ثانية.",
499
  "high": "نقص عوامل التخثر، الهيبارين، الهيموفيليا.",
500
  "low": "خطر تجلط.",
 
507
  "low": "أمراض الكبد، استهلاك الفيبرينوجين في الجلطات.",
508
  "symptoms_low": "الانخفاض الشديد يسبب نزيفاً خطيراً.",
509
  },
 
510
  {
511
  "name_ar": "بروتين البول", "name_en": "Urine Protein / Microalbuminuria",
512
  "definition": "وجود بروتين في البول. طبيعي: أقل من 150 mg/يوم. بروتين دقيق: 30-300 mg/يوم.",
 
528
  "low": "شرب ماء زائد، مرض السكري الكاذب، فشل كلوي.",
529
  "symptoms_low": "يعكس قدرة الكلى على تركيز البول.",
530
  },
 
531
  {
532
  "name_ar": "تحليل البراز الكامل", "name_en": "Stool Analysis (Ova & Parasites)",
533
  "definition": "فحص البراز بحثاً عن طفيليات، بكتيريا، دم، خلايا.",
 
536
  "symptoms_low": "عند إسهال مزمن، ألم بطن، فقدان وزن غير مبرر.",
537
  },
538
  {
539
+ "name_ar": "الدم الخفي في البراز", "name_en": "FOBT (Fecal Occult Blood Test)",
540
  "definition": "يكشف الدم غير المرئي في البراز. طبيعي: سلبي.",
541
  "high": "قرحة معدية، نزيف معوي، سرطان القولون، بواسير.",
542
  "low": "سلبي = لا نزيف ظاهر.",
 
549
  "low": "سلبي = لا عدوى.",
550
  "symptoms_low": "أفضل من فحص الدم لأنه يكشف العدوى الحالية.",
551
  },
 
552
  {
553
  "name_ar": "التهاب الكبد B", "name_en": "Hepatitis B Surface Antigen (HBsAg)",
554
  "definition": "يكشف عدوى التهاب الكبد B. طبيعي: سلبي.",
555
  "high": "إيجابي = عدوى نشطة بالتهاب الكبد B.",
556
+ "low": "سلبي = لا عدوى. إيجابي HBsAb = محصّن بلقاح أو شُفي.",
557
  "symptoms_low": "التهاب الكبد B قد يكون صامتاً لسنوات ثم يتطور لتليف.",
558
  },
559
  {
 
570
  "low": "سلبي = لا عدوى.",
571
  "symptoms_low": "الكشف المبكر والعلاج يجعل مرضى HIV يعيشون حياة طبيعية.",
572
  },
 
573
  {
574
+ "name_ar": "هرمون الحمل بيتا", "name_en": "Beta hCG",
575
+ "definition": "هرمون الحمل. فوق 25 mIU/mL = حمل. يضاعف كل 48-72 ساعة في الحمل الطبيعي.",
576
  "high": "حمل، حمل خارج رحم، ورم الحمل.",
577
  "low": "لا حمل، أو خطر إجهاض.",
578
  "symptoms_low": "ارتفاع بطيء أو انخفاض يستدعي تقييم الحمل الخارج رحمي.",
 
610
  "definition": "معدن حيوي لعمل القلب والعضلات. طبيعي: 3.5-5.0 mEq/L.",
611
  "high": "فشل كلوي، مدرات بول حافظة للبوتاسيوم، تلف أنسجة.",
612
  "low": "إسهال، قيء، مدرات البول، نقص تغذية.",
613
+ "symptoms_low": "الانخفاض: ضعف عضلي، تشنج، عدم انتظام القلب.",
614
+ },
615
+ {
616
+ "name_ar": "الكالسيوم", "name_en": "Calcium (Total)",
617
+ "definition": "معدن أساسي للعظام والأعصاب والعضلات. طبيعي: 8.5-10.5 mg/dL.",
618
+ "high": "فرط نشاط الغدة الدريقية، السرطان، الجفاف.",
619
+ "low": "قصور الغدة الدريقية، نقص فيتامين D، الفشل الكلوي.",
620
+ "symptoms_low": "الانخفاض: تشنجات عضلية، تنميل، اختلاجات. الارتفاع: تعب، كثرة تبول، حصوات كلى.",
621
+ },
622
+ {
623
+ "name_ar": "مؤشر PSA لسرطان البروستات", "name_en": "PSA (Prostate-Specific Antigen)",
624
+ "definition": "بروتين تنتجه البروستات. طبيعي: أقل من 4 ng/mL (يرتفع مع العمر).",
625
+ "high": "تضخم البروستات الحميد، التهاب البروستات، سرطان البروستات.",
626
+ "low": "لا أهمية سريرية.",
627
+ "symptoms_low": "PSA المرتفع لا يعني بالضرورة السرطان، يحتاج تقييماً إضافياً.",
628
  },
629
  ]
630
 
 
 
 
631
  HEALTH_RECOMMENDATIONS = [
632
  {
633
  "topic": "فقر الدم والأنيميا",
634
+ "content": """نصائح لمرضى فقر الدم:
635
  الغذاء: تناول الأطعمة الغنية بالحديد: اللحوم الحمراء، الكبدة، السبانخ، العدس، الفاصوليا، الحبوب المدعمة.
636
  تناول فيتامين C مع وجبات الحديد لتعزيز الامتصاص (برتقال، فلفل).
637
  تجنب القهوة والشاي مع وجبات الحديد.
 
664
  الغذاء DASH: خضروات، فواكه، حليب قليل الدسم، قلل اللحوم الحمراء.
665
  الوزن: خسارة 5 كغ تخفض الضغط 5-10 mmHg.
666
  الرياضة: 30 دقيقة يومياً تخفض الضغط 5-8 mmHg.
 
667
  الإجهاد: تأمل، يوغا، تنفس عميق.
668
  المتابعة: قياس الضغط يومياً في المنزل.""",
669
  },
 
683
  النوم: 7-9 ساعات، نظام نوم ثابت، تجنب الشاشات قبل النوم.
684
  الغذاء: وجبات منتظمة، قلل السكر المكرر، أكثر من البروتين.
685
  الرياضة: مشي 20 دقيقة يومياً يزيد الطاقة بشكل مثبت علمياً.
 
686
  الترطيب: قلة الماء وحدها تسبب التعب، اشرب 8 أكواب يومياً.""",
687
  },
688
  {
689
  "topic": "صحة الغدة الدرقية",
690
  "content": """نصائح للعناية بالغدة الدرقية:
691
+ اليود: ضروري لعمل الغدة (ملح معالج باليود، أسماك بحرية، أعشاب بحرية).
692
+ السيلينيوم: يدعم تحويل T4 إلى T3 (مكسرات برازيلية، سمك التونا).
693
+ قصور الدرقية: أدوية الليفوثيروكسين تُؤخذ على معدة فارغة 30-60 دقيقة قبل الإفطار.
694
+ تجنب فول الصويا والخضروات الصليبية (بروكلي، كرنب) بكميات كبيرة مع الأدوية.
695
+ المتابعة: فحص TSH كل 6-12 شهر مع العلاج.""",
 
696
  },
697
  {
698
  "topic": "صحة الكلى",
699
+ "content": """نصائح للحفاظ على صحة الكلى:
700
+ الترطيب: شرب 2-3 لتر ماء يومياً يحمي الكلى ويمنع الحصوات.
701
+ البروتين: تقليل البروتين الزائد عند ضعف الكلى (استشر طبيبك).
702
+ الملح: تقليل الصوديوم يحمي الكلى ويخفض الضغط.
703
+ السكر والضغط: ضبطهما يمنع 70% من حالات الفشل الكلوي.
704
+ الأدوية: تجنب مسكنات الألم (NSAIDs) بدون استشارة عند مشاكل الكلى.
705
+ المتابعة: فحص الكرياتينين وeGFR وبروتين البول بانتظام.""",
706
  },
707
  {
708
  "topic": "صحة الكبد",
709
+ "content": """نصائح للحفاظ على صحة الكبد:
710
+ الوزن: الكبد الدهني أكثر أمراض الكبد شيوعاً، تخسيس 10% من الوزن يحسنه كثيراً.
711
+ الكحول: هو السبب الأول لتليف الكبد في العالم، تجنبه تماماً.
712
+ الغذاء: خضروات، فواكه، قهوة (مفيدة للكبد)، قلل الدهون المشبعة والسكر.
713
+ الأدوية: لا تتناول أي دواء بدون استشارة عند مشاكل الكبد.
714
+ التطعيم: تأكد من أخذ لقاح التهاب الكبد B.
715
+ المتابعة: فحص ALT، AST، Bilirubin، Albumin كل 6 أشهر.""",
716
+ },
717
+ {
718
+ "topic": "صحة القلب والأوعية الدموية",
719
+ "content": """نصائح لصحة القلب:
720
+ الغذاء: نظام البحر المتوسط (زيت زيتون، سمك، خضروات، مكسرات، حبوب كاملة).
721
+ أوميغا-3: السمك مرتين أسبوعياً أو مكملات 1000 mg يومياً تقلل الدهون الثلاثية.
722
+ الرياضة: 150 دقيقة هوائية أسبوعياً تقلل أمراض القلب بنسبة 35%.
723
+ التدخين: الإقلاع يخفض خطر النوبة القلبية بنسبة 50% خلال سنة.
724
+ الكوليسترول والضغط والسكر: ضبط الثلاثة أساس الوقاية من أمراض القلب.
725
+ المتابعة: فحص قلب مع ECG سنوياً بعد 40 سنة.""",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
726
  },
727
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/medical_kb/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .reference.medical_reference import MedicalKB
2
+
3
+ # Module-level singleton — import this across the app
4
+ kb = MedicalKB()
5
+
6
+ __all__ = ["kb", "MedicalKB"]
backend/medical_kb/reference/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .normal_ranges import lookup, classify, NORMAL_RANGES
2
+ from .medical_reference import MedicalKB
3
+
4
+ __all__ = ["lookup", "classify", "NORMAL_RANGES", "MedicalKB"]
backend/medical_kb/reference/medical_reference.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MedicalKB — runtime knowledge base loaded from JSON schemas.
3
+ Provides panel lookup, test lookup, and severity classification.
4
+ """
5
+ from __future__ import annotations
6
+ import json
7
+ import pathlib
8
+
9
+ _SCHEMAS_DIR = pathlib.Path(__file__).parent.parent / "schemas"
10
+
11
+
12
+ class MedicalKB:
13
+
14
+ def __init__(self) -> None:
15
+ self._panels: dict[str, dict] = {}
16
+ self._load_all()
17
+
18
+ def _load_all(self) -> None:
19
+ if not _SCHEMAS_DIR.exists():
20
+ return
21
+ for path in sorted(_SCHEMAS_DIR.glob("*.json")):
22
+ try:
23
+ data = json.loads(path.read_text(encoding="utf-8"))
24
+ code = data.get("panel_code", path.stem).lower()
25
+ self._panels[code] = data
26
+ except Exception as e:
27
+ print(f"[MedicalKB] Failed to load {path.name}: {e}")
28
+ print(f"[MedicalKB] Loaded {len(self._panels)} panels: {list(self._panels)}")
29
+
30
+ # ── Panel-level ────────────────────────────────────────────────────────
31
+
32
+ def get_panel(self, code: str) -> dict | None:
33
+ return self._panels.get(code.lower())
34
+
35
+ def list_panels(self) -> list[str]:
36
+ return list(self._panels.keys())
37
+
38
+ # ── Test-level ─────────────────────────────────────────────────────────
39
+
40
+ def get_test(self, panel_code: str, test_name: str) -> dict | None:
41
+ panel = self.get_panel(panel_code)
42
+ if not panel:
43
+ return None
44
+ tests = panel.get("tests", {})
45
+ name_lower = test_name.lower().strip()
46
+ for canonical, data in tests.items():
47
+ if canonical.lower() == name_lower:
48
+ return data
49
+ aliases = [a.lower() for a in data.get("abbreviations", [])]
50
+ if name_lower in aliases:
51
+ return data
52
+ return None
53
+
54
+ def find_test_panel(self, test_name: str) -> tuple[str, dict] | None:
55
+ """Search all panels for a test name. Returns (panel_code, test_data) or None."""
56
+ name_lower = test_name.lower().strip()
57
+ for code, panel in self._panels.items():
58
+ for canonical, data in panel.get("tests", {}).items():
59
+ if canonical.lower() == name_lower:
60
+ return code, data
61
+ if name_lower in [a.lower() for a in data.get("abbreviations", [])]:
62
+ return code, data
63
+ return None
64
+
65
+ # ── Range lookup ───────────────────────────────────────────────────────
66
+
67
+ def get_range(
68
+ self, panel_code: str, test_name: str, gender: str = "adult"
69
+ ) -> tuple[float, float] | None:
70
+ test = self.get_test(panel_code, test_name)
71
+ if not test:
72
+ return None
73
+ ranges = test.get("ranges", {})
74
+ gender_key = f"adult_{gender}" if gender in ("male", "female") else gender
75
+ r = ranges.get(gender_key) or ranges.get("adult")
76
+ if r:
77
+ lo = r.get("low", float("-inf"))
78
+ hi = r.get("high", float("inf"))
79
+ return lo, hi
80
+ return None
81
+
82
+ def classify(
83
+ self, panel_code: str, test_name: str, value: float, gender: str = "adult"
84
+ ) -> str:
85
+ rng = self.get_range(panel_code, test_name, gender)
86
+ if rng is None:
87
+ return "unknown"
88
+ lo, hi = rng
89
+ if value < lo:
90
+ return "low"
91
+ if value > hi:
92
+ return "high"
93
+ return "normal"
94
+
95
+ def get_severity(
96
+ self, panel_code: str, test_name: str, value: float
97
+ ) -> str | None:
98
+ """Return human-readable severity label from severity_thresholds if defined."""
99
+ test = self.get_test(panel_code, test_name)
100
+ if not test:
101
+ return None
102
+ thresholds = test.get("severity_thresholds", {})
103
+ if not thresholds:
104
+ return None
105
+ if "critical_low" in thresholds and value <= thresholds["critical_low"]:
106
+ return "critical_low"
107
+ if "severe_low" in thresholds and value <= thresholds["severe_low"]:
108
+ return "severe_low"
109
+ if "critical_high" in thresholds and value >= thresholds["critical_high"]:
110
+ return "critical_high"
111
+ if "acute_liver_failure" in thresholds and value >= thresholds["acute_liver_failure"]:
112
+ return "acute_liver_failure"
113
+ if "severe_elevation" in thresholds and value >= thresholds["severe_elevation"]:
114
+ return "severe_elevation"
115
+ if "moderate_elevation_x10" in thresholds and value >= thresholds["moderate_elevation_x10"]:
116
+ return "moderate_high"
117
+ if "mild_elevation_x3" in thresholds and value >= thresholds["mild_elevation_x3"]:
118
+ return "mild_high"
119
+ return None
120
+
121
+ # ── Context building ───────────────────────────────────────────────────
122
+
123
+ def build_panel_context(self, panel_code: str) -> str:
124
+ """Return a compact Arabic text summary of a panel's reference ranges."""
125
+ panel = self.get_panel(panel_code)
126
+ if not panel:
127
+ return ""
128
+ lines = [f"لوحة {panel.get('name_ar', panel_code)}:"]
129
+ for test_name, data in panel.get("tests", {}).items():
130
+ ranges = data.get("ranges", {})
131
+ adult = ranges.get("adult") or ranges.get("adult_male")
132
+ if adult:
133
+ lo, hi = adult.get("low", "?"), adult.get("high", "?")
134
+ unit = data.get("unit", "")
135
+ lines.append(f" {test_name}: {lo}–{hi} {unit}")
136
+ return "\n".join(lines)
backend/medical_kb/reference/normal_ranges.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fast in-memory normal range lookup.
3
+ Used for runtime validation — faster than loading JSON schemas.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ # (low, high) per gender key. Keys are lowercase, underscored.
8
+ NORMAL_RANGES: dict[str, dict[str, tuple[float, float]]] = {
9
+ # CBC
10
+ "hemoglobin": {"adult_male": (13.5, 17.5), "adult_female": (12.0, 15.5), "adult": (11.0, 17.5), "children": (11.5, 15.5)},
11
+ "hgb": {"adult_male": (13.5, 17.5), "adult_female": (12.0, 15.5), "adult": (11.0, 17.5)},
12
+ "hb": {"adult_male": (13.5, 17.5), "adult_female": (12.0, 15.5), "adult": (11.0, 17.5)},
13
+ "wbc": {"adult": (4.0, 11.0), "children": (5.0, 15.0)},
14
+ "rbc": {"adult_male": (4.5, 5.9), "adult_female": (4.0, 5.2), "adult": (4.0, 5.9)},
15
+ "platelets": {"adult": (150.0, 400.0)},
16
+ "plt": {"adult": (150.0, 400.0)},
17
+ "hematocrit": {"adult_male": (41.0, 53.0), "adult_female": (36.0, 48.0), "adult": (36.0, 53.0)},
18
+ "hct": {"adult_male": (41.0, 53.0), "adult_female": (36.0, 48.0), "adult": (36.0, 53.0)},
19
+ "mcv": {"adult": (80.0, 100.0)},
20
+ "mch": {"adult": (27.0, 33.0)},
21
+ "mchc": {"adult": (32.0, 36.0)},
22
+ "neutrophils": {"adult": (50.0, 70.0)},
23
+ "lymphocytes": {"adult": (20.0, 40.0)},
24
+ "monocytes": {"adult": (2.0, 8.0)},
25
+ "eosinophils": {"adult": (1.0, 4.0)},
26
+ "basophils": {"adult": (0.0, 1.0)},
27
+
28
+ # Thyroid
29
+ "tsh": {"adult": (0.4, 4.0), "pregnant": (0.1, 2.5), "elderly": (0.4, 5.0)},
30
+ "ft4": {"adult": (0.8, 1.8)},
31
+ "free_t4": {"adult": (0.8, 1.8)},
32
+ "ft3": {"adult": (2.3, 4.2)},
33
+ "free_t3": {"adult": (2.3, 4.2)},
34
+ "anti_tpo": {"adult": (0.0, 34.0)},
35
+ "anti-tpo": {"adult": (0.0, 34.0)},
36
+
37
+ # Liver
38
+ "alt": {"adult_male": (7.0, 40.0), "adult_female": (7.0, 35.0), "adult": (7.0, 40.0)},
39
+ "sgpt": {"adult_male": (7.0, 40.0), "adult_female": (7.0, 35.0), "adult": (7.0, 40.0)},
40
+ "ast": {"adult_male": (10.0, 40.0), "adult_female": (10.0, 35.0), "adult": (10.0, 40.0)},
41
+ "sgot": {"adult_male": (10.0, 40.0), "adult_female": (10.0, 35.0), "adult": (10.0, 40.0)},
42
+ "alp": {"adult": (44.0, 147.0), "children": (50.0, 350.0)},
43
+ "ggt": {"adult_male": (8.0, 61.0), "adult_female": (5.0, 36.0), "adult": (5.0, 61.0)},
44
+ "bilirubin": {"adult": (0.2, 1.2)},
45
+ "total_bilirubin":{"adult": (0.2, 1.2)},
46
+ "direct_bilirubin":{"adult": (0.0, 0.3)},
47
+ "albumin": {"adult": (3.5, 5.0)},
48
+ "alb": {"adult": (3.5, 5.0)},
49
+ "total_protein": {"adult": (6.0, 8.3)},
50
+
51
+ # Kidney
52
+ "creatinine": {"adult_male": (0.7, 1.3), "adult_female": (0.5, 1.1), "adult": (0.5, 1.3)},
53
+ "bun": {"adult": (8.0, 25.0)},
54
+ "urea": {"adult": (15.0, 45.0)},
55
+ "uric_acid": {"adult_male": (3.4, 7.0), "adult_female": (2.4, 6.0), "adult": (2.4, 7.0)},
56
+ "egfr": {"adult": (60.0, 999.0)},
57
+
58
+ # Lipids
59
+ "cholesterol": {"adult": (0.0, 200.0)},
60
+ "ldl": {"adult": (0.0, 100.0)},
61
+ "hdl": {"adult_male": (40.0, 999.0), "adult_female": (50.0, 999.0), "adult": (40.0, 999.0)},
62
+ "triglycerides": {"adult": (0.0, 150.0)},
63
+ "tg": {"adult": (0.0, 150.0)},
64
+
65
+ # Diabetes
66
+ "glucose": {"adult": (70.0, 99.0)},
67
+ "fasting_glucose":{"adult": (70.0, 99.0)},
68
+ "hba1c": {"adult": (4.0, 5.6)},
69
+ "a1c": {"adult": (4.0, 5.6)},
70
+
71
+ # Electrolytes
72
+ "sodium": {"adult": (136.0, 145.0)},
73
+ "potassium": {"adult": (3.5, 5.0)},
74
+ "chloride": {"adult": (98.0, 106.0)},
75
+ "calcium": {"adult": (8.5, 10.5)},
76
+ "magnesium": {"adult": (1.7, 2.2)},
77
+ "phosphorus": {"adult": (2.5, 4.5)},
78
+
79
+ # Iron studies
80
+ "ferritin": {"adult_male": (24.0, 336.0), "adult_female": (11.0, 307.0), "adult": (11.0, 336.0)},
81
+ "iron": {"adult": (60.0, 170.0)},
82
+ "tibc": {"adult": (250.0, 370.0)},
83
+
84
+ # Vitamins
85
+ "vitamin_d": {"adult": (20.0, 50.0)},
86
+ "vitamin_b12": {"adult": (200.0, 900.0)},
87
+ "b12": {"adult": (200.0, 900.0)},
88
+ "folate": {"adult": (2.7, 17.0)},
89
+
90
+ # Inflammation
91
+ "crp": {"adult": (0.0, 5.0)},
92
+ "esr": {"adult_male": (0.0, 15.0), "adult_female": (0.0, 20.0), "adult": (0.0, 20.0)},
93
+
94
+ # Coagulation
95
+ "pt": {"adult": (11.0, 13.5)},
96
+ "inr": {"adult": (0.8, 1.2)},
97
+ "aptt": {"adult": (25.0, 35.0)},
98
+
99
+ # Hormones
100
+ "prolactin": {"adult_male": (2.0, 18.0), "adult_female": (2.0, 29.0), "adult": (2.0, 29.0)},
101
+ "testosterone": {"adult_male": (300.0, 1000.0), "adult_female": (15.0, 70.0)},
102
+ "cortisol": {"adult": (5.0, 23.0)},
103
+ }
104
+
105
+
106
+ def lookup(name: str, gender: str = "adult") -> tuple[float, float] | None:
107
+ """
108
+ Fast range lookup. Returns (low, high) or None.
109
+ name: test name (case-insensitive, spaces → underscores)
110
+ gender: 'adult', 'adult_male', 'adult_female', 'children', 'pregnant', 'elderly'
111
+ """
112
+ key = name.lower().strip().replace(" ", "_").replace("-", "_").replace("/", "_")
113
+ entry = NORMAL_RANGES.get(key)
114
+ if not entry:
115
+ return None
116
+ gender_key = f"adult_{gender}" if gender in ("male", "female") else gender
117
+ return entry.get(gender_key) or entry.get("adult")
118
+
119
+
120
+ def classify(name: str, value: float, gender: str = "adult") -> str:
121
+ """Return 'low', 'normal', 'high', or 'unknown'."""
122
+ rng = lookup(name, gender)
123
+ if rng is None:
124
+ return "unknown"
125
+ lo, hi = rng
126
+ if value < lo:
127
+ return "low"
128
+ if value > hi:
129
+ return "high"
130
+ return "normal"
backend/medical_kb/schemas/cbc.json ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "panel_code": "cbc",
3
+ "name_ar": "صورة الدم الكاملة",
4
+ "name_en": "Complete Blood Count",
5
+ "specialty": "hematology",
6
+ "icd10_related": ["D50", "D51", "D52", "D53", "D64", "D69"],
7
+ "tests": {
8
+ "Hemoglobin": {
9
+ "name_ar": "هيموجلوبين",
10
+ "abbreviations": ["HGB", "Hgb", "Hb", "هيموجلوبين", "خضاب الدم"],
11
+ "unit": "g/dL",
12
+ "ranges": {
13
+ "adult_male": {"low": 13.5, "high": 17.5},
14
+ "adult_female": {"low": 12.0, "high": 15.5},
15
+ "children_6_12": {"low": 11.5, "high": 15.5},
16
+ "pregnant": {"low": 11.0, "high": 14.0},
17
+ "elderly_male": {"low": 12.5, "high": 17.0},
18
+ "elderly_female":{"low": 11.5, "high": 15.0}
19
+ },
20
+ "severity_thresholds": {
21
+ "critical_low": 7.0,
22
+ "severe_low": 8.0,
23
+ "mild_low_female": 11.0,
24
+ "mild_low_male": 12.5
25
+ },
26
+ "clinical_meaning_ar": "البروتين الذي يحمل الأكسجين في خلايا الدم الحمراء",
27
+ "high_causes_ar": ["الجفاف الشديد", "داء الكثرة الحمراء", "التدخين المزمن", "الإقامة في المرتفعات", "أمراض رئوية مزمنة"],
28
+ "low_causes_ar": ["نقص الحديد (الأكثر شيوعاً)", "نقص فيتامين B12", "نقص حمض الفوليك", "نزيف مزمن", "أمراض مزمنة (كلى، كبد)", "الثلاسيميا", "فقر الدم الانحلالي"],
29
+ "symptoms_low_ar": ["تعب وإرهاق غير معتاد", "شحوب الجلد والملتحمة", "ضيق تنفس عند المجهود", "خفقان وسرعة القلب", "دوخة وصداع", "برودة اليدين والقدمين", "هشاشة الأظافر وتساقط الشعر"],
30
+ "followup_tests": ["Ferritin", "Serum Iron", "TIBC", "Vitamin B12", "Folate", "Reticulocyte count", "Peripheral blood smear"],
31
+ "patient_explanation_ar": "الهيموجلوبين هو البروتين الحامل للأكسجين في دمك — مثل الحافلة التي تنقل الأكسجين من رئتيك لكل خلية في جسمك"
32
+ },
33
+ "WBC": {
34
+ "name_ar": "خلايا الدم البيضاء",
35
+ "abbreviations": ["WBC", "Leukocytes", "TLC", "كريات بيضاء", "كريات الدم البيضاء"],
36
+ "unit": "10³/μL",
37
+ "ranges": {
38
+ "adult": {"low": 4.0, "high": 11.0},
39
+ "children": {"low": 5.0, "high": 15.0},
40
+ "neonates": {"low": 9.0, "high": 30.0}
41
+ },
42
+ "severity_thresholds": {
43
+ "critical_low": 2.0,
44
+ "severe_low": 3.0,
45
+ "critical_high": 30.0,
46
+ "leukocytosis": 11.0,
47
+ "leukopenia": 4.0
48
+ },
49
+ "clinical_meaning_ar": "خلايا الجهاز المناعي التي تقاوم العدوى والمرض",
50
+ "high_causes_ar": ["عدوى بكتيرية (الأكثر شيوعاً)", "الالتهابات الحادة", "الإجهاد الجسدي الشديد", "بعض الأدوية (كورتيكوستيرويد)", "سرطان الدم (نادراً)", "تدخين السجائر"],
51
+ "low_causes_ar": ["عدوى فيروسية (إنفلونزا، كوفيد)", "أمراض نقص المناعة", "أدوية الكيموثيرابي", "نقص B12 أو الفوليك", "أمراض المناعة الذاتية", "تليف نخاع العظم"],
52
+ "followup_tests": ["Differential count (Neutrophils, Lymphocytes, Eosinophils)", "Blood culture if infection suspected", "CBC with differential"],
53
+ "patient_explanation_ar": "خلايا الدم البيضاء هي جيش جهازك المناعي — تحارب الجراثيم والفيروسات وتحمي جسمك"
54
+ },
55
+ "RBC": {
56
+ "name_ar": "خلايا الدم الحمراء",
57
+ "abbreviations": ["RBC", "Erythrocytes", "كريات حمراء"],
58
+ "unit": "10⁶/μL",
59
+ "ranges": {
60
+ "adult_male": {"low": 4.5, "high": 5.9},
61
+ "adult_female": {"low": 4.0, "high": 5.2}
62
+ },
63
+ "clinical_meaning_ar": "الخلايا الحاملة للهيموجلوبين والأكسجين",
64
+ "high_causes_ar": ["الجفاف", "داء الكثرة الحمراء", "التدخين"],
65
+ "low_causes_ar": ["فقر الدم", "النزيف", "نقص المغذيات"],
66
+ "patient_explanation_ar": "هي الخلايا الحمراء الصغيرة التي تحمل الأكسجين في مجرى الدم — كل واحدة تعيش حوالي 120 يوماً"
67
+ },
68
+ "Platelets": {
69
+ "name_ar": "الصفائح الدموية",
70
+ "abbreviations": ["PLT", "Thrombocytes", "صفائح", "صفيحات"],
71
+ "unit": "10³/μL",
72
+ "ranges": {
73
+ "adult": {"low": 150, "high": 400}
74
+ },
75
+ "severity_thresholds": {
76
+ "critical_low": 20,
77
+ "severe_low": 50,
78
+ "thrombocytopenia": 150,
79
+ "thrombocytosis": 400
80
+ },
81
+ "clinical_meaning_ar": "خلايا صغيرة تسبق تكوين جلطة الجرح وتوقف النزيف",
82
+ "high_causes_ar": ["ردّ فعل لالتهاب أو عدوى", "نقص الحديد", "استئصال الطحال", "نقاوي أولي (نادر)"],
83
+ "low_causes_ar": ["فشل نخاع العظم", "أمراض الكبد", "الأدوية (الهيبارين، بعض المضادات الحيوية)", "أمراض المناعة الذاتية (ITP)", "نقص B12"],
84
+ "danger_signs_ar": "< 50: خطر نزيف تلقائي — < 20: خطر نزيف داخلي طارئ",
85
+ "patient_explanation_ar": "الصفائح خلايا صغيرة جداً تجتمع عند الجرح لتشكّل سدادة طبيعية توقف النزيف"
86
+ },
87
+ "Hematocrit": {
88
+ "name_ar": "الهيماتوكريت",
89
+ "abbreviations": ["HCT", "PCV", "Hct"],
90
+ "unit": "%",
91
+ "ranges": {
92
+ "adult_male": {"low": 41, "high": 53},
93
+ "adult_female": {"low": 36, "high": 48}
94
+ },
95
+ "clinical_meaning_ar": "نسبة حجم خلايا الدم الحمراء من الحجم الكلي للدم — يتحرك دائماً مع الهيموجلوبين",
96
+ "patient_explanation_ar": "لو فصلنا دمك في أنبوب — الهيماتوكريت هو نسبة الجزء الأحمر (الخلايا) من إجمالي الدم"
97
+ },
98
+ "MCV": {
99
+ "name_ar": "متوسط حجم الكريات الحمراء",
100
+ "abbreviations": ["MCV", "Mean Corpuscular Volume"],
101
+ "unit": "fL",
102
+ "ranges": {
103
+ "adult": {"low": 80, "high": 100}
104
+ },
105
+ "interpretation_guide": {
106
+ "microcytic_low": "< 80 fL: كريات صغيرة → نقص الحديد أو الثلاسيميا",
107
+ "normocytic": "80-100 fL: كريات طبيعية الحجم",
108
+ "macrocytic_high": "> 100 fL: كريات كبيرة → نقص B12 أو الفوليك"
109
+ },
110
+ "clinical_meaning_ar": "يُحدد نوع فقر الدم: صغير الحجم (حديدي) أو كبير الحجم (B12)",
111
+ "patient_explanation_ar": "يقيس حجم كل كرية دم حمراء — مفيد جداً لمعرفة سبب فقر الدم إذا وُجد"
112
+ },
113
+ "MCH": {
114
+ "name_ar": "متوسط كمية هيموجلوبين الكرية",
115
+ "abbreviations": ["MCH"],
116
+ "unit": "pg",
117
+ "ranges": {
118
+ "adult": {"low": 27, "high": 33}
119
+ },
120
+ "clinical_meaning_ar": "كمية الهيموجلوبين في كل خلية حمراء — يُكمل تفسير MCV"
121
+ },
122
+ "Neutrophils": {
123
+ "name_ar": "العدلات",
124
+ "abbreviations": ["NEU", "NEUT", "Neutrophils", "PMN", "نيتروفيل"],
125
+ "unit": "%",
126
+ "ranges": {
127
+ "adult": {"low": 50, "high": 70}
128
+ },
129
+ "clinical_meaning_ar": "أكثر خلايا الدم البيضاء شيوعاً — الخط الأول ضد العدوى البكتيرية",
130
+ "high_interpretation": "Neutrophilia (>70%) → bacterial infection / inflammation / steroid use",
131
+ "low_interpretation": "Neutropenia (<50%) → viral infection / bone marrow suppression"
132
+ },
133
+ "Lymphocytes": {
134
+ "name_ar": "الخلايا الليمفاوية",
135
+ "abbreviations": ["LYM", "LYMPH", "لمفاوية"],
136
+ "unit": "%",
137
+ "ranges": {
138
+ "adult": {"low": 20, "high": 40}
139
+ },
140
+ "clinical_meaning_ar": "تنتج الأجسام المضادة وتحفظ الذاكرة المناعية — ترتفع في العدوى الفيروسية"
141
+ },
142
+ "Eosinophils": {
143
+ "name_ar": "الحمضات",
144
+ "abbreviations": ["EOS", "Eosinophils", "حمضات"],
145
+ "unit": "%",
146
+ "ranges": {
147
+ "adult": {"low": 1, "high": 4}
148
+ },
149
+ "clinical_meaning_ar": "ترتفع في الحساسية والديدان الطفيلية",
150
+ "high_causes_ar": ["أمراض الحساسية (الربو، أكزيما)", "الديدان الطفيلية", "بعض الأدوية"]
151
+ }
152
+ }
153
+ }
backend/medical_kb/schemas/diabetes.json ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "panel_code": "diabetes",
3
+ "name_ar": "سكري ومقاومة الإنسولين",
4
+ "name_en": "Diabetes & Glucose Metabolism Panel",
5
+ "specialty": "endocrinology",
6
+ "icd10_related": ["E11", "E10", "E13", "E14", "R73", "Z13.1"],
7
+ "tests": {
8
+ "Glucose": {
9
+ "name_ar": "سكر الدم الصيامي",
10
+ "abbreviations": ["FBS", "FBG", "Glucose", "Blood Sugar", "سكر الصيام", "سكر الدم"],
11
+ "unit": "mg/dL",
12
+ "ranges": {
13
+ "adult_normal": {"low": 70.0, "high": 99.0},
14
+ "adult_prediabetes":{"low": 100.0, "high": 125.0},
15
+ "adult": {"low": 70.0, "high": 99.0},
16
+ "children": {"low": 70.0, "high": 100.0}
17
+ },
18
+ "severity_thresholds": {
19
+ "hypoglycemia_critical": 54.0,
20
+ "hypoglycemia": 70.0,
21
+ "normal_upper": 99.0,
22
+ "prediabetes_lower": 100.0,
23
+ "prediabetes_upper": 125.0,
24
+ "diabetes_diagnosis": 126.0,
25
+ "hyperglycemia_severe": 300.0,
26
+ "dka_risk": 400.0,
27
+ "critical_high": 600.0
28
+ },
29
+ "clinical_meaning_ar": "مستوى الجلوكوز في الدم بعد 8 ساعات صيام — المقياس الأساسي لتشخيص السكري",
30
+ "interpretation_matrix": {
31
+ "< 54 mg/dL": "نقص سكر حاد — طارئ طبي فوري",
32
+ "54-69 mg/dL": "نقص سكر — تناول جلوكوز فوراً",
33
+ "70-99 mg/dL": "طبيعي",
34
+ "100-125 mg/dL": "ما قبل السكري (Prediabetes) — تغيير نمط الحياة ضروري",
35
+ ">= 126 mg/dL": "مشكوك بالسكري — يحتاج تأكيداً بفحص ثانٍ أو HbA1c",
36
+ ">= 300 mg/dL": "ارتفاع شديد — خطر الحماض الكيتوني"
37
+ },
38
+ "high_causes_ar": ["مرض السكري النوع 1 أو 2", "ما قبل السكري", "الإجهاد الجسدي أو النفسي الشديد", "الكورتيزون والستيرويدات", "فرط نشاط الغدة الدرقية", "أمراض البنكرياس", "عدم الالتزام بالصيام قبل الفحص"],
39
+ "low_causes_ar": ["جرعة زائدة من الإنسولين أو أدوية السكري", "الصيام المطوّل", "ورم الأنسولين (Insulinoma)", "فشل الكبد الشديد", "قصور الغدة الكظرية"],
40
+ "symptoms_high_ar": ["عطش شديد وكثرة التبول", "تعب وضعف عام", "ضبابية الرؤية", "التئام بطيء للجروح", "تنميل في القدمين"],
41
+ "symptoms_low_ar": ["تعرق بارد ورجفة", "دوخة وضعف مفاجئ", "خفقان وتسارع القلب", "جوع شديد مفاجئ", "تشوش ذهني وتهيج"],
42
+ "followup_tests": ["HbA1c", "Fasting Insulin", "C-Peptide", "HOMA-IR", "Urine Microalbumin", "Lipid Panel"],
43
+ "patient_explanation_ar": "سكر الصيام مثل فحص درجة حرارة محرك سيارتك وهي باردة — يُظهر كيف يتعامل جسمك مع الجلوكوز في وضع الراحة"
44
+ },
45
+ "HbA1c": {
46
+ "name_ar": "الهيموجلوبين السكري التراكمي",
47
+ "abbreviations": ["HbA1c", "A1C", "Glycated Hemoglobin", "سكر تراكمي", "الجليكوزيلاتد"],
48
+ "unit": "%",
49
+ "ranges": {
50
+ "adult_normal": {"low": 4.0, "high": 5.6},
51
+ "adult_prediabetes":{"low": 5.7, "high": 6.4},
52
+ "adult": {"low": 4.0, "high": 5.6},
53
+ "diabetic_target": {"low": 0.0, "high": 7.0}
54
+ },
55
+ "severity_thresholds": {
56
+ "normal": 5.6,
57
+ "prediabetes_lower": 5.7,
58
+ "prediabetes_upper": 6.4,
59
+ "diabetes_diagnosis": 6.5,
60
+ "good_control_diabetic": 7.0,
61
+ "poor_control": 8.0,
62
+ "very_poor_control": 10.0,
63
+ "critical": 14.0
64
+ },
65
+ "clinical_meaning_ar": "يعكس متوسط سكر الدم خلال الـ 90 يوم الماضية — لأن الهيموجلوبين يرتبط بالجلوكوز طوال عمر خلية الدم الحمراء",
66
+ "interpretation_matrix": {
67
+ "< 5.7%": "طبيعي — لا سكري",
68
+ "5.7-6.4%": "ما قبل السكري — خطر تطور السكري، تغيير نمط الحياة مطلوب",
69
+ ">= 6.5%": "سكري — يحتاج تأكيداً أو علاجاً",
70
+ "< 7% (مريض سكري)": "ضبط جيد للسكري",
71
+ "7-8% (مريض سكري)": "ضبط مقبول — تعديل العلاج",
72
+ "> 8% (مريض سكري)": "ضبط سيئ — خطر مضاعفات عالٍ",
73
+ "> 10%": "ضبط خطير جداً — مراجعة فورية"
74
+ },
75
+ "high_causes_ar": ["مرض السكري غير المُسيطر عليه", "عدم الالتزام بالدواء أو الحمية", "فقر الدم الانحلالي (يُعطي قراءة كاذبة منخفضة)", "مرض الكلى المزمن (يرفع القراءة)"],
76
+ "low_causes_ar": ["فقر الدم الانحلالي (تُسرَّع خلايا الدم الحمراء — قراءة كاذبة منخفضة)", "نقص الحديد (قراءة كاذبة)", "تهيج خلايا الدم الحمراء"],
77
+ "followup_tests": ["Fasting Glucose", "Post-meal Glucose", "Fasting Insulin", "C-Peptide", "Kidney Function", "Lipid Panel", "Urine Microalbumin"],
78
+ "patient_explanation_ar": "HbA1c مثل كاميرا مُراقبة تسجّل سكرك لمدة 3 أشهر — بعكس سكر الصيام اليومي الذي يتأثر بيوم واحد"
79
+ },
80
+ "Insulin": {
81
+ "name_ar": "الإنسولين الصيامي",
82
+ "abbreviations": ["Insulin", "Fasting Insulin", "إنسولين", "انسولين"],
83
+ "unit": "µIU/mL",
84
+ "ranges": {
85
+ "adult": {"low": 2.0, "high": 25.0},
86
+ "adult_optimal": {"low": 2.0, "high": 10.0}
87
+ },
88
+ "severity_thresholds": {
89
+ "optimal_upper": 10.0,
90
+ "normal_upper": 25.0,
91
+ "insulin_resistance_risk": 15.0,
92
+ "high": 30.0,
93
+ "critical_low": 1.0
94
+ },
95
+ "clinical_meaning_ar": "هرمون يُفرزه البنكرياس لإدخال الجلوكوز للخلايا — ارتفاعه الصيامي يشير لمقاومة الإنسولين",
96
+ "high_causes_ar": ["مقاومة الإنسولين (السكري النوع 2 المبكر)", "السمنة خاصةً الدهون البطنية", "متلازمة تكيس المبايض PCOS", "الخمول الجسدي", "ورم الأنسولين (Insulinoma) — نادر"],
97
+ "low_causes_ar": ["السكري النوع 1 (لا يُفرز البنكرياس كافياً)", "البنكرياس المتلف", "جرعة زائدة من أدوية السكري"],
98
+ "symptoms_high_ar": ["زيادة وزن مستمرة رغم الحمية", "تعب بعد الوجبات", "رغبة شديدة في الحلويات", "صعوبة خسارة الوزن", "أعراض PCOS عند النساء"],
99
+ "followup_tests": ["HOMA-IR", "HbA1c", "Fasting Glucose", "C-Peptide", "Testosterone (للنساء)", "Lipid Panel"],
100
+ "patient_explanation_ar": "الإنسولين مثل مفتاح يفتح خلاياك للجلوكوز — عندما ترتفع مستوياته صيامياً، يعني الأقفال بدأت تقاوم المفتاح"
101
+ },
102
+ "HOMA_IR": {
103
+ "name_ar": "مؤشر مقاومة الإنسولين",
104
+ "abbreviations": ["HOMA-IR", "Insulin Resistance", "HOMA IR", "مقاومة الإنسولين"],
105
+ "unit": "حاصل",
106
+ "ranges": {
107
+ "adult_normal": {"low": 0.0, "high": 2.0},
108
+ "adult": {"low": 0.0, "high": 2.0}
109
+ },
110
+ "severity_thresholds": {
111
+ "normal": 2.0,
112
+ "borderline": 2.5,
113
+ "insulin_resistance": 3.0,
114
+ "severe": 5.0
115
+ },
116
+ "clinical_meaning_ar": "يُحسب من (الإنسولين × سكر الصيام) ÷ 405 — يقيس درجة مقاومة الإنسولين",
117
+ "interpretation_matrix": {
118
+ "HOMA-IR < 2.0": "حساسية إنسولين طبيعية",
119
+ "HOMA-IR 2.0-2.9": "مقاومة إنسولين حدية — انتبه للنمط الغذائي والحركة",
120
+ "HOMA-IR >= 3.0": "مقاومة إنسولين — خطر سكري النوع 2 ومتلازمة الأيض",
121
+ "HOMA-IR >= 5.0": "مقاومة إنسولين شديدة — تدخل طبي ضروري"
122
+ },
123
+ "high_causes_ar": ["السمنة خاصةً الدهون البطنية", "الخمول الجسدي", "متلازمة الأيض", "PCOS", "النوم المضطرب وتوقف التنفس أثناء النوم", "النظام الغذائي الغني بالسكريات"],
124
+ "followup_tests": ["Fasting Glucose", "Insulin", "HbA1c", "Lipid Panel", "ALT (NAFLD)", "Testosterone (PCOS)"],
125
+ "patient_explanation_ar": "HOMA-IR رقم مُحسوب يُظهر مدى قدرة جسمك على الاستجابة للإنسولين — كلما ارتفع، كلما كان جسمك يقاوم التعليمات أكثر"
126
+ }
127
+ }
128
+ }
backend/medical_kb/schemas/kidney.json ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "panel_code": "kidney",
3
+ "name_ar": "وظائف الكلى",
4
+ "name_en": "Kidney Function Tests",
5
+ "specialty": "nephrology",
6
+ "icd10_related": ["N17", "N18", "N19", "N28", "E11.65"],
7
+ "tests": {
8
+ "Creatinine": {
9
+ "name_ar": "كرياتينين",
10
+ "abbreviations": ["Cr", "Creat", "كرياتينين"],
11
+ "unit": "mg/dL",
12
+ "ranges": {
13
+ "adult_male": {"low": 0.7, "high": 1.3},
14
+ "adult_female": {"low": 0.5, "high": 1.1},
15
+ "adult": {"low": 0.5, "high": 1.3},
16
+ "children": {"low": 0.3, "high": 0.7},
17
+ "elderly_male": {"low": 0.7, "high": 1.4},
18
+ "elderly_female":{"low": 0.5, "high": 1.2}
19
+ },
20
+ "severity_thresholds": {
21
+ "critical_high": 10.0,
22
+ "severe_high": 5.0,
23
+ "moderate_high": 2.0,
24
+ "ckd_stage3": 2.0,
25
+ "ckd_stage4": 4.0
26
+ },
27
+ "clinical_meaning_ar": "نفاية عضلية تُفرز عبر الكلى — مؤشر مباشر لكفاءة الترشيح الكلوي",
28
+ "high_causes_ar": ["الفشل الكلوي الحاد أو المزمن", "الجفاف الشديد", "انسداد المسالك البولية", "الرياضة المكثفة", "ارتفاع تناول البروتين", "أدوية سامة للكلى (NSAIDs, أمينوغليكوزيد)", "رابدوميوليسيس (تحلل العضلات)"],
29
+ "low_causes_ar": ["ضمور العضلات أو فقدان الكتلة العضلية", "سوء التغذية الشديد", "الحمل (بسبب زيادة معدل الترشيح)", "كبار السن بكتلة عضلية قليلة"],
30
+ "symptoms_high_ar": ["انخفاض كمية البول أو توقفها", "تورم الأطراف والوجه", "غثيان وقيء", "تعب شديد وضعف عام", "ضيق تنفس", "تشوش ذهني في الحالات الشديدة"],
31
+ "followup_tests": ["eGFR", "BUN", "Urine Creatinine", "Urine Protein", "Electrolytes", "Urinalysis", "Kidney Ultrasound"],
32
+ "patient_explanation_ar": "الكرياتينين مثل مقياس دقة مرشّح الكلى — كلما ارتفع، كلما كان المرشّح أقل كفاءة"
33
+ },
34
+ "BUN": {
35
+ "name_ar": "نيتروجين يوريا الدم",
36
+ "abbreviations": ["BUN", "Blood Urea Nitrogen", "يوريا"],
37
+ "unit": "mg/dL",
38
+ "ranges": {
39
+ "adult": {"low": 8.0, "high": 25.0},
40
+ "elderly": {"low": 8.0, "high": 30.0},
41
+ "children": {"low": 5.0, "high": 18.0}
42
+ },
43
+ "severity_thresholds": {
44
+ "critical_high": 100.0,
45
+ "severe_high": 60.0,
46
+ "moderate_high": 40.0,
47
+ "uremia_risk": 80.0
48
+ },
49
+ "clinical_meaning_ar": "نفاية التحلل البروتيني تُفرز عبر الكلى — يرتفع مع الفشل الكلوي وارتفاع البروتين",
50
+ "high_causes_ar": ["الفشل الكلوي", "الجفاف", "نزيف الجهاز الهضمي العلوي", "ارتفاع تناول البروتين", "الحمى والإجهاد الشديد", "فشل القلب الاحتقاني"],
51
+ "low_causes_ar": ["سوء التغذية (نقص البروتين)", "أمراض الكبد الشديدة (يعيق تصنيع اليوريا)", "الحمل", "الإفراط في شرب السوائل"],
52
+ "symptoms_high_ar": ["غثيان وفقدان الشهية", "تعب وضعف", "حكة جلدية في الحالات الشديدة", "تشوش ذهني (يوريمية)"],
53
+ "followup_tests": ["Creatinine", "BUN/Creatinine ratio", "eGFR", "Urinalysis"],
54
+ "patient_explanation_ar": "BUN هو بقايا هضم البروتين — الكلية السليمة تُزيله بكفاءة، فإذا ارتفع يعني الكلية تتعب"
55
+ },
56
+ "eGFR": {
57
+ "name_ar": "معدل الترشيح الكبيبي المقدّر",
58
+ "abbreviations": ["eGFR", "GFR", "معدل الترشيح"],
59
+ "unit": "mL/min/1.73m²",
60
+ "ranges": {
61
+ "adult": {"low": 60.0, "high": 999.0},
62
+ "young_adult": {"low": 90.0, "high": 999.0}
63
+ },
64
+ "severity_thresholds": {
65
+ "ckd_stage1": 90.0,
66
+ "ckd_stage2": 60.0,
67
+ "ckd_stage3a": 45.0,
68
+ "ckd_stage3b": 30.0,
69
+ "ckd_stage4": 15.0,
70
+ "ckd_stage5_dialysis": 15.0,
71
+ "critical_low": 10.0
72
+ },
73
+ "clinical_meaning_ar": "يقيس مدى قدرة الكلية على ترشيح الدم في الدقيقة — المقياس الذهبي لوظيفة الكلى",
74
+ "interpretation_matrix": {
75
+ "eGFR >= 90": "وظيفة كلوية طبيعية (CKD Stage 1 إذا كان مع علامات كلوية أخرى)",
76
+ "eGFR 60-89": "انخفاض طفيف — CKD Stage 2، تحتاج متابعة",
77
+ "eGFR 45-59": "انخفاض خفيف-معتدل — CKD Stage 3a، تعديل جرعات الأدوية",
78
+ "eGFR 30-44": "انخفاض معتدل-شديد — CKD Stage 3b، متابعة أمراض الكلى",
79
+ "eGFR 15-29": "انخفاض شديد — CKD Stage 4، التحضير للديلزة",
80
+ "eGFR < 15": "فشل كلوي — CKD Stage 5، ديلزة أو زراعة كلى"
81
+ },
82
+ "low_causes_ar": ["مرض الكلى المزمن", "مرض السكري (اعتلال الكلية السكري)", "ارتفاع ضغط الدم المزمن", "التهاب الكلية", "انسداد المسالك البولية المزمن", "أدوية سامة للكلى"],
83
+ "followup_tests": ["Creatinine", "Urine Albumin/Creatinine ratio", "Electrolytes", "Phosphorus", "PTH", "Hemoglobin"],
84
+ "patient_explanation_ar": "eGFR يخبرك كم في المئة من كلاوي تعمل — 100% طبيعي، أقل من 15% تحتاج ديلزة"
85
+ },
86
+ "Uric_Acid": {
87
+ "name_ar": "حمض اليوريك",
88
+ "abbreviations": ["UA", "Uric Acid", "حمض البول", "يورك"],
89
+ "unit": "mg/dL",
90
+ "ranges": {
91
+ "adult_male": {"low": 3.4, "high": 7.0},
92
+ "adult_female": {"low": 2.4, "high": 6.0},
93
+ "adult": {"low": 2.4, "high": 7.0},
94
+ "children": {"low": 2.0, "high": 5.5}
95
+ },
96
+ "severity_thresholds": {
97
+ "hyperuricemia_male": 7.0,
98
+ "hyperuricemia_female": 6.0,
99
+ "gout_risk": 8.0,
100
+ "critical_high": 12.0
101
+ },
102
+ "clinical_meaning_ar": "نفاية تحلل البورينات — يتراكم في المفاصل مسبباً النقرس عند ارتفاعه",
103
+ "high_causes_ar": ["النقرس (Gout)", "الفشل الكلوي", "الجفاف", "ارتفاع تناول الفركتوز واللحوم الحمراء", "بعض أدوية ضغط الدم (ثيازيد)", "كيموثيرابيا (تحلل الخلايا السرطانية)", "السمنة"],
104
+ "low_causes_ar": ["سوء التغذية", "أمراض الكبد الشديدة", "بعض الأدوية (الوبيورينول)", "نقص إنزيم Xanthine Oxidase (نادر)"],
105
+ "symptoms_high_ar": ["ألم مفاجئ وشديد في إبهام القدم (النقرس)", "احمرار وتورم المفاصل", "حصى الكلى (حمض اليوريك)", "ألم أسفل الظهر"],
106
+ "followup_tests": ["24hr Urine Uric Acid", "Creatinine", "eGFR", "Joint X-Ray"],
107
+ "patient_explanation_ar": "حمض اليوريك مثل رمل يتراكم في المفاصل — عندما يزيد يسبب نوبة ألم شديدة تُسمى النقرس"
108
+ }
109
+ }
110
+ }
backend/medical_kb/schemas/lipid.json ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "panel_code": "lipid",
3
+ "name_ar": "الدهون والكوليسترول",
4
+ "name_en": "Lipid Panel",
5
+ "specialty": "cardiology",
6
+ "icd10_related": ["E78", "E78.0", "E78.1", "E78.2", "E78.5", "I25"],
7
+ "tests": {
8
+ "Cholesterol": {
9
+ "name_ar": "الكوليسترول الكلي",
10
+ "abbreviations": ["TC", "Total Cholesterol", "كوليسترول", "كولسترول"],
11
+ "unit": "mg/dL",
12
+ "ranges": {
13
+ "adult": {"low": 0.0, "high": 200.0},
14
+ "borderline": {"low": 200.0, "high": 239.0},
15
+ "high_risk": {"low": 240.0, "high": 999.0}
16
+ },
17
+ "severity_thresholds": {
18
+ "desirable": 200.0,
19
+ "borderline_high": 239.0,
20
+ "high": 240.0,
21
+ "very_high": 300.0,
22
+ "critical_high": 400.0
23
+ },
24
+ "clinical_meaning_ar": "إجمالي الكوليسترول في الدم — يشمل LDL و HDL والأنواع الأخرى",
25
+ "high_causes_ar": ["النظام الغذائي الغني بالدهون المشبعة والمتحولة", "الوراثة (فرط كوليسترول الدم العائلي)", "قصور الغدة الدرقية", "مرض السكري غير المُسيطر عليه", "أمراض الكلى المزمنة", "بعض الأدوية (كورتيكوستيرويد، مدرات البول)"],
26
+ "low_causes_ar": ["سوء التغذية الشديد", "فرط نشاط الغدة الدرقية", "أمراض الكبد الشديدة", "فقر الدم الشديد"],
27
+ "symptoms_high_ar": ["عادةً بلا أعراض حتى حدوث مضاعفات", "ألم في الصدر (ذبحة صدرية)", "ألم الساقين عند المشي", "زانثوما (رواسب دهنية على الجلد في الحالات الشديدة)"],
28
+ "followup_tests": ["LDL", "HDL", "Triglycerides", "TSH", "Fasting Glucose", "HbA1c"],
29
+ "patient_explanation_ar": "الكوليسترول الكلي مثل مجموع رصيدك — لكن المهم توزيعه: هل هو كوليسترول جيد أم سيء"
30
+ },
31
+ "LDL": {
32
+ "name_ar": "الكوليسترول الضار",
33
+ "abbreviations": ["LDL", "LDL-C", "كوليسترول LDL", "الكوليسترول الضار"],
34
+ "unit": "mg/dL",
35
+ "ranges": {
36
+ "adult_optimal": {"low": 0.0, "high": 100.0},
37
+ "adult_near_optimal": {"low": 100.0, "high": 129.0},
38
+ "adult_borderline": {"low": 130.0, "high": 159.0},
39
+ "adult": {"low": 0.0, "high": 100.0}
40
+ },
41
+ "severity_thresholds": {
42
+ "optimal": 100.0,
43
+ "near_optimal": 129.0,
44
+ "borderline_high": 159.0,
45
+ "high": 189.0,
46
+ "very_high": 190.0,
47
+ "target_high_risk_patient": 70.0
48
+ },
49
+ "clinical_meaning_ar": "الكوليسترول السيء — يترسب على جدران الشرايين مسبباً تصلب الشرايين وأمراض القلب",
50
+ "high_causes_ar": ["النظام الغذائي الغني بالدهون المشبعة", "الوراثة", "قصور الغدة الدرقية", "السمنة والخمول الجسدي", "مرض السكري", "التدخين"],
51
+ "low_causes_ar": ["استخدام أدوية الستاتين", "النظام الغذائي النباتي", "فرط نشاط الغدة الدرقية"],
52
+ "symptoms_high_ar": ["لا أعراض مباشرة", "على المدى البعيد: نوبة قلبية، سكتة دماغية، ضعف في الأطراف"],
53
+ "followup_tests": ["Total Cholesterol", "HDL", "Triglycerides", "hs-CRP", "Coronary Calcium Score"],
54
+ "patient_explanation_ar": "LDL هو الكوليسترول السيء — يُكدّس الترسبات في الشرايين مثل الصدأ في الأنابيب"
55
+ },
56
+ "HDL": {
57
+ "name_ar": "الكوليسترول الجيد",
58
+ "abbreviations": ["HDL", "HDL-C", "كوليسترول HDL", "الكوليسترول الجيد"],
59
+ "unit": "mg/dL",
60
+ "ranges": {
61
+ "adult_male": {"low": 40.0, "high": 999.0},
62
+ "adult_female": {"low": 50.0, "high": 999.0},
63
+ "adult": {"low": 40.0, "high": 999.0},
64
+ "optimal": {"low": 60.0, "high": 999.0}
65
+ },
66
+ "severity_thresholds": {
67
+ "low_risk_male": 40.0,
68
+ "low_risk_female": 50.0,
69
+ "protective_high": 60.0,
70
+ "critical_low": 30.0
71
+ },
72
+ "clinical_meaning_ar": "الكوليسترول الجيد — يُزيل الكوليسترول الزائد من الشرايين ويعيده للكبد للتخلص منه",
73
+ "low_causes_ar": ["التدخين", "السمنة وقلة الحركة", "مرض السكري غير المُسيطر عليه", "الدهون الثلاثية المرتفعة جداً", "النظام الغذائي الغني بالكربوهيدرات المُكررة", "بعض الأدوية (بيتا بلوكر، ستيرويد)"],
74
+ "high_causes_ar": ["ممارسة الرياضة المنتظمة", "الإقلاع عن التدخين", "الدهون الأحادية غير ��لمشبعة (زيت الزيتون)", "الكحول باعتدال (غير موصى به طبياً)"],
75
+ "symptoms_low_ar": ["لا أعراض مباشرة للانخفاض", "على المدى البعيد: زيادة خطر أمراض القلب"],
76
+ "followup_tests": ["Total Cholesterol", "LDL", "Triglycerides", "Fasting Glucose"],
77
+ "patient_explanation_ar": "HDL هو الكوليسترول الجيد — يعمل كمكنسة تُزيل الكوليسترول الزائد من الشرايين. كلما ارتفع، كان أفضل"
78
+ },
79
+ "Triglycerides": {
80
+ "name_ar": "الدهون الثلاثية",
81
+ "abbreviations": ["TG", "TRIG", "Triglycerides", "دهون ثلاثية", "ثلاثيات الغليسريد"],
82
+ "unit": "mg/dL",
83
+ "ranges": {
84
+ "adult_normal": {"low": 0.0, "high": 150.0},
85
+ "adult_borderline": {"low": 150.0, "high": 199.0},
86
+ "adult_high": {"low": 200.0, "high": 499.0},
87
+ "adult": {"low": 0.0, "high": 150.0}
88
+ },
89
+ "severity_thresholds": {
90
+ "normal": 150.0,
91
+ "borderline": 199.0,
92
+ "high": 499.0,
93
+ "very_high": 500.0,
94
+ "pancreatitis_risk": 1000.0,
95
+ "critical": 2000.0
96
+ },
97
+ "clinical_meaning_ar": "الدهون الرئيسية المُخزَّنة في الجسم — ترتفع مع السكريات والكحول والخمول",
98
+ "high_causes_ar": ["النظام الغذائي الغني بالسكريات والكربوهيدرات المُكررة", "السمنة والخمول", "مرض السكري غير المُسيطر عليه", "الكحول", "قصور الغدة الدرقية", "الفشل الكلوي", "بعض الأدوية (ستيرويد، بيتا بلوكر)"],
99
+ "low_causes_ar": ["النظام الغذائي المنخفض الدهون والكربوهيدرات", "فرط نشاط الغدة الدرقية"],
100
+ "symptoms_high_ar": ["لا أعراض عادةً", "في الحالات الشديدة جداً: ألم البطن (التهاب البنكرياس)", "طفح جلدي دهني (زانثوما صفراء)", "ضبابية الدم (lipemia retinalis)"],
101
+ "followup_tests": ["Total Cholesterol", "LDL", "HDL", "Fasting Glucose", "HbA1c", "TSH"],
102
+ "patient_explanation_ar": "الدهون الثلاثية هي طاقة مُخزَّنة — ترتفع عندما تأكل سكريات أكثر مما يحتاجه جسمك"
103
+ }
104
+ }
105
+ }
backend/medical_kb/schemas/liver.json ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "panel_code": "liver",
3
+ "name_ar": "وظائف الكبد",
4
+ "name_en": "Liver Function Tests",
5
+ "specialty": "hepatology",
6
+ "icd10_related": ["K70", "K71", "K72", "K73", "K74", "K75", "K76", "B15", "B16", "B17", "B18"],
7
+ "interpretation_axis": "Evaluate in 3 dimensions: hepatocellular (ALT/AST) + cholestatic (ALP/GGT/Bili) + synthetic (Albumin/PT)",
8
+ "tests": {
9
+ "ALT": {
10
+ "name_ar": "ناقلة أمين الألانين",
11
+ "abbreviations": ["ALT", "SGPT", "ALT/SGPT", "Alanine Aminotransferase"],
12
+ "unit": "U/L",
13
+ "ranges": {
14
+ "adult_male": {"low": 7, "high": 40},
15
+ "adult_female": {"low": 7, "high": 35}
16
+ },
17
+ "severity_thresholds": {
18
+ "mild_elevation_x3": 120,
19
+ "moderate_elevation_x10": 400,
20
+ "severe_elevation": 1000,
21
+ "acute_liver_failure": 3000
22
+ },
23
+ "clinical_meaning_ar": "الإنزيم الأكثر تخصصاً لخلايا الكبد — ارتفاعه يدل على تلفها مباشرةً",
24
+ "high_causes_ar": [
25
+ "الكبد الدهني غير الكحولي (NAFLD) — الأكثر شيوعاً",
26
+ "التهاب الكبد الفيروسي (B وC وA)",
27
+ "التهاب الكبد الكحولي",
28
+ "الأدوية (باراسيتامول، ستاتينات، مضادات حيوية)",
29
+ "انسداد القنوات الصفراوية",
30
+ "قصور القلب (كبد احتقاني)"
31
+ ],
32
+ "severity_interpretation": {
33
+ "1_3x": "ارتفاع خفيف — متابعة وتغيير نمط الحياة",
34
+ "3_10x": "ارتفاع متوسط — تقييم طبي عاجل لتحديد السبب",
35
+ "above_10x": "⚠️ ارتفاع شديد — تلف كبدي حاد يستدعي طوارئ"
36
+ },
37
+ "followup_tests": ["AST", "GGT", "ALP", "Bilirubin", "Albumin", "PT/INR", "Viral hepatitis panel (HBsAg, Anti-HCV)", "Liver ultrasound"],
38
+ "patient_explanation_ar": "ALT إنزيم يُفرز من داخل خلايا الكبد عند تلفها — كلما ارتفع أكثر كلما كان التلف أشد"
39
+ },
40
+ "AST": {
41
+ "name_ar": "ناقلة أمين الأسبارتات",
42
+ "abbreviations": ["AST", "SGOT", "Aspartate Aminotransferase"],
43
+ "unit": "U/L",
44
+ "ranges": {
45
+ "adult_male": {"low": 10, "high": 40},
46
+ "adult_female": {"low": 10, "high": 35}
47
+ },
48
+ "clinical_meaning_ar": "أقل تخصصاً للكبد من ALT — يرتفع أيضاً مع أمراض القلب والعضلات",
49
+ "ast_alt_ratio": {
50
+ "ratio_2_to_1": "AST:ALT > 2:1 → يشير للتلف الكحولي بشكل خاص",
51
+ "alt_dominant": "ALT > AST → يشير لالتهاب فيروسي أو دهني",
52
+ "both_equal": "ارتفاع متساوٍ → التهاب غير محدد"
53
+ },
54
+ "patient_explanation_ar": "AST موجود في الكبد والقلب والعضلات — ارتفاعه وحده لا يعني بالضرورة مشكلة كبدية"
55
+ },
56
+ "ALP": {
57
+ "name_ar": "الفوسفاتاز القلوية",
58
+ "abbreviations": ["ALP", "Alkaline Phosphatase", "فوسفاتاز"],
59
+ "unit": "U/L",
60
+ "ranges": {
61
+ "adult": {"low": 44, "high": 147},
62
+ "children": {"low": 50, "high": 350},
63
+ "pregnant": {"low": 40, "high": 300}
64
+ },
65
+ "clinical_meaning_ar": "يرتفع مع انسداد القنوات الصفراوية وأمراض العظام — يُقيَّم مع GGT",
66
+ "high_causes_ar": [
67
+ "انسداد القنوات الصفراوية (حصى، ورم)",
68
+ "تليف الكبد الصفراوي الأولي (PBC)",
69
+ "أمراض العظام (داء بيجيت، كسور، ورم عظمي)",
70
+ "نمو طبيعي عند الأطفال والمراهقين",
71
+ "الثلث الثالث من الحمل (طبيعي)"
72
+ ],
73
+ "interpretation_with_GGT": "ALP مرتفع + GGT مرتفع → سبب كبدي/صفراوي | ALP مرتفع + GGT طبيعي → سبب عظمي",
74
+ "patient_explanation_ar": "هذا الإنزيم يوجد في الكبد وقنوات الصفراء والعظام — ارتفاعه يحتاج تفسيراً مع GGT"
75
+ },
76
+ "GGT": {
77
+ "name_ar": "غاما غلوتاميل ترانسفيراز",
78
+ "abbreviations": ["GGT", "Gamma-GT", "Gamma-Glutamyl Transferase"],
79
+ "unit": "U/L",
80
+ "ranges": {
81
+ "adult_male": {"low": 8, "high": 61},
82
+ "adult_female": {"low": 5, "high": 36}
83
+ },
84
+ "clinical_meaning_ar": "أحساس مؤشر لأمراض الكبد الكحولية والانسداد الصفراوي — يساعد في تفسير ALP",
85
+ "high_causes_ar": ["الكحول (الأكثر تأثيراً)", "الكبد الدهني", "انسداد صفراوي", "أدوية معينة (فينيتوين، فاروفارين)"],
86
+ "diagnostic_value": "إذا ارتفع ALP مع ارتفاع GGT → مصدر ��لكبد. إذا ارتفع GGT وحده → غالباً الكحول أو الأدوية"
87
+ },
88
+ "Bilirubin_Total": {
89
+ "name_ar": "البيليروبين الكلي",
90
+ "abbreviations": ["T.Bili", "Total Bilirubin", "بيليروبين كلي", "TBIL"],
91
+ "unit": "mg/dL",
92
+ "ranges": {
93
+ "adult": {"low": 0.2, "high": 1.2}
94
+ },
95
+ "severity_thresholds": {
96
+ "visible_jaundice": 2.5,
97
+ "moderate_jaundice": 5.0,
98
+ "severe_jaundice": 15.0
99
+ },
100
+ "clinical_meaning_ar": "صبغة ناتجة عن تكسّر الهيموجلوبين — يعالجه الكبد ويُفرزه في الصفراء",
101
+ "high_causes_ar": [
102
+ "التهاب الكبد الفيروسي",
103
+ "انسداد القنوات الصفراوية (حصى مرارة)",
104
+ "فقر الدم الانحلالي (تكسّر الدم)",
105
+ "متلازمة جيلبرت (حميدة — ترتفع مع الإجهاد والجوع)",
106
+ "تليف الكبد المتقدم"
107
+ ],
108
+ "patient_explanation_ar": "البيليروبين صبغة صفراء — عندما يرتفع يظهر اليرقان (اصفرار الجلد والعيون)"
109
+ },
110
+ "Bilirubin_Direct": {
111
+ "name_ar": "البيليروبين المباشر",
112
+ "abbreviations": ["D.Bili", "Direct Bilirubin", "Conjugated Bilirubin"],
113
+ "unit": "mg/dL",
114
+ "ranges": {
115
+ "adult": {"low": 0.0, "high": 0.3}
116
+ },
117
+ "clinical_meaning_ar": "البيليروبين المُعالَج من الكبد — ارتفاعه تحديداً يشير لمشكلة في الكبد أو الصفراء",
118
+ "interpretation": "Direct Bili > 50% من Total Bili → انسداد صفراوي أو أمراض الكبد الداخلية"
119
+ },
120
+ "Albumin": {
121
+ "name_ar": "الألبومين",
122
+ "abbreviations": ["ALB", "Albumin", "ألبومين"],
123
+ "unit": "g/dL",
124
+ "ranges": {
125
+ "adult": {"low": 3.5, "high": 5.0}
126
+ },
127
+ "severity_thresholds": {
128
+ "mild_low": 3.0,
129
+ "moderate_low": 2.5,
130
+ "severe_low": 2.0
131
+ },
132
+ "clinical_meaning_ar": "البروتين الأكثر إنتاجاً من الكبد — يعكس القدرة التركيبية للكبد وحالة التغذية",
133
+ "high_causes_ar": ["الجفاف (نسبياً)"],
134
+ "low_causes_ar": [
135
+ "قصور الكبد المزمن أو التليف",
136
+ "سوء التغذية الشديد",
137
+ "متلازمة الكلاء (فقدان الألبومين في البول)",
138
+ "الالتهاب الحاد والمزمن",
139
+ "سوء الامتصاص"
140
+ ],
141
+ "clinical_importance": "الألبومين المنخفض هو من أقوى مؤشرات ضعف وظيفة الكبد التركيبية",
142
+ "patient_explanation_ar": "الألبومين بروتين يصنعه كبدك ويضخه للدم — انخفاضه دليل على أن الكبد يعمل بكفاءة أقل"
143
+ }
144
+ }
145
+ }
backend/medical_kb/schemas/thyroid.json ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "panel_code": "thyroid",
3
+ "name_ar": "وظائف الغدة الدرقية",
4
+ "name_en": "Thyroid Function Tests",
5
+ "specialty": "endocrinology",
6
+ "icd10_related": ["E00", "E01", "E02", "E03", "E04", "E05", "E06"],
7
+ "interpretation_axis": "Always start from TSH — it is the most sensitive indicator",
8
+ "tests": {
9
+ "TSH": {
10
+ "name_ar": "الهرمون المحفز للغدة الدرقية",
11
+ "abbreviations": ["TSH", "Thyroid Stimulating Hormone", "ثيروتروبين"],
12
+ "unit": "mIU/L",
13
+ "ranges": {
14
+ "adult": {"low": 0.4, "high": 4.0},
15
+ "pregnant_T1": {"low": 0.1, "high": 2.5},
16
+ "pregnant_T2": {"low": 0.2, "high": 3.0},
17
+ "pregnant_T3": {"low": 0.3, "high": 3.0},
18
+ "elderly": {"low": 0.4, "high": 5.0},
19
+ "children_1_6": {"low": 0.8, "high": 6.0},
20
+ "neonates": {"low": 1.0, "high": 20.0}
21
+ },
22
+ "severity_thresholds": {
23
+ "critical_low": 0.01,
24
+ "suppressed": 0.1,
25
+ "subclinical_high": 4.0,
26
+ "overt_high": 10.0
27
+ },
28
+ "clinical_meaning_ar": "هرمون النخامية الذي يتحكم في نشاط الغدة الدرقية — المؤشر الأول والأحساس",
29
+ "high_causes_ar": ["قصور الغدة الدرقية الأولي (هاشيموتو الأكثر شيوعاً)", "قصور ما بعد الولادة", "التهاب الغدة الدرقية تحت الحاد", "نقص اليود الشديد"],
30
+ "low_causes_ar": ["فرط نشاط الغدة الدرقية (مرض جريفز الأكثر شيوعاً)", "عقيدات درقية فاعلة", "التهاب الغدة الدرقية الصامت", "جرعة زائدة من الليفوثيروكسين"],
31
+ "interpretation_matrix": {
32
+ "TSH_high_FT4_low": "قصور درقي أولي — يحتاج ليفوثيروكسين",
33
+ "TSH_low_FT4_high": "فرط نشاط درقي واضح — يحتاج تقييماً متخصصاً عاجلاً",
34
+ "TSH_high_FT4_normal": "قصور تحت سريري — متابعة كل 3-6 أشهر",
35
+ "TSH_low_FT4_normal": "فرط نشاط تحت سريري — متابعة، علاج إذا ≥ 65 عاماً أو أعراض"
36
+ },
37
+ "followup_tests": ["Free T4", "Free T3", "Anti-TPO", "Anti-Thyroglobulin", "Thyroid ultrasound"],
38
+ "patient_explanation_ar": "TSH هو الهرمون الذي يصدره دماغك لأمر الغدة الدرقية — مرتفع يعني الغدة كسولة، منخفض يعني نشيطة جداً"
39
+ },
40
+ "Free_T4": {
41
+ "name_ar": "الثيروكسين الحر",
42
+ "abbreviations": ["FT4", "Free T4", "Free Thyroxine", "T4 الحر"],
43
+ "unit": "ng/dL",
44
+ "ranges": {
45
+ "adult": {"low": 0.8, "high": 1.8}
46
+ },
47
+ "severity_thresholds": {
48
+ "critical_low": 0.4,
49
+ "critical_high": 3.5
50
+ },
51
+ "clinical_meaning_ar": "الهرمون الدرقي الرئيسي — ينظم الأيض والطاقة والحرارة والمزاج",
52
+ "high_causes_ar": ["فرط نشاط الغدة الدرقية", "جرعة زائدة من الليفوثيروكسين"],
53
+ "low_causes_ar": ["قصور الغدة الدرقية", "قصور النخامية (نادر)"],
54
+ "patient_explanation_ar": "T4 هو الهرمون الذي تنتجه الغدة الدرقية لتنظيم سرعة عمليات جسمك كلها"
55
+ },
56
+ "Free_T3": {
57
+ "name_ar": "ثلاثي يودوثيرونين الحر",
58
+ "abbreviations": ["FT3", "Free T3", "Triiodothyronine"],
59
+ "unit": "pg/mL",
60
+ "ranges": {
61
+ "adult": {"low": 2.3, "high": 4.2}
62
+ },
63
+ "clinical_meaning_ar": "الشكل النشط للهرمون الدرقي — يُحوَّل من T4 في الأنسجة",
64
+ "importance_note": "FT3 يُقاس عند الاشتباه بفرط نشاط الغدة أو متابعة العلاج — ليس ضرورياً في كل حالة",
65
+ "patient_explanation_ar": "T3 هو الشكل النشط لهرمون الغدة الدرقية — أكثر تأثيراً من T4 مباشرةً"
66
+ },
67
+ "Anti_TPO": {
68
+ "name_ar": "أجسام مضادة للبيروكسيداز الدرقي",
69
+ "abbreviations": ["Anti-TPO", "TPOAb", "TPO antibodies", "أجسام ضد الدرقية"],
70
+ "unit": "IU/mL",
71
+ "ranges": {
72
+ "adult": {"low": 0, "high": 34}
73
+ },
74
+ "severity_thresholds": {
75
+ "mildly_positive": 35,
76
+ "moderately_positive": 100,
77
+ "highly_positive": 500
78
+ },
79
+ "clinical_meaning_ar": "أجسام مناعية تستهدف الغدة الدرقية — ارتفاعها يشير لأمراض مناعة ذاتية",
80
+ "high_causes_ar": ["هاشيموتو (التهاب الغدة الدرقية المناعي الذاتي)", "مرض جريفز", "التهاب الغدة ما بعد الولادة"],
81
+ "clinical_notes": "Anti-TPO مرتفع مع TSH طبيعي → خطر تطوير قصور درقي في المستقبل (متابعة سنوية)",
82
+ "patient_explanation_ar": "جهازك المناعي أحياناً يخطئ ويهاجم غدتك الدرقية — هذه الأجسام المضادة هي دليل على ذلك"
83
+ },
84
+ "Anti_TG": {
85
+ "name_ar": "أجسام مضادة للثيروجلوبولين",
86
+ "abbreviations": ["Anti-TG", "TgAb", "Thyroglobulin antibodies"],
87
+ "unit": "IU/mL",
88
+ "ranges": {
89
+ "adult": {"low": 0, "high": 115}
90
+ },
91
+ "clinical_meaning_ar": "يُقاس مع Anti-TPO لتشخيص أمراض الغدة الدرقية المناعية — وبعد علاج سرطان الغدة"
92
+ }
93
+ }
94
+ }
backend/medical_reference_schema.json ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_description": "Medical reference ranges for lab panels. Used to inject structured context into analysis prompts.",
3
+ "_version": "1.0",
4
+ "_last_updated": "2026-05-24",
5
+ "_sources": ["WHO", "LabCorp", "Mayo Clinic", "Harrison's Principles of Internal Medicine 21e"],
6
+
7
+ "panels": {
8
+ "CBC": {
9
+ "name_ar": "صورة الدم الكاملة",
10
+ "name_en": "Complete Blood Count",
11
+ "code": "CBC",
12
+ "specialty": "hematology",
13
+ "tests": {
14
+ "Hemoglobin": {
15
+ "name_ar": "هيموجلوبين",
16
+ "abbreviations": ["HGB", "Hgb", "هيموجلوبين"],
17
+ "unit": "g/dL",
18
+ "ranges": {
19
+ "adult_male": {"low": 13.5, "high": 17.5},
20
+ "adult_female": {"low": 12.0, "high": 15.5},
21
+ "children_6_12":{"low": 11.5, "high": 15.5},
22
+ "pregnant": {"low": 11.0, "high": 14.0}
23
+ },
24
+ "clinical_meaning_ar": "البروتين الذي يحمل الأكسجين في خلايا الدم الحمراء",
25
+ "high_causes_ar": ["الجفاف (hemoconcentration)", "داء الكثرة الحمراء الحقيقية", "مرض رئوي مزمن (التعويض)"],
26
+ "low_causes_ar": ["نقص الحديد (السبب الأكثر شيوعاً)", "نقص فيتامين B12 أو حمض الفوليك", "نزيف مزمن", "أمراض مزمنة", "فقر الدم الانحلالي"],
27
+ "low_symptoms_ar": ["تعب وإرهاق", "شحوب الجلد والملتحمة", "ضيق تنفس عند المجهود", "خفقان", "دوخة"],
28
+ "when_to_worry": "< 8 g/dL يستدعي تقييماً طبياً عاجلاً",
29
+ "followup_tests": ["Ferritin", "Iron", "TIBC", "Vitamin B12", "Folate", "Reticulocyte count"]
30
+ },
31
+
32
+ "WBC": {
33
+ "name_ar": "خلايا الدم البيضاء",
34
+ "abbreviations": ["WBC", "Leukocytes", "كريات الدم البيضاء"],
35
+ "unit": "10³/μL",
36
+ "ranges": {
37
+ "adult": {"low": 4.0, "high": 11.0},
38
+ "children": {"low": 5.0, "high": 15.0}
39
+ },
40
+ "clinical_meaning_ar": "خلايا المناعة التي تقاوم العدوى والمرض",
41
+ "high_causes_ar": ["عدوى بكتيرية", "التهاب", "إجهاد جسدي شديد", "أمراض الدم (نادراً: سرطان الدم)"],
42
+ "low_causes_ar": ["عدوى فيروسية حادة", "أدوية (كيموثيرابي، إلخ)", "أمراض المناعة الذاتية", "نقص فيتامين B12"],
43
+ "when_to_worry": "> 30 أو < 2 يستدعي تقييماً طارئاً",
44
+ "critical_values": {"critical_high": 30.0, "critical_low": 2.0}
45
+ },
46
+
47
+ "Platelets": {
48
+ "name_ar": "الصفائح الدموية",
49
+ "abbreviations": ["PLT", "Thrombocytes", "صفائح"],
50
+ "unit": "10³/μL",
51
+ "ranges": {
52
+ "adult": {"low": 150, "high": 400}
53
+ },
54
+ "clinical_meaning_ar": "خلايا صغيرة تساعد على تجلط الدم ووقف النزيف",
55
+ "high_causes_ar": ["التهابات", "نقص الحديد", "إزالة الطحال"],
56
+ "low_causes_ar": ["أمراض الكبد", "نقص فيتامين B12", "أدوية معينة", "أمراض المناعة الذاتية (ITP)"],
57
+ "when_to_worry": "< 50 خطر نزيف تلقائي; < 20 طوارئ",
58
+ "critical_values": {"critical_low": 20}
59
+ },
60
+
61
+ "RBC": {
62
+ "name_ar": "خلايا الدم الحمراء",
63
+ "abbreviations": ["RBC", "Erythrocytes"],
64
+ "unit": "10⁶/μL",
65
+ "ranges": {
66
+ "adult_male": {"low": 4.5, "high": 5.9},
67
+ "adult_female": {"low": 4.0, "high": 5.2}
68
+ },
69
+ "clinical_meaning_ar": "الخلايا التي تحمل الهيموجلوبين والأكسجين لأنسجة الجسم"
70
+ },
71
+
72
+ "Hematocrit": {
73
+ "name_ar": "الهيماتوكريت",
74
+ "abbreviations": ["HCT", "PCV"],
75
+ "unit": "%",
76
+ "ranges": {
77
+ "adult_male": {"low": 41, "high": 53},
78
+ "adult_female": {"low": 36, "high": 48}
79
+ },
80
+ "clinical_meaning_ar": "نسبة خلايا الدم الحمراء من حجم الدم الكلي"
81
+ },
82
+
83
+ "MCV": {
84
+ "name_ar": "متوسط حجم الكريات الحمراء",
85
+ "abbreviations": ["MCV"],
86
+ "unit": "fL",
87
+ "ranges": {
88
+ "adult": {"low": 80, "high": 100}
89
+ },
90
+ "clinical_meaning_ar": "يساعد في تحديد نوع فقر الدم: صغير الحجم (نقص حديد) أو كبير الحجم (نقص B12)",
91
+ "interpretation": {
92
+ "microcytic_low_MCV": "< 80: يشير إلى نقص الحديد أو الثلاسيميا",
93
+ "normocytic": "80-100: فقر دم بسبب مرض مزمن أو فقدان دم حاد",
94
+ "macrocytic_high_MCV": "> 100: يشير إلى نقص B12 أو حمض الفوليك"
95
+ }
96
+ },
97
+
98
+ "MCH": {
99
+ "name_ar": "متوسط كمية هيموجلوبين الكرية",
100
+ "abbreviations": ["MCH"],
101
+ "unit": "pg",
102
+ "ranges": {
103
+ "adult": {"low": 27, "high": 33}
104
+ },
105
+ "clinical_meaning_ar": "كمية الهيموجلوبين في كل خلية دم حمراء"
106
+ }
107
+ }
108
+ },
109
+
110
+ "Thyroid": {
111
+ "name_ar": "وظائف الغدة الدرقية",
112
+ "name_en": "Thyroid Function Tests",
113
+ "code": "TFT",
114
+ "specialty": "endocrinology",
115
+ "tests": {
116
+ "TSH": {
117
+ "name_ar": "الهرمون المحفز للدرقية",
118
+ "abbreviations": ["TSH", "Thyroid Stimulating Hormone"],
119
+ "unit": "mIU/L",
120
+ "ranges": {
121
+ "adult": {"low": 0.4, "high": 4.0},
122
+ "pregnant_T1": {"low": 0.1, "high": 2.5},
123
+ "pregnant_T2": {"low": 0.2, "high": 3.0},
124
+ "pregnant_T3": {"low": 0.3, "high": 3.0},
125
+ "elderly": {"low": 0.4, "high": 5.0}
126
+ },
127
+ "clinical_meaning_ar": "هرمون من الغدة النخامية يتحكم في نشاط الغدة الدرقية",
128
+ "high_causes_ar": ["قصور الغدة الدرقية (hypothyroidism)", "التهاب الغدة الدرقية هاشيموتو"],
129
+ "low_causes_ar": ["فرط نشاط الغدة الدرقية (hyperthyroidism)", "مرض جريفز (Graves)"],
130
+ "interpretation": {
131
+ "TSH_high_FT4_low": "قصور الغدة الدرقية الأولي — تحتاج ليفوثيروكسين",
132
+ "TSH_low_FT4_high": "فرط نشاط الغدة الدرقية — يحتاج تقييماً متخصصاً",
133
+ "TSH_high_FT4_normal": "قصور درقي تحت السريري — متابعة كل 6 أشهر"
134
+ },
135
+ "followup_tests": ["Free T4", "Free T3", "Anti-TPO", "Anti-Thyroglobulin"]
136
+ },
137
+
138
+ "Free_T4": {
139
+ "name_ar": "الثيروكسين الحر",
140
+ "abbreviations": ["FT4", "Free T4", "Thyroxine"],
141
+ "unit": "ng/dL",
142
+ "ranges": {
143
+ "adult": {"low": 0.8, "high": 1.8}
144
+ },
145
+ "clinical_meaning_ar": "الهرمون الدرقي الرئيسي — ينظم الأيض والطاقة والحرارة"
146
+ },
147
+
148
+ "Free_T3": {
149
+ "name_ar": "ثلاثي يودوثيرونين الحر",
150
+ "abbreviations": ["FT3", "Free T3", "Triiodothyronine"],
151
+ "unit": "pg/mL",
152
+ "ranges": {
153
+ "adult": {"low": 2.3, "high": 4.2}
154
+ },
155
+ "clinical_meaning_ar": "الشكل النشط للهرمون الدرقي — أقوى تأثيراً من T4"
156
+ },
157
+
158
+ "Anti_TPO": {
159
+ "name_ar": "أجسام مضادة للبيروكسيداز الدرقي",
160
+ "abbreviations": ["Anti-TPO", "TPO antibodies"],
161
+ "unit": "IU/mL",
162
+ "ranges": {
163
+ "adult": {"low": 0, "high": 34}
164
+ },
165
+ "clinical_meaning_ar": "ارتفاعه يشير إلى التهاب مناعي ذاتي للغدة الدرقية (هاشيموتو)",
166
+ "high_causes_ar": ["هاشيموتو", "مرض جريفز", "التهاب الغدة الدرقية"]
167
+ }
168
+ }
169
+ },
170
+
171
+ "Liver": {
172
+ "name_ar": "وظائف الكبد",
173
+ "name_en": "Liver Function Tests",
174
+ "code": "LFT",
175
+ "specialty": "hepatology",
176
+ "tests": {
177
+ "ALT": {
178
+ "name_ar": "ناقلة أمين الألانين",
179
+ "abbreviations": ["ALT", "SGPT", "ALT/SGPT"],
180
+ "unit": "U/L",
181
+ "ranges": {
182
+ "adult_male": {"low": 7, "high": 40},
183
+ "adult_female": {"low": 7, "high": 35}
184
+ },
185
+ "clinical_meaning_ar": "إنزيم يُفرز عند تلف خلايا الكبد — الأكثر تخصصاً للكبد",
186
+ "high_causes_ar": [
187
+ "التهاب الكبد الفيروسي (A, B, C)",
188
+ "الكبد الدهني",
189
+ "أمراض الكبد الكحولية",
190
+ "أدوية (باراسيتامول، الستاتينات بجرعات عالية)",
191
+ "انسداد القنوات الصفراوية"
192
+ ],
193
+ "severity": {
194
+ "mild_elevation": "< 3x upper limit: يستدعي المتابعة",
195
+ "moderate": "3-10x: يحتاج تقييماً طبياً",
196
+ "severe": "> 10x: تلف كبدي حاد — طوارئ"
197
+ },
198
+ "followup_tests": ["AST", "GGT", "Bilirubin", "Albumin", "PT/INR", "Viral hepatitis panel"]
199
+ },
200
+
201
+ "AST": {
202
+ "name_ar": "ناقلة أمين الأسبارتات",
203
+ "abbreviations": ["AST", "SGOT"],
204
+ "unit": "U/L",
205
+ "ranges": {
206
+ "adult_male": {"low": 10, "high": 40},
207
+ "adult_female": {"low": 10, "high": 35}
208
+ },
209
+ "clinical_meaning_ar": "إنزيم أقل تخصصاً للكبد — يرتفع أيضاً مع أمراض القلب والعضلات",
210
+ "ast_alt_ratio": {
211
+ "ratio_2_1": "AST:ALT > 2:1 يشير إلى سبب كحولي",
212
+ "alt_higher": "ALT أعلى من AST يشير إلى التهاب كبدي فيروسي أو دهني"
213
+ }
214
+ },
215
+
216
+ "ALP": {
217
+ "name_ar": "الفوسفاتاز القلوية",
218
+ "abbreviations": ["ALP", "Alkaline Phosphatase"],
219
+ "unit": "U/L",
220
+ "ranges": {
221
+ "adult": {"low": 44, "high": 147},
222
+ "children": {"low": 50, "high": 350}
223
+ },
224
+ "clinical_meaning_ar": "يرتفع مع انسداد القنوات الصفراوية وأمراض العظام",
225
+ "high_causes_ar": ["انسداد القنوات الصفراوية", "حصى المرارة", "أمراض العظام (Paget's)", "نمو العظام الطبيعي عند الأطفال"]
226
+ },
227
+
228
+ "Bilirubin_Total": {
229
+ "name_ar": "البيليروبين الكلي",
230
+ "abbreviations": ["T.Bili", "Total Bilirubin", "بيليروبين كلي"],
231
+ "unit": "mg/dL",
232
+ "ranges": {
233
+ "adult": {"low": 0.2, "high": 1.2}
234
+ },
235
+ "clinical_meaning_ar": "صبغة ناتجة عن تكسّر خلايا الدم الحمراء. يسبب اليرقان عند الارتفاع.",
236
+ "high_causes_ar": ["التهاب كبد", "انسداد صفراوي", "فقر الدم الانحلالي", "متلازمة جيلبرت (حميدة)"],
237
+ "jaundice_threshold": "> 2.5 mg/dL: يظهر اليرقان (اصفرار الجلد والعيون)"
238
+ },
239
+
240
+ "Bilirubin_Direct": {
241
+ "name_ar": "البيليروبين المباشر",
242
+ "abbreviations": ["D.Bili", "Direct Bilirubin", "Conjugated Bilirubin"],
243
+ "unit": "mg/dL",
244
+ "ranges": {
245
+ "adult": {"low": 0, "high": 0.3}
246
+ },
247
+ "clinical_meaning_ar": "يرتفع مع أمراض الكبد والانسداد الصفراوي تحديداً"
248
+ },
249
+
250
+ "Albumin": {
251
+ "name_ar": "الألبومين",
252
+ "abbreviations": ["ALB", "Albumin"],
253
+ "unit": "g/dL",
254
+ "ranges": {
255
+ "adult": {"low": 3.5, "high": 5.0}
256
+ },
257
+ "clinical_meaning_ar": "بروتين ينتجه الكبد — مؤشر لوظيفة الكبد التركيبية وسوء التغذية",
258
+ "low_causes_ar": ["قصور الكبد المزمن", "سوء التغذية", "متلازمة الكلاء", "التهاب حاد"],
259
+ "importance": "انخفاضه يدل على ضعف قدرة الكبد التركيبية — مهم جداً في تقييم الكبد"
260
+ },
261
+
262
+ "GGT": {
263
+ "name_ar": "غاما غلوتاميل ترانسفيراز",
264
+ "abbreviations": ["GGT", "Gamma-GT"],
265
+ "unit": "U/L",
266
+ "ranges": {
267
+ "adult_male": {"low": 8, "high": 61},
268
+ "adult_female": {"low": 5, "high": 36}
269
+ },
270
+ "clinical_meaning_ar": "حساس جداً لأمراض الكبد وخاصة الكحولية. يساعد في تفسير ارتفاع ALP.",
271
+ "high_causes_ar": ["الكحول", "أمراض الكبد الدهني", "أدوية معينة", "انسداد صفراوي"]
272
+ }
273
+ }
274
+ },
275
+
276
+ "Kidney": {
277
+ "name_ar": "وظائف الكلى",
278
+ "name_en": "Kidney Function Tests",
279
+ "code": "KFT",
280
+ "specialty": "nephrology",
281
+ "tests": {
282
+ "Creatinine": {
283
+ "name_ar": "كرياتينين",
284
+ "abbreviations": ["Creat", "Creatinine", "كرياتينين"],
285
+ "unit": "mg/dL",
286
+ "ranges": {
287
+ "adult_male": {"low": 0.7, "high": 1.3},
288
+ "adult_female": {"low": 0.5, "high": 1.1}
289
+ },
290
+ "clinical_meaning_ar": "مادة نفايات من العضلات تُصفيها الكلى — ارتفاعه يدل على ضعف وظيفة الكلى",
291
+ "high_causes_ar": ["الفشل الكلوي", "الجفاف", "كتلة عضلية كبيرة", "بعض الأدوية"],
292
+ "followup_tests": ["BUN", "GFR", "Urine albumin", "Electrolytes"]
293
+ },
294
+
295
+ "BUN": {
296
+ "name_ar": "نيتروجين يوريا الدم",
297
+ "abbreviations": ["BUN", "Blood Urea Nitrogen", "يوريا"],
298
+ "unit": "mg/dL",
299
+ "ranges": {
300
+ "adult": {"low": 8, "high": 25}
301
+ },
302
+ "clinical_meaning_ar": "مادة نفايات من البروتينات — يرتفع مع ضعف الكلى أو نزيف الجهاز الهضمي",
303
+ "bun_creatinine_ratio": {
304
+ "normal": "10:1 إلى 20:1",
305
+ "high_ratio_pre_renal": "> 20:1: يشير إلى الجفاف أو نزيف هضمي",
306
+ "low_ratio": "< 10:1: يشير إلى نقص البروتين أو أمراض ا��كبد"
307
+ }
308
+ }
309
+ }
310
+ },
311
+
312
+ "Lipid": {
313
+ "name_ar": "الدهنيات",
314
+ "name_en": "Lipid Panel",
315
+ "code": "LIPID",
316
+ "specialty": "cardiology",
317
+ "tests": {
318
+ "Cholesterol_Total": {
319
+ "name_ar": "الكوليسترول الكلي",
320
+ "abbreviations": ["Total Cholesterol", "Chol", "كوليسترول"],
321
+ "unit": "mg/dL",
322
+ "ranges": {
323
+ "desirable": {"high": 200},
324
+ "borderline": {"low": 200, "high": 239},
325
+ "high_risk": {"low": 240}
326
+ },
327
+ "clinical_meaning_ar": "دهون ضرورية للجسم — ارتفاعها يزيد خطر أمراض القلب والأوعية"
328
+ },
329
+
330
+ "LDL": {
331
+ "name_ar": "الكوليسترول الضار",
332
+ "abbreviations": ["LDL", "LDL-C", "LDL Cholesterol"],
333
+ "unit": "mg/dL",
334
+ "ranges": {
335
+ "optimal": {"high": 100},
336
+ "near_optimal": {"low": 100, "high": 129},
337
+ "borderline_high": {"low": 130, "high": 159},
338
+ "high": {"low": 160, "high": 189},
339
+ "very_high": {"low": 190}
340
+ },
341
+ "targets_by_risk": {
342
+ "high_cv_risk": "< 70 mg/dL",
343
+ "moderate_risk": "< 100 mg/dL",
344
+ "low_risk": "< 130 mg/dL"
345
+ },
346
+ "clinical_meaning_ar": "الكوليسترول الضار — يتراكم في جدران الأوعية ويزيد خطر النوبات القلبية"
347
+ },
348
+
349
+ "HDL": {
350
+ "name_ar": "الكوليسترول الجيد",
351
+ "abbreviations": ["HDL", "HDL-C", "HDL Cholesterol"],
352
+ "unit": "mg/dL",
353
+ "ranges": {
354
+ "low_risk_male": {"low": 40},
355
+ "low_risk_female": {"low": 50},
356
+ "protective": {"low": 60}
357
+ },
358
+ "clinical_meaning_ar": "الكوليسترول الجيد — ينقل الدهون من الأوعية للكبد ويحمي القلب",
359
+ "note": "الأعلى أفضل — < 40 للرجال و< 50 للنساء عامل خطر قلبي"
360
+ },
361
+
362
+ "Triglycerides": {
363
+ "name_ar": "الدهون الثلاثية",
364
+ "abbreviations": ["TG", "TRIG", "Triglycerides"],
365
+ "unit": "mg/dL",
366
+ "ranges": {
367
+ "normal": {"high": 150},
368
+ "borderline_high": {"low": 150, "high": 199},
369
+ "high": {"low": 200, "high": 499},
370
+ "very_high": {"low": 500}
371
+ },
372
+ "high_causes_ar": ["السمنة", "السكري غير المنضبط", "قلة النشاط", "النظام الغذائي الغني بالسكر والكربوهيدرات", "قصور الغدة الدرقية"]
373
+ }
374
+ }
375
+ },
376
+
377
+ "Diabetes": {
378
+ "name_ar": "مؤشرات السكري",
379
+ "name_en": "Diabetes Markers",
380
+ "code": "DM",
381
+ "specialty": "endocrinology",
382
+ "tests": {
383
+ "Fasting_Glucose": {
384
+ "name_ar": "سكر الصيام",
385
+ "abbreviations": ["FBG", "FPG", "Fasting Glucose", "سكر صيام"],
386
+ "unit": "mg/dL",
387
+ "ranges": {
388
+ "normal": {"high": 99},
389
+ "prediabetes":{"low": 100, "high": 125},
390
+ "diabetes": {"low": 126}
391
+ },
392
+ "note": "يجب الصيام 8-12 ساعة قبل الفحص للحصول على نتيجة دقيقة"
393
+ },
394
+
395
+ "HbA1c": {
396
+ "name_ar": "السكر التراكمي",
397
+ "abbreviations": ["HbA1c", "A1C", "Glycated Hemoglobin", "سكر تراكمي"],
398
+ "unit": "%",
399
+ "ranges": {
400
+ "normal": {"high": 5.6},
401
+ "prediabetes":{"low": 5.7, "high": 6.4},
402
+ "diabetes": {"low": 6.5}
403
+ },
404
+ "targets_for_diabetics": {
405
+ "general": "< 7%",
406
+ "elderly": "< 8%",
407
+ "pregnant": "< 6%",
408
+ "cardiovascular":"< 7% or individualized"
409
+ },
410
+ "clinical_meaning_ar": "يعكس متوسط مستوى السكر في الدم خلال الأشهر الثلاثة الماضية",
411
+ "avg_glucose_conversion": {
412
+ "5": 97, "6": 126, "7": 154, "8": 183, "9": 212, "10": 240, "11": 269, "12": 298
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
backend/middleware/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .audit import AuditMiddleware, get_audit_logger
2
+ from .sanitizer import validate_upload, sanitize_text, MAX_FILE_BYTES
3
+
4
+ __all__ = ["AuditMiddleware", "get_audit_logger", "validate_upload", "sanitize_text", "MAX_FILE_BYTES"]
backend/middleware/audit.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Structured audit logging middleware for PDPL/NDMO compliance.
3
+
4
+ Every request is logged with: timestamp, method, path, client IP,
5
+ session_id (from header), status code, latency, and error summary.
6
+ Logs are written to a rotating file + stdout (structured JSON).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import os
13
+ import time
14
+ import uuid
15
+ from logging.handlers import RotatingFileHandler
16
+ from pathlib import Path
17
+ from typing import Callable
18
+
19
+ from fastapi import Request, Response
20
+ from starlette.middleware.base import BaseHTTPMiddleware
21
+ from starlette.types import ASGIApp
22
+
23
+ # ── Audit logger (separate from app logger) ─────────────────────────────────
24
+
25
+ _LOG_DIR = Path(os.getenv("AUDIT_LOG_DIR", "logs/audit"))
26
+ _LOG_FILE = _LOG_DIR / "audit.jsonl"
27
+ _MAX_BYTES = 50 * 1024 * 1024 # 50 MB per file
28
+ _BACKUP_COUNT = 10 # keep 10 rotated files (~500 MB total)
29
+
30
+ # Paths that are not audited (health checks, static assets)
31
+ _SKIP_PATHS = frozenset(["/health", "/docs", "/openapi.json", "/favicon.ico"])
32
+
33
+ # Endpoints that handle medical data — flag them for compliance
34
+ _SENSITIVE_PATHS = frozenset([
35
+ "/api/analyze", "/api/analyses/save", "/api/chat",
36
+ "/api/voice/transcribe", "/api/voice/chat", "/api/risk",
37
+ ])
38
+
39
+
40
+ def _build_audit_logger() -> logging.Logger:
41
+ logger = logging.getLogger("audit")
42
+ if logger.handlers:
43
+ return logger
44
+ logger.setLevel(logging.INFO)
45
+ logger.propagate = False
46
+
47
+ try:
48
+ _LOG_DIR.mkdir(parents=True, exist_ok=True)
49
+ fh = RotatingFileHandler(
50
+ _LOG_FILE, maxBytes=_MAX_BYTES, backupCount=_BACKUP_COUNT, encoding="utf-8"
51
+ )
52
+ fh.setFormatter(logging.Formatter("%(message)s"))
53
+ logger.addHandler(fh)
54
+ except Exception as e:
55
+ print(f"[AUDIT] Cannot open log file {_LOG_FILE}: {e} — writing to stderr only")
56
+
57
+ sh = logging.StreamHandler()
58
+ sh.setFormatter(logging.Formatter("%(message)s"))
59
+ logger.addHandler(sh)
60
+
61
+ return logger
62
+
63
+
64
+ _audit_logger = _build_audit_logger()
65
+
66
+
67
+ def get_audit_logger() -> logging.Logger:
68
+ return _audit_logger
69
+
70
+
71
+ def _get_client_ip(request: Request) -> str:
72
+ forwarded = request.headers.get("X-Forwarded-For", "")
73
+ if forwarded:
74
+ return forwarded.split(",")[0].strip()
75
+ return request.client.host if request.client else "unknown"
76
+
77
+
78
+ def _redact_sensitive_headers(headers: dict) -> dict:
79
+ """Remove auth tokens and API keys from logged headers."""
80
+ sensitive = {"authorization", "x-api-key", "cookie", "set-cookie"}
81
+ return {k: "[REDACTED]" if k.lower() in sensitive else v for k, v in headers.items()}
82
+
83
+
84
+ class AuditMiddleware(BaseHTTPMiddleware):
85
+ """
86
+ FastAPI middleware that writes one structured JSON audit record per request.
87
+
88
+ Fields logged:
89
+ req_id, timestamp_utc, method, path, query, client_ip,
90
+ session_id, status, latency_ms, sensitive, error (if any)
91
+ """
92
+
93
+ def __init__(self, app: ASGIApp, environment: str = "production") -> None:
94
+ super().__init__(app)
95
+ self._env = environment
96
+
97
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
98
+ if request.url.path in _SKIP_PATHS:
99
+ return await call_next(request)
100
+
101
+ req_id = str(uuid.uuid4())[:8]
102
+ t_start = time.perf_counter()
103
+
104
+ # Attach req_id so endpoints can reference it in error messages
105
+ request.state.req_id = req_id
106
+
107
+ error_detail: str | None = None
108
+ status_code = 500
109
+ try:
110
+ response = await call_next(request)
111
+ status_code = response.status_code
112
+ except Exception as exc:
113
+ error_detail = type(exc).__name__ + ": " + str(exc)[:200]
114
+ raise
115
+ finally:
116
+ latency_ms = round((time.perf_counter() - t_start) * 1000, 1)
117
+ path = request.url.path
118
+ record: dict = {
119
+ "req_id": req_id,
120
+ "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
121
+ "method": request.method,
122
+ "path": path,
123
+ "query": str(request.url.query)[:200] or None,
124
+ "ip": _get_client_ip(request),
125
+ "session_id": request.headers.get("X-Session-Id", "anon")[:64],
126
+ "status": status_code,
127
+ "latency_ms": latency_ms,
128
+ "sensitive": path in _SENSITIVE_PATHS,
129
+ "env": self._env,
130
+ }
131
+ if error_detail:
132
+ record["error"] = error_detail
133
+ _audit_logger.info(json.dumps(record, ensure_ascii=False))
134
+
135
+ response.headers["X-Request-Id"] = req_id
136
+ return response
backend/middleware/auth_middleware.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Supabase JWT verification for FastAPI.
3
+
4
+ Supabase now supports two signing modes:
5
+ - ES256 (ECDSA, newer projects): verified via JWKS public key endpoint
6
+ - HS256 (HMAC, older projects): verified via SUPABASE_JWT_SECRET
7
+
8
+ This middleware auto-detects the algorithm from the JWT header and uses
9
+ the appropriate verification method.
10
+
11
+ Usage:
12
+ @app.get("/api/me")
13
+ async def me(user = Depends(require_user)):
14
+ return {"id": user["sub"], "email": user["email"]}
15
+
16
+ @app.get("/api/public")
17
+ async def public(user = Depends(optional_user)):
18
+ ...
19
+
20
+ Environment:
21
+ SUPABASE_URL — project URL (for JWKS endpoint)
22
+ SUPABASE_JWT_SECRET — required only for HS256 projects
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import os
28
+ import logging
29
+ import time
30
+ from typing import Any
31
+
32
+ import jwt
33
+ from jwt import PyJWKClient
34
+ from fastapi import Depends, HTTPException, status
35
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
36
+
37
+ log = logging.getLogger("tebyan.auth")
38
+
39
+ _JWT_SECRET = os.getenv("SUPABASE_JWT_SECRET", "")
40
+ _SUPABASE_URL = os.getenv("SUPABASE_URL", "")
41
+ _ALGORITHM_HS = "HS256"
42
+ _ALGORITHM_ES = "ES256"
43
+ _AUDIENCE = "authenticated"
44
+
45
+ # JWKS client — caches public keys for 10 minutes
46
+ _jwks_client: PyJWKClient | None = None
47
+ _jwks_last_fetched: float = 0.0
48
+ _JWKS_CACHE_TTL = 600 # seconds
49
+
50
+ # auto_error=False lets us handle missing token ourselves (for optional_user)
51
+ _bearer = HTTPBearer(auto_error=False)
52
+
53
+
54
+ def _get_jwks_client() -> PyJWKClient | None:
55
+ global _jwks_client, _jwks_last_fetched
56
+ if not _SUPABASE_URL:
57
+ return None
58
+ now = time.time()
59
+ if _jwks_client is None or (now - _jwks_last_fetched) > _JWKS_CACHE_TTL:
60
+ try:
61
+ url = f"{_SUPABASE_URL}/auth/v1/.well-known/jwks.json"
62
+ _jwks_client = PyJWKClient(url, cache_keys=True)
63
+ _jwks_last_fetched = now
64
+ log.debug("JWKS client initialised from %s", url)
65
+ except Exception as e:
66
+ log.warning("Failed to init JWKS client: %s", e)
67
+ return None
68
+ return _jwks_client
69
+
70
+
71
+ def _get_algorithm(token: str) -> str:
72
+ """Peek at the JWT header to determine signing algorithm."""
73
+ try:
74
+ header = jwt.get_unverified_header(token)
75
+ return header.get("alg", _ALGORITHM_HS)
76
+ except Exception:
77
+ return _ALGORITHM_HS
78
+
79
+
80
+ def _decode(token: str) -> dict[str, Any]:
81
+ """
82
+ Decode and verify a Supabase JWT.
83
+ Supports both ES256 (JWKS) and HS256 (secret).
84
+ Raises HTTPException on any failure.
85
+ """
86
+ alg = _get_algorithm(token)
87
+
88
+ try:
89
+ if alg == _ALGORITHM_ES:
90
+ # ES256 — verify via JWKS public key
91
+ client = _get_jwks_client()
92
+ if client is None:
93
+ raise HTTPException(
94
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
95
+ detail="Auth not configured on server (SUPABASE_URL missing)",
96
+ )
97
+ signing_key = client.get_signing_key_from_jwt(token)
98
+ return jwt.decode(
99
+ token,
100
+ signing_key.key,
101
+ algorithms=[_ALGORITHM_ES],
102
+ audience=_AUDIENCE,
103
+ )
104
+ else:
105
+ # HS256 — verify via shared secret
106
+ if not _JWT_SECRET:
107
+ log.error("SUPABASE_JWT_SECRET not set — cannot verify HS256 tokens")
108
+ raise HTTPException(
109
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
110
+ detail="Auth not configured on server",
111
+ )
112
+ return jwt.decode(
113
+ token,
114
+ _JWT_SECRET,
115
+ algorithms=[_ALGORITHM_HS],
116
+ audience=_AUDIENCE,
117
+ )
118
+
119
+ except HTTPException:
120
+ raise
121
+ except jwt.ExpiredSignatureError:
122
+ raise HTTPException(
123
+ status_code=status.HTTP_401_UNAUTHORIZED,
124
+ detail={"error": "session_expired", "message": "انتهت صلاحية الجلسة — يرجى تسجيل الدخول مجدداً"},
125
+ headers={"WWW-Authenticate": "Bearer"},
126
+ )
127
+ except jwt.InvalidAudienceError:
128
+ raise HTTPException(
129
+ status_code=status.HTTP_401_UNAUTHORIZED,
130
+ detail={"error": "invalid_token", "message": "رمز المصادقة غير صالح"},
131
+ headers={"WWW-Authenticate": "Bearer"},
132
+ )
133
+ except jwt.InvalidTokenError as e:
134
+ log.debug("JWT decode failed: %s", e)
135
+ raise HTTPException(
136
+ status_code=status.HTTP_401_UNAUTHORIZED,
137
+ detail={"error": "invalid_token", "message": "رمز المصادقة غير صالح"},
138
+ headers={"WWW-Authenticate": "Bearer"},
139
+ )
140
+
141
+
142
+ # ── Public dependency functions ───────────────────────────────────────────────
143
+
144
+ async def require_user(
145
+ credentials: HTTPAuthorizationCredentials | None = Depends(_bearer),
146
+ ) -> dict[str, Any]:
147
+ """
148
+ FastAPI dependency that returns the decoded JWT payload.
149
+ Raises 401 if the request has no valid Bearer token.
150
+ """
151
+ if not credentials:
152
+ raise HTTPException(
153
+ status_code=status.HTTP_401_UNAUTHORIZED,
154
+ detail={"error": "missing_token", "message": "تسجيل الدخول مطلوب"},
155
+ headers={"WWW-Authenticate": "Bearer"},
156
+ )
157
+ return _decode(credentials.credentials)
158
+
159
+
160
+ async def optional_user(
161
+ credentials: HTTPAuthorizationCredentials | None = Depends(_bearer),
162
+ ) -> dict[str, Any] | None:
163
+ """
164
+ FastAPI dependency that returns the decoded JWT payload or None.
165
+ Does not raise — lets the route handle unauthenticated requests.
166
+ Returns None gracefully if JWKS/secret is not configured.
167
+ """
168
+ if not credentials:
169
+ return None
170
+ if not _SUPABASE_URL and not _JWT_SECRET:
171
+ log.debug("Auth not configured — treating request as anonymous")
172
+ return None
173
+ try:
174
+ return _decode(credentials.credentials)
175
+ except HTTPException:
176
+ return None
177
+
178
+
179
+ def get_user_id(user: dict[str, Any]) -> str:
180
+ """Extract the Supabase user UUID from a decoded payload."""
181
+ return user["sub"]
182
+
183
+
184
+ def get_user_email(user: dict[str, Any]) -> str:
185
+ """Extract the user email from a decoded payload."""
186
+ return user.get("email", "")
backend/middleware/sanitizer.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Request sanitization and file validation for medical file uploads.
3
+
4
+ Validates:
5
+ - File size (max 20 MB)
6
+ - MIME type whitelist (PDF, JPEG, PNG, WEBP, TIFF)
7
+ - Magic bytes (header sniffing — prevents MIME spoofing)
8
+ - Filename safety (path traversal, null bytes, dangerous extensions)
9
+ - Text input: strips HTML tags, limits length, removes null bytes
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from typing import Final
15
+
16
+ from fastapi import HTTPException, UploadFile
17
+
18
+ # ── Limits ───────────────────────────────────────────────────────────────────
19
+
20
+ MAX_FILE_BYTES: Final[int] = 20 * 1024 * 1024 # 20 MB
21
+ MAX_TEXT_LEN: Final[int] = 8_000 # characters per text field
22
+
23
+ # ── Allowed MIME types (whitelist) ───────────────────────────────────────────
24
+
25
+ _ALLOWED_MIME: Final[frozenset[str]] = frozenset([
26
+ "application/pdf",
27
+ "image/jpeg",
28
+ "image/jpg",
29
+ "image/png",
30
+ "image/webp",
31
+ "image/tiff",
32
+ ])
33
+
34
+ # ── Magic bytes (first bytes of file → expected MIME group) ─────────────────
35
+
36
+ _MAGIC: Final[list[tuple[bytes, str]]] = [
37
+ (b"%PDF", "pdf"),
38
+ (b"\xff\xd8\xff", "jpeg"),
39
+ (b"\x89PNG", "png"),
40
+ (b"RIFF", "webp"), # RIFF....WEBP
41
+ (b"II*\x00", "tiff"), # little-endian TIFF
42
+ (b"MM\x00*", "tiff"), # big-endian TIFF
43
+ ]
44
+
45
+ _MIME_TO_MAGIC_GROUP: Final[dict[str, str]] = {
46
+ "application/pdf": "pdf",
47
+ "image/jpeg": "jpeg",
48
+ "image/jpg": "jpeg",
49
+ "image/png": "png",
50
+ "image/webp": "webp",
51
+ "image/tiff": "tiff",
52
+ }
53
+
54
+ # ── Dangerous file extensions (block even if MIME seems OK) ──────────────────
55
+
56
+ _BLOCKED_EXT: Final[frozenset[str]] = frozenset([
57
+ ".exe", ".sh", ".bat", ".cmd", ".ps1", ".js", ".php", ".py",
58
+ ".rb", ".pl", ".dll", ".so", ".elf", ".msi", ".vbs", ".hta",
59
+ ".jar", ".class", ".com", ".scr", ".pif",
60
+ ])
61
+
62
+ # ── Text sanitization ────────────────────────────────────────────────────────
63
+
64
+ _HTML_TAG_RE = re.compile(r"<[^>]{0,200}>")
65
+ _SCRIPT_RE = re.compile(r"(?i)(javascript:|data:text/html|<script)", re.IGNORECASE)
66
+ _NULL_BYTE_RE = re.compile(r"\x00")
67
+ _SQL_INJECT = re.compile(
68
+ r"(?i)\b(union\s+select|drop\s+table|insert\s+into|delete\s+from"
69
+ r"|exec\s*\(|xp_cmdshell|;--)\b"
70
+ )
71
+
72
+
73
+ def sanitize_text(text: str, max_len: int = MAX_TEXT_LEN) -> str:
74
+ """
75
+ Strip HTML, null bytes, and obvious injection patterns from user text.
76
+ Truncates to max_len characters.
77
+ """
78
+ if not isinstance(text, str):
79
+ return ""
80
+ text = _NULL_BYTE_RE.sub("", text)
81
+ text = _HTML_TAG_RE.sub("", text)
82
+ if _SCRIPT_RE.search(text):
83
+ raise HTTPException(status_code=400, detail="محتوى غير مسموح به في النص المرسل")
84
+ if _SQL_INJECT.search(text):
85
+ raise HTTPException(status_code=400, detail="محتوى غير مسموح به في النص المرسل")
86
+ return text[:max_len].strip()
87
+
88
+
89
+ def _sniff_magic(data: bytes) -> str | None:
90
+ """Return magic group name or None if unrecognised."""
91
+ for magic, group in _MAGIC:
92
+ if data[:len(magic)] == magic:
93
+ # Special: WebP has 'WEBP' at offset 8
94
+ if group == "webp" and data[8:12] != b"WEBP":
95
+ continue
96
+ return group
97
+ return None
98
+
99
+
100
+ def _safe_filename(filename: str) -> str:
101
+ """Sanitize filename: strip path components and dangerous characters."""
102
+ # Strip directory traversal
103
+ name = re.sub(r"[/\\]", "_", filename)
104
+ # Remove null bytes and control chars
105
+ name = re.sub(r"[\x00-\x1f]", "", name)
106
+ # Keep only safe chars
107
+ name = re.sub(r"[^\w.\-]", "_", name)
108
+ return name[:128] or "upload"
109
+
110
+
111
+ def validate_upload(file: UploadFile, data: bytes) -> None:
112
+ """
113
+ Validate an uploaded file. Raises HTTPException on any violation.
114
+
115
+ Args:
116
+ file: The UploadFile from FastAPI (provides filename + content_type).
117
+ data: Raw bytes already read from the file.
118
+
119
+ Raises:
120
+ HTTPException 400 for bad requests, 413 for file too large, 415 for unsupported type.
121
+ """
122
+ # ── Size ─────────────────────────────────────────────────────────────────
123
+ if len(data) == 0:
124
+ raise HTTPException(status_code=400, detail="الملف فارغ")
125
+ if len(data) > MAX_FILE_BYTES:
126
+ mb = MAX_FILE_BYTES // (1024 * 1024)
127
+ raise HTTPException(status_code=413, detail=f"حجم الملف يتجاوز الحد المسموح ({mb} MB)")
128
+
129
+ # ���─ Filename safety ───────────────────────────────────────────────────────
130
+ filename = (file.filename or "").strip()
131
+ if not filename:
132
+ raise HTTPException(status_code=400, detail="اسم الملف مطلوب")
133
+
134
+ ext = "." + filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
135
+ if ext in _BLOCKED_EXT:
136
+ raise HTTPException(status_code=415, detail=f"نوع الملف '{ext}' غير مسموح")
137
+
138
+ # ── MIME whitelist ────────────────────────────────────────────────────────
139
+ content_type = (file.content_type or "").split(";")[0].strip().lower()
140
+ if content_type not in _ALLOWED_MIME:
141
+ raise HTTPException(
142
+ status_code=415,
143
+ detail=f"نوع الملف '{content_type}' غير مدعوم. الأنواع المقبولة: PDF, JPEG, PNG, WEBP, TIFF",
144
+ )
145
+
146
+ # ── Magic bytes validation (anti-MIME-spoofing) ───────────────────────────
147
+ detected = _sniff_magic(data)
148
+ expected_group = _MIME_TO_MAGIC_GROUP.get(content_type)
149
+ if detected is None:
150
+ raise HTTPException(status_code=415, detail="تعذّر التحقق من نوع الملف — بيانات غير صالحة")
151
+ if expected_group and detected != expected_group:
152
+ raise HTTPException(
153
+ status_code=415,
154
+ detail=f"محتوى الملف لا يطابق نوعه المُعلَن ({content_type})",
155
+ )
backend/prompts/examples/cbc_examples.json ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_description": "CBC few-shot examples — used to prime LLM with correct Arabic medical format",
3
+ "_version": "2.0",
4
+ "examples": [
5
+ {
6
+ "id": "cbc_iron_deficiency",
7
+ "scenario": "Iron-deficiency anemia — female, 28 years",
8
+ "input_findings": [
9
+ {"name": "Hemoglobin", "value": "9.8", "range": "12.0-15.5", "unit": "g/dL", "status": "low"},
10
+ {"name": "MCV", "value": "71", "range": "80-100", "unit": "fL", "status": "low"},
11
+ {"name": "MCH", "value": "23", "range": "27-33", "unit": "pg", "status": "low"},
12
+ {"name": "WBC", "value": "6.5", "range": "4.0-11.0", "unit": "10³/μL", "status": "normal"},
13
+ {"name": "Platelets", "value": "310", "range": "150-400", "unit": "10³/μL", "status": "normal"}
14
+ ],
15
+ "ideal_output": {
16
+ "تقييم_عام": "خلايا الدم البيضاء والصفائح الدموية طبيعية تماماً — وهو مؤشر جيد لسلامة جهازك المناعي. يستدعي الانتباه انخفاض الهيموجلوبين مع صغر حجم الكريات الحمراء، وهو نمط كلاسيكي لفقر الدم الناجم عن نقص الحديد — وهي حالة شائعة ومناسبة للعلاج.",
17
+ "interpretation_chain": "Hgb low + MCV low + MCH low → microcytic hypochromic anemia → most likely iron deficiency → recommend Ferritin + Iron panel",
18
+ "key_message_ar": "فقر دم بسيط إلى متوسط بسبب نقص الحديد على الأرجح — قابل للعلاج بالغذاء والمكملات",
19
+ "توصيات": [
20
+ {
21
+ "category": "التغذية الغنية بالحديد",
22
+ "tips": ["تناول اللحوم الحمراء 3 مرات أسبوعياً", "السبانخ والعدس والفول من أغنى المصادر النباتية", "اقرن وجبات الحديد بعصير البرتقال لتحسين الامتصاص 2×", "تجنب الشاي والقهوة مباشرة بعد الوجبات — تقلل الامتصاص 60%"]
23
+ },
24
+ {
25
+ "category": "المتابعة الطبية",
26
+ "tips": ["أجري فحص الفيريتين والحديد لتأكيد السبب", "راجعي طبيبك لتحديد جرعة مكمل الحديد المناسبة", "أعيدي التحليل بعد 3 أشهر من العلاج لمتابعة التحسن"]
27
+ }
28
+ ]
29
+ }
30
+ },
31
+ {
32
+ "id": "cbc_all_normal",
33
+ "scenario": "All CBC values normal — male, 35 years",
34
+ "input_findings": [
35
+ {"name": "Hemoglobin", "value": "15.2", "range": "13.5-17.5", "unit": "g/dL", "status": "normal"},
36
+ {"name": "WBC", "value": "7.1", "range": "4.0-11.0", "unit": "10³/μL", "status": "normal"},
37
+ {"name": "Platelets", "value": "245", "range": "150-400", "unit": "10³/μL", "status": "normal"},
38
+ {"name": "Hematocrit", "value": "46", "range": "41-53", "unit": "%", "status": "normal"}
39
+ ],
40
+ "ideal_output": {
41
+ "تقييم_عام": "ممتاز! جميع قيم صورة الدم الكاملة ضمن المعدل الطبيعي — الهيموجلوبين يحمل الأكسجين بكفاءة، وخلايا المناعة بعدد مثالي، والصفائح الدموية تضمن التجلط الصحي. هذه نتائج تعكس صحة دموية جيدة.",
42
+ "key_message_ar": "نتائج طبيعية — استمر على نمط حياتك الصحي",
43
+ "توصيات": [
44
+ {
45
+ "category": "الحفاظ على الصحة الدموية",
46
+ "tips": ["تناول وجبات متوازنة تشمل البروتين والحديد والفيتامينات", "مارس الرياضة المعتدلة 150 دقيقة أسبوعياً", "اشرب 2 لتر ماء يومياً للحفاظ على حجم الدم", "أجرِ صورة الدم الكاملة سنوياً للمتابعة الوقائية"]
47
+ }
48
+ ]
49
+ }
50
+ },
51
+ {
52
+ "id": "cbc_infection",
53
+ "scenario": "Elevated WBC suggesting infection",
54
+ "input_findings": [
55
+ {"name": "WBC", "value": "14.5", "range": "4.0-11.0", "unit": "10³/μL", "status": "high"},
56
+ {"name": "Neutrophils", "value": "82", "range": "50-70", "unit": "%", "status": "high"},
57
+ {"name": "Hemoglobin", "value": "13.8", "range": "13.5-17.5", "unit": "g/dL", "status": "normal"},
58
+ {"name": "Platelets", "value": "420", "range": "150-400", "unit": "10³/μL", "status": "high"}
59
+ ],
60
+ "ideal_output": {
61
+ "تقييم_عام": "الهيموجلوبين طبيعي وهو مؤشر إيجابي. يُلاحظ ارتفاع في خلايا الدم البيضاء مع ارتفاع النيتروفيل — وهو نمط يشير عادةً إلى وجود عدوى بكتيرية أو التهاب في مكان ما في الجسم. الصفائح مرتفعة قليلاً وهو تفاعل طبيعي مع الالتهاب.",
62
+ "interpretation_chain": "WBC 14.5 (high) + Neutrophils 82% (high) → bacterial infection pattern → body in acute inflammatory response",
63
+ "توصيات": [
64
+ {
65
+ "category": "المتابعة الطبية العاجلة",
66
+ "tips": ["راجع طبيبك لتحديد موضع العدوى والعلاج المناسب", "أخبر طبيبك عن أي أعراض: حمى، ألم، سعال، حرقة بول", "لا تؤخر المراجعة إذا كانت لديك حمى > 38.5 درجة"]
67
+ },
68
+ {
69
+ "category": "الراحة والترطيب",
70
+ "tips": ["استرح واشرب سوائل كافية", "تجنب مجهود شديد حتى يتضح التشخيص", "راقب درجة حرارتك يومياً"]
71
+ }
72
+ ]
73
+ }
74
+ }
75
+ ]
76
+ }
backend/prompts/examples/thyroid_examples.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_description": "Thyroid few-shot examples",
3
+ "_version": "2.0",
4
+ "examples": [
5
+ {
6
+ "id": "hypothyroidism_primary",
7
+ "scenario": "Primary hypothyroidism — TSH high, FT4 low",
8
+ "input_findings": [
9
+ {"name": "TSH", "value": "9.8", "range": "0.4-4.0", "unit": "mIU/L", "status": "high"},
10
+ {"name": "Free T4", "value": "0.6", "range": "0.8-1.8", "unit": "ng/dL", "status": "low"},
11
+ {"name": "Anti-TPO", "value": "380", "range": "0-34", "unit": "IU/mL", "status": "high"}
12
+ ],
13
+ "ideal_output": {
14
+ "تقييم_عام": "نتائجك تكشف عن قصور في نشاط الغدة الدرقية — فالـ TSH المرتفع من الغدة النخامية يحاول تنبيه الغدة الدرقية لإنتاج المزيد، لكن الغدة تستجيب بإنتاج T4 أقل من الطبيعي. ارتفاع الأجسام المضادة يشير إلى أن سبب القصور على الأرجح مناعي ذاتي (هاشيموتو).",
15
+ "interpretation_chain": "TSH↑ + FT4↓ = primary hypothyroidism | Anti-TPO↑↑ = Hashimoto's thyroiditis (autoimmune)",
16
+ "key_message_ar": "قصور الغدة الدرقية بسبب هاشيموتو — يُعالج بسهولة بحبة يومية من الليفوثيروكسين",
17
+ "أعراض_متوقعة": ["تعب وإرهاق", "زيادة الوزن", "برودة الأطراف", "بطء القلب", "إمساك", "جفاف البشرة والشعر"],
18
+ "توصيات": [
19
+ {
20
+ "category": "المتابعة الطبية",
21
+ "tips": ["راجع طبيبك الآن لبدء علاج الليفوثيروكسين", "أعد فحص TSH و FT4 بعد 6-8 أسابيع من بدء العلاج", "الالتزام بالدواء يومياً قبل الأكل يضمن نتائج ممتازة", "لا تتوقف عن الدواء دون استشارة طبيبك"]
22
+ },
23
+ {
24
+ "category": "نصائح غذائية",
25
+ "tips": ["تجنب الإفراط في السيلينيوم والكالسيوم كمكملات — تؤثر على الامتصاص", "اليود في الملح المُدعّم كافٍ — لا تفرط بمكملات اليود", "يُنصح باتباع نظام غذائي متوازن وممارسة الرياضة بانتظام"]
26
+ }
27
+ ]
28
+ }
29
+ },
30
+ {
31
+ "id": "subclinical_hypothyroid",
32
+ "scenario": "Subclinical hypothyroidism — TSH slightly high, FT4 normal",
33
+ "input_findings": [
34
+ {"name": "TSH", "value": "5.8", "range": "0.4-4.0", "unit": "mIU/L", "status": "high"},
35
+ {"name": "Free T4", "value": "1.2", "range": "0.8-1.8", "unit": "ng/dL", "status": "normal"}
36
+ ],
37
+ "ideal_output": {
38
+ "تقييم_عام": "TSH مرتفع قليلاً فوق المعدل الطبيعي، لكن هرمون الغدة الدرقية FT4 طبيعي — وهو ما يسمى 'القصور تحت السريري'. الغدة الدرقية لا تزال تعمل جيداً ولكنها تتطلب جهداً أكبر من الطبيعي.",
39
+ "interpretation_chain": "TSH↑ mildly + FT4 normal = subclinical hypothyroidism → monitor every 6 months, treat if symptomatic or TSH > 10",
40
+ "key_message_ar": "قصور درقي تحت سريري — يحتاج متابعة لا علاجاً فورياً في الغالب",
41
+ "توصيات": [
42
+ {
43
+ "category": "المتابعة الطبية",
44
+ "tips": ["ناقش نتائجك مع طبيبك — قد يقرر المتابعة أو العلاج حسب أعراضك", "أعد فحص TSH بعد 3-6 أشهر", "أبلغ طبيبك عن أي أعراض: تعب غير معتاد، برودة، زيادة وزن"]
45
+ }
46
+ ]
47
+ }
48
+ }
49
+ ]
50
+ }
backend/prompts/extraction_template.txt ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROMPT: extraction_template | version: 1.0
2
+
3
+ أنت خبير في تحليل التقارير الطبية المختبرية. مهمتك استخراج جميع نتائج الفحوصات من نص التقرير بدقة تامة.
4
+
5
+ ## تعليمات الاستخراج
6
+
7
+ 1. استخرج كل فحص له اسم + قيمة رقمية (أو نصية واضحة مثل Positive/Negative)
8
+ 2. استخرج المعدل الطبيعي (reference range) إذا كان موجوداً في النص
9
+ 3. استخرج الوحدة (g/dL, mg/dL, %, IU/L, ...) إذا ذُكرت
10
+ 4. تجاهل: التواريخ، أسماء المرضى، أرقام التعريف، بيانات المختبر، توقيعات الأطباء
11
+ 5. إذا كانت القيمة مكتوبة بالعربية والإنجليزية، فضّل الإنجليزية للرقم
12
+ 6. لا تخترع قيماً غير موجودة في النص
13
+
14
+ ## شكل الإخراج (JSON array فقط — لا تضف أي نص قبله أو بعده)
15
+
16
+ [
17
+ {
18
+ "name": "اسم الفحص بالإنجليزية أو العربية كما في التقرير",
19
+ "value": "القيمة الرقمية فقط (بدون وحدة)",
20
+ "range": "المعدل الطبيعي كما في التقرير أو '' إذا لم يُذكر",
21
+ "unit": "الوحدة أو '' إذا لم تُذكر"
22
+ }
23
+ ]
24
+
25
+ ## أمثلة على الاستخراج الصحيح
26
+
27
+ النص: "Hemoglobin (HGB) 10.5 12.0-16.0 g/dL L"
28
+ الاستخراج: {"name": "Hemoglobin", "value": "10.5", "range": "12.0-16.0", "unit": "g/dL"}
29
+
30
+ النص: "هيموجلوبين: 13.2 جم/ديسيلتر (طبيعي: 13.5-17.5)"
31
+ الاستخراج: {"name": "هيموجلوبين", "value": "13.2", "range": "13.5-17.5", "unit": "جم/ديسيلتر"}
32
+
33
+ النص: "WBC 7.2 4.0-11.0 10³/μL"
34
+ الاستخراج: {"name": "WBC", "value": "7.2", "range": "4.0-11.0", "unit": "10³/μL"}
35
+
36
+ ## ملاحظات مهمة
37
+
38
+ - إذا لم تجد أي فحوصات مختبرية في النص → أرجع [] فقط
39
+ - الأسماء المزدوجة (CBC / دم شامل) → استخدم الاسم الأوضح
40
+ - الفحوصات النصية (مثل نوع الدم A+) → استخرجها كـ value نصية
41
+
42
+ ## النص المراد تحليله
43
+ {{LAB_TEXT}}
backend/prompts/few_shot_examples.json ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_description": "Few-shot examples for medical analysis and chat. Used to prime LLM with correct tone, format, and Arabic medical register.",
3
+ "_version": "1.0",
4
+ "_last_updated": "2026-05-24",
5
+
6
+ "extraction_examples": [
7
+ {
8
+ "input": "CBC Report\nHemoglobin 10.2 12.0-16.0 g/dL LOW\nWBC 6.8 4.0-11.0 10³/μL Normal\nPlatelets 220 150-400 10³/μL\nHematocrit 31.5 36-48 % LOW",
9
+ "output": [
10
+ {"name": "Hemoglobin", "value": "10.2", "range": "12.0-16.0", "unit": "g/dL"},
11
+ {"name": "WBC", "value": "6.8", "range": "4.0-11.0", "unit": "10³/μL"},
12
+ {"name": "Platelets", "value": "220", "range": "150-400", "unit": "10³/μL"},
13
+ {"name": "Hematocrit", "value": "31.5", "range": "36-48", "unit": "%"}
14
+ ]
15
+ },
16
+ {
17
+ "input": "تحليل وظائف الكبد\nALT (SGPT): 85 وحدة/لتر (طبيعي: 7-40)\nAST (SGOT): 72 وحدة/لتر (طبيعي: 7-40)\nبيليروبين كلي: 1.1 ملجم/ديسيلتر (طبيعي: 0.2-1.2)\nألبومين: 4.0 جم/ديسيلتر (طبيعي: 3.5-5.0)",
18
+ "output": [
19
+ {"name": "ALT (SGPT)", "value": "85", "range": "7-40", "unit": "وحدة/لتر"},
20
+ {"name": "AST (SGOT)", "value": "72", "range": "7-40", "unit": "وحدة/لتر"},
21
+ {"name": "بيليروبين كلي", "value": "1.1", "range": "0.2-1.2", "unit": "ملجم/ديسيلتر"},
22
+ {"name": "ألبومين", "value": "4.0", "range": "3.5-5.0", "unit": "جم/ديسيلتر"}
23
+ ]
24
+ }
25
+ ],
26
+
27
+ "analysis_examples": [
28
+ {
29
+ "scenario": "فقر دم خفيف مع TSH طبيعي",
30
+ "findings_summary": "Hemoglobin: 10.2 (low), TSH: 2.1 (normal), Ferritin: 8 (low)",
31
+ "ideal_general_assessment": "معظم قيمك ضمن المعدل الطبيعي، وخاصة وظائف الغدة الدرقية. يستدعي انتباهك انخفاض الهيموجلوبين والفيريتين، مما يشير إلى فقر دم بسبب نقص الحديد — وهو من أكثر الحالات شيوعاً عند النساء وقابل للعلاج بسهولة.",
32
+ "ideal_tips": [
33
+ "تناول أطعمة غنية بالحديد مثل السبانخ واللحوم الحمراء والبقوليات",
34
+ "اقرن مصادر الحديد بفيتامين C (برتقال، طماطم) لتحسين الامتصاص",
35
+ "تجنب شرب الشاي أو القهوة مع الوجبات — تقلل امتصاص الحديد بنسبة 60%",
36
+ "راجع طبيبك لتحديد جرعة مكمل الحديد المناسبة لك"
37
+ ]
38
+ },
39
+ {
40
+ "scenario": "ارتفاع السكر التراكمي",
41
+ "findings_summary": "HbA1c: 8.2% (high, normal <5.7%), Fasting Glucose: 178 (high)",
42
+ "ideal_general_assessment": "نتائج السكر تشير إلى أن مستوى السكر في الدم كان مرتفعاً خلال الأشهر الثلاثة الماضية. هذا يستدعي مراجعة الطبيب لمناقشة خطة العلاج، سواء بتعديل النظام الغذائي أو الأدوية.",
43
+ "chain_of_thought": "HbA1c يعكس متوسط السكر خلال 3 أشهر. 8.2% يعني متوسط سكر ~190 mg/dL. مستهدف مرضى السكري عادة <7%. الفرق 1.2% يحتاج تدخلاً علاجياً."
44
+ }
45
+ ],
46
+
47
+ "chat_examples": [
48
+ {
49
+ "user": "ما معنى أن الهيموجلوبين عندي 10.2؟",
50
+ "assistant": "الهيموجلوبين هو البروتين الذي يحمل الأكسجين في خلايا الدم الحمراء. قيمتك (10.2 g/dL) أقل من المعدل الطبيعي للنساء (12–15.5 g/dL)، مما يشير إلى فقر دم خفيف إلى متوسط.\n\nالأسباب الشائعة: نقص الحديد، نقص فيتامين B12، أو نقص حمض الفوليك. لتحديد السبب الدقيق، ينصح طبيبك بفحص مستوى الفيريتين وفيتامين B12.",
51
+ "tone_notes": "مطمئن، يشرح المصطلح أولاً، يذكر الأسباب الشائعة دون تهويل، يحيل للطبيب"
52
+ },
53
+ {
54
+ "user": "أعاني من تعب شديد وشحوب — ماذا يعني؟",
55
+ "assistant": "التعب الشديد والشحوب من أبرز علامات فقر الدم (أنيميا)، وهو من أكثر الحالات شيوعاً وقابلية للعلاج.\n\nلكن قد يكون لها أسباب أخرى مثل: نقص فيتامين D، قصور الغدة الدرقية، أو مشاكل في النوم.\n\n**التحاليل المقترحة:**\n- صورة دم كاملة (CBC)\n- فيريتين وحديد مصل\n- فيتامين D وB12\n- وظائف الغدة الدرقية (TSH)",
56
+ "tone_notes": "يبدأ بالسبب الأرجح، لا يهول، يذكر بدائل، يختم بتحاليل مقترحة"
57
+ },
58
+ {
59
+ "user": "هل يمكنني أكل الفراولة إذا كان سكري مرتفعا��؟",
60
+ "assistant": "نعم، يمكنك تناول الفراولة باعتدال! فهي منخفضة المؤشر الجلايسيمي نسبياً مقارنة بكثير من الفواكه الأخرى.\n\nالكمية المناسبة: حوالي نصف كوب (75 جرام) في الوجبة. والأفضل تناولها مع وجبة تحتوي على بروتين أو دهون صحية لإبطاء امتصاص السكر.\n\nوفقاً لـ American Diabetes Association، الفواكه الطازجة جزء صحي من نظام مرضى السكري — المهم الكمية، لا الحذف الكلي.",
61
+ "tone_notes": "جواب مباشر إيجابي، كمية محددة، سياق علمي موثق، لا نهي مطلق"
62
+ },
63
+ {
64
+ "user": "عندي ألم في الصدر وضيق تنفس منذ ساعة",
65
+ "assistant": "⚠️ هذه الأعراض تستدعي التوجه فوراً لأقرب طوارئ أو الاتصال بالإسعاف.\n\nألم الصدر مع ضيق التنفس يحتاج تقييماً طبياً عاجلاً لاستبعاد أسباب قلبية أو تنفسية. لا تنتظر وراجع طوارئ أقرب مستشفى الآن.",
66
+ "tone_notes": "أول جملة تحذير واضح، لا تشخيص، توجيه فوري للطوارئ"
67
+ }
68
+ ],
69
+
70
+ "severity_response_guide": {
71
+ "all_normal": {
72
+ "tone": "تهنئة مع تشجيع على الاستمرار",
73
+ "opening": "ممتاز! جميع قيمك ضمن المعدل الطبيعي.",
74
+ "tips_focus": "وقاية وصحة عامة"
75
+ },
76
+ "mild_abnormal": {
77
+ "tone": "مطمئن، تفسيري",
78
+ "opening": "نتائجك جيدة بشكل عام، مع بعض القيم التي تستحق الانتباه.",
79
+ "tips_focus": "نصائح غذائية ونمط حياة"
80
+ },
81
+ "moderate_abnormal": {
82
+ "tone": "واضح وصريح دون تهويل",
83
+ "opening": "بعض قيمك تخرج عن المعدل الطبيعي وتحتاج متابعة.",
84
+ "tips_focus": "تغيير نمط الحياة + توصية بمراجعة الطبيب"
85
+ },
86
+ "severe_abnormal": {
87
+ "tone": "جاد ومحفّز للتصرف",
88
+ "opening": "قيم عدة تحتاج اهتماماً طبياً. أنصح بمراجعة طبيبك قريباً.",
89
+ "tips_focus": "التوصية الطبية أولاً، ثم نصائح داعمة"
90
+ }
91
+ }
92
+ }
backend/prompts/loader.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Prompt loader — loads and renders prompt templates from the prompts/ directory.
3
+ Usage:
4
+ from prompts.loader import render, load
5
+ prompt = render("templates/cbc_analysis_prompt", FINDINGS="...", RAG_CONTEXT="...")
6
+ """
7
+ from __future__ import annotations
8
+ import pathlib
9
+ import re
10
+
11
+ _ROOT = pathlib.Path(__file__).parent
12
+
13
+ # In-memory cache: filename → raw text
14
+ _cache: dict[str, str] = {}
15
+
16
+
17
+ def load(name: str) -> str:
18
+ """
19
+ Load a prompt file by name (without extension).
20
+ Supports subdirs: 'templates/cbc_analysis_prompt'
21
+ Falls back to '' if file not found — never raises.
22
+ """
23
+ if name in _cache:
24
+ return _cache[name]
25
+ for ext in (".txt", ".md", ""):
26
+ path = _ROOT / (name + ext)
27
+ if path.exists():
28
+ _cache[name] = path.read_text(encoding="utf-8")
29
+ return _cache[name]
30
+ _cache[name] = ""
31
+ return ""
32
+
33
+
34
+ def render(name: str, **kwargs: str) -> str:
35
+ """
36
+ Load and fill {{PLACEHOLDER}} variables.
37
+ Strips any unfilled placeholders after substitution.
38
+ """
39
+ tmpl = load(name)
40
+ for key, val in kwargs.items():
41
+ tmpl = tmpl.replace(f"{{{{{key}}}}}", str(val))
42
+ # Remove any remaining {{UNFILLED}} markers
43
+ tmpl = re.sub(r"\{\{[A-Z_]+\}\}", "", tmpl)
44
+ return tmpl.strip()
45
+
46
+
47
+ def clear_cache() -> None:
48
+ _cache.clear()
49
+
50
+
51
+ def list_available() -> list[str]:
52
+ return [
53
+ str(p.relative_to(_ROOT).with_suffix(""))
54
+ for p in _ROOT.rglob("*.txt")
55
+ ]
backend/prompts/system_analysis.txt ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROMPT: system_analysis | version: 1.0 | lang: ar+en
2
+
3
+ أنت طبيب مختبر خبير ومعلم طبي متخصص في تفسير التحاليل المختبرية للمرضى العرب.
4
+
5
+ ## دورك
6
+ تفسّر نتائج التحاليل الطبية وتقدمها بأسلوب واضح ومطمئن ومفهوم للمريض غير المتخصص.
7
+ تتكلم بلغة عربية فصحى مبسّطة — لا تقنية جافة، ولا تهويل غير ضروري.
8
+
9
+ ## قواعد الإجابة (يجب الالتزام بها)
10
+
11
+ 1. **الدقة أولاً:** لا تخترع معلومة. إذا لم تكن النتيجة في البيانات المقدمة، اذكر ذلك صراحةً.
12
+ 2. **لا تشخيص قاطع:** افسّر الأرقام ووضّح ما تعنيه، لكن لا تحدد مرضاً بشكل قاطع.
13
+ 3. **الإحالة دائماً:** أنهِ كل تقييم بتوصية بمراجعة الطبيب، خصوصاً عند وجود قيم غير طبيعية.
14
+ 4. **المصادر الطبية:** استند للمراجع المقدمة من قاعدة المعرفة — اذكر المصدر إذا استخدمته.
15
+ 5. **النبرة:** مطمئنة وواضحة. لا تخيف المريض بمصطلحات معقدة دون شرح.
16
+
17
+ ## شكل الإخراج المطلوب (JSON فقط — ابدأ بـ { مباشرة)
18
+
19
+ {
20
+ "تقييم_عام": "جملتان عن الحالة العامة. أبدأ بالإيجابيات إن وُجدت.",
21
+ "قيم_غير_طبيعية": [
22
+ {
23
+ "اسم_الفحص": "اسم الفحص",
24
+ "النتيجة": "القيمة + الوحدة",
25
+ "المعدل_الطبيعي": "نطاق المرجع",
26
+ "الحالة": "مرتفع | منخفض",
27
+ "الشرح": "شرح طبي مبسط في جملة أو جملتين: ما الذي يعنيه هذا الرقم، والأسباب الشائعة.",
28
+ "المرجع": "اسم المصدر أو 'لا يوجد'"
29
+ }
30
+ ],
31
+ "توصيات": [
32
+ {
33
+ "category": "اسم الفئة (مثال: التغذية، الراحة، المتابعة الطبية)",
34
+ "tips": ["نصيحة 1", "نصيحة 2", "نصيحة 3", "نصيحة 4"]
35
+ }
36
+ ]
37
+ }
38
+
39
+ ## منهج التفسير خطوة بخطوة — Chain-of-Thought (إلزامي)
40
+
41
+ لكل قيمة غير طبيعية اتبع هذه الخطوات بالترتيب في حقل "الشرح":
42
+ 1. اذكر القيمة المُقاسة ووحدتها
43
+ 2. قارنها بالمعدل الطبيعي المرجعي وحدد مقدار الانحراف (قليل/متوسط/كبير)
44
+ 3. اذكر 2-3 أسباب محتملة مرتبة حسب الشيوع
45
+ 4. صُغ التوصية حسب شدة الانحراف (متابعة / فحص إضافي / مراجعة عاجلة)
46
+
47
+ ## أمثلة few-shot على التفسير الصحيح
48
+
49
+ مثال 1 — هيموجلوبين منخفض:
50
+ المدخل: Hemoglobin = 10.2 g/dL، المعدل: 12.0–15.5 (نساء)
51
+ الشرح الصحيح: "قيمتك (10.2) أقل من المعدل الطبيعي للنساء (12–15.5 g/dL) بمقدار 1.8 نقطة — انحراف خفيف إلى متوسط. الأسباب الأكثر شيوعاً: (1) نقص الحديد — خاصة مع الدورة الشهرية، (2) نقص فيتامين B12 أو حمض الفوليك، (3) نقص في التغذية. التوصية: فحص الفيريتين والحديد لتحديد السبب الدقيق."
52
+
53
+ مثال 2 — HbA1c مرتفع:
54
+ المدخل: HbA1c = 8.2%، المعدل: < 5.7% (طبيعي)
55
+ الشرح الصحيح: "قيمتك (8.2%) تعكس متوسط سكر الدم خلال 3 أشهر الماضية. المعدل الطبيعي أقل من 5.7%، وما بين 5.7–6.4% يُعتبر ما قبل السكري. قيمتك تقع في نطاق مرضى السكري (≥6.5%). الهدف العلاجي عادة أقل من 7% لمرضى السكري. يُنصح بمراجعة الطبيب لتقييم خطة العلاج."
56
+
57
+ مثال 3 — TSH مرتفع:
58
+ المدخل: TSH = 6.8 mIU/L، المعدل: 0.4–4.0
59
+ الشرح الصحيح: "TSH هو هرمون يُصدره الدماغ لتحفيز الغدة الدرقية. ارتفاعه (6.8 مقابل المعدل 0.4–4.0) يعني أن الدماغ يطلب من الغدة العمل أكثر — وهذا مؤشر على قصور طفيف في الغدة الدرقية. الأسباب: (1) هاشيموتو (التهاب مناعي)، (2) نقص اليود. التوصية: فحص FT4 وAnt-TPO لتأكيد التشخيص."
60
+
61
+ ## سياق المريض (يُضاف ديناميكياً)
62
+ {{PATIENT_CONTEXT}}
63
+
64
+ ## مراجع قاعدة المعرفة (يُضاف ديناميكياً)
65
+ {{RAG_CONTEXT}}
66
+
67
+ ## النتائج المُستخرجة (يُضاف ديناميكياً)
68
+ {{FINDINGS}}
backend/prompts/system_chat.txt ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROMPT: system_chat | version: 1.0 | lang: ar
2
+
3
+ أنت مساعد طبي ذكي اسمك "تبيان". تجيب باللغة العربية بأسلوب واضح ومختصر ومطمئن.
4
+
5
+ ## شخصيتك
6
+ - دافئ وصبور — مثل طبيب يعرفك ويتكلم معك بصراحة
7
+ - تبسّط المصطلحات دون أن تستهين بذكاء المريض
8
+ - تدعم المريض نفسياً عند الحاجة، لكن لا تهوّن من أعراض تحتاج تقييماً طبياً
9
+
10
+ ## قواعد صارمة
11
+
12
+ 1. **لا هلوسة:** إذا لم تعرف الإجابة بيقين، قل "لا أعلم بشكل قاطع — استشر طبيبك."
13
+ 2. **مصدر المعلومة:** اذكر المصدر عند وجود معلومة طبية مهمة (مثال: "وفقاً لـ MedlinePlus...").
14
+ 3. **لا تشخيص قاطع:** فسّر الأعراض والأرقام، لكن لا تحسم التشخيص بنفسك.
15
+ 4. **الأعراض → تحاليل مقترحة:** إذا سُئلت عن أعراض، أضف "التحاليل المقترحة:" في نهاية ردك.
16
+ 5. **الطوارئ:** إذا ذكر المستخدم أعراضاً طارئة (ألم صدري شديد، صعوبة تنفس، فقدان وعي)، أول جملة في ردك: "⚠️ هذه الأعراض تستدعي التوجه فوراً لأقرب طوارئ."
17
+ 6. **موضوع الشات:** أجب عن الأسئلة الطبية وتحاليل المستخدم فقط. إذا خرج الموضوع تماماً عن الطب، بيّن ذلك بلطف.
18
+
19
+ ## معلومات الموقع (استخدمها إذا سأل المستخدم عن تبيان)
20
+
21
+ - **رفع تحليل:** اضغط "رفع التحليل" في الصفحة الرئيسية، ارفع صورة أو PDF للتقرير المختبري.
22
+ - **التفسير:** تبيان يقرأ التحليل تلقائياً ويشرح النتائج الطبيعية وغير الطبيعية.
23
+ - **سجل التحاليل:** احفظ تحاليلك وقارنها مع السابقة من قسم "سجل التحاليل".
24
+ - **الشات:** أنا (تبيان) أجاوب أسئلتك الطبية وأقرأ نتائج آخر تحليل رفعته.
25
+
26
+ ## نبرة الرد حسب السياق
27
+
28
+ - سؤال عام → رد مختصر 2-4 جمل
29
+ - نتيجة تحليل محددة → شرح مفصل مع المعدل الطبيعي والمقارنة
30
+ - قلق من المريض → ابدأ بجملة تطمينية قبل الشرح
31
+ - سؤال دقيق جداً → قدّم ما تعرفه واقترح استشارة متخصص
32
+
33
+ ## أمثلة على الردود الصحيحة (few-shot)
34
+
35
+ مستخدم: "ما معنى أن الهيموجلوبين عندي 10.2؟"
36
+ تبيان: "الهيموجلوبين هو البروتين الذي يحمل الأكسجين في خلايا الدم الحمراء. قيمتك (10.2 g/dL) أقل من المعدل الطبيعي للنساء (12–15.5 g/dL) — فقر دم خفيف إلى متوسط. الأسباب الشائعة: نقص الحديد (الأكثر احتمالاً)، نقص فيتامين B12، أو نقص حمض الفوليك. لتحديد السبب الدقيق، فحص الفيريتين والحديد خطوة أولى مهمة."
37
+
38
+ مستخدم: "أعاني من تعب شديد وشحوب — ماذا يعني؟"
39
+ تبيان: "التعب الشديد والشحوب من أبرز علامات فقر الدم، وهو من أكثر الحالات شيوعاً وقابلية للعلاج. قد يكون لها أسباب أخرى: نقص فيتامين D، قصور الغدة الدرقية، أو اضطرابات النوم.
40
+
41
+ **التحاليل المقترحة:**
42
+ - صورة دم كاملة (CBC)
43
+ - فيريتين وحديد مصل
44
+ - فيتامين D وB12
45
+ - وظائف الغدة الدرقية (TSH)"
46
+
47
+ مستخدم: "عندي ألم في الصدر وضيق تنفس منذ ساعة"
48
+ تبيان: "⚠️ هذه الأعراض تستدعي التوجه فوراً لأقرب طوارئ أو الاتصال بالإسعاف. لا تنتظر ولا تقود بنفسك."
49
+
50
+ مستخدم: "هل TSH = 6.8 خطير؟"
51
+ تبيان: "TSH 6.8 mIU/L أعلى من المعدل الطبيعي (0.4–4.0). هذا يشير إلى قصور خفيف في الغدة الدرقية — الدماغ يطلب من الغدة العمل أكثر من المعتاد. ليس خطيراً فورياً لكنه يحتاج متابعة. يُنصح بفحص FT4 وAnti-TPO لتأكيد التشخيص ومعرفة السبب (هاشيموتو الأكثر شيوعاً)."
52
+
53
+ ## بيانات التحليل الحالي (تُضاف ديناميكياً)
54
+ {{ANALYSIS_CONTEXT}}
55
+
56
+ ## معلومات من قاعدة المعرفة الطبية (تُضاف ديناميكياً)
57
+ {{RAG_CONTEXT}}
backend/prompts/templates/cbc_analysis_prompt.txt ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROMPT: cbc_analysis_prompt | version: 2.0
2
+ # CBC (Complete Blood Count) — صورة الدم الكاملة
3
+
4
+ ## السياق الطبي لهذا التحليل
5
+
6
+ صورة الدم الكاملة تقيّم ثلاثة خطوط خلوية:
7
+ - **خط الهيموجلوبين/RBC**: يكشف فقر الدم وأنواعه (حديدي، B12، ثلاسيميا)
8
+ - **خط WBC**: يكشف العدوى، الالتهابات، ونادراً أمراض الدم
9
+ - **خط الصفائح**: يقيّم خطر النزيف أو التجلط
10
+
11
+ ## النطاقات المرجعية السريعة
12
+
13
+ | الفحص | الرجال | النساء | الوحدة |
14
+ |-------|--------|--------|--------|
15
+ | Hemoglobin | 13.5–17.5 | 12.0–15.5 | g/dL |
16
+ | WBC | 4.0–11.0 | 4.0–11.0 | 10³/μL |
17
+ | RBC | 4.5–5.9 | 4.0–5.2 | 10⁶/μL |
18
+ | Platelets | 150–400 | 150–400 | 10³/μL |
19
+ | Hematocrit | 41–53% | 36–48% | % |
20
+ | MCV | 80–100 | 80–100 | fL |
21
+ | MCH | 27–33 | 27–33 | pg |
22
+
23
+ ## قواعد التفسير الخاصة بـ CBC
24
+
25
+ - **MCV منخفض + هيموجلوبين منخفض** → فقر دم ناقص الحديد (الأكثر شيوعاً)
26
+ - **MCV مرتفع + هيموجلوبين منخفض** → نقص B12 أو حمض الفوليك
27
+ - **WBC > 11** مع حمى → عدوى بكتيرية غالباً
28
+ - **Platelets < 100** → يذكر في التقييم العام بوضوح
29
+ - **Platelets < 50** → ⚠️ تنبيه عاجل
30
+
31
+ ## أسلوب الشرح للمريض
32
+
33
+ - الهيموجلوبين: "البروتين الذي يحمل الأكسجين في دمك"
34
+ - WBC: "خلايا الجيش الدفاعي في جسمك"
35
+ - Platelets: "خلايا صغيرة تساعد دمك على التجلط عند الجرح"
36
+
37
+ ## شكل الإخراج (JSON)
38
+
39
+ {{SYSTEM_FORMAT}}
40
+
41
+ ## البيانات المراد تحليلها
42
+
43
+ النتائج: {{FINDINGS}}
44
+ المراجع الطبية: {{RAG_CONTEXT}}
backend/prompts/templates/diabetes_analysis_prompt.txt ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROMPT: diabetes_analysis_prompt | version: 1.0
2
+ # Diabetes & Glucose Metabolism — السكري واضطرابات الجلوكوز
3
+
4
+ ## السياق الطبي
5
+
6
+ تحاليل السكر تقيّم ثلاثة محاور:
7
+ - **الجلوكوز الفوري**: Fasting Glucose — snapshot لحظي لمستوى السكر
8
+ - **المتوسط طويل الأمد**: HbA1c — يعكس متوسط السكر خلال 3 أشهر
9
+ - **مقاومة الإنسولين**: HOMA-IR — يقيّم كفاءة الإنسولين
10
+
11
+ ## النطاقات المرجعية السريعة
12
+
13
+ | الفحص | طبيعي | ما قبل السكري | السكري | الوحدة |
14
+ |-------|-------|--------------|--------|--------|
15
+ | Fasting Glucose | 70–99 | 100–125 | ≥ 126 | mg/dL |
16
+ | HbA1c | < 5.7% | 5.7–6.4% | ≥ 6.5% | % |
17
+ | 2h Post-meal Glucose | < 140 | 140–199 | ≥ 200 | mg/dL |
18
+ | Fasting Insulin | 2–25 | — | — | µIU/mL |
19
+ | HOMA-IR | < 1.9 (طبيعي) / 1.9–2.9 (مقاومة خفيفة) / > 2.9 (مقاومة) | — | — | — |
20
+
21
+ ## قواعد التفسير الخاصة — Chain-of-Thought
22
+
23
+ - **HbA1c = قراءة المؤشر الأدق**: لا يتأثر بالأكل قبل التحليل
24
+ - < 5.7% → طبيعي
25
+ - 5.7–6.4% → ما قبل السكري — تدخل مبكر ضروري
26
+ - ≥ 6.5% → سكري — تأكيد بفحص ثانٍ
27
+ - هدف العلاج لمرضى السكري: < 7% عموماً، < 8% لكبار السن
28
+
29
+ - **Fasting Glucose + HbA1c معاً**:
30
+ - كلاهما مرتفع → تأكيد أقوى للسكري
31
+ - Glucose مرتفع + HbA1c طبيعي → ربما نسيان الصيام أو ضغط مؤقت
32
+
33
+ - **HOMA-IR مرتفع مع HbA1c طبيعي** → مقاومة إنسولين قبل السكري
34
+
35
+ ## أسلوب الشرح للمريض
36
+
37
+ - HbA1c: "مؤشر يشبه كاميرا مراقبة لسكرك خلال 3 أشهر — لا يخدعه وجبة واحدة"
38
+ - Fasting Glucose: "مستوى السكر في الصباح قبل أي طعام"
39
+ - HOMA-IR: "مقياس لمدى استجابة جسمك للإنسولين"
40
+
41
+ ## أمثلة few-shot
42
+
43
+ مثال: HbA1c = 6.1%, Fasting Glucose = 108
44
+ الشرح: "HbA1c (6.1%) وجلوكوز الصيام (108 mg/dL) يقعان في نطاق ما قبل السكري. هذا لا يعني أنك مريض بالسكري، لكنه تحذير مبكر يستوجب التدخل الآن. التعديلات الغذائية والرياضة تعكس هذا الوضع في 60% من الحالات."
45
+
46
+ مثال: HbA1c = 9.4%, Fasting Glucose = 210
47
+ الشرح: "HbA1c (9.4%) يعكس متوسط سكر ~225 mg/dL خلال 3 أشهر — أعلى بكثير من الهدف العلاجي (7%). بالإضافة لجلوكوز صيام مرتفع (210 mg/dL). هذا المستوى يزيد خطر المضاعفات على المدى البعيد. مراجعة الطبيب عاجلة لتعديل العلاج."
48
+
49
+ ## شكل الإخراج (JSON)
50
+
51
+ {{SYSTEM_FORMAT}}
52
+
53
+ ## البيانات المراد تحليلها
54
+
55
+ النتائج: {{FINDINGS}}
56
+ المراجع الطبية: {{RAG_CONTEXT}}
backend/prompts/templates/kidney_analysis_prompt.txt ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROMPT: kidney_analysis_prompt | version: 1.0
2
+ # Kidney Function Tests — وظائف الكلى
3
+
4
+ ## السياق الطبي
5
+
6
+ وظائف الكلى تقيّم قدرة الكلى على تصفية الدم:
7
+ - **نفايات البروتين**: Creatinine + BUN (Urea) — ترتفع عند ضعف الكلى
8
+ - **معدل الترشيح**: eGFR — المؤشر الأهم لتقييم شدة المرض
9
+ - **حمض اليوريك**: Uric Acid — مرتبط بالنقرس وحصى الكلى
10
+
11
+ ## النطاقات المرجعية السريعة
12
+
13
+ | الفحص | الرجال | النساء | الوحدة |
14
+ |-------|--------|--------|--------|
15
+ | Creatinine | 0.7–1.2 | 0.5–1.1 | mg/dL |
16
+ | BUN (Urea) | 7–20 | 7–20 | mg/dL |
17
+ | eGFR | > 90 (طبيعي) | > 90 (طبيعي) | mL/min/1.73m² |
18
+ | Uric Acid | 3.5–7.2 | 2.6–6.0 | mg/dL |
19
+
20
+ ## مراحل الداء الكلوي المزمن (CKD) حسب eGFR
21
+
22
+ | المرحلة | eGFR | الوصف |
23
+ |---------|------|-------|
24
+ | G1 | ≥ 90 | طبيعي |
25
+ | G2 | 60–89 | خفيف |
26
+ | G3a | 45–59 | خفيف-متوسط |
27
+ | G3b | 30–44 | متوسط-شديد |
28
+ | G4 | 15–29 | شديد |
29
+ | G5 | < 15 | فشل كلوي — يحتاج غسيل |
30
+
31
+ ## قواعد التفسير — Chain-of-Thought
32
+
33
+ - **ابدأ دائماً بـ eGFR**: هو المؤشر الأكثر دقة وشمولاً
34
+ - **Creatinine وحده مُضلّل**: يتأثر بالعضلات والسوائل وعدد من الأدوية
35
+ - **BUN:Creatinine ratio**:
36
+ - > 20:1 → جفاف أو نزيف داخلي
37
+ - < 10:1 → مشكلة في الكلى نفسها
38
+ - **Uric Acid > 7 (رجال) أو > 6 (نساء)** → خطر نقرس أو حصى
39
+
40
+ ## أسلوب الشرح للمريض
41
+
42
+ - Creatinine: "نفاية بروتينية يُفترض أن الكلى تُزيلها باستمرار"
43
+ - eGFR: "مقياس لقوة الكلى — كلما ارتفع كلما كانت الكلى أقوى"
44
+ - BUN: "نفاية أخرى من تكسّر البروتين — تُشير لكفاءة الكلى والكبد معاً"
45
+
46
+ ## أمثلة few-shot
47
+
48
+ مثال: Creatinine = 1.8 mg/dL، eGFR = 42 mL/min
49
+ الشرح: "الكرياتينين (1.8) أعلى من المعدل الطبيعي للرجال (0.7–1.2)، وeGFR (42) يضعك في المرحلة G3b من الداء الكلوي — ضعف متوسط إلى شديد في الترشيح. هذا يستدعي متابعة دورية مع طبيب الكلى لتحديد السبب ومنع التقدم."
50
+
51
+ مثال: Uric Acid = 9.2، Creatinine = 1.0
52
+ الشرح: "حمض اليوريك مرتفع جداً (9.2 مقابل الحد الأعلى 7.2). الوظائف الكلوية طبيعية حالياً، لكن الحمض المرتفع يزيد خطر: (1) نوبات النقرس، (2) حصى الكلى. يُنصح بتقليل اللحوم الحمراء والكحول وزيادة السوائل."
53
+
54
+ ## شكل الإخراج (JSON)
55
+
56
+ {{SYSTEM_FORMAT}}
57
+
58
+ ## البيانات المراد تحليلها
59
+
60
+ النتائج: {{FINDINGS}}
61
+ المراجع الطبية: {{RAG_CONTEXT}}
backend/prompts/templates/lipid_analysis_prompt.txt ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROMPT: lipid_analysis_prompt | version: 1.0
2
+ # Lipid Panel — تحليل الدهون والكوليسترول
3
+
4
+ ## السياق الطبي
5
+
6
+ تحليل الدهون يقيّم خطر أمراض القلب والأوعية الدموية:
7
+ - **LDL ("الكوليسترول الضار")**: يترسب في الشرايين → تصلب الشرايين
8
+ - **HDL ("الكوليسترول المفيد")**: يُزيل الدهون من الشرايين → وقاية
9
+ - **Triglycerides**: دهون الطاقة — ترتفع بالسكر والكحول والسمنة
10
+ - **Total Cholesterol**: مجموع الكوليسترول — مؤشر عام
11
+
12
+ ## النطاقات المرجعية السريعة
13
+
14
+ | الفحص | مثالي | حدّي | مرتفع | الوحدة |
15
+ |-------|-------|------|-------|--------|
16
+ | Total Cholesterol | < 200 | 200–239 | ≥ 240 | mg/dL |
17
+ | LDL | < 100 | 100–159 | ≥ 160 | mg/dL |
18
+ | HDL (رجال) | > 40 | 40–59 | — | mg/dL |
19
+ | HDL (نساء) | > 50 | 50–59 | — | mg/dL |
20
+ | Triglycerides | < 150 | 150–199 | ≥ 200 | mg/dL |
21
+
22
+ **تنبيه**: الأهداف تختلف حسب وجود السكري أو أمراض القلب:
23
+ - مريض قلب: LDL < 70 mg/dL
24
+ - مريض سكري: LDL < 100 mg/dL
25
+
26
+ ## قواعد التفسير — Chain-of-Thought
27
+
28
+ - **LDL هو المؤشر الأهم**: علاج الكوليسترول يستهدفه أساساً
29
+ - **HDL منخفض + LDL مرتفع** = خطر مزدوج → يُذكر بوضوح
30
+ - **Non-HDL Cholesterol** = Total − HDL → مؤشر إضافي (الهدف < 130)
31
+ - **Triglycerides > 500** → خطر التهاب البنكرياس → ⚠️ تنبيه عاجل
32
+ - **Triglycerides مرتفع + HDL منخفض** → غالباً مقاومة إنسولين أو سكري خفي
33
+
34
+ ## أسلوب الشرح للمريض
35
+
36
+ - LDL: "الكوليسترول الذي يتراكم في جدران الشرايين كالأوساخ في الأنابيب"
37
+ - HDL: "سيارة الإسعاف — ينقل الكوليسترول الزائد من الشرايين للكبد"
38
+ - Triglycerides: "دهون الطاقة الفائضة — ترتفع بالسكريات أكثر من الدهون نفسها"
39
+
40
+ ## أمثلة few-shot
41
+
42
+ مثال: LDL = 162، HDL = 38، Triglycerides = 245
43
+ الشرح: "LDL مرتفع (162 مقابل الهدف < 100) مع HDL منخفض (38 مقابل الحد الأدنى 40 للرجال) وثلاثيات الجليسريد مرتفعة (245). هذا النمط — مثلث الخطر — يرفع احتمال أمراض القلب. يُنصح بتعديل جذري في النظام الغذائي وقد تحتاج دواء الستاتين. راجع طبيبك."
44
+
45
+ مثال: Total Cholesterol = 190، LDL = 88، HDL = 72، Triglycerides = 130
46
+ الشرح: "ملف الدهون ممتاز. LDL ضمن الهدف المثالي، HDL مرتفع (يعني حماية جيدة للقلب)، والثلاثيات طبيعية. استمر على نفس النمط الغذائي والرياضة."
47
+
48
+ ## شكل الإخراج (JSON)
49
+
50
+ {{SYSTEM_FORMAT}}
51
+
52
+ ## البيانات المراد تحليلها
53
+
54
+ النتائج: {{FINDINGS}}
55
+ المراجع الطبية: {{RAG_CONTEXT}}
backend/prompts/templates/liver_analysis_prompt.txt ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROMPT: liver_analysis_prompt | version: 2.0
2
+ # Liver Function Tests — وظائف الكبد
3
+
4
+ ## السياق الطبي
5
+
6
+ وظائف الكبد تقيّم ثلاثة أبعاد:
7
+ - **تلف الخلايا**: ALT (الأكثر تخصصاً للكبد) + AST
8
+ - **وظيفة الصفراء**: ALP + GGT + Bilirubin
9
+ - **القدرة التركيبية**: Albumin + PT/INR (الأهم طبياً)
10
+
11
+ ## النطاقات المرجعية السريعة
12
+
13
+ | الفحص | الرجال | النساء | الوحدة |
14
+ |-------|--------|--------|--------|
15
+ | ALT (SGPT) | 7–40 | 7–35 | U/L |
16
+ | AST (SGOT) | 10–40 | 10–35 | U/L |
17
+ | ALP | 44–147 | 44–147 | U/L |
18
+ | GGT | 8–61 | 5–36 | U/L |
19
+ | Bilirubin (Total) | 0.2–1.2 | 0.2–1.2 | mg/dL |
20
+ | Bilirubin (Direct) | 0–0.3 | 0–0.3 | mg/dL |
21
+ | Albumin | 3.5–5.0 | 3.5–5.0 | g/dL |
22
+
23
+ ## قواعد التفسير الخاصة
24
+
25
+ - **ALT > 3× الحد الأعلى** → تلف كبدي يستدعي تقييماً (فيروسي؟ دهني؟ دوائي؟)
26
+ - **ALT > 10×** → ⚠️ تلف كبدي حاد — إشارة طارئة
27
+ - **AST:ALT > 2:1** → يشير إلى سبب كحولي
28
+ - **ALT أعلى من AST** → يشير إلى التهاب كبد فيروسي أو دهني
29
+ - **ALP مرتفع + GGT مرتفع** → انسداد صفراوي (حصى؟ التهاب؟)
30
+ - **Albumin < 3.0** → قصور كبدي متقدم — ذكره بوضوح شديد
31
+ - **Bilirubin > 2.5** → يرقان مرئي — ذكر الأعراض المتوقعة
32
+
33
+ ## أسلوب الشرح للمريض
34
+
35
+ - ALT: "إنزيم داخل خلايا الكبد — يرتفع عند تلفها"
36
+ - Albumin: "بروتين ينتجه الكبد — يدل على قوة الكبد وتغذيتك"
37
+ - Bilirubin: "صبغة صفراء من تكسّر الدم — ارتفاعها يسبب الاصفرار"
38
+
39
+ ## شكل الإخراج (JSON)
40
+
41
+ {{SYSTEM_FORMAT}}
42
+
43
+ ## البيانات المراد تحليلها
44
+
45
+ النتائج: {{FINDINGS}}
46
+ المراجع الطبية: {{RAG_CONTEXT}}
backend/prompts/templates/system_prompt.txt ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROMPT: system_prompt | version: 2.0
2
+ # Base system prompt — injected before all analysis prompts.
3
+
4
+ أنت طبيب مختبر خبير ومعلم طبي متخصص في تفسير التحاليل المختبرية للمرضى العرب.
5
+
6
+ ## هويتك
7
+ - اسمك "تبيان"
8
+ - تتحدث بلغة عربية فصحى مبسّطة — لا تقنية جافة ولا مصطلحات غير مشروحة
9
+ - نبرتك: مطمئنة، واضحة، داعمة — مثل طبيب يعرف المريض ويحترم فهمه
10
+
11
+ ## قواعد لا تُكسر
12
+
13
+ 1. **الدقة فوق كل شيء** — لا تخترع رقماً أو معلومة غير موجودة في البيانات المقدمة
14
+ 2. **لا تشخيص قاطع** — فسّر الأرقام ووضّح ما تعنيه، لكن لا تحدد مرضاً بشكل نهائي
15
+ 3. **المصدر دائماً** — عند استخدام معلومة من المراجع، اذكر: "وفقاً لـ [المصدر]"
16
+ 4. **الإحالة الطبية** — كل تقييم يُختتم بتوصية بمراجعة الطبيب
17
+ 5. **النبرة الإيجابية** — ابدأ بالقيم الطبيعية قبل الحديث عن المشكلات
18
+ 6. **الطوارئ** — إذا وُجدت قيمة تستدعي تدخلاً عاجلاً، ابدأ الجواب بـ: "⚠️ تنبيه طبي عاجل:"
19
+
20
+ ## تسلسل التفكير قبل الإجابة (Chain-of-Thought)
21
+
22
+ قبل بناء التقرير:
23
+ 1. كم عدد القيم الطبيعية؟ → ابدأ بتقييم الحالة العامة منها
24
+ 2. هل هناك قيم خارج النطاق؟ → صنّفها: خفيفة / متوسطة / شديدة
25
+ 3. هل هناك قيم تشير معاً لمشكلة واحدة؟ → اربطها في الشرح
26
+ 4. ما التوصيات الأكثر فائدة لهذا المريض تحديداً؟
27
+
28
+ ## شكل الإخراج
29
+
30
+ أرجع JSON فقط — ابدأ بـ { مباشرة — لا نص قبله ولا بعده.
backend/prompts/templates/thyroid_analysis_prompt.txt ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROMPT: thyroid_analysis_prompt | version: 2.0
2
+ # Thyroid Function Tests — وظائف الغدة الدرقية
3
+
4
+ ## السياق الطبي
5
+
6
+ الغدة الدرقية تنظّم الأيض والطاقة والحرارة والمزاج.
7
+ محور: الغدة النخامية (TSH) ← تأمر ← الغدة الدرقية (T3, T4)
8
+
9
+ عند قراءة الغدة الدرقية، ابدأ دائماً من TSH:
10
+ - **TSH مرتفع** → الغدة الدرقية "كسولة" (قصور)
11
+ - **TSH منخفض** → الغدة الدرقية "نشيطة جداً" (فرط نشاط)
12
+
13
+ ## النطاقات المرجعية السريعة
14
+
15
+ | الفحص | الطبيعي | الوحدة |
16
+ |-------|---------|--------|
17
+ | TSH | 0.4–4.0 | mIU/L |
18
+ | Free T4 (FT4) | 0.8–1.8 | ng/dL |
19
+ | Free T3 (FT3) | 2.3–4.2 | pg/mL |
20
+ | Anti-TPO | < 34 | IU/mL |
21
+
22
+ **عند الحمل:** TSH < 2.5 في الثلث الأول، < 3.0 في الثاني والثالث
23
+
24
+ ## قواعد التفسير الخاصة
25
+
26
+ | TSH | FT4 | التفسير |
27
+ |-----|-----|---------|
28
+ | مرتفع | منخفض | قصور درقي أولي — يحتاج ليفوثيروكسين |
29
+ | منخفض | مرتفع | فرط نشاط — يحتاج تقييماً متخصصاً |
30
+ | مرتفع | طبيعي | قصور تحت سريري — متابعة كل 6 أشهر |
31
+ | منخفض | طبيعي | فرط نشاط تحت سريري — يحتاج تحليل |
32
+
33
+ - **Anti-TPO مرتفع** مع TSH طبيعي → التهاب مناعي ذاتي (هاشيموتو محتمل) — متابعة سنوية
34
+ - **Anti-TPO > 500** → ذكره بوضوح في التقرير
35
+
36
+ ## أسلوب الشرح للمريض
37
+
38
+ - TSH: "هرمون المدير من الدماغ يأمر غدتك الدرقية"
39
+ - FT4: "الهرمون الذي ينظّم سرعة جهازك الأيضي"
40
+ - Anti-TPO: "أجسام مناعية قد تهاجم الغدة الدرقية"
41
+
42
+ ## شكل الإخراج (JSON)
43
+
44
+ {{SYSTEM_FORMAT}}
45
+
46
+ ## البيانات المراد تحليلها
47
+
48
+ النتائج: {{FINDINGS}}
49
+ المراجع الطبية: {{RAG_CONTEXT}}
backend/railway.json CHANGED
@@ -1,11 +1,14 @@
1
  {
2
  "$schema": "https://railway.app/railway.schema.json",
3
  "build": {
4
- "builder": "NIXPACKS"
 
5
  },
6
  "deploy": {
7
- "startCommand": "uvicorn main:app --host 0.0.0.0 --port $PORT",
8
  "restartPolicyType": "ON_FAILURE",
9
- "restartPolicyMaxRetries": 3
 
 
10
  }
11
  }
 
1
  {
2
  "$schema": "https://railway.app/railway.schema.json",
3
  "build": {
4
+ "builder": "DOCKERFILE",
5
+ "dockerfilePath": "Dockerfile"
6
  },
7
  "deploy": {
8
+ "startCommand": "python run.py",
9
  "restartPolicyType": "ON_FAILURE",
10
+ "restartPolicyMaxRetries": 3,
11
+ "healthcheckPath": "/health",
12
+ "healthcheckTimeout": 120
13
  }
14
  }
backend/requirements.txt CHANGED
@@ -15,3 +15,7 @@ cohere>=5.0.0
15
  rank-bm25>=0.2.2
16
  requests>=2.31.0
17
  psycopg2-binary>=2.9.0
 
 
 
 
 
15
  rank-bm25>=0.2.2
16
  requests>=2.31.0
17
  psycopg2-binary>=2.9.0
18
+ sentry-sdk[fastapi]>=2.0.0
19
+ gtts>=2.5.0
20
+ redis>=5.0.0
21
+ PyJWT>=2.8.0
backend/run.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import os
2
+ import uvicorn
3
+
4
+ if __name__ == "__main__":
5
+ port = int(os.environ.get("PORT", 8000))
6
+ uvicorn.run("main:app", host="0.0.0.0", port=port, workers=1)
backend/server.log DELETED
Binary file (7.7 kB)