nsr51324 commited on
Commit
dac03af
·
verified ·
1 Parent(s): 9cc7df5

Upload 21 files

Browse files
.gitattributes CHANGED
@@ -37,3 +37,4 @@ AHD_english.xlsx filter=lfs diff=lfs merge=lfs -text
37
  notebooks/AHD_english_cleaned.xlsx filter=lfs diff=lfs merge=lfs -text
38
  questions.index filter=lfs diff=lfs merge=lfs -text
39
  rag_model/faiss/questions.index filter=lfs diff=lfs merge=lfs -text
 
 
37
  notebooks/AHD_english_cleaned.xlsx filter=lfs diff=lfs merge=lfs -text
38
  questions.index filter=lfs diff=lfs merge=lfs -text
39
  rag_model/faiss/questions.index filter=lfs diff=lfs merge=lfs -text
40
+ cloudflared.exe filter=lfs diff=lfs merge=lfs -text
API_DEPLOYMENT_PLAN.md ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CortexRAG - API Deployment & Integration Guide
2
+
3
+ This document outlines the step-by-step roadmap to wrap the **CortexRAG** model into a production-ready FastAPI service, run it locally, expose it publicly via Cloudflare Tunnel, and provide integration documentation for external developers.
4
+
5
+ ---
6
+
7
+ ## Workflow Overview
8
+
9
+ ```mermaid
10
+ flowchart LR
11
+ A[Local RAG Model] --> B[FastAPI Wrapper]
12
+ B --> C[Swagger Testing /docs]
13
+ C --> D[Cloudflare Tunnel]
14
+ D --> E[Public HTTPS Endpoint]
15
+ E --> F[Client Applications]
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Step-by-Step Implementation Roadmap
21
+
22
+ ### Step 1: Model Code Verification & Preparation
23
+ - Verify model initialization, FAISS index loading, sentence-transformer embedding model, reranker, and LLM (e.g., Groq API / local LLM).
24
+ - Structure model code cleanly into a reusable class/module (e.g., `rag_pipeline.py`).
25
+
26
+ ### Step 2: FastAPI Web Service (`app.py` / `main.py`)
27
+ - Create lightweight FastAPI application.
28
+ - Define request model (`QueryRequest`) and response model (`QueryResponse`).
29
+ - Define endpoint: `POST /query` (or `/predict`).
30
+ - Add lifecycle events (`lifespan` / `@app.on_event("startup")`) to load heavy ML/FAISS models once into memory on startup.
31
+
32
+ ### Step 3: Local Testing via FastAPI Swagger UI
33
+ - Launch server locally:
34
+ ```bash
35
+ uvicorn app:app --reload --host 127.0.0.1 --port 8000
36
+ ```
37
+ - Navigate to `http://127.0.0.1:8000/docs` to test input payload, validation, error handling, and JSON output structure.
38
+
39
+ ### Step 4: Developer Manual Verification
40
+ - Execute tests using `curl`, Postman, or Python `requests` script to verify:
41
+ - Valid queries return correct RAG answers and source documents.
42
+ - Invalid inputs return standard `422 Unprocessable Entity` or structured error messages.
43
+
44
+ ### Step 5: Public Exposure via Cloudflare Tunnel
45
+ - Install Cloudflare CLI (`cloudflared`).
46
+ - Run ad-hoc public tunnel:
47
+ ```bash
48
+ cloudflared tunnel --url http://127.0.0.1:8000
49
+ ```
50
+ - Copy generated HTTPS URL (e.g., `https://your-tunnel-subdomain.trycloudflare.com`).
51
+
52
+ ### Step 6: Public Endpoint Testing
53
+ - Validate public URL with live queries:
54
+ ```bash
55
+ curl -X POST "https://your-tunnel-subdomain.trycloudflare.com/query" \
56
+ -H "Content-Type: application/json" \
57
+ -d "{\"question\": \"What are the symptoms of acute hypertension?\", \"top_k\": 5}"
58
+ ```
59
+
60
+ ### Step 7: Developer Integration Specification
61
+ Provide client developers with exact details required for integration:
62
+
63
+ | Attribute | Value |
64
+ | :--- | :--- |
65
+ | **Base URL** | `https://<your-cloudflare-tunnel-url>` |
66
+ | **Endpoint** | `/query` |
67
+ | **HTTP Method** | `POST` |
68
+ | **Headers** | `Content-Type: application/json` |
69
+
70
+ #### Request Body (JSON)
71
+ ```json
72
+ {
73
+ "question": "What are the common side effects of Lisinopril?",
74
+ "top_k": 6
75
+ }
76
+ ```
77
+
78
+ #### Response Body (JSON)
79
+ ```json
80
+ {
81
+ "status": "success",
82
+ "question": "What are the common side effects of Lisinopril?",
83
+ "answer": "Common side effects include dizziness, cough, headache...",
84
+ "sources": [
85
+ {
86
+ "id": 1024,
87
+ "text": "Lisinopril documentation excerpt...",
88
+ "score": 0.89
89
+ }
90
+ ],
91
+ "execution_time_seconds": 0.42
92
+ }
93
+ ```
README.md CHANGED
@@ -1,149 +1,226 @@
1
  ---
2
- language:
3
- - en
4
- library_name: sentence-transformers
 
 
 
5
  tags:
6
- - rag
7
- - retrieval-augmented-generation
8
- - medical
9
- - question-answering
10
- - semantic-search
11
- - faiss
12
- - cross-encoder
13
- - medical-qa
14
- pipeline_tag: question-answering
15
- license: apache-2.0
16
  ---
17
 
18
- # Medical RAG System
19
 
20
- A retrieval-augmented generation (RAG) system for answering medical questions using a curated English medical Question-Answer knowledge base.
21
 
22
- The system combines semantic retrieval (Sentence Transformers + FAISS), medical query expansion, Cross-Encoder reranking, evidence deduplication, confidence gating, and LLM-based answer generation.
 
 
23
 
24
- > **Important:** This system is for research and educational purposes only. It is not a medical diagnostic system and should not replace professional medical advice.
 
 
 
 
25
 
26
  ---
27
 
28
- ## 1. Pipeline
29
-
30
- ```text
31
- User Question
32
-
33
-
34
- Query Expansion (lay terms → medical terms)
35
-
36
-
37
- Sentence Transformer Embedding
38
-
39
-
40
- FAISS Vector Search (Top 20 candidates)
41
-
42
-
43
- Cross-Encoder Reranking (Top 6 evidence)
44
-
45
-
46
- Near-Duplicate Removal
47
-
48
-
49
- Confidence Gate ──reject──▶ "Insufficient evidence"
50
- │ pass
51
-
52
- LLM Generation
53
-
54
-
55
- Evidence-Based Answer (with doc_id citations)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  ```
57
 
58
- ## 2. Knowledge Base
59
 
60
- ~16,384 medical Question-Answer records with columns `Question`, `Answer`, `Category`, `doc_id`.
61
 
62
- ## 3. Retrieval
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
- - **Embedding model:** `sentence-transformers/all-MiniLM-L6-v2`, embeddings normalized so FAISS `IndexFlatIP` behaves as cosine similarity.
65
- - **Query Expansion:** a small lay-term → medical-term dictionary is appended to the query before embedding (e.g. `underactive thyroid → hypothyroidism`, `high blood sugar → hyperglycemia`). The original query is never replaced, only extended.
66
- - **Reranking:** `cross-encoder/ms-marco-MiniLM-L-6-v2` scores `(query, Answer)` — not `(query, Question)` — since many records share the same question with different answers; this identifies which *evidence* is actually useful.
67
- - **Evidence deduplication:** near-identical answers are collapsed via `difflib.SequenceMatcher` (threshold 0.92).
68
 
69
- ## 4. Confidence Gate
70
 
71
- Evidence is only sent to the LLM if **all** conditions hold:
 
 
72
 
73
- ```python
74
- MIN_RERANK_SCORE = -8
75
- MIN_SIMILARITY_FLOOR = 0.40
76
- MIN_SUPPORT_COUNT = 2
77
- SUPPORT_SCORE = -5.0
 
 
 
 
 
 
 
 
 
 
78
  ```
79
 
80
- 1. At least one result exists.
81
- 2. Top rerank score `MIN_RERANK_SCORE`.
82
- 3. Top similarity ≥ `MIN_SIMILARITY_FLOOR`.
83
- 4. At least `MIN_SUPPORT_COUNT` results score ≥ `SUPPORT_SCORE`.
 
84
 
85
- Otherwise, the system explicitly refuses to answer rather than guessing.
 
 
86
 
87
- ## 5. Generation
88
 
89
- LLM: Groq-hosted `openai/gpt-oss-20b`, instructed to answer **only** from retrieved evidence, cite sources by `[doc_id]`, flag disagreement between sources, avoid diagnosis/prescriptions, and include a medical disclaimer. API key is read from `GROQ_API_KEY` (never hard-coded).
90
 
91
- ## 6. Example
92
 
93
- **Q:** *What signs might suggest that my thyroid is not producing enough hormones?*
94
- **Evidence:** 6 thyroid-related records retrieved and reranked.
95
- **Answer:** Evidence-based summary (fatigue, feeling cold, weight gain, constipation, menstrual changes, hair loss) + disclaimer.
96
 
97
- **Out-of-domain example Q:** *What is the best treatment for a broken leg?*
98
- Retrieval returned diabetic-foot documents (lexically similar), but the gate/LLM correctly identified them as inappropriate evidence and refused to answer — demonstrating that similarity alone doesn't guarantee relevance.
99
 
100
- ## 7. Evaluation Summary
101
 
102
- | Metric | Result |
103
- |---|---:|
104
- | Knowledge Base Size | 16,384 records |
105
- | Self-Retrieval Top-1 | 100% |
106
- | Retrieval Recall@1 / @5 / @10 | 69.0% / 89.9% / 95.9% |
107
- | Retrieval MRR | 1.000 |
108
- | Retrieval + Reranker Recall@1 / @5 / @10 | 8.3% / 22.2% / 38.8% |
109
- | Retrieval + Reranker MRR | 0.237 |
110
- | Category Recall (retrieval / +reranker) | 97% / 98% |
111
- | Confidence Gate Accuracy | 88% (n=50) |
112
- | Example End-to-End Latency | 0.99 sec |
113
 
114
- **Key finding:** the general-purpose Cross-Encoder scores notably *worse* than plain retrieval on this duplicate-question-based benchmark. This isn't hidden — it indicates the reranker (trained for general passage relevance) doesn't align well with medical relevance judgments, and needs validation against a manually reviewed gold set before being trusted in production. A semi-automatic gold-set workflow (exact duplicates + semantic candidates ≥0.90 similarity, human-reviewed 1/0 labels) is included for this purpose; Category-match is used only as a secondary sanity check, not ground truth.
115
 
116
- The confidence-gate threshold was tuned via 5-fold cross-validation rather than a single split, to check stability rather than overfit to one small sample.
 
 
 
 
 
117
 
118
- ## 8. Limitations
119
 
120
- - Knowledge base doesn't cover every condition/scenario.
121
- - Semantic similarity ≠ appropriate evidence (see broken-leg example).
122
- - Reranker not fine-tuned on this medical domain yet.
123
- - Manual gold set is still small — evaluation should be treated as preliminary.
124
- - Not for diagnosis, emergencies, prescriptions, or personalized treatment.
125
 
126
- ## 9. Installation
 
127
 
128
- ```bash
129
- pip install sentence-transformers faiss-cpu numpy pandas openpyxl groq
 
130
  ```
131
 
132
- ```python
133
- import os
134
- GROQ_API_KEY = os.environ["GROQ_API_KEY"] # never hard-code keys
 
 
 
135
  ```
136
 
137
- ## 10. Future Work
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
- Complete manual gold-set annotation → re-evaluate reranker against it → consider a medical-domain reranker → expand confidence-gate validation set → add citations/monitoring/auth → deploy behind a REST API → evaluate faithfulness/hallucination separately from retrieval quality.
 
 
140
 
141
- ## 11. Responsible Use
 
 
 
 
 
142
 
143
- Not a diagnostic tool, clinical decision-support system, prescription system, or emergency service. Consult a qualified healthcare professional for medical decisions.
 
 
 
 
144
 
145
- ## References
 
 
 
 
 
 
 
 
 
 
146
 
147
- - [Sentence Transformers](https://www.sbert.net/)
148
- - [FAISS](https://github.com/facebookresearch/faiss)
149
- - [Cross-Encoder](https://www.sbert.net/examples/applications/cross-encoder/README.html)
 
1
  ---
2
+ title: CortexRAG Medical RAG API
3
+ emoji: 🩺
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 8000
8
  tags:
9
+ - rag
10
+ - medical-ai
11
+ - sentence-transformers
12
+ - faiss
13
+ - fastapi
14
+ - cross-encoder
15
+ - groq
 
 
 
16
  ---
17
 
18
+ # 🩺 CortexRAG - Advanced Medical RAG API
19
 
20
+ **CortexRAG** is a high-performance, domain-specific Retrieval-Augmented Generation (RAG) system engineered for medical and clinical question answering. It combines semantic vector search (FAISS + Sentence Transformers), cross-encoder re-ranking, and high-speed LLM inference (Groq / Llama) wrapped in a lightweight **FastAPI** REST interface.
21
 
22
+ ---
23
+
24
+ ## 🌟 Features
25
 
26
+ - **Semantic Embedding Engine**: `all-MiniLM-L6-v2` dense vector retrieval using FAISS indexing.
27
+ - **Precision Re-ranking**: Cross-encoder scoring (`cross-encoder/ms-marco-MiniLM-L-6-v2`) for optimal document relevance.
28
+ - **Medical Synonym Expansion**: Context-aware synonym mapping for expanded search recall.
29
+ - **Ultra-Fast REST API**: Built on FastAPI with asynchronous request handling and Pydantic validation.
30
+ - **Cloudflare Tunnel Ready**: Zero-trust public exposure without complex firewall configuration.
31
 
32
  ---
33
 
34
+ ## 🏗 System Architecture
35
+
36
+ ```
37
+ ┌───────────────────────────┐
38
+ Client Request │
39
+ └─────────────┬─────────────┘
40
+ POST /query
41
+
42
+ ┌───────────────────────────┐
43
+ │ FastAPI Web Server │
44
+ └─────────────┬─────────────┘
45
+
46
+ ┌─────────────────────┴─────────────────────┐
47
+
48
+
49
+ ┌───────────────────────────┐ ┌───────────────────────────┐
50
+ │ Query Vectorization │ Medical Synonym Expansion │
51
+ │ (all-MiniLM-L6-v2) │ └─────────────┬─────────────┘
52
+ └─────────────┬─────────────┘ │
53
+
54
+ └─────────────────────┬─────────────────────┘
55
+
56
+
57
+ ┌───────────────────────────┐
58
+ │ FAISS Vector Index │
59
+ └─────────────┬─────────────┘
60
+ │ Top-N Candidate Docs
61
+
62
+ ┌───────────────────────────┐
63
+ │ Cross-Encoder Reranker │
64
+ │ (ms-marco-MiniLM-L-6-v2) │
65
+ └─────────────┬─────────────┘
66
+ │ Top-K Ranked Context
67
+
68
+ ┌───────────────────────────┐
69
+ │ LLM Synthesis (Groq) │
70
+ └─────────────┬─────────────┘
71
+
72
+
73
+ ┌───────────────────────────┐
74
+ │ JSON API Response │
75
+ └───────────────────────────┘
76
  ```
77
 
78
+ ---
79
 
80
+ ## 📁 Repository Structure
81
 
82
+ ```
83
+ CortexRAG/
84
+ ├── API_DEPLOYMENT_PLAN.md # Step-by-step API & tunnel setup documentation
85
+ ├── README.md # Hugging Face & GitHub Project Card
86
+ ├── question_embeddings.npy # Pre-computed dense embeddings matrix
87
+ ├── questions.index # Binary FAISS vector search index
88
+ ├── notebooks/ # Experimental notebooks & cleaning scripts
89
+ │ ├── Medical_RAG_Sytem.ipynb
90
+ │ └── rag_data_cleaning.ipynb
91
+ └── rag_model/ # Core RAG engine configurations & resources
92
+ ├── rag_config.json # Search, score & model parameters
93
+ ├── requirements.txt # Python dependency specifications
94
+ └── models/ # Synonyms & model metadata
95
+ └── medical_synonyms.json
96
+ ```
97
 
98
+ ---
 
 
 
99
 
100
+ ## Quick Start & Installation
101
 
102
+ ### 1. Prerequisites
103
+ - Python 3.9+
104
+ - Pip package manager
105
 
106
+ ### 2. Environment Setup
107
+ ```bash
108
+ # Clone repository
109
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/CortexRAG
110
+ cd CortexRAG
111
+
112
+ # Create virtual environment
113
+ python -m venv venv
114
+ # Activate on Windows:
115
+ venv\Scripts\activate
116
+ # Activate on Linux/macOS:
117
+ source venv/bin/activate
118
+
119
+ # Install dependencies
120
+ pip install -r rag_model/requirements.txt fastapi uvicorn pydantic
121
  ```
122
 
123
+ ### 3. Environment Variables
124
+ Set your Groq API Key (or other LLM provider keys):
125
+ ```bash
126
+ # Windows PowerShell
127
+ $env:GROQ_API_KEY="your_groq_api_key_here"
128
 
129
+ # Linux/macOS
130
+ export GROQ_API_KEY="your_groq_api_key_here"
131
+ ```
132
 
133
+ ---
134
 
135
+ ## 🚀 Running the Local API
136
 
137
+ Start the server using `uvicorn`:
138
 
139
+ ```bash
140
+ uvicorn app:app --host 127.0.0.1 --port 8000 --reload
141
+ ```
142
 
143
+ Interactive API Documentation (Swagger UI) is available at:
144
+ 👉 **`http://127.0.0.1:8000/docs`**
145
 
146
+ ---
147
 
148
+ ## 🌐 Exposing Publicly via Cloudflare Tunnel
 
 
 
 
 
 
 
 
 
 
149
 
150
+ To expose your local FastAPI server securely to the internet without port forwarding:
151
 
152
+ 1. Download [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-local-tunnel/).
153
+ 2. Run the tunnel pointing to your local port:
154
+ ```bash
155
+ cloudflared tunnel --url http://127.0.0.1:8000
156
+ ```
157
+ 3. Use the generated URL (e.g. `https://xxx.trycloudflare.com`) as your public API endpoint.
158
 
159
+ ---
160
 
161
+ ## 🔌 API Reference & Integration Guide
 
 
 
 
162
 
163
+ ### Endpoint
164
+ `POST /query`
165
 
166
+ ### Request Headers
167
+ ```http
168
+ Content-Type: application/json
169
  ```
170
 
171
+ ### Request Payload Example
172
+ ```json
173
+ {
174
+ "question": "What are the first-line treatments for type 2 diabetes?",
175
+ "top_k": 6
176
+ }
177
  ```
178
 
179
+ ### Response Payload Example
180
+ ```json
181
+ {
182
+ "status": "success",
183
+ "question": "What are the first-line treatments for type 2 diabetes?",
184
+ "answer": "First-line pharmacological management for type 2 diabetes typically includes Metformin alongside lifestyle modifications...",
185
+ "retrieved_context": [
186
+ {
187
+ "doc_id": 42,
188
+ "text": "Metformin remains the initial drug of choice for monotherapy...",
189
+ "rerank_score": 4.85
190
+ }
191
+ ],
192
+ "execution_time_sec": 0.38
193
+ }
194
+ ```
195
 
196
+ ### Python Integration Example
197
+ ```python
198
+ import requests
199
 
200
+ url = "https://your-cloudflare-url.trycloudflare.com/query"
201
+ payload = {
202
+ "question": "What are the common causes of chest pain?",
203
+ "top_k": 5
204
+ }
205
+ headers = {"Content-Type": "application/json"}
206
 
207
+ response = requests.post(url, json=payload, headers=headers)
208
+ print(response.json())
209
+ ```
210
+
211
+ ---
212
 
213
+ ## 🛠 Configuration Parameters (`rag_config.json`)
214
+
215
+ | Parameter | Default | Description |
216
+ | :--- | :--- | :--- |
217
+ | `embedding_model` | `all-MiniLM-L6-v2` | SentenceTransformer embedding model |
218
+ | `reranker_model` | `cross-encoder/ms-marco-MiniLM-L-6-v2` | Precision reranking cross-encoder model |
219
+ | `retrieve_top_n` | `20` | Initial FAISS vector retrieval candidate count |
220
+ | `rerank_top_k` | `6` | Number of context snippets passed to LLM |
221
+ | `min_similarity_floor` | `0.4` | Cosine similarity threshold |
222
+
223
+ ---
224
 
225
+ ## 📜 License
226
+ This project is released under the **MIT License**.
 
__pycache__/app.cpython-310.pyc ADDED
Binary file (12.1 kB). View file
 
app.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import json
4
+ import difflib
5
+ from typing import List, Optional, Dict, Any
6
+ from contextlib import asynccontextmanager
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import faiss
11
+ from sentence_transformers import SentenceTransformer, CrossEncoder
12
+ from groq import Groq
13
+
14
+ from fastapi import FastAPI, HTTPException, status
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from pydantic import BaseModel, Field
17
+
18
+ # ==========================================
19
+ # Paths & Default Configurations
20
+ # ==========================================
21
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
22
+ FAISS_INDEX_PATH = os.path.join(BASE_DIR, "questions.index")
23
+ DATASET_PATH = os.path.join(BASE_DIR, "notebooks", "AHD_english_cleaned.xlsx")
24
+ SYNONYMS_PATH = os.path.join(BASE_DIR, "rag_model", "models", "medical_synonyms.json")
25
+ CONFIG_PATH = os.path.join(BASE_DIR, "rag_model", "rag_config.json")
26
+
27
+ DEFAULT_GROQ_API_KEY = os.getenv("GROQ_API_KEY", "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")
28
+ LLM_MODEL = os.getenv("LLM_MODEL", "openai/gpt-oss-20b")
29
+
30
+ # Load config if available
31
+ if os.path.exists(CONFIG_PATH):
32
+ with open(CONFIG_PATH, "r", encoding="utf-8") as f:
33
+ RAG_CONFIG = json.load(f)
34
+ else:
35
+ RAG_CONFIG = {
36
+ "embedding_model": "all-MiniLM-L6-v2",
37
+ "reranker_model": "cross-encoder/ms-marco-MiniLM-L-6-v2",
38
+ "retrieve_top_n": 20,
39
+ "rerank_top_k": 6,
40
+ }
41
+
42
+ # Dynamic Global State
43
+ rag_resources: Dict[str, Any] = {}
44
+
45
+ SYSTEM_PROMPT = """
46
+ You are a medical RAG assistant specialized ONLY in:
47
+ - Diabetes
48
+ - Endocrinology
49
+ - Thyroid disorders
50
+ - Hormonal disorders
51
+ - Endocrine glands and related disorders
52
+ - Nutrition, glucose management, insulin, and complications DIRECTLY related to diabetes/endocrinology
53
+
54
+ You operate in STRICT RAG MODE.
55
+
56
+ ==================================================
57
+ CRITICAL DOMAIN RULE
58
+ ==================================================
59
+ The USER QUESTION itself determines whether the question is in-domain.
60
+ A question is IN-DOMAIN only if its actual subject is diabetes, endocrinology,
61
+ thyroid, hormones, endocrine glands/disorders, or a problem explicitly stated
62
+ by the user to be related to diabetes/endocrinology.
63
+
64
+ If the user's actual question is about another body system or another medical
65
+ specialty, it is OUT-OF-DOMAIN.
66
+
67
+ ==================================================
68
+ TYPE 1 — IN-DOMAIN MEDICAL QUESTION
69
+ ==================================================
70
+ If and ONLY IF the USER QUESTION itself is in-domain:
71
+ - Answer using ONLY the RETRIEVED MEDICAL EVIDENCE.
72
+ - Do NOT use pretrained/background medical knowledge.
73
+ - Do NOT guess or invent missing details.
74
+ - Cite supporting [doc_id] for every medical claim.
75
+
76
+ ==================================================
77
+ TYPE 2 — APP / IDENTITY QUESTIONS
78
+ ==================================================
79
+ Only for direct questions about the assistant/application itself:
80
+ Answer briefly and naturally.
81
+
82
+ ==================================================
83
+ TYPE 3 — EVERYTHING ELSE / OUT OF DOMAIN / INSUFFICIENT EVIDENCE
84
+ ==================================================
85
+ For ALL TYPE 3 cases, reply with ONLY one short refusal:
86
+ If the user wrote in Arabic:
87
+ "معرفش، السؤال ده مش جزء من تخصصي (السكر والغدد الصماء)."
88
+ If the user wrote in English:
89
+ "I don't know — this is outside my specialty (diabetes and endocrinology)."
90
+
91
+ ==================================================
92
+ FINAL SAFETY NOTE FOR TYPE 1 ONLY
93
+ ==================================================
94
+ At the end of every TYPE 1 medical answer, write:
95
+ "This information is based on the available medical evidence and is for general informational purposes. It is not a diagnosis or a substitute for professional medical advice."
96
+ """
97
+
98
+ # ==========================================
99
+ # RAG Helper Logic
100
+ # ==========================================
101
+ def expand_query(query: str, synonyms: dict) -> str:
102
+ lower_q = query.lower()
103
+ extra_terms = [
104
+ med_term
105
+ for phrase, med_term in synonyms.items()
106
+ if phrase in lower_q and med_term not in lower_q
107
+ ]
108
+ if not extra_terms:
109
+ return query
110
+ return f"{query} ({', '.join(dict.fromkeys(extra_terms))})"
111
+
112
+ def vector_search(query: str, top_n: int = 20):
113
+ embedder = rag_resources["embedder"]
114
+ faiss_index = rag_resources["faiss_index"]
115
+ metadata_store = rag_resources["metadata_store"]
116
+ synonyms = rag_resources["synonyms"]
117
+
118
+ query_for_embedding = expand_query(query, synonyms)
119
+ q_emb = embedder.encode(
120
+ [query_for_embedding],
121
+ convert_to_numpy=True,
122
+ normalize_embeddings=True
123
+ ).astype("float32")
124
+
125
+ scores, idxs = faiss_index.search(q_emb, top_n)
126
+ retrieved = []
127
+ for score, idx in zip(scores[0], idxs[0]):
128
+ if idx == -1:
129
+ continue
130
+ payload = metadata_store[int(idx)]
131
+ retrieved.append({
132
+ "doc_id": payload["doc_id"],
133
+ "Question": payload["question"],
134
+ "Answer": payload["answer"],
135
+ "Category": payload["category"],
136
+ "similarity": float(score),
137
+ })
138
+ return pd.DataFrame(retrieved)
139
+
140
+ def rerank(query: str, candidates: pd.DataFrame, top_k: int = 6, alpha: float = 0.6) -> pd.DataFrame:
141
+ if candidates.empty:
142
+ return candidates
143
+
144
+ reranker = rag_resources["reranker"]
145
+ pairs = [
146
+ (query, f"Question: {row['Question']}\nAnswer: {row['Answer']}")
147
+ for _, row in candidates.iterrows()
148
+ ]
149
+
150
+ rerank_scores = reranker.predict(pairs)
151
+ reranked = candidates.copy()
152
+ reranked["rerank_score"] = rerank_scores
153
+
154
+ def norm(s):
155
+ s = s.astype(float)
156
+ rng = s.max() - s.min()
157
+ return (s - s.min()) / rng if rng > 0 else s * 0
158
+
159
+ reranked["sim_norm"] = norm(reranked["similarity"])
160
+ reranked["rerank_norm"] = norm(reranked["rerank_score"])
161
+ reranked["final_score"] = alpha * reranked["rerank_norm"] + (1 - alpha) * reranked["sim_norm"]
162
+
163
+ return reranked.sort_values("final_score", ascending=False).head(top_k).reset_index(drop=True)
164
+
165
+ def is_near_duplicate(text_a: str, text_b: str, threshold: float = 0.92) -> bool:
166
+ return difflib.SequenceMatcher(None, text_a, text_b).ratio() > threshold
167
+
168
+ def build_evidence(reranked: pd.DataFrame, max_answer_chars: int = 700) -> List[dict]:
169
+ evidence = []
170
+ seen_answers = []
171
+
172
+ for _, row in reranked.iterrows():
173
+ answer = str(row["Answer"])
174
+ if any(is_near_duplicate(answer, seen) for seen in seen_answers):
175
+ continue
176
+ seen_answers.append(answer)
177
+
178
+ evidence.append({
179
+ "doc_id": int(row["doc_id"]),
180
+ "question": str(row["Question"]),
181
+ "answer": answer[:max_answer_chars] + ("..." if len(answer) > max_answer_chars else ""),
182
+ "category": str(row["Category"]),
183
+ "similarity": round(float(row["similarity"]), 3),
184
+ "rerank_score": round(float(row["rerank_score"]), 3),
185
+ })
186
+ return evidence
187
+
188
+ def generate_answer(query: str, user_data: str, evidence: List[dict]) -> str:
189
+ groq_client = rag_resources["groq_client"]
190
+ if not evidence:
191
+ return "I don't know — this is outside my specialty (diabetes and endocrinology)."
192
+
193
+ evidence_blocks = [
194
+ f"[{e['doc_id']}] (category: {e['category']})\nRelated question: {e['question']}\nAnswer: {e['answer']}"
195
+ for e in evidence
196
+ ]
197
+ evidence_text = "\n\n".join(evidence_blocks)
198
+
199
+ user_message = f"""
200
+ USER QUESTION:
201
+ {query}
202
+
203
+ USER DATA:
204
+ {user_data}
205
+
206
+ RETRIEVED MEDICAL EVIDENCE:
207
+ {evidence_text}
208
+
209
+ TASK:
210
+ Answer the USER QUESTION using ONLY the RETRIEVED MEDICAL EVIDENCE.
211
+ Cite every important medical claim using the relevant [doc_id].
212
+ If the evidence is insufficient or irrelevant, give a clear refusal.
213
+ """
214
+
215
+ try:
216
+ response = groq_client.chat.completions.create(
217
+ model=LLM_MODEL,
218
+ messages=[
219
+ {"role": "system", "content": SYSTEM_PROMPT},
220
+ {"role": "user", "content": user_message}
221
+ ],
222
+ temperature=0.1,
223
+ max_tokens=800,
224
+ )
225
+ return response.choices[0].message.content
226
+ except Exception as e:
227
+ # Fallback or error handling for Groq API
228
+ return f"Error generating answer from LLM: {str(e)}"
229
+
230
+ # ==========================================
231
+ # FastAPI Lifecycle & App Initialization
232
+ # ==========================================
233
+ @asynccontextmanager
234
+ async def lifespan(app: FastAPI):
235
+ print("Loading RAG resources...")
236
+
237
+ # Load FAISS index
238
+ if not os.path.exists(FAISS_INDEX_PATH):
239
+ raise RuntimeError(f"FAISS index file not found at: {FAISS_INDEX_PATH}")
240
+ faiss_index = faiss.read_index(FAISS_INDEX_PATH)
241
+
242
+ # Load Dataset for Metadata
243
+ if not os.path.exists(DATASET_PATH):
244
+ raise RuntimeError(f"Dataset file not found at: {DATASET_PATH}")
245
+ df = pd.read_excel(DATASET_PATH, engine="openpyxl").reset_index(drop=True)
246
+ df["doc_id"] = df.index
247
+ metadata_store = [
248
+ {
249
+ "doc_id": row["doc_id"],
250
+ "question": row["Question"],
251
+ "answer": row["Answer"],
252
+ "category": row["Category"],
253
+ }
254
+ for _, row in df.iterrows()
255
+ ]
256
+
257
+ # Load Synonyms
258
+ synonyms = {}
259
+ if os.path.exists(SYNONYMS_PATH):
260
+ with open(SYNONYMS_PATH, "r", encoding="utf-8") as f:
261
+ synonyms = json.load(f)
262
+
263
+ # Load Transformer Models
264
+ embedder = SentenceTransformer(RAG_CONFIG.get("embedding_model", "all-MiniLM-L6-v2"))
265
+ reranker = CrossEncoder(RAG_CONFIG.get("reranker_model", "cross-encoder/ms-marco-MiniLM-L-6-v2"))
266
+
267
+ # Load Groq Client
268
+ groq_client = Groq(api_key=DEFAULT_GROQ_API_KEY)
269
+
270
+ # Save to global state
271
+ rag_resources["faiss_index"] = faiss_index
272
+ rag_resources["metadata_store"] = metadata_store
273
+ rag_resources["synonyms"] = synonyms
274
+ rag_resources["embedder"] = embedder
275
+ rag_resources["reranker"] = reranker
276
+ rag_resources["groq_client"] = groq_client
277
+
278
+ print(f"RAG resources loaded successfully! Index size: {faiss_index.ntotal}")
279
+ yield
280
+ print("Shutting down RAG resources.")
281
+ rag_resources.clear()
282
+
283
+ app = FastAPI(
284
+ title="CortexRAG Medical API",
285
+ description="Production-ready FastAPI interface for CortexRAG Medical Question Answering Engine.",
286
+ version="1.0.0",
287
+ lifespan=lifespan
288
+ )
289
+
290
+ app.add_middleware(
291
+ CORSMiddleware,
292
+ allow_origins=["*"],
293
+ allow_credentials=True,
294
+ allow_methods=["*"],
295
+ allow_headers=["*"],
296
+ )
297
+
298
+ # ==========================================
299
+ # Request / Response Schemas
300
+ # ==========================================
301
+ class QueryRequest(BaseModel):
302
+ question: str = Field(..., example="What are the common symptoms of hypothyroidism?", min_length=2)
303
+ user_data: Optional[str] = Field(default="", example="Age: 45, Gender: Female")
304
+ top_k: Optional[int] = Field(default=6, ge=1, le=20)
305
+
306
+ class EvidenceItem(BaseModel):
307
+ doc_id: int
308
+ question: str
309
+ answer: str
310
+ category: str
311
+ similarity: float
312
+ rerank_score: float
313
+
314
+ class QueryResponse(BaseModel):
315
+ status: str
316
+ question: str
317
+ answer: str
318
+ evidence: List[EvidenceItem]
319
+ execution_time_sec: float
320
+
321
+ # ==========================================
322
+ # Endpoints
323
+ # ==========================================
324
+ @app.get("/", tags=["Health"])
325
+ def root():
326
+ return {
327
+ "status": "online",
328
+ "service": "CortexRAG Medical API",
329
+ "documentation": "/docs"
330
+ }
331
+
332
+ @app.get("/health", tags=["Health"])
333
+ def health_check():
334
+ return {
335
+ "status": "healthy",
336
+ "faiss_index_entries": rag_resources.get("faiss_index", None).ntotal if "faiss_index" in rag_resources else 0,
337
+ "model_loaded": "embedder" in rag_resources
338
+ }
339
+
340
+ @app.post("/predict", response_model=QueryResponse, tags=["RAG Inference"])
341
+ @app.post("/query", response_model=QueryResponse, tags=["RAG Inference"])
342
+ def predict(payload: QueryRequest):
343
+ start_time = time.time()
344
+
345
+ try:
346
+ # Step 1: Vector Search
347
+ candidates = vector_search(payload.question, top_n=RAG_CONFIG.get("retrieve_top_n", 20))
348
+
349
+ # Step 2: Cross Encoder Rerank
350
+ reranked_df = rerank(payload.question, candidates, top_k=payload.top_k)
351
+
352
+ # Step 3: Deduplicate Evidence
353
+ evidence = build_evidence(reranked_df)
354
+
355
+ # Step 4: Generate LLM Answer
356
+ answer = generate_answer(payload.question, payload.user_data or "", evidence)
357
+
358
+ elapsed = round(time.time() - start_time, 3)
359
+
360
+ return QueryResponse(
361
+ status="success",
362
+ question=payload.question,
363
+ answer=answer,
364
+ evidence=[EvidenceItem(**item) for item in evidence],
365
+ execution_time_sec=elapsed
366
+ )
367
+
368
+ except Exception as e:
369
+ raise HTTPException(
370
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
371
+ detail=f"An error occurred during inference: {str(e)}"
372
+ )
373
+
374
+ if __name__ == "__main__":
375
+ import uvicorn
376
+ uvicorn.run("app:app", host="127.0.0.1", port=8000, reload=True)
cloudflared.exe ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c29eee2b121f5436a642eed69fd9767da7e7b8c510fa50aaa130337f931357b5
3
+ size 54893480
gold_set_for_manual_review.xlsx CHANGED
Binary files a/gold_set_for_manual_review.xlsx and b/gold_set_for_manual_review.xlsx differ
 
notebooks/Medical_RAG_Sytem.ipynb CHANGED
The diff for this file is too large to render. See raw diff
 
rag_model/data/metadata_store.json ADDED
The diff for this file is too large to render. See raw diff