Spaces:
Running
Running
رغد 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
- .env.example +30 -0
- .env.production.example +27 -0
- .github/workflows/deploy.yml +41 -0
- .gitignore +31 -0
- AGENTS.md +145 -0
- API_REFERENCE.md +245 -0
- ARCHITECTURE.md +112 -0
- DEPLOYMENT.md +230 -0
- app.py +0 -726
- backend/.dockerignore +13 -0
- backend/.env.example +30 -0
- backend/Dockerfile +17 -6
- backend/evaluate.py +60 -39
- backend/ingest_medical_kb.py +357 -0
- backend/ingest_medlineplus.py +380 -89
- backend/main.py +827 -370
- backend/{ingest_full.py → medical_data.py} +92 -726
- backend/medical_kb/__init__.py +6 -0
- backend/medical_kb/reference/__init__.py +4 -0
- backend/medical_kb/reference/medical_reference.py +136 -0
- backend/medical_kb/reference/normal_ranges.py +130 -0
- backend/medical_kb/schemas/cbc.json +153 -0
- backend/medical_kb/schemas/diabetes.json +128 -0
- backend/medical_kb/schemas/kidney.json +110 -0
- backend/medical_kb/schemas/lipid.json +105 -0
- backend/medical_kb/schemas/liver.json +145 -0
- backend/medical_kb/schemas/thyroid.json +94 -0
- backend/medical_reference_schema.json +418 -0
- backend/middleware/__init__.py +4 -0
- backend/middleware/audit.py +136 -0
- backend/middleware/auth_middleware.py +186 -0
- backend/middleware/sanitizer.py +155 -0
- backend/prompts/examples/cbc_examples.json +76 -0
- backend/prompts/examples/thyroid_examples.json +50 -0
- backend/prompts/extraction_template.txt +43 -0
- backend/prompts/few_shot_examples.json +92 -0
- backend/prompts/loader.py +55 -0
- backend/prompts/system_analysis.txt +68 -0
- backend/prompts/system_chat.txt +57 -0
- backend/prompts/templates/cbc_analysis_prompt.txt +44 -0
- backend/prompts/templates/diabetes_analysis_prompt.txt +56 -0
- backend/prompts/templates/kidney_analysis_prompt.txt +61 -0
- backend/prompts/templates/lipid_analysis_prompt.txt +55 -0
- backend/prompts/templates/liver_analysis_prompt.txt +46 -0
- backend/prompts/templates/system_prompt.txt +30 -0
- backend/prompts/templates/thyroid_analysis_prompt.txt +49 -0
- backend/railway.json +6 -3
- backend/requirements.txt +4 -0
- backend/run.py +6 -0
- 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
|
| 8 |
libglib2.0-0 \
|
| 9 |
libsm6 \
|
| 10 |
libxext6 \
|
| 11 |
-
|
| 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 ["
|
|
|
|
| 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
|
| 10 |
os.environ['TRANSFORMERS_VERBOSITY'] = 'error'
|
| 11 |
|
| 12 |
from groq import Groq
|
| 13 |
-
from
|
| 14 |
-
from
|
|
|
|
| 15 |
|
| 16 |
-
GROQ_API_KEY
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 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.
|
| 28 |
{"q": "ما أسباب انخفاض خلايا الدم البيضاء؟", "ref": "أمراض المناعة العلاج الكيميائي أمراض النخاع"},
|
| 29 |
{"q": "ما معنى ارتفاع TSH؟", "ref": "قصور الغدة الدرقية الغدة خاملة"},
|
|
|
|
|
|
|
| 30 |
]
|
| 31 |
|
| 32 |
|
| 33 |
-
def get_rag_answer(
|
| 34 |
-
results =
|
| 35 |
-
|
| 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 =
|
| 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(
|
| 49 |
prompt = f"""قيّم الإجابة التالية بدقة. أجب بـ JSON فقط بهذا الشكل:
|
| 50 |
{{"faithfulness": X, "answer_relevance": X, "context_precision": X, "notes": "..."}}
|
| 51 |
|
| 52 |
-
|
| 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 =
|
| 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("
|
| 79 |
print("=" * 60)
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
print(f"[DB] {db._collection.count()} chunks loaded\n")
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
for i, item in enumerate(TEST_QUESTIONS, 1):
|
| 90 |
-
print(f"[{i}/{len(TEST_QUESTIONS)}] {item['q'][:
|
| 91 |
-
answer, context = get_rag_answer(
|
| 92 |
-
metrics = evaluate_single(
|
| 93 |
|
| 94 |
for k in totals:
|
| 95 |
totals[k] += metrics.get(k, 0)
|
| 96 |
|
| 97 |
results.append({
|
| 98 |
"question": item["q"],
|
| 99 |
-
"answer": answer[:
|
| 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
|
| 110 |
-
print(f" Answer Relevance (صلة الإجابة): {
|
| 111 |
-
print(f" Context Precision (دقة السياق): {
|
| 112 |
-
print(f" المتوسط العام: {
|
| 113 |
print("=" * 60)
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
with open("eval_results.json", "w", encoding="utf-8") as f:
|
| 116 |
-
json.dump(
|
| 117 |
-
print("
|
|
|
|
| 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 |
-
سكريبت استيراد الموسوعة الطبية
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
-
import os
|
| 6 |
-
import re
|
| 7 |
-
import time
|
| 8 |
-
import requests
|
| 9 |
import xml.etree.ElementTree as ET
|
| 10 |
-
from
|
| 11 |
-
from langchain_chroma import Chroma
|
| 12 |
-
from langchain_core.documents import Document
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
|
|
|
| 18 |
|
| 19 |
-
#
|
| 20 |
-
|
| 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", "
|
| 70 |
-
("
|
| 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
|
| 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 |
-
("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
("rheumatoid arthritis autoimmune joint", "rheumatoid arthritis", "disease"),
|
|
|
|
| 100 |
]
|
| 101 |
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
url = "https://wsearch.nlm.nih.gov/ws/query"
|
| 107 |
-
params = {"db": "healthTopics", "term": search_term, "retmax":
|
| 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)
|
| 124 |
-
|
| 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 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
def main():
|
| 144 |
-
|
| 145 |
-
|
|
|
|
| 146 |
|
| 147 |
-
|
| 148 |
-
db = Chroma(persist_directory=DB_PATH, embedding_function=embeddings)
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
for item in results:
|
| 157 |
-
for
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
metadata
|
| 161 |
-
"source":
|
| 162 |
-
"topic_name":
|
| 163 |
-
"topic_type":
|
| 164 |
-
"title":
|
| 165 |
-
"language":
|
| 166 |
-
"chunk_index":
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
print(f"[
|
| 177 |
-
|
| 178 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
|
|
|
|
|
|
| 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 |
-
|
| 45 |
-
EMBED_MODEL = "intfloat/multilingual-e5-large"
|
| 46 |
|
| 47 |
app = FastAPI(title="تبيان الطبي API")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
app.add_middleware(
|
| 49 |
CORSMiddleware,
|
| 50 |
-
allow_origins=
|
| 51 |
-
allow_methods=["
|
| 52 |
-
allow_headers=["
|
|
|
|
|
|
|
| 53 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
def ensure_analyses_table():
|
| 57 |
"""ينشئ جدول analyses في Supabase تلقائياً إذا لم يكن موجوداً"""
|
| 58 |
if not SUPABASE_DB_URL:
|
| 59 |
-
|
| 60 |
return
|
| 61 |
try:
|
| 62 |
-
|
| 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 |
-
|
| 81 |
except Exception as e:
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
@app.on_event("startup")
|
| 86 |
async def startup():
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
|
| 90 |
@lru_cache(maxsize=1)
|
| 91 |
-
def
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 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 |
-
|
| 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 |
-
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
try:
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 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 |
-
|
| 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":
|
| 329 |
-
"Authorization": f"Bearer {
|
| 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 |
-
|
| 420 |
content = await file.read()
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
"""
|
| 548 |
-
|
| 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 |
-
|
|
|
|
| 569 |
|
| 570 |
-
if not is_medical_query(
|
| 571 |
def nm():
|
| 572 |
yield "أنا مساعد طبي متخصص. يسعدني الإجابة على أسئلتك الصحية وتحاليلك الطبية."
|
| 573 |
return StreamingResponse(nm(), media_type="text/plain; charset=utf-8")
|
| 574 |
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 597 |
for f in abnormal:
|
| 598 |
direction = "مرتفع" if f.get("status") == "high" else "منخفض"
|
| 599 |
-
|
| 600 |
f" • {f['name']}: {f['value']} {f.get('unit','')} "
|
| 601 |
f"(المعدل: {f.get('range','')}) — {direction}"
|
| 602 |
)
|
| 603 |
if normal:
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
system += "\n\nبيانات تحليل المريض (استند إليها مباشرة عند الإجابة):\n" + "\n".join(ctx_lines)
|
| 607 |
except Exception:
|
| 608 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 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 |
-
|
| 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
|
| 695 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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": "بروتين في خلايا الدم الحمراء يحمل الأكسجين
|
| 32 |
-
"high": "كثرة الحمر، الجفاف، أمراض الرئة المزمنة
|
| 33 |
-
"low": "فقر الدم
|
| 34 |
"symptoms_low": "تعب، شحوب، ضيق تنفس، دوخة، خفقان قلب.",
|
| 35 |
},
|
| 36 |
{
|
| 37 |
"name_ar": "خلايا الدم الحمراء", "name_en": "Red Blood Cells (RBC)",
|
| 38 |
-
"definition": "عدد خلايا الدم الحمراء
|
| 39 |
"high": "كثرة الحمر، الجفاف، أمراض القلب الخلقية.",
|
| 40 |
"low": "فقر الدم، نزيف، فشل كلوي، نقص غذائي.",
|
| 41 |
"symptoms_low": "إرهاق، شحوب، ضعف عام.",
|
| 42 |
},
|
| 43 |
{
|
| 44 |
"name_ar": "خلايا الدم البيضاء", "name_en": "White Blood Cells (WBC)",
|
| 45 |
-
"definition": "خلايا الجهاز المناعي
|
| 46 |
"high": "عدوى بكتيرية، التهاب، أمراض الدم كاللوكيميا.",
|
| 47 |
"low": "أمراض المناعة، العلاج الكيميائي، أمراض النخاع العظمي.",
|
| 48 |
"symptoms_low": "تكرار الإصابة بالعدوى، حمى متكررة.",
|
| 49 |
},
|
| 50 |
{
|
| 51 |
"name_ar": "الصفائح الدموية", "name_en": "Platelets (PLT)",
|
| 52 |
-
"definition": "خلايا مسؤولة عن تخثر الدم
|
| 53 |
"high": "التهاب، نزيف، نقص الحديد، خطر تجلط الدم.",
|
| 54 |
"low": "نزيف تلقائي، أمراض الكبد، الذئبة، العلاج الكيميائي.",
|
| 55 |
"symptoms_low": "كدمات سهلة، نزيف اللثة، نزيف طويل عند الجرح.",
|
| 56 |
},
|
| 57 |
{
|
| 58 |
"name_ar": "سكر الدم الصائم", "name_en": "Fasting Blood Glucose",
|
| 59 |
-
"definition": "مستوى السكر
|
| 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
|
| 80 |
-
"definition": "الكوليسترول منخفض الكثافة
|
| 81 |
"high": "تصلب الشرايين، أمراض القلب، جلطات.",
|
| 82 |
"low": "مثالي، يقلل خطر أمراض القلب.",
|
| 83 |
-
"symptoms_low": "لا أعراض.",
|
| 84 |
},
|
| 85 |
{
|
| 86 |
-
"name_ar": "الكوليسترول النافع", "name_en": "HDL Cholesterol
|
| 87 |
-
"definition": "الكوليسترول عالي الكثافة
|
| 88 |
"high": "حماية من أمراض القلب.",
|
| 89 |
"low": "خطر أمراض القلب، قلة الرياضة، التدخين، السمنة.",
|
| 90 |
"symptoms_low": "لا أعراض مباشرة لكن خطر قلبي مرتفع.",
|
|
@@ -97,7 +74,7 @@ LAB_DEFINITIONS = [
|
|
| 97 |
"symptoms_low": "الارتفاع الشديد يسبب التهاب البنكرياس.",
|
| 98 |
},
|
| 99 |
{
|
| 100 |
-
"name_ar": "هرمون الغدة الدرقية
|
| 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": "إنزيمات الكبد
|
| 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": "الحمضات
|
| 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: م
|
| 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
|
| 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
|
| 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": "أكثر خلايا الدم البيضاء شيوعاً.
|
| 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": "قياس السكر في أي وقت
|
| 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": "البيليروبين الم
|
| 400 |
"high": "انسداد القنوات الصفراوية، التهاب الكبد، حصوات المرارة.",
|
| 401 |
"low": "لا أهمية سريرية.",
|
| 402 |
"symptoms_low": "ارتفاعه مع اليرقان يشير لمشكلة في تصريف الصفراء.",
|
| 403 |
},
|
| 404 |
-
# كلى إضافي
|
| 405 |
{
|
| 406 |
-
"name_ar": "اليوريا في الدم", "name_en": "
|
| 407 |
-
"definition": "ناتج تكسير البروتين
|
| 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 ال
|
| 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 ال
|
| 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
|
| 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": "هرمون
|
| 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
|
| 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
|
| 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": "هرمون الحمل
|
| 612 |
-
"definition": "هرمون الحمل. يضاعف كل 48-72 ساعة في الحمل الطبيعي.
|
| 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 |
-
ال
|
| 723 |
-
المتابعة: TSH كل 6-12 شهرا
|
| 724 |
-
الحمل: TSH يجب أن يكون أقل من 2.5 خلال الحمل.""",
|
| 725 |
},
|
| 726 |
{
|
| 727 |
"topic": "صحة الكلى",
|
| 728 |
-
"content": """نصائح للحفاظ على
|
| 729 |
-
ال
|
| 730 |
-
ال
|
| 731 |
-
ال
|
| 732 |
-
ال
|
| 733 |
-
السك
|
| 734 |
-
المتابعة: كرياتينين و
|
| 735 |
},
|
| 736 |
{
|
| 737 |
"topic": "صحة الكبد",
|
| 738 |
-
"content": """نصائح لصحة الكبد:
|
| 739 |
-
الوزن: ال
|
| 740 |
-
ال
|
| 741 |
-
ال
|
| 742 |
-
الأدوية: لا ت
|
| 743 |
-
التطعيم: لقاح التهاب الكبد
|
| 744 |
-
المتابعة: ALT
|
| 745 |
-
},
|
| 746 |
-
{
|
| 747 |
-
"topic": "
|
| 748 |
-
"content": """نصائح للقلب
|
| 749 |
-
الغذاء: نظام البحر المتوسط
|
| 750 |
-
|
| 751 |
-
ال
|
| 752 |
-
الن
|
| 753 |
-
ال
|
| 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": "
|
|
|
|
| 5 |
},
|
| 6 |
"deploy": {
|
| 7 |
-
"startCommand": "
|
| 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)
|
|
|