Skip to content

Commit 5c9b427

Browse files
committed
Added rag features
1 parent aaac519 commit 5c9b427

12 files changed

Lines changed: 490 additions & 333 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:587f223a00318da2e6181e47be8e8d133cb354c6d10a3bf2b61a0eec70bea177
3+
size 22484000
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:a545742ed5f9e3ad7971a65470e401ab943cb853438d631a10135760fc124587
3+
size 100
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:cb6f636174c8836330257a4107c0b4d5276da7ef60569063ad49849cccd0074c
3+
size 220694
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:dcfdc8595cfc4464d9c91ebba31facc865e9014e9a97ebc9c574d58b758dacb9
3+
size 28000
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:59552bb1d187ffe10aa708706e68b72eb6e28f1916034dde682211a8a0cb4c63
3+
size 61524

backend/chroma_db/chroma.sqlite3

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:b06cc3f72e27da6221f33d972c76f898b9d77b91148689ee27ce9603f72ade36
3+
size 121520128

backend/embedder.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from sentence_transformers import SentenceTransformer
2+
3+
# Load once at module level — not on every call
4+
model = SentenceTransformer("pritamdeka/S-PubMedBert-MS-MARCO")
5+
6+
def embed(text: str) -> list[float]:
7+
"""Convert text to embedding vector."""
8+
return model.encode(text, convert_to_numpy=True).tolist()
9+
10+
def embed_batch(texts: list[str]) -> list[list[float]]:
11+
"""Convert a batch of texts to embedding vectors."""
12+
return model.encode(texts, convert_to_numpy=True, batch_size=32).tolist()

backend/engine.py

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from itertools import combinations
22
import requests
33
import json
4+
from retriever import retrieve
45

56
DRUG_CLASS_MAP = {
67
"penicillin": ["amoxicillin", "ampicillin", "penicillin", "flucloxacillin"],
@@ -12,85 +13,88 @@
1213

1314

1415
def load_fallback_data():
15-
with open("data/fallback_interactions.json","r") as f:
16+
with open("data/fallback_interactions.json", "r") as f:
1617
data = json.load(f)
1718
return data
1819

20+
1921
def check_interactions_fallback(medicines: list[str], fallback_data: list) -> list:
20-
pairs = list(combinations(medicines,2))
22+
pairs = list(combinations(medicines, 2))
2123
results = []
2224
for pair in pairs:
2325
drug_1 = pair[0]
2426
drug_2 = pair[1]
25-
2627
for entry in fallback_data:
27-
if (entry["drug_a"].lower() == drug_1.lower() and entry["drug_b"].lower() == drug_2.lower()) or (entry["drug_a"].lower() == drug_2.lower() and entry["drug_b"].lower() == drug_1.lower()):
28+
if (
29+
entry["drug_a"].lower() == drug_1.lower()
30+
and entry["drug_b"].lower() == drug_2.lower()
31+
) or (
32+
entry["drug_a"].lower() == drug_2.lower()
33+
and entry["drug_b"].lower() == drug_1.lower()
34+
):
2835
results.append(entry)
29-
3036
return results
3137

38+
3239
def check_allergies(medicines: list[str], known_allergies: list[str]) -> list:
33-
# For each medicine, check if it matches any known allergy
34-
# Return a list of allergy alerts
3540
matches = []
3641
for medicine in medicines:
3742
for known_allergy in known_allergies:
38-
#exact matching
3943
if medicine.lower() == known_allergy.lower():
4044
matches.append({
4145
"medicine": medicine,
4246
"reason": f"Patient is allergic to {known_allergy}",
4347
"severity": "high"
4448
})
45-
46-
#mapping with drug classes
4749
else:
4850
for drug_class, drug_items in DRUG_CLASS_MAP.items():
49-
if known_allergy.lower() == drug_class.lower() and medicine.lower() in drug_items:
51+
if (
52+
known_allergy.lower() == drug_class.lower()
53+
and medicine.lower() in drug_items
54+
):
5055
matches.append({
5156
"medicine": medicine,
5257
"reason": f"{drug_class} class",
5358
"severity": "high"
5459
})
55-
5660
return matches
5761

62+
5863
def parse_llm_response(content: str) -> dict:
5964
try:
6065
content = content.strip()
61-
#removing md output if any by the llm
6266
if content.startswith("```"):
6367
content = content.split("```")[1]
6468
if content.startswith("json"):
6569
content = content[4:]
6670

67-
#finding { } brackets in json to extract pure json
68-
6971
start = content.index("{")
70-
end = content.rindex("}")+1
72+
end = content.rindex("}") + 1
7173
content = content[start:end]
7274

73-
#loading content
7475
data = json.loads(content)
75-
76-
#ensuring required fields atleast contain default values
77-
data.setdefault("interactions",[])
78-
data.setdefault("allergy_alerts",[])
79-
data.setdefault("requires_doctor_review",True)
76+
data.setdefault("interactions", [])
77+
data.setdefault("allergy_alerts", [])
78+
data.setdefault("requires_doctor_review", True)
8079

8180
return data
82-
81+
8382
except Exception:
8483
return {
8584
"interactions": [],
8685
"allergy_alerts": [],
8786
"requires_doctor_review": True
8887
}
89-
88+
89+
9090
def check_interactions_llm(medicines: list[str], patient_history) -> dict:
9191
with open("prompts/system_prompt.txt", "r") as f:
9292
system_prompt = f.read()
9393

94+
# Retrieve relevant clinical context from knowledge base
95+
retrieved_context = retrieve(medicines, top_k=10)
96+
97+
# Build user message with retrieved evidence
9498
user_message = json.dumps({
9599
"medicines": medicines,
96100
"patient_history": {
@@ -99,24 +103,53 @@ def check_interactions_llm(medicines: list[str], patient_history) -> dict:
99103
"known_allergies": patient_history.known_allergies,
100104
"current_medications": patient_history.current_medications,
101105
"conditions": patient_history.conditions
102-
}
106+
},
107+
"clinical_reference_data": retrieved_context,
108+
"instruction": "Analyze ONLY the medicines listed above. Use clinical_reference_data as supporting evidence only. Return ONLY valid JSON matching the required output schema. Do not summarize the reference data."
103109
}, indent=2)
104110

111+
105112
response = requests.post(
106113
"http://localhost:11434/api/chat",
107114
json={
108115
"model": "qwen2.5",
109116
"messages": [
110-
{"role": "system", "content": system_prompt},
111-
{"role": "user", "content": user_message}
117+
{
118+
"role": "system",
119+
"content": system_prompt
120+
},
121+
{
122+
"role": "user",
123+
"content": f"CLINICAL REFERENCE DATA (use as evidence only, do not summarize):\n\n{retrieved_context}"
124+
},
125+
{
126+
"role": "assistant",
127+
"content": "I have reviewed the clinical reference data. I will use it as supporting evidence only. I am ready to analyze the medicines you specify and return JSON in the required schema."
128+
},
129+
{
130+
"role": "user",
131+
"content": f"Now analyze this prescription and return ONLY valid JSON:\n\n{json.dumps({'medicines': medicines, 'patient_history': {'age': patient_history.age, 'weight': patient_history.weight, 'known_allergies': patient_history.known_allergies, 'current_medications': patient_history.current_medications, 'conditions': patient_history.conditions}}, indent=2)}"
132+
}
112133
],
113134
"stream": False
114135
},
115-
timeout = 30
136+
timeout=120
116137
)
117138

118139
response.raise_for_status()
119-
120140
content = response.json()["message"]["content"]
121141
return parse_llm_response(content)
122142

143+
144+
if __name__ == "__main__":
145+
class MockHistory:
146+
age = "80"
147+
weight = "60"
148+
known_allergies = []
149+
current_medications = []
150+
conditions = ["Atrial Fibrillation"]
151+
152+
medicines = ["Digoxin","Verapamil"]
153+
result = check_interactions_llm(medicines, MockHistory())
154+
print("FINAL RESULT:")
155+
print(json.dumps(result, indent=2))

backend/ingest.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import json
2+
import os
3+
import pickle
4+
from rank_bm25 import BM25Okapi
5+
import chromadb
6+
from embedder import embed_batch
7+
8+
# ── Paths ──────────────────────────────────────────────
9+
DATA_PATH = "data/drug-label-0001-of-0013.json"
10+
CHROMA_PATH = "chroma_db"
11+
BM25_PATH = "bm25_index.pkl"
12+
13+
# ── ChromaDB setup ─────────────────────────────────────
14+
client = chromadb.PersistentClient(path=CHROMA_PATH)
15+
collection = client.get_or_create_collection(
16+
name="drug_interactions",
17+
metadata={"hnsw:space": "cosine"}
18+
)
19+
20+
def load_records(path: str) -> list[dict]:
21+
"""Load only records that have drug_interactions field."""
22+
print("Loading bulk data...")
23+
with open(path, "r") as f:
24+
data = json.load(f)
25+
26+
records = [
27+
r for r in data["results"]
28+
if "drug_interactions" in r and r["drug_interactions"]
29+
]
30+
print(f"Found {len(records)} records with interaction data")
31+
return records
32+
33+
def extract_drug_name(record: dict) -> str:
34+
openfda = record.get("openfda", {})
35+
generic = openfda.get("generic_name", [])
36+
brand = openfda.get("brand_name", [])
37+
spl = record.get("spl_product_data_elements", [])
38+
39+
if generic:
40+
return generic[0].upper()
41+
elif brand:
42+
return brand[0].upper()
43+
elif spl:
44+
return spl[0].split()[0].upper()
45+
else:
46+
return "UNKNOWN"
47+
48+
def chunk_text(text: str, drug_name: str, chunk_size: int = 600, overlap: int = 100) -> list[str]:
49+
"""Split interaction text into overlapping chunks."""
50+
words = text.split()
51+
chunks = []
52+
start = 0
53+
54+
while start < len(words):
55+
end = start + chunk_size
56+
chunk = " ".join(words[start:end])
57+
# Prepend drug name to every chunk for BM25 keyword matching
58+
chunks.append(f"{drug_name}: {chunk}")
59+
start += chunk_size - overlap
60+
61+
return chunks
62+
63+
def ingest():
64+
# ── Check if already ingested ──────────────────────
65+
existing = collection.count()
66+
if existing > 0:
67+
print(f"ChromaDB already has {existing} chunks. Skipping ingestion.")
68+
print("Delete chroma_db/ folder to re-ingest.")
69+
return
70+
71+
records = load_records(DATA_PATH)
72+
73+
all_chunks = []
74+
all_ids = []
75+
all_metadata = []
76+
77+
print("Chunking records...")
78+
for i, record in enumerate(records):
79+
drug_name = extract_drug_name(record)
80+
interaction_text = record["drug_interactions"][0]
81+
chunks = chunk_text(interaction_text, drug_name)
82+
83+
for j, chunk in enumerate(chunks):
84+
all_chunks.append(chunk)
85+
all_ids.append(f"{i}_{j}")
86+
all_metadata.append({"drug_name": drug_name})
87+
88+
print(f"Total chunks: {len(all_chunks)}")
89+
90+
# ── Embed and store in ChromaDB in batches ─────────
91+
print("Embedding and storing in ChromaDB...")
92+
batch_size = 50
93+
94+
for start in range(0, len(all_chunks), batch_size):
95+
end = start + batch_size
96+
batch_chunks = all_chunks[start:end]
97+
batch_ids = all_ids[start:end]
98+
batch_metadata = all_metadata[start:end]
99+
100+
embeddings = embed_batch(batch_chunks)
101+
102+
collection.add(
103+
documents=batch_chunks,
104+
embeddings=embeddings,
105+
ids=batch_ids,
106+
metadatas=batch_metadata
107+
)
108+
109+
if start % 500 == 0:
110+
print(f" Processed {start}/{len(all_chunks)} chunks...")
111+
112+
# ── Build and save BM25 index ──────────────────────
113+
print("Building BM25 index...")
114+
tokenized = [chunk.lower().split() for chunk in all_chunks]
115+
bm25 = BM25Okapi(tokenized)
116+
117+
with open(BM25_PATH, "wb") as f:
118+
pickle.dump((bm25, all_chunks), f)
119+
120+
print(f"\nIngestion complete.")
121+
print(f"ChromaDB chunks: {collection.count()}")
122+
print(f"BM25 index saved to {BM25_PATH}")
123+
124+
if __name__ == "__main__":
125+
ingest()

0 commit comments

Comments
 (0)