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

1""" 

2Clinical Prompt Builder 

3 

4Purpose: 

5- Convert structured FACT context into a safe, clinical-grade LLM prompt 

6- Enforce medical guardrails 

7- Prevent hallucination, diagnosis, or prescription 

8 

9This file contains ZERO business logic. 

10Only formatting + safety instructions. 

11""" 

12 

13from typing import Dict, Any, List 

14 

15 

16# ------------------------------------------------------------------ 

17# Public API 

18# ------------------------------------------------------------------ 

19 

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 """ 

28 

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", {}) 

33 

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. 

40 

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. 

57 

58======================== 

59PATIENT FACTS 

60======================== 

61{_format_patient(patient)} 

62 

63======================== 

64WEARABLE OBSERVATIONS (FACTS) 

65======================== 

66{_format_wearables(wearables)} 

67 

68======================== 

69MEDICATION SAFETY FACTS 

70======================== 

71{_format_drug_facts(drug_facts)} 

72 

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)} 

80 

81======================== 

82USER QUESTION 

83======================== 

84{question} 

85 

86======================== 

87RESPONSE FORMAT (MANDATORY — FOLLOW EXACTLY) 

88======================== 

89 

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. 

94 

95## Your Data This Week 

96| Metric | Reading | Normal Range | Date | 

97|--------|---------|--------------|------| 

98[one row per reading, real values only, no placeholder text] 

99 

100 

101 

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. 

106 

107## What to Monitor 

108- List specific measurable things the patient should track. 

109- Be concrete (e.g., "check BP every morning after waking"). 

110 

111## When to Seek Medical Help 

112- Describe clear, specific situations requiring professional attention. 

113- Use the patient's actual condition and medication names. 

114 

115## Safety Notes 

116- Brief, non-prescriptive safety guidance only. 

117- Only mention medications or conditions relevant to the question. 

118 

119Always end with exactly this line: 

120"Consult your healthcare provider before making any changes." 

121 

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""" 

141 

142 return prompt.strip() 

143 

144 

145# ------------------------------------------------------------------ 

146# Formatting helpers 

147# ------------------------------------------------------------------ 

148 

149def _format_patient(patient: Dict[str, Any]) -> str: 

150 if not patient: 

151 return "No patient data available." 

152 

153 lines = [f"Patient ID: {patient.get('patient_id', 'Unknown')}"] 

154 

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 ) 

163 

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 ) 

176 

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 ) 

189 

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 ) 

201 

202 return "\n".join(lines) 

203 

204 

205def _format_wearables(wearables: Dict[str, Any]) -> str: 

206 if not wearables or not wearables.get("available"): 

207 return "No wearable data available." 

208 

209 lines = [] 

210 for m in wearables.get("metrics", []): 

211 

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" 

221 

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 ) 

230 

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}") 

238 

239 return "\n".join(lines) if lines else "No wearable metrics recorded." 

240 

241 

242def _format_drug_facts(drug_facts: Dict[str, Any]) -> str: 

243 if not drug_facts: 

244 return "No medication safety data available." 

245 

246 lines = [] 

247 

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 ) 

255 

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 ) 

262 

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 ) 

269 

270 return "\n".join(lines) if lines else "No known medication risks identified." 

271 

272 

273def _format_papers(papers: List[Dict[str, Any]]) -> str: 

274 if not papers: 

275 return "No relevant research papers found." 

276 

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}") 

286 

287 return "\n".join(lines)