karthikmulugu08 commited on
Commit
e8b2cbb
·
verified ·
1 Parent(s): e6ed707

Fix: strip non-ASCII chars before sending to llama-server

Browse files
Files changed (1) hide show
  1. safety_checker.py +70 -132
safety_checker.py CHANGED
@@ -1,78 +1,19 @@
1
  """
2
  ClinIQ — Proactive Drug Safety Checker.
3
 
4
- Runs automatically after document ingestion no question needed.
5
- Uses Qwen2.5-3B-Instruct to:
6
- 1. Extract all medications + allergies as structured JSON
7
- 2. Reason over combinations for contraindications and drug interactions
8
- 3. Return colour-coded alerts: DANGER / WARNING / INFO
9
-
10
- This is the signature feature that makes ClinIQ unique:
11
- a 3B model acting as an on-device pharmacist for small clinics.
12
  """
13
 
14
  from __future__ import annotations
15
-
16
- import json
17
- import re
18
  from dataclasses import dataclass, field
19
  from typing import List, Optional
20
 
21
- SAFETY_SYSTEM_PROMPT = """You are a clinical pharmacist assistant with deep knowledge of drug-allergy
22
- contraindications and drug interactions. You work for small clinics with no pharmacist on staff.
23
- Your job is to protect patients by flagging dangerous medication combinations BEFORE harm occurs.
24
- Be precise. Cite the clinical basis for each alert. Output ONLY valid JSON."""
25
-
26
- EXTRACTION_PROMPT = """Read the clinical documents below carefully.
27
-
28
- Task 1 — Find every medication/drug listed under headings like MEDICATIONS, CURRENT MEDICATIONS, MEDICATIONS AT DISCHARGE, PLAN, or ASSESSMENT.
29
- Task 2 — Find every allergy listed under headings like ALLERGIES, ALLERGY, or any sentence mentioning "allergic to" or "allergy".
30
-
31
- Documents:
32
- {context}
33
-
34
- Output ONLY valid JSON, no markdown, no explanation:
35
- {{
36
- "medications": [
37
- {{"name": "drug name", "dose": "dose", "frequency": "frequency"}}
38
- ],
39
- "allergies": [
40
- {{"substance": "substance name", "reaction": "reaction description", "severity": "mild|moderate|severe|unknown"}}
41
- ]
42
- }}"""
43
-
44
- SAFETY_PROMPT = """You are a clinical pharmacist. Given the patient's current medications and allergies,
45
- identify ALL safety concerns.
46
-
47
- Medications:
48
- {medications}
49
-
50
- Allergies:
51
- {allergies}
52
-
53
- Analyse for:
54
- 1. Drug-allergy contraindications (same drug class as allergen)
55
- 2. Drug-drug interactions (dangerous combinations)
56
- 3. Missing standard-of-care medications or monitoring
57
- 4. Dosing concerns given the diagnoses mentioned
58
-
59
- Output ONLY this JSON (no markdown):
60
- {{
61
- "alerts": [
62
- {{
63
- "level": "DANGER|WARNING|INFO",
64
- "title": "short title",
65
- "detail": "clinical explanation with the specific drug names",
66
- "recommendation": "what the clinician should do"
67
- }}
68
- ],
69
- "overall_safety": "SAFE|REVIEW_NEEDED|URGENT"
70
- }}"""
71
-
72
 
73
  @dataclass
74
  class SafetyAlert:
75
- level: str # DANGER | WARNING | INFO
76
  title: str
77
  detail: str
78
  recommendation: str
@@ -81,8 +22,8 @@ class SafetyAlert:
81
  @dataclass
82
  class SafetyReport:
83
  medications: List[dict] = field(default_factory=list)
84
- allergies: List[dict] = field(default_factory=list)
85
- alerts: List[SafetyAlert] = field(default_factory=list)
86
  overall_safety: str = "SAFE"
87
  error: Optional[str] = None
88
 
@@ -90,77 +31,78 @@ class SafetyReport:
90
  def has_danger(self) -> bool:
91
  return any(a.level == "DANGER" for a in self.alerts)
92
 
93
- @property
94
- def alert_count(self) -> dict:
95
- counts = {"DANGER": 0, "WARNING": 0, "INFO": 0}
96
- for a in self.alerts:
97
- counts[a.level] = counts.get(a.level, 0) + 1
98
- return counts
99
 
 
 
 
 
 
 
 
100
 
101
- def run_safety_check(context: str, call_model_fn) -> SafetyReport:
102
- """
103
- Full safety pipeline:
104
- Step 1 — extract structured medications + allergies
105
- Step 2 — reason over them for safety concerns
106
 
107
- call_model_fn(prompt, max_tokens, json_mode) -> str
108
- """
109
- report = SafetyReport()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- # ── Step 1: Extract ──────────────────────────────────────────────────────
112
- extract_prompt = (
113
- "<|im_start|>system\n"
114
- "Extract medications and allergies from clinical documents. Output ONLY valid JSON.<|im_end|>\n"
115
- "<|im_start|>user\n"
116
- + EXTRACTION_PROMPT.format(context=context[:3000])
117
- + "\n<|im_end|>\n<|im_start|>assistant\n"
118
- )
119
 
120
- try:
121
- raw = call_model_fn(extract_prompt, 800, True)
122
- match = re.search(r'\{.*\}', raw, re.DOTALL)
123
- if match:
124
- data = json.loads(match.group())
125
- report.medications = data.get("medications", [])
126
- report.allergies = data.get("allergies", [])
127
- except Exception as e:
128
- report.error = f"Extraction failed: {e}"
129
  return report
130
 
131
- if not report.medications and not report.allergies:
132
- report.overall_safety = "SAFE"
133
- return report
 
 
134
 
135
- # ── Step 2: Safety reasoning ──────────────────────────────────────────────
136
- safety_prompt = (
137
- "<|im_start|>system\n"
138
- + SAFETY_SYSTEM_PROMPT
139
- + "<|im_end|>\n<|im_start|>user\n"
140
- + SAFETY_PROMPT.format(
141
- medications=json.dumps(report.medications, indent=2),
142
- allergies=json.dumps(report.allergies, indent=2),
143
- )
144
- + "\n<|im_end|>\n<|im_start|>assistant\n"
145
- )
146
 
147
  try:
148
- raw = call_model_fn(safety_prompt, 1000, True)
 
149
  match = re.search(r'\{.*\}', raw, re.DOTALL)
150
- if match:
151
- data = json.loads(match.group())
152
- report.alerts = [
153
- SafetyAlert(
154
- level=a.get("level", "INFO"),
155
- title=a.get("title", ""),
156
- detail=a.get("detail", ""),
157
- recommendation=a.get("recommendation", ""),
158
- )
159
- for a in data.get("alerts", [])
160
- ]
161
- report.overall_safety = data.get("overall_safety", "REVIEW_NEEDED")
 
 
 
 
 
 
 
162
  except Exception as e:
163
- report.error = f"Safety analysis failed: {e}"
164
 
165
  return report
166
 
@@ -168,15 +110,11 @@ def run_safety_check(context: str, call_model_fn) -> SafetyReport:
168
  def report_to_dict(report: SafetyReport) -> dict:
169
  return {
170
  "overall_safety": report.overall_safety,
171
- "medications": report.medications,
172
- "allergies": report.allergies,
173
  "alerts": [
174
- {
175
- "level": a.level,
176
- "title": a.title,
177
- "detail": a.detail,
178
- "recommendation": a.recommendation,
179
- }
180
  for a in report.alerts
181
  ],
182
  "error": report.error,
 
1
  """
2
  ClinIQ — Proactive Drug Safety Checker.
3
 
4
+ Single-call design: one prompt extracts medications/allergies AND identifies
5
+ safety concerns simultaneously. This is faster and avoids context overflow.
 
 
 
 
 
 
6
  """
7
 
8
  from __future__ import annotations
9
+ import json, re
 
 
10
  from dataclasses import dataclass, field
11
  from typing import List, Optional
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  @dataclass
15
  class SafetyAlert:
16
+ level: str # DANGER | WARNING | INFO
17
  title: str
18
  detail: str
19
  recommendation: str
 
22
  @dataclass
23
  class SafetyReport:
24
  medications: List[dict] = field(default_factory=list)
25
+ allergies: List[dict] = field(default_factory=list)
26
+ alerts: List[SafetyAlert] = field(default_factory=list)
27
  overall_safety: str = "SAFE"
28
  error: Optional[str] = None
29
 
 
31
  def has_danger(self) -> bool:
32
  return any(a.level == "DANGER" for a in self.alerts)
33
 
 
 
 
 
 
 
34
 
35
+ # Single combined prompt — extract AND analyse in one call
36
+ _COMBINED_PROMPT = """\
37
+ <|im_start|>system
38
+ You are a clinical pharmacist AI. Analyse the clinical documents and identify drug safety concerns.
39
+ Be thorough — patient safety depends on your accuracy. Output ONLY valid JSON.<|im_end|>
40
+ <|im_start|>user
41
+ Analyse the following clinical documents.
42
 
43
+ Step 1: Extract every medication and allergy mentioned (look for MEDICATIONS, ALLERGIES, ALLERGY sections and any drug/allergy mentions in the text).
44
+ Step 2: Identify DANGER (drug-allergy contraindications), WARNING (drug interactions, drug-disease contraindications), or INFO alerts.
 
 
 
45
 
46
+ Documents:
47
+ {context}
48
+
49
+ Output ONLY this JSON (no markdown, no explanation):
50
+ {{
51
+ "medications": [{{"name": "...", "dose": "...", "frequency": "..."}}],
52
+ "allergies": [{{"substance": "...", "reaction": "...", "severity": "mild|moderate|severe|unknown"}}],
53
+ "alerts": [
54
+ {{
55
+ "level": "DANGER|WARNING|INFO",
56
+ "title": "concise title",
57
+ "detail": "clinical explanation naming the specific drugs involved",
58
+ "recommendation": "what the clinician should do"
59
+ }}
60
+ ],
61
+ "overall_safety": "SAFE|REVIEW_NEEDED|URGENT"
62
+ }}
63
+ <|im_end|>
64
+ <|im_start|>assistant
65
+ """
66
 
 
 
 
 
 
 
 
 
67
 
68
+ def run_safety_check(context: str, call_model_fn) -> SafetyReport:
69
+ report = SafetyReport()
70
+ if not context.strip():
 
 
 
 
 
 
71
  return report
72
 
73
+ # Strip non-ASCII chars (em-dashes etc. corrupt llama-server JSON parsing)
74
+ import re
75
+ context = re.sub(r'[^\x00-\x7F]', '-', context)
76
+ # Keep well under the safe limit (~3000 chars of context)
77
+ trimmed = context[:3000]
78
 
79
+ prompt = _COMBINED_PROMPT.format(context=trimmed)
 
 
 
 
 
 
 
 
 
 
80
 
81
  try:
82
+ raw = call_model_fn(prompt, 1000, False)
83
+ # Extract JSON from response
84
  match = re.search(r'\{.*\}', raw, re.DOTALL)
85
+ if not match:
86
+ report.error = "Model did not return JSON"
87
+ return report
88
+
89
+ data = json.loads(match.group())
90
+ report.medications = data.get("medications", [])
91
+ report.allergies = data.get("allergies", [])
92
+ report.overall_safety = data.get("overall_safety", "REVIEW_NEEDED")
93
+ report.alerts = [
94
+ SafetyAlert(
95
+ level = a.get("level", "INFO"),
96
+ title = a.get("title", ""),
97
+ detail = a.get("detail", ""),
98
+ recommendation = a.get("recommendation", ""),
99
+ )
100
+ for a in data.get("alerts", [])
101
+ ]
102
+ except json.JSONDecodeError as e:
103
+ report.error = f"JSON parse error: {e}"
104
  except Exception as e:
105
+ report.error = str(e)
106
 
107
  return report
108
 
 
110
  def report_to_dict(report: SafetyReport) -> dict:
111
  return {
112
  "overall_safety": report.overall_safety,
113
+ "medications": report.medications,
114
+ "allergies": report.allergies,
115
  "alerts": [
116
+ {"level": a.level, "title": a.title,
117
+ "detail": a.detail, "recommendation": a.recommendation}
 
 
 
 
118
  for a in report.alerts
119
  ],
120
  "error": report.error,