Coverage for app \ rag \ prompt_builder.py: 99%
72 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-24 13:18 +0530
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-24 13:18 +0530
1"""
2Clinical Prompt Builder
4Purpose:
5- Convert structured FACT context into a safe, clinical-grade LLM prompt
6- Enforce medical guardrails
7- Prevent hallucination, diagnosis, or prescription
9This file contains ZERO business logic.
10Only formatting + safety instructions.
11"""
13from typing import Dict, Any, List
16# ------------------------------------------------------------------
17# Public API
18# ------------------------------------------------------------------
20def build_medical_prompt(
21 question: str,
22 context: Dict[str, Any],
23) -> str:
24 """
25 Build a clinical-grade prompt for medical explanation.
26 Context contains FACTS ONLY.
27 """
29 patient = context.get("patient", {})
30 wearables = context.get("wearables", {})
31 papers = context.get("papers", [])
32 drug_facts = context.get("drug_facts", {}) or context.get("drug_interactions", {})
34 prompt = f"""
35You are a clinical explanation assistant.
36You are NOT a doctor.
37You do NOT diagnose diseases.
38You do NOT prescribe or recommend new medications.
39You provide educational, safety-focused explanations only.
41CRITICAL SAFETY RULES (MUST FOLLOW):
42- Use ONLY the information explicitly provided below.
43- Do NOT introduce new medical facts, mechanisms, or interactions.
44- Do NOT infer drug interactions beyond those listed.
45- Do NOT assume missing patient data.
46- ALWAYS reference the patient's ACTUAL numbers — never use generic ranges alone.
47- ALWAYS directly answer the question asked FIRST (yes/no + brief reason).
48- If information is insufficient, state this clearly.
49- NEVER expose internal system labels like "insufficient-data", "non-numeric",
50 or "N/A" to the user — replace with plain language like "not enough data yet".
51- Your role is explanation, not decision-making.
52- If no research papers are provided, write ONLY:
53 "No research papers available for this query."
54 Do NOT add any "general knowledge" or assumptions after this.
55- The Direct Answer must be YES or NO — pick one and stay consistent
56 throughout the entire response.
58========================
59PATIENT FACTS
60========================
61{_format_patient(patient)}
63========================
64WEARABLE OBSERVATIONS (FACTS)
65========================
66{_format_wearables(wearables)}
68========================
69MEDICATION SAFETY FACTS
70========================
71{_format_drug_facts(drug_facts)}
73========================
74RELEVANT MEDICAL LITERATURE
75========================
76Rules: Cite ONLY papers listed below. Include journal + year.
77If the section below says "No research papers available", skip ## What the Research Says entirely.
78========================
79{_format_papers(papers)}
81========================
82USER QUESTION
83========================
84{question}
86========================
87RESPONSE FORMAT (MANDATORY — FOLLOW EXACTLY)
88========================
90## Direct Answer
91- Answer YES or NO to the question first.
92- In 2-3 sentences explain why, using the patient's actual data.
93- Do NOT use generic explanations. Reference their real numbers.
95## Your Data This Week
96| Metric | Reading | Normal Range | Date |
97|--------|---------|--------------|------|
98[one row per reading, real values only, no placeholder text]
102## Key Considerations
103- Summarize the most relevant safety or health considerations.
104- Maximum 3 bullet points.
105- Stay strictly on topic — do NOT mention unrelated conditions.
107## What to Monitor
108- List specific measurable things the patient should track.
109- Be concrete (e.g., "check BP every morning after waking").
111## When to Seek Medical Help
112- Describe clear, specific situations requiring professional attention.
113- Use the patient's actual condition and medication names.
115## Safety Notes
116- Brief, non-prescriptive safety guidance only.
117- Only mention medications or conditions relevant to the question.
119Always end with exactly this line:
120"Consult your healthcare provider before making any changes."
122========================
123STRICT OUTPUT RULES
124========================
125- Direct Answer: ONE word first — YES or NO. Then 2 sentences max. Do NOT skip.
126- Data Table: real values and dates ONLY. Zero narrative text in table cells.
127- Research: Cite ONLY the papers provided in RELEVANT MEDICAL LITERATURE above.
128 Include journal name and year. If that section contains "No research papers available",
129 skip ## What the Research Says entirely. Do NOT fabricate findings.
130- Do NOT introduce conditions or medications not listed in Patient Facts.
131- Do NOT mention any metric unrelated to the question.
132- Do NOT add explanations inside table cells.
133- Do NOT leak any internal system values or labels into the response.
134- Do NOT use generic advice that ignores the patient's actual numbers.
135- Maximum 2 sentences per bullet point.
136- EVERY section is mandatory except ## What the Research Says (skip if no papers).
137- Never truncate mid-sentence — shorten bullet points if needed but complete every section.
138- Keep total response concise — quality over length.
139========================
140"""
142 return prompt.strip()
145# ------------------------------------------------------------------
146# Formatting helpers
147# ------------------------------------------------------------------
149def _format_patient(patient: Dict[str, Any]) -> str:
150 if not patient:
151 return "No patient data available."
153 lines = [f"Patient ID: {patient.get('patient_id', 'Unknown')}"]
155 # Demographics
156 demo = patient.get("demographics", {})
157 if demo:
158 lines.append(
159 f"Demographics: Age {demo.get('age', 'N/A')}, "
160 f"Gender {demo.get('gender', 'N/A')}, "
161 f"Blood Type {demo.get('blood_type', 'N/A')}"
162 )
164 # Conditions
165 conditions = patient.get("conditions", [])
166 if conditions:
167 lines.append("\nConditions:")
168 for c in conditions:
169 diagnosed = f", Diagnosed: {c.get('diagnosed')}" if c.get("diagnosed") else ""
170 lines.append(
171 f" - {c.get('name')} "
172 f"(Severity: {c.get('severity')}, "
173 f"Status: {c.get('status')}"
174 f"{diagnosed})"
175 )
177 # Medications
178 meds = patient.get("medications", [])
179 if meds:
180 lines.append("\nMedications:")
181 for m in meds:
182 purpose = f" — Purpose: {m.get('purpose')}" if m.get("purpose") else ""
183 treats = f" | Treats: {m.get('treats')}" if m.get("treats") else ""
184 lines.append(
185 f" - {m.get('name')} "
186 f"({m.get('dosage')}, {m.get('frequency')})"
187 f"{purpose}{treats}"
188 )
190 # Lab Results
191 labs = patient.get("lab_results", [])
192 if labs:
193 lines.append("\nRecent Lab Results:")
194 for l in labs:
195 lines.append(
196 f" - {l.get('name')}: {l.get('result')} {l.get('unit', '')} "
197 f"(Normal: {l.get('normal_range', 'N/A')}, "
198 f"Status: {l.get('status', 'N/A')}, "
199 f"Date: {l.get('date', 'N/A')})"
200 )
202 return "\n".join(lines)
205def _format_wearables(wearables: Dict[str, Any]) -> str:
206 if not wearables or not wearables.get("available"):
207 return "No wearable data available."
209 lines = []
210 for m in wearables.get("metrics", []):
212 # Sanitize trend — never expose raw internal system labels
213 trend = m.get("trend", "")
214 if (
215 not trend
216 or "insufficient" in trend.lower()
217 or "non-numeric" in trend.lower()
218 or trend.strip() == "N/A"
219 ):
220 trend = "monitoring ongoing — more readings needed"
222 lines.append(
223 f" - {m.get('metric', 'Unknown Metric')}: "
224 f"Latest {m.get('latest_value', 'not recorded')}, "
225 f"Previous {m.get('previous_value', 'not recorded')}, "
226 f"Avg {m.get('average_value', 'not recorded')}, "
227 f"Normal Range: {m.get('normal_range', 'N/A')}, "
228 f"Trend: {trend}"
229 )
231 # Show individual dated readings if available
232 readings = m.get("readings", [])
233 if readings:
234 for r in readings:
235 date = r.get("date", "unknown date")
236 value = r.get("value", "unknown value")
237 lines.append(f" [{date}] → {value}")
239 return "\n".join(lines) if lines else "No wearable metrics recorded."
242def _format_drug_facts(drug_facts: Dict[str, Any]) -> str:
243 if not drug_facts:
244 return "No medication safety data available."
246 lines = []
248 # Drug-Drug interactions
249 for f in drug_facts.get("drug_drug_interactions", []):
250 drugs = ", ".join(f.get("drugs_involved", []))
251 lines.append(
252 f" - Drug–Drug ({f.get('severity', 'unknown')}): "
253 f"{drugs} → {f.get('interaction', 'N/A')}"
254 )
256 # Drug-Condition interactions
257 for f in drug_facts.get("drug_condition_interactions", []):
258 lines.append(
259 f" - Drug–Condition ({f.get('severity', 'unknown')}): "
260 f"{f.get('drug')} contraindicated in {f.get('condition')}"
261 )
263 # Drug effect facts
264 for f in drug_facts.get("drug_effect_facts", []):
265 lines.append(
266 f" - Drug Effect: {f.get('drug')} → {f.get('effect')} "
267 f"(Mechanism: {f.get('mechanism', 'N/A')})"
268 )
270 return "\n".join(lines) if lines else "No known medication risks identified."
273def _format_papers(papers: List[Dict[str, Any]]) -> str:
274 if not papers:
275 return "No relevant research papers found."
277 lines = []
278 for i, p in enumerate(papers[:3], start=1):
279 lines.append(
280 f"[{i}] {p.get('title', 'Untitled')} "
281 f"({p.get('journal', 'Unknown Journal')}, {p.get('year', 'N/A')})"
282 )
283 preview = p.get("text_preview", "")[:300]
284 if preview:
285 lines.append(f" Summary: {preview}")
287 return "\n".join(lines)