ghitaben commited on
Commit
793d027
·
1 Parent(s): 1abf8b9

Add ChromaDB vector store for unstructured document retrieval and import functionality

Browse files
.gitignore CHANGED
@@ -1 +1,4 @@
1
  .DS_Store
 
 
 
 
1
  .DS_Store
2
+ .env
3
+ data/
4
+ *.pyc
app.py CHANGED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Med-I-C: AMR-Guard Demo Application
3
+ Infection Lifecycle Orchestrator - Streamlit Interface
4
+ """
5
+
6
+ import streamlit as st
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ # Add project root to path
11
+ PROJECT_ROOT = Path(__file__).parent
12
+ sys.path.insert(0, str(PROJECT_ROOT))
13
+
14
+ from src.tools import (
15
+ query_antibiotic_info,
16
+ get_antibiotics_by_category,
17
+ interpret_mic_value,
18
+ get_breakpoints_for_pathogen,
19
+ query_resistance_pattern,
20
+ get_most_effective_antibiotics,
21
+ calculate_mic_trend,
22
+ check_drug_interactions,
23
+ screen_antibiotic_safety,
24
+ search_clinical_guidelines,
25
+ get_treatment_recommendation,
26
+ get_empirical_therapy_guidance,
27
+ )
28
+
29
+ # Page configuration
30
+ st.set_page_config(
31
+ page_title="Med-I-C: AMR-Guard",
32
+ page_icon="🦠",
33
+ layout="wide",
34
+ initial_sidebar_state="expanded"
35
+ )
36
+
37
+ # Custom CSS
38
+ st.markdown("""
39
+ <style>
40
+ .main-header {
41
+ font-size: 2.5rem;
42
+ font-weight: bold;
43
+ color: #1E88E5;
44
+ margin-bottom: 0;
45
+ }
46
+ .sub-header {
47
+ font-size: 1.2rem;
48
+ color: #666;
49
+ margin-top: 0;
50
+ }
51
+ .risk-high {
52
+ background-color: #FFCDD2;
53
+ padding: 10px;
54
+ border-radius: 5px;
55
+ border-left: 4px solid #D32F2F;
56
+ }
57
+ .risk-moderate {
58
+ background-color: #FFE0B2;
59
+ padding: 10px;
60
+ border-radius: 5px;
61
+ border-left: 4px solid #F57C00;
62
+ }
63
+ .risk-low {
64
+ background-color: #C8E6C9;
65
+ padding: 10px;
66
+ border-radius: 5px;
67
+ border-left: 4px solid #388E3C;
68
+ }
69
+ .info-box {
70
+ background-color: #E3F2FD;
71
+ padding: 15px;
72
+ border-radius: 5px;
73
+ margin: 10px 0;
74
+ }
75
+ </style>
76
+ """, unsafe_allow_html=True)
77
+
78
+
79
+ def main():
80
+ # Header
81
+ st.markdown('<p class="main-header">🦠 Med-I-C: AMR-Guard</p>', unsafe_allow_html=True)
82
+ st.markdown('<p class="sub-header">Infection Lifecycle Orchestrator Demo</p>', unsafe_allow_html=True)
83
+
84
+ # Sidebar navigation
85
+ st.sidebar.title("Navigation")
86
+ page = st.sidebar.radio(
87
+ "Select Module",
88
+ [
89
+ "🏠 Overview",
90
+ "💊 Stage 1: Empirical Advisor",
91
+ "🔬 Stage 2: Lab Interpretation",
92
+ "📊 MIC Trend Analysis",
93
+ "⚠️ Drug Safety Check",
94
+ "📚 Clinical Guidelines Search"
95
+ ]
96
+ )
97
+
98
+ if page == "🏠 Overview":
99
+ show_overview()
100
+ elif page == "💊 Stage 1: Empirical Advisor":
101
+ show_empirical_advisor()
102
+ elif page == "🔬 Stage 2: Lab Interpretation":
103
+ show_lab_interpretation()
104
+ elif page == "📊 MIC Trend Analysis":
105
+ show_mic_trend_analysis()
106
+ elif page == "⚠️ Drug Safety Check":
107
+ show_drug_safety()
108
+ elif page == "📚 Clinical Guidelines Search":
109
+ show_guidelines_search()
110
+
111
+
112
+ def show_overview():
113
+ st.header("System Overview")
114
+
115
+ col1, col2 = st.columns(2)
116
+
117
+ with col1:
118
+ st.subheader("Stage 1: Empirical Phase")
119
+ st.markdown("""
120
+ **The "First 24 Hours"**
121
+
122
+ Before lab results are available, the system:
123
+ - Analyzes patient history and risk factors
124
+ - Suggests empirical antibiotics based on:
125
+ - Suspected pathogen
126
+ - Local resistance patterns
127
+ - WHO stewardship guidelines (ACCESS → WATCH → RESERVE)
128
+ - Checks drug interactions with current medications
129
+ """)
130
+
131
+ with col2:
132
+ st.subheader("Stage 2: Targeted Phase")
133
+ st.markdown("""
134
+ **The "Lab Interpretation"**
135
+
136
+ Once antibiogram is available, the system:
137
+ - Interprets MIC values against EUCAST breakpoints
138
+ - Detects "MIC Creep" from historical data
139
+ - Refines antibiotic selection
140
+ - Provides evidence-based treatment recommendations
141
+ """)
142
+
143
+ st.divider()
144
+
145
+ st.subheader("Knowledge Sources")
146
+
147
+ col1, col2, col3, col4 = st.columns(4)
148
+
149
+ with col1:
150
+ st.metric("WHO EML", "264", "antibiotics classified")
151
+ with col2:
152
+ st.metric("ATLAS Data", "10K+", "susceptibility records")
153
+ with col3:
154
+ st.metric("Breakpoints", "41", "pathogen groups")
155
+ with col4:
156
+ st.metric("Interactions", "191K+", "drug pairs")
157
+
158
+
159
+ def show_empirical_advisor():
160
+ st.header("💊 Stage 1: Empirical Advisor")
161
+ st.markdown("*Recommend empirical therapy before lab results*")
162
+
163
+ col1, col2 = st.columns([2, 1])
164
+
165
+ with col1:
166
+ infection_type = st.selectbox(
167
+ "Infection Type",
168
+ ["Urinary Tract Infection (UTI)", "Pneumonia", "Sepsis",
169
+ "Skin/Soft Tissue", "Intra-abdominal", "Meningitis"]
170
+ )
171
+
172
+ suspected_pathogen = st.text_input(
173
+ "Suspected Pathogen (optional)",
174
+ placeholder="e.g., E. coli, Klebsiella pneumoniae"
175
+ )
176
+
177
+ risk_factors = st.multiselect(
178
+ "Risk Factors",
179
+ ["Prior MRSA infection", "Recent antibiotic use (<90 days)",
180
+ "Healthcare-associated", "Immunocompromised",
181
+ "Renal impairment", "Prior MDR infection"]
182
+ )
183
+
184
+ with col2:
185
+ st.markdown("**WHO Stewardship Categories**")
186
+ st.markdown("""
187
+ - **ACCESS**: First-line, low resistance
188
+ - **WATCH**: Higher resistance potential
189
+ - **RESERVE**: Last resort antibiotics
190
+ """)
191
+
192
+ if st.button("Get Empirical Recommendation", type="primary"):
193
+ with st.spinner("Searching guidelines and resistance data..."):
194
+ # Get recommendations from guidelines
195
+ guidance = get_empirical_therapy_guidance(
196
+ infection_type.split("(")[0].strip(),
197
+ risk_factors
198
+ )
199
+
200
+ st.subheader("Recommendations")
201
+
202
+ if guidance.get("recommendations"):
203
+ for i, rec in enumerate(guidance["recommendations"][:3], 1):
204
+ with st.expander(f"Guideline Excerpt {i} (Relevance: {rec.get('relevance_score', 0):.2f})"):
205
+ st.markdown(rec.get("content", ""))
206
+ st.caption(f"Source: {rec.get('source', 'IDSA Guidelines')}")
207
+
208
+ # If pathogen specified, show resistance patterns
209
+ if suspected_pathogen:
210
+ st.subheader(f"Resistance Patterns for {suspected_pathogen}")
211
+
212
+ effective = get_most_effective_antibiotics(suspected_pathogen, min_susceptibility=70)
213
+
214
+ if effective:
215
+ st.markdown("**Most Effective Antibiotics (>70% susceptibility)**")
216
+ for ab in effective[:5]:
217
+ st.write(f"- **{ab.get('antibiotic')}**: {ab.get('avg_susceptibility', 0):.1f}% susceptible")
218
+ else:
219
+ st.info("No resistance data found for this pathogen.")
220
+
221
+
222
+ def show_lab_interpretation():
223
+ st.header("🔬 Stage 2: Lab Interpretation")
224
+ st.markdown("*Interpret antibiogram MIC values*")
225
+
226
+ col1, col2 = st.columns(2)
227
+
228
+ with col1:
229
+ pathogen = st.text_input(
230
+ "Identified Pathogen",
231
+ placeholder="e.g., Escherichia coli, Pseudomonas aeruginosa"
232
+ )
233
+
234
+ antibiotic = st.text_input(
235
+ "Antibiotic",
236
+ placeholder="e.g., Ciprofloxacin, Meropenem"
237
+ )
238
+
239
+ mic_value = st.number_input(
240
+ "MIC Value (mg/L)",
241
+ min_value=0.001,
242
+ max_value=1024.0,
243
+ value=1.0,
244
+ step=0.5
245
+ )
246
+
247
+ with col2:
248
+ st.markdown("**How to Read Results**")
249
+ st.markdown("""
250
+ - **S (Susceptible)**: MIC ≤ breakpoint - antibiotic likely effective
251
+ - **I (Intermediate)**: May work with higher doses
252
+ - **R (Resistant)**: MIC > breakpoint - do not use
253
+ """)
254
+
255
+ if st.button("Interpret MIC", type="primary"):
256
+ if pathogen and antibiotic:
257
+ with st.spinner("Checking breakpoints..."):
258
+ result = interpret_mic_value(pathogen, antibiotic, mic_value)
259
+
260
+ interpretation = result.get("interpretation", "UNKNOWN")
261
+
262
+ if interpretation == "SUSCEPTIBLE":
263
+ st.success(f"✅ **{interpretation}**")
264
+ elif interpretation == "RESISTANT":
265
+ st.error(f"❌ **{interpretation}**")
266
+ elif interpretation == "INTERMEDIATE":
267
+ st.warning(f"⚠️ **{interpretation}**")
268
+ else:
269
+ st.info(f"❓ **{interpretation}**")
270
+
271
+ st.markdown(f"**Details:** {result.get('message', '')}")
272
+
273
+ if result.get("breakpoints"):
274
+ bp = result["breakpoints"]
275
+ st.markdown(f"""
276
+ **Breakpoints:**
277
+ - S ≤ {bp.get('susceptible', 'N/A')} mg/L
278
+ - R > {bp.get('resistant', 'N/A')} mg/L
279
+ """)
280
+
281
+ if result.get("notes"):
282
+ st.info(f"**Note:** {result.get('notes')}")
283
+ else:
284
+ st.warning("Please enter both pathogen and antibiotic names.")
285
+
286
+
287
+ def show_mic_trend_analysis():
288
+ st.header("📊 MIC Trend Analysis")
289
+ st.markdown("*Detect MIC creep over time*")
290
+
291
+ st.markdown("""
292
+ Enter historical MIC values to detect resistance velocity.
293
+ **MIC Creep**: A gradual increase in MIC that may predict treatment failure
294
+ even when the organism is still classified as "Susceptible".
295
+ """)
296
+
297
+ # Input for historical MICs
298
+ num_readings = st.slider("Number of historical readings", 2, 6, 3)
299
+
300
+ mic_values = []
301
+ cols = st.columns(num_readings)
302
+
303
+ for i, col in enumerate(cols):
304
+ with col:
305
+ mic = col.number_input(
306
+ f"MIC {i+1}",
307
+ min_value=0.001,
308
+ max_value=256.0,
309
+ value=float(2 ** i), # Default: 1, 2, 4, ...
310
+ key=f"mic_{i}"
311
+ )
312
+ mic_values.append({"date": f"T{i}", "mic_value": mic})
313
+
314
+ if st.button("Analyze Trend", type="primary"):
315
+ result = calculate_mic_trend(mic_values)
316
+
317
+ risk_level = result.get("risk_level", "UNKNOWN")
318
+
319
+ if risk_level == "HIGH":
320
+ st.markdown(f'<div class="risk-high"><strong>🚨 HIGH RISK</strong><br>{result.get("alert", "")}</div>',
321
+ unsafe_allow_html=True)
322
+ elif risk_level == "MODERATE":
323
+ st.markdown(f'<div class="risk-moderate"><strong>⚠️ MODERATE RISK</strong><br>{result.get("alert", "")}</div>',
324
+ unsafe_allow_html=True)
325
+ else:
326
+ st.markdown(f'<div class="risk-low"><strong>✅ LOW RISK</strong><br>{result.get("alert", "")}</div>',
327
+ unsafe_allow_html=True)
328
+
329
+ st.divider()
330
+
331
+ col1, col2, col3 = st.columns(3)
332
+
333
+ with col1:
334
+ st.metric("Baseline MIC", f"{result.get('baseline_mic', 'N/A')} mg/L")
335
+ with col2:
336
+ st.metric("Current MIC", f"{result.get('current_mic', 'N/A')} mg/L")
337
+ with col3:
338
+ st.metric("Fold Change", f"{result.get('ratio', 'N/A')}x")
339
+
340
+ st.markdown(f"**Trend:** {result.get('trend', 'N/A')}")
341
+ st.markdown(f"**Resistance Velocity:** {result.get('velocity', 'N/A')}x per time point")
342
+
343
+
344
+ def show_drug_safety():
345
+ st.header("⚠️ Drug Safety Check")
346
+ st.markdown("*Screen for drug interactions*")
347
+
348
+ col1, col2 = st.columns(2)
349
+
350
+ with col1:
351
+ antibiotic = st.text_input(
352
+ "Proposed Antibiotic",
353
+ placeholder="e.g., Ciprofloxacin"
354
+ )
355
+
356
+ current_meds = st.text_area(
357
+ "Current Medications (one per line)",
358
+ placeholder="Warfarin\nMetformin\nAmlodipine",
359
+ height=150
360
+ )
361
+
362
+ with col2:
363
+ allergies = st.text_area(
364
+ "Known Allergies (one per line)",
365
+ placeholder="Penicillin\nSulfa",
366
+ height=100
367
+ )
368
+
369
+ if st.button("Check Safety", type="primary"):
370
+ if antibiotic:
371
+ medications = [m.strip() for m in current_meds.split("\n") if m.strip()]
372
+ allergy_list = [a.strip() for a in allergies.split("\n") if a.strip()]
373
+
374
+ with st.spinner("Checking interactions..."):
375
+ result = screen_antibiotic_safety(antibiotic, medications, allergy_list)
376
+
377
+ if result.get("safe_to_use"):
378
+ st.success("✅ No critical safety concerns identified")
379
+ else:
380
+ st.error("❌ SAFETY CONCERNS IDENTIFIED")
381
+
382
+ # Show alerts
383
+ if result.get("alerts"):
384
+ st.subheader("Alerts")
385
+ for alert in result["alerts"]:
386
+ level = alert.get("level", "WARNING")
387
+ if level == "CRITICAL":
388
+ st.error(f"🚨 {alert.get('message', '')}")
389
+ else:
390
+ st.warning(f"⚠️ {alert.get('message', '')}")
391
+
392
+ # Show allergy warnings
393
+ if result.get("allergy_warnings"):
394
+ st.subheader("Allergy Warnings")
395
+ for warn in result["allergy_warnings"]:
396
+ st.error(f"🚫 {warn.get('message', '')}")
397
+
398
+ # Show interactions
399
+ if result.get("interactions"):
400
+ st.subheader("Drug Interactions Found")
401
+ for interaction in result["interactions"][:5]:
402
+ severity = interaction.get("severity", "unknown")
403
+ icon = "🔴" if severity == "major" else "🟡" if severity == "moderate" else "🟢"
404
+ st.markdown(f"""
405
+ {icon} **{interaction.get('drug_1')}** ↔ **{interaction.get('drug_2')}**
406
+ - Severity: {severity.upper()}
407
+ - {interaction.get('interaction_description', '')}
408
+ """)
409
+ else:
410
+ st.warning("Please enter an antibiotic name.")
411
+
412
+
413
+ def show_guidelines_search():
414
+ st.header("📚 Clinical Guidelines Search")
415
+ st.markdown("*Search IDSA treatment guidelines*")
416
+
417
+ query = st.text_input(
418
+ "Search Query",
419
+ placeholder="e.g., treatment for ESBL E. coli UTI"
420
+ )
421
+
422
+ pathogen_filter = st.selectbox(
423
+ "Filter by Pathogen Type (optional)",
424
+ ["All", "ESBL-E", "CRE", "CRAB", "DTR-PA", "S.maltophilia", "AmpC-E"]
425
+ )
426
+
427
+ if st.button("Search Guidelines", type="primary"):
428
+ if query:
429
+ with st.spinner("Searching clinical guidelines..."):
430
+ filter_value = None if pathogen_filter == "All" else pathogen_filter
431
+
432
+ results = search_clinical_guidelines(query, pathogen_filter=filter_value, n_results=5)
433
+
434
+ if results:
435
+ st.subheader(f"Found {len(results)} relevant excerpts")
436
+
437
+ for i, result in enumerate(results, 1):
438
+ with st.expander(
439
+ f"Result {i} - {result.get('pathogen_type', 'General')} "
440
+ f"(Relevance: {result.get('relevance_score', 0):.2f})"
441
+ ):
442
+ st.markdown(result.get("content", ""))
443
+ st.caption(f"Source: {result.get('source', 'IDSA Guidelines')}")
444
+ else:
445
+ st.info("No results found. Try a different query or remove the filter.")
446
+ else:
447
+ st.warning("Please enter a search query.")
448
+
449
+
450
+ if __name__ == "__main__":
451
+ main()
docs/KNOWLEDGE_STORAGE_STRATEGY.md ADDED
@@ -0,0 +1,611 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Med-I-C Knowledge Storage Strategy
2
+
3
+ ## Overview
4
+
5
+ This document defines how each document in the `docs/` folder will be stored and queried to support the **AMR-Guard: Infection Lifecycle Orchestrator** workflow.
6
+
7
+ ---
8
+
9
+ ## Document Classification Summary
10
+
11
+ | Document | Type | Storage | Purpose in Workflow |
12
+ |----------|------|---------|---------------------|
13
+ | EML exports (ACCESS/RESERVE/WATCH) | XLSX | **SQLite** | Antibiotic classification & stewardship |
14
+ | ATLAS Susceptibility Data | XLSX | **SQLite** | Pathogen resistance patterns |
15
+ | MIC Breakpoint Tables | XLSX | **SQLite** | Susceptibility interpretation |
16
+ | Drug Interactions | CSV | **SQLite** | Drug safety screening |
17
+ | IDSA Guidance (ciae403.pdf) | PDF | **ChromaDB** | Clinical treatment guidelines |
18
+ | MIC Breakpoint Tables (PDF) | PDF | **ChromaDB** | Reference documentation |
19
+
20
+ ---
21
+
22
+ ## Part 1: Structured Data (SQLite)
23
+
24
+ ### 1.1 EML Antibiotic Classification Tables
25
+
26
+ **Source Files:**
27
+ - `antibiotic_guidelines/EML export ACCESS group.xlsx`
28
+ - `antibiotic_guidelines/EML export RESERVE group.xlsx`
29
+ - `antibiotic_guidelines/EML export WATCH group.xlsx`
30
+
31
+ **Database Table: `eml_antibiotics`**
32
+
33
+ ```sql
34
+ CREATE TABLE eml_antibiotics (
35
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
36
+ medicine_name TEXT NOT NULL,
37
+ who_category TEXT NOT NULL, -- 'ACCESS', 'RESERVE', 'WATCH'
38
+ eml_section TEXT,
39
+ formulations TEXT,
40
+ indication TEXT,
41
+ atc_codes TEXT,
42
+ combined_with TEXT,
43
+ status TEXT,
44
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
45
+ );
46
+
47
+ CREATE INDEX idx_medicine_name ON eml_antibiotics(medicine_name);
48
+ CREATE INDEX idx_who_category ON eml_antibiotics(who_category);
49
+ CREATE INDEX idx_atc_codes ON eml_antibiotics(atc_codes);
50
+ ```
51
+
52
+ **Usage in Workflow:**
53
+ - **Agent 1 (Intake Historian):** Query to identify antibiotic stewardship category
54
+ - **Agent 4 (Clinical Pharmacologist):** Suggest ACCESS antibiotics first, escalate to WATCH/RESERVE only when necessary
55
+
56
+ ---
57
+
58
+ ### 1.2 ATLAS Pathogen Susceptibility Data
59
+
60
+ **Source File:** `pathogen_resistance/ATLAS Susceptibility Data Export.xlsx`
61
+
62
+ **Database Tables:**
63
+
64
+ ```sql
65
+ CREATE TABLE atlas_susceptibility_percent (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ pathogen TEXT NOT NULL,
68
+ antibiotic TEXT NOT NULL,
69
+ region TEXT,
70
+ year INTEGER,
71
+ susceptibility_percent REAL,
72
+ sample_size INTEGER,
73
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
74
+ );
75
+
76
+ CREATE TABLE atlas_susceptibility_absolute (
77
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
78
+ pathogen TEXT NOT NULL,
79
+ antibiotic TEXT NOT NULL,
80
+ region TEXT,
81
+ year INTEGER,
82
+ susceptible_count INTEGER,
83
+ intermediate_count INTEGER,
84
+ resistant_count INTEGER,
85
+ total_isolates INTEGER,
86
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
87
+ );
88
+
89
+ CREATE INDEX idx_pathogen ON atlas_susceptibility_percent(pathogen);
90
+ CREATE INDEX idx_antibiotic ON atlas_susceptibility_percent(antibiotic);
91
+ CREATE INDEX idx_pathogen_abs ON atlas_susceptibility_absolute(pathogen);
92
+ ```
93
+
94
+ **Usage in Workflow:**
95
+ - **Agent 1 (Empirical Phase):** Retrieve local/regional resistance patterns for empirical therapy
96
+ - **Agent 3 (Trend Analyst):** Compare current MIC with population-level trends
97
+
98
+ ---
99
+
100
+ ### 1.3 MIC Breakpoint Tables
101
+
102
+ **Source File:** `mic_breakpoints/v_16.0__BreakpointTables.xlsx`
103
+
104
+ **Database Tables:**
105
+
106
+ ```sql
107
+ CREATE TABLE mic_breakpoints (
108
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
109
+ pathogen_group TEXT NOT NULL, -- e.g., 'Enterobacterales', 'Staphylococcus'
110
+ antibiotic TEXT NOT NULL,
111
+ route TEXT, -- 'IV', 'Oral', 'Topical'
112
+ mic_susceptible REAL, -- S breakpoint (mg/L)
113
+ mic_resistant REAL, -- R breakpoint (mg/L)
114
+ disk_susceptible REAL, -- Zone diameter (mm)
115
+ disk_resistant REAL,
116
+ notes TEXT,
117
+ eucast_version TEXT DEFAULT '16.0',
118
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
119
+ );
120
+
121
+ CREATE TABLE dosage_guidance (
122
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
123
+ antibiotic TEXT NOT NULL,
124
+ standard_dose TEXT,
125
+ high_dose TEXT,
126
+ renal_adjustment TEXT,
127
+ notes TEXT
128
+ );
129
+
130
+ CREATE INDEX idx_bp_pathogen ON mic_breakpoints(pathogen_group);
131
+ CREATE INDEX idx_bp_antibiotic ON mic_breakpoints(antibiotic);
132
+ ```
133
+
134
+ **Usage in Workflow:**
135
+ - **Agent 2 (Vision Specialist):** Validate extracted MIC values against breakpoints
136
+ - **Agent 3 (Trend Analyst):** Interpret S/I/R classification from MIC values
137
+ - **Agent 4 (Clinical Pharmacologist):** Use dosage guidance for prescriptions
138
+
139
+ ---
140
+
141
+ ### 1.4 Drug Interactions Database
142
+
143
+ **Source File:** `drug_safety/db_drug_interactions.csv`
144
+
145
+ **Database Table:**
146
+
147
+ ```sql
148
+ CREATE TABLE drug_interactions (
149
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
150
+ drug_1 TEXT NOT NULL,
151
+ drug_2 TEXT NOT NULL,
152
+ interaction_description TEXT,
153
+ severity TEXT, -- Derived: 'major', 'moderate', 'minor'
154
+ mechanism TEXT, -- Derived from description
155
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
156
+ );
157
+
158
+ CREATE INDEX idx_drug_1 ON drug_interactions(drug_1);
159
+ CREATE INDEX idx_drug_2 ON drug_interactions(drug_2);
160
+ CREATE INDEX idx_severity ON drug_interactions(severity);
161
+
162
+ -- View for bidirectional lookup
163
+ CREATE VIEW drug_interaction_lookup AS
164
+ SELECT drug_1, drug_2, interaction_description, severity FROM drug_interactions
165
+ UNION ALL
166
+ SELECT drug_2, drug_1, interaction_description, severity FROM drug_interactions;
167
+ ```
168
+
169
+ **Usage in Workflow:**
170
+ - **Agent 4 (Clinical Pharmacologist):** Check for interactions with patient's current medications
171
+ - **Safety Alerts:** Flag potential toxicity issues
172
+
173
+ ---
174
+
175
+ ## Part 2: Unstructured Data (ChromaDB)
176
+
177
+ ### 2.1 IDSA Clinical Guidelines
178
+
179
+ **Source File:** `antibiotic_guidelines/ciae403.pdf`
180
+
181
+ **ChromaDB Collection: `idsa_treatment_guidelines`**
182
+
183
+ ```python
184
+ collection_config = {
185
+ "name": "idsa_treatment_guidelines",
186
+ "metadata": {
187
+ "source": "IDSA 2024 Guidance",
188
+ "doi": "10.1093/cid/ciae403",
189
+ "version": "2024"
190
+ },
191
+ "embedding_function": "sentence-transformers/all-MiniLM-L6-v2"
192
+ }
193
+
194
+ # Document chunking strategy
195
+ chunk_config = {
196
+ "chunk_size": 1000,
197
+ "chunk_overlap": 200,
198
+ "separators": ["\n\n", "\n", ". "],
199
+ "metadata_fields": ["section", "pathogen_type", "recommendation_type"]
200
+ }
201
+ ```
202
+
203
+ **Metadata Schema per Chunk:**
204
+ ```python
205
+ {
206
+ "section": "Treatment Recommendations",
207
+ "pathogen_type": "ESBL-E | CRE | CRAB | DTR-PA | S.maltophilia",
208
+ "recommendation_strength": "Strong | Conditional",
209
+ "evidence_quality": "High | Moderate | Low",
210
+ "page_number": int
211
+ }
212
+ ```
213
+
214
+ **Usage in Workflow:**
215
+ - **Agent 1 (Empirical Phase):** Retrieve treatment recommendations for suspected pathogens
216
+ - **Agent 4 (Clinical Pharmacologist):** Provide evidence-based justification for antibiotic selection
217
+
218
+ ---
219
+
220
+ ### 2.2 MIC Breakpoint Reference (PDF)
221
+
222
+ **Source File:** `mic_breakpoints/v_16.0_Breakpoint_Tables.pdf`
223
+
224
+ **ChromaDB Collection: `mic_reference_docs`**
225
+
226
+ ```python
227
+ collection_config = {
228
+ "name": "mic_reference_docs",
229
+ "metadata": {
230
+ "source": "EUCAST Breakpoint Tables",
231
+ "version": "16.0"
232
+ },
233
+ "embedding_function": "sentence-transformers/all-MiniLM-L6-v2"
234
+ }
235
+ ```
236
+
237
+ **Usage in Workflow:**
238
+ - **Supplementary Context:** Provide detailed explanations for breakpoint interpretations
239
+ - **Edge Cases:** Handle unusual pathogens or antibiotic combinations not in structured tables
240
+
241
+ ---
242
+
243
+ ## Part 3: Query Tools Definition
244
+
245
+ ### Tool 1: `query_antibiotic_info`
246
+
247
+ **Purpose:** Retrieve antibiotic classification and formulation details
248
+
249
+ ```python
250
+ def query_antibiotic_info(
251
+ antibiotic_name: str,
252
+ include_category: bool = True,
253
+ include_formulations: bool = True
254
+ ) -> dict:
255
+ """
256
+ Query EML antibiotic database for classification and details.
257
+
258
+ Args:
259
+ antibiotic_name: Name of the antibiotic (partial match supported)
260
+ include_category: Include WHO stewardship category
261
+ include_formulations: Include available formulations
262
+
263
+ Returns:
264
+ dict with antibiotic details, category, indications
265
+
266
+ Used by: Agent 1, Agent 4
267
+ """
268
+ ```
269
+
270
+ **SQL Query:**
271
+ ```sql
272
+ SELECT medicine_name, who_category, formulations, indication, combined_with
273
+ FROM eml_antibiotics
274
+ WHERE LOWER(medicine_name) LIKE LOWER(?)
275
+ ORDER BY who_category; -- ACCESS first, then WATCH, then RESERVE
276
+ ```
277
+
278
+ ---
279
+
280
+ ### Tool 2: `query_resistance_pattern`
281
+
282
+ **Purpose:** Get susceptibility data for pathogen-antibiotic combinations
283
+
284
+ ```python
285
+ def query_resistance_pattern(
286
+ pathogen: str,
287
+ antibiotic: str = None,
288
+ region: str = None,
289
+ year: int = None
290
+ ) -> dict:
291
+ """
292
+ Query ATLAS susceptibility data for resistance patterns.
293
+
294
+ Args:
295
+ pathogen: Pathogen name (e.g., "E. coli", "K. pneumoniae")
296
+ antibiotic: Optional specific antibiotic to check
297
+ region: Optional geographic region filter
298
+ year: Optional year filter (defaults to most recent)
299
+
300
+ Returns:
301
+ dict with susceptibility percentages and trends
302
+
303
+ Used by: Agent 1 (Empirical), Agent 3 (Trend Analysis)
304
+ """
305
+ ```
306
+
307
+ **SQL Query:**
308
+ ```sql
309
+ SELECT antibiotic, susceptibility_percent, sample_size, year
310
+ FROM atlas_susceptibility_percent
311
+ WHERE LOWER(pathogen) LIKE LOWER(?)
312
+ AND (antibiotic = ? OR ? IS NULL)
313
+ AND (region = ? OR ? IS NULL)
314
+ ORDER BY year DESC, susceptibility_percent DESC;
315
+ ```
316
+
317
+ ---
318
+
319
+ ### Tool 3: `interpret_mic_value`
320
+
321
+ **Purpose:** Classify MIC as S/I/R based on EUCAST breakpoints
322
+
323
+ ```python
324
+ def interpret_mic_value(
325
+ pathogen: str,
326
+ antibiotic: str,
327
+ mic_value: float,
328
+ route: str = "IV"
329
+ ) -> dict:
330
+ """
331
+ Interpret MIC value against EUCAST breakpoints.
332
+
333
+ Args:
334
+ pathogen: Pathogen name or group
335
+ antibiotic: Antibiotic name
336
+ mic_value: MIC value in mg/L
337
+ route: Administration route (IV, Oral)
338
+
339
+ Returns:
340
+ dict with interpretation (S/I/R), breakpoint values, dosing notes
341
+
342
+ Used by: Agent 2, Agent 3
343
+ """
344
+ ```
345
+
346
+ **SQL Query:**
347
+ ```sql
348
+ SELECT mic_susceptible, mic_resistant, notes
349
+ FROM mic_breakpoints
350
+ WHERE LOWER(pathogen_group) LIKE LOWER(?)
351
+ AND LOWER(antibiotic) LIKE LOWER(?)
352
+ AND (route = ? OR route IS NULL);
353
+ ```
354
+
355
+ **Interpretation Logic:**
356
+ ```python
357
+ if mic_value <= mic_susceptible:
358
+ return "Susceptible"
359
+ elif mic_value > mic_resistant:
360
+ return "Resistant"
361
+ else:
362
+ return "Intermediate (Susceptible, Increased Exposure)"
363
+ ```
364
+
365
+ ---
366
+
367
+ ### Tool 4: `check_drug_interactions`
368
+
369
+ **Purpose:** Screen for drug-drug interactions
370
+
371
+ ```python
372
+ def check_drug_interactions(
373
+ target_drug: str,
374
+ patient_medications: list[str],
375
+ severity_filter: str = None
376
+ ) -> list[dict]:
377
+ """
378
+ Check for interactions between target drug and patient's medications.
379
+
380
+ Args:
381
+ target_drug: Antibiotic being considered
382
+ patient_medications: List of patient's current medications
383
+ severity_filter: Optional filter ('major', 'moderate', 'minor')
384
+
385
+ Returns:
386
+ list of interaction dicts with severity and description
387
+
388
+ Used by: Agent 4 (Safety Check)
389
+ """
390
+ ```
391
+
392
+ **SQL Query:**
393
+ ```sql
394
+ SELECT drug_1, drug_2, interaction_description, severity
395
+ FROM drug_interaction_lookup
396
+ WHERE LOWER(drug_1) LIKE LOWER(?)
397
+ AND LOWER(drug_2) IN (SELECT LOWER(value) FROM json_each(?))
398
+ AND (severity = ? OR ? IS NULL)
399
+ ORDER BY severity DESC;
400
+ ```
401
+
402
+ ---
403
+
404
+ ### Tool 5: `search_clinical_guidelines`
405
+
406
+ **Purpose:** RAG search over IDSA guidelines for treatment recommendations
407
+
408
+ ```python
409
+ def search_clinical_guidelines(
410
+ query: str,
411
+ pathogen_filter: str = None,
412
+ n_results: int = 5
413
+ ) -> list[dict]:
414
+ """
415
+ Semantic search over IDSA clinical guidelines.
416
+
417
+ Args:
418
+ query: Natural language query about treatment
419
+ pathogen_filter: Optional pathogen type filter
420
+ n_results: Number of results to return
421
+
422
+ Returns:
423
+ list of relevant guideline excerpts with metadata
424
+
425
+ Used by: Agent 1 (Empirical), Agent 4 (Justification)
426
+ """
427
+ ```
428
+
429
+ **ChromaDB Query:**
430
+ ```python
431
+ results = collection.query(
432
+ query_texts=[query],
433
+ n_results=n_results,
434
+ where={"pathogen_type": pathogen_filter} if pathogen_filter else None,
435
+ include=["documents", "metadatas", "distances"]
436
+ )
437
+ ```
438
+
439
+ ---
440
+
441
+ ### Tool 6: `calculate_mic_trend`
442
+
443
+ **Purpose:** Analyze MIC creep over time
444
+
445
+ ```python
446
+ def calculate_mic_trend(
447
+ patient_id: str,
448
+ pathogen: str,
449
+ antibiotic: str,
450
+ historical_mics: list[dict] # [{date, mic_value}, ...]
451
+ ) -> dict:
452
+ """
453
+ Calculate resistance velocity and MIC trend.
454
+
455
+ Args:
456
+ patient_id: Patient identifier
457
+ pathogen: Identified pathogen
458
+ antibiotic: Target antibiotic
459
+ historical_mics: List of historical MIC readings
460
+
461
+ Returns:
462
+ dict with trend analysis, resistance_velocity, risk_level
463
+
464
+ Used by: Agent 3 (Trend Analyst)
465
+ """
466
+ ```
467
+
468
+ **Logic:**
469
+ ```python
470
+ # Calculate resistance velocity
471
+ if len(historical_mics) >= 2:
472
+ baseline_mic = historical_mics[0]["mic_value"]
473
+ current_mic = historical_mics[-1]["mic_value"]
474
+
475
+ ratio = current_mic / baseline_mic
476
+
477
+ if ratio >= 4: # Two-step dilution increase
478
+ risk_level = "HIGH"
479
+ alert = "MIC Creep Detected - Risk of Treatment Failure"
480
+ elif ratio >= 2:
481
+ risk_level = "MODERATE"
482
+ alert = "MIC Trending Upward - Monitor Closely"
483
+ else:
484
+ risk_level = "LOW"
485
+ alert = None
486
+ ```
487
+
488
+ ---
489
+
490
+ ## Part 4: Workflow Integration
491
+
492
+ ### Stage 1: Empirical Phase (Before Lab Results)
493
+
494
+ ```
495
+ Input: Patient history, symptoms, infection site
496
+
497
+
498
+ ┌─────────────────────────────────────────────────────────┐
499
+ │ Agent 1: Intake Historian (MedGemma 1.5) │
500
+ │ ├── Tool: search_clinical_guidelines() │
501
+ │ │ └── ChromaDB: idsa_treatment_guidelines │
502
+ │ ├── Tool: query_resistance_pattern() │
503
+ │ │ └── SQLite: atlas_susceptibility_percent │
504
+ │ └── Tool: query_antibiotic_info() │
505
+ │ └── SQLite: eml_antibiotics │
506
+ └─────────────────────────────────────────────────────────┘
507
+
508
+
509
+ ┌─────────────────────────────────────────────────────────┐
510
+ │ Agent 4: Clinical Pharmacologist (TxGemma) │
511
+ │ ├── Tool: check_drug_interactions() │
512
+ │ │ └── SQLite: drug_interactions │
513
+ │ └── Tool: query_antibiotic_info() [dosing] │
514
+ │ └── SQLite: eml_antibiotics + dosage_guidance │
515
+ └─────────────────────────────────────────────────────────┘
516
+
517
+
518
+ Output: Empirical therapy recommendation with safety check
519
+ ```
520
+
521
+ ### Stage 2: Targeted Phase (After Lab Results)
522
+
523
+ ```
524
+ Input: Lab report (antibiogram image/PDF)
525
+
526
+
527
+ ┌─────────────────────────────────────────────────────────┐
528
+ │ Agent 2: Vision Specialist (MedGemma 4B) │
529
+ │ ├── Extract: Pathogen name, MIC values │
530
+ │ └── Tool: interpret_mic_value() │
531
+ │ └── SQLite: mic_breakpoints │
532
+ └─────────────────────────────────────────────────────────┘
533
+
534
+
535
+ ┌─────────────────────────────────────────────────────────┐
536
+ │ Agent 3: Trend Analyst (MedGemma 27B) │
537
+ │ ├── Tool: calculate_mic_trend() │
538
+ │ │ └── Patient historical data + current MIC │
539
+ │ └── Tool: query_resistance_pattern() │
540
+ │ └── SQLite: atlas_susceptibility (population data) │
541
+ └─────────────────────────────────────────────────────────┘
542
+
543
+
544
+ ┌─────────────────────────────────────────────────────────┐
545
+ │ Agent 4: Clinical Pharmacologist (TxGemma) │
546
+ │ ├── Tool: search_clinical_guidelines() │
547
+ │ │ └── ChromaDB: idsa_treatment_guidelines │
548
+ │ ├── Tool: check_drug_interactions() │
549
+ │ │ └── SQLite: drug_interactions │
550
+ │ └── Generate: Final prescription with justification │
551
+ └─────────────────────────────────────────────────────────┘
552
+
553
+
554
+ Output: Targeted therapy with MIC trend analysis & safety alerts
555
+ ```
556
+
557
+ ---
558
+
559
+ ## Part 5: Implementation Checklist
560
+
561
+ ### SQLite Setup
562
+ - [ ] Create database schema with all tables
563
+ - [ ] Import EML Excel files (ACCESS, RESERVE, WATCH)
564
+ - [ ] Import ATLAS susceptibility data (both sheets)
565
+ - [ ] Import MIC breakpoint tables (41 sheets)
566
+ - [ ] Import drug interactions CSV
567
+ - [ ] Add severity classification to interactions
568
+ - [ ] Create indexes for efficient queries
569
+
570
+ ### ChromaDB Setup
571
+ - [ ] Initialize ChromaDB persistent storage
572
+ - [ ] Process ciae403.pdf with chunking strategy
573
+ - [ ] Process MIC breakpoint PDF
574
+ - [ ] Add metadata to all chunks
575
+ - [ ] Test semantic search queries
576
+
577
+ ### Tool Implementation
578
+ - [ ] Implement `query_antibiotic_info()`
579
+ - [ ] Implement `query_resistance_pattern()`
580
+ - [ ] Implement `interpret_mic_value()`
581
+ - [ ] Implement `check_drug_interactions()`
582
+ - [ ] Implement `search_clinical_guidelines()`
583
+ - [ ] Implement `calculate_mic_trend()`
584
+ - [ ] Create unified tool interface for LangGraph
585
+
586
+ ---
587
+
588
+ ## File Structure
589
+
590
+ ```
591
+ Med-I-C/
592
+ ├── docs/ # Source documents
593
+ ├── data/
594
+ │ ├── medic.db # SQLite database
595
+ │ └── chroma/ # ChromaDB persistent storage
596
+ ├── src/
597
+ │ ├── db/
598
+ │ │ ├── schema.sql # Database schema
599
+ │ │ └── import_data.py # Data import scripts
600
+ │ ├── tools/
601
+ │ │ ├── antibiotic_tools.py # query_antibiotic_info, interpret_mic
602
+ │ │ ├── resistance_tools.py # query_resistance_pattern, calculate_mic_trend
603
+ │ │ ├── safety_tools.py # check_drug_interactions
604
+ │ │ └── rag_tools.py # search_clinical_guidelines
605
+ │ └── agents/
606
+ │ ├── intake_historian.py # Agent 1
607
+ │ ├── vision_specialist.py # Agent 2
608
+ │ ├── trend_analyst.py # Agent 3
609
+ │ └── clinical_pharmacologist.py # Agent 4
610
+ └── KNOWLEDGE_STORAGE_STRATEGY.md # This document
611
+ ```
pyproject.toml CHANGED
@@ -1,7 +1,7 @@
1
  [project]
2
  name = "Med-I-C"
3
  version = "0.1.0"
4
- description = "Add your description here"
5
  readme = "README.md"
6
  requires-python = ">=3.10"
7
  dependencies = [
@@ -25,4 +25,5 @@ dependencies = [
25
  "pypdf",
26
  "langchain-community>=0.4.1",
27
  "jq>=1.11.0",
 
28
  ]
 
1
  [project]
2
  name = "Med-I-C"
3
  version = "0.1.0"
4
+ description = "AMR-Guard: Infection Lifecycle Orchestrator Demo"
5
  readme = "README.md"
6
  requires-python = ">=3.10"
7
  dependencies = [
 
25
  "pypdf",
26
  "langchain-community>=0.4.1",
27
  "jq>=1.11.0",
28
+ "pandas>=2.0.0",
29
  ]
setup_demo.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Setup script for Med-I-C Demo
4
+ Initializes the database and imports all data.
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ # Add project root to path
11
+ PROJECT_ROOT = Path(__file__).parent
12
+ sys.path.insert(0, str(PROJECT_ROOT))
13
+
14
+
15
+ def main():
16
+ print("=" * 60)
17
+ print("Med-I-C Demo Setup")
18
+ print("AMR-Guard: Infection Lifecycle Orchestrator")
19
+ print("=" * 60)
20
+ print()
21
+
22
+ # Step 1: Import structured data into SQLite
23
+ print("Step 1: Importing structured data into SQLite...")
24
+ print("-" * 40)
25
+
26
+ from src.db.import_data import import_all_data
27
+
28
+ # Limit interactions to 50k for faster demo setup
29
+ structured_results = import_all_data(interactions_limit=50000)
30
+
31
+ # Step 2: Import PDFs into ChromaDB
32
+ print("\nStep 2: Importing PDFs into ChromaDB (Vector Store)...")
33
+ print("-" * 40)
34
+
35
+ from src.db.vector_store import import_all_vectors
36
+
37
+ vector_results = import_all_vectors()
38
+
39
+ # Summary
40
+ print("\n" + "=" * 60)
41
+ print("Setup Complete!")
42
+ print("=" * 60)
43
+ print("\nData imported:")
44
+ print(f" - EML Antibiotics: {structured_results.get('eml_antibiotics', 0)} records")
45
+ print(f" - ATLAS Susceptibility: {structured_results.get('atlas_susceptibility', 0)} records")
46
+ print(f" - MIC Breakpoints: {structured_results.get('mic_breakpoints', 0)} records")
47
+ print(f" - Drug Interactions: {structured_results.get('drug_interactions', 0)} records")
48
+ print(f" - IDSA Guidelines: {vector_results.get('idsa_guidelines', 0)} chunks")
49
+ print(f" - MIC Reference: {vector_results.get('mic_reference', 0)} chunks")
50
+
51
+ print("\nTo run the demo app:")
52
+ print(" uv run streamlit run app.py")
53
+ print()
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
src/__init__.py CHANGED
@@ -0,0 +1 @@
 
 
1
+ """Med-I-C: AMR-Guard - Infection Lifecycle Orchestrator."""
src/agents.py CHANGED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from langchain.agents import create_agent
3
+ from langchain.chat_models import init_chat_model
4
+ from dotenv import load_dotenv
5
+
6
+ os.environ["GOOGLE_API_KEY"] = load_dotenv().get("GOOGLE_API_KEY")
7
+
8
+ model = init_chat_model(
9
+ "google_genai:gemini-2.5-flash-lite",
10
+ # Kwargs passed to the model:
11
+ temperature=0.7,
12
+ timeout=30,
13
+ max_tokens=1000,
14
+ )
15
+
16
+ Intake_Historian = create_agent(model=model, tools=["google_search"], verbose=True)
src/agents/__init__.py ADDED
File without changes
src/db/__init__.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Database modules for Med-I-C."""
2
+
3
+ from .database import (
4
+ init_database,
5
+ get_connection,
6
+ execute_query,
7
+ execute_insert,
8
+ execute_many,
9
+ DB_PATH,
10
+ DATA_DIR,
11
+ DOCS_DIR,
12
+ )
13
+
14
+ from .vector_store import (
15
+ get_chroma_client,
16
+ search_guidelines,
17
+ search_mic_reference,
18
+ import_all_vectors,
19
+ )
20
+
21
+ __all__ = [
22
+ "init_database",
23
+ "get_connection",
24
+ "execute_query",
25
+ "execute_insert",
26
+ "execute_many",
27
+ "DB_PATH",
28
+ "DATA_DIR",
29
+ "DOCS_DIR",
30
+ "get_chroma_client",
31
+ "search_guidelines",
32
+ "search_mic_reference",
33
+ "import_all_vectors",
34
+ ]
src/db/database.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Database connection and initialization for Med-I-C."""
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+ from contextlib import contextmanager
6
+
7
+ # Project paths
8
+ PROJECT_ROOT = Path(__file__).parent.parent.parent
9
+ DATA_DIR = PROJECT_ROOT / "data"
10
+ DOCS_DIR = PROJECT_ROOT / "docs"
11
+ DB_PATH = DATA_DIR / "medic.db"
12
+ SCHEMA_PATH = Path(__file__).parent / "schema.sql"
13
+
14
+
15
+ def init_database() -> None:
16
+ """Initialize the database with schema."""
17
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
18
+
19
+ with get_connection() as conn:
20
+ with open(SCHEMA_PATH, 'r') as f:
21
+ conn.executescript(f.read())
22
+ conn.commit()
23
+ print(f"Database initialized at {DB_PATH}")
24
+
25
+
26
+ @contextmanager
27
+ def get_connection():
28
+ """Context manager for database connections."""
29
+ conn = sqlite3.connect(DB_PATH)
30
+ conn.row_factory = sqlite3.Row
31
+ try:
32
+ yield conn
33
+ finally:
34
+ conn.close()
35
+
36
+
37
+ def execute_query(query: str, params: tuple = ()) -> list[dict]:
38
+ """Execute a query and return results as list of dicts."""
39
+ with get_connection() as conn:
40
+ cursor = conn.execute(query, params)
41
+ columns = [description[0] for description in cursor.description]
42
+ return [dict(zip(columns, row)) for row in cursor.fetchall()]
43
+
44
+
45
+ def execute_insert(query: str, params: tuple = ()) -> int:
46
+ """Execute an insert and return the last row id."""
47
+ with get_connection() as conn:
48
+ cursor = conn.execute(query, params)
49
+ conn.commit()
50
+ return cursor.lastrowid
51
+
52
+
53
+ def execute_many(query: str, params_list: list[tuple]) -> None:
54
+ """Execute many inserts."""
55
+ with get_connection() as conn:
56
+ conn.executemany(query, params_list)
57
+ conn.commit()
58
+
59
+
60
+ if __name__ == "__main__":
61
+ init_database()
src/db/import_data.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Data import scripts for Med-I-C structured documents."""
2
+
3
+ import pandas as pd
4
+ import re
5
+ from pathlib import Path
6
+ from .database import (
7
+ get_connection, init_database, execute_many,
8
+ DOCS_DIR, DB_PATH
9
+ )
10
+
11
+
12
+ def safe_float(value):
13
+ """Safely convert a value to float, returning None on failure."""
14
+ if pd.isna(value):
15
+ return None
16
+ try:
17
+ return float(value)
18
+ except (ValueError, TypeError):
19
+ return None
20
+
21
+
22
+ def safe_int(value):
23
+ """Safely convert a value to int, returning None on failure."""
24
+ if pd.isna(value):
25
+ return None
26
+ try:
27
+ return int(float(value))
28
+ except (ValueError, TypeError):
29
+ return None
30
+
31
+
32
+ def classify_severity(description: str) -> str:
33
+ """Classify drug interaction severity based on description keywords."""
34
+ if not description:
35
+ return "unknown"
36
+
37
+ desc_lower = description.lower()
38
+
39
+ # Major severity indicators
40
+ major_keywords = [
41
+ "cardiotoxic", "nephrotoxic", "hepatotoxic", "neurotoxic",
42
+ "fatal", "death", "severe", "contraindicated", "arrhythmia",
43
+ "qt prolongation", "seizure", "bleeding", "hemorrhage",
44
+ "serotonin syndrome", "neuroleptic malignant"
45
+ ]
46
+
47
+ # Moderate severity indicators
48
+ moderate_keywords = [
49
+ "increase", "decrease", "reduce", "enhance", "inhibit",
50
+ "metabolism", "concentration", "absorption", "excretion",
51
+ "therapeutic effect", "adverse effect", "toxicity"
52
+ ]
53
+
54
+ for keyword in major_keywords:
55
+ if keyword in desc_lower:
56
+ return "major"
57
+
58
+ for keyword in moderate_keywords:
59
+ if keyword in desc_lower:
60
+ return "moderate"
61
+
62
+ return "minor"
63
+
64
+
65
+ def import_eml_antibiotics() -> int:
66
+ """Import WHO EML antibiotic classification data."""
67
+ print("Importing EML antibiotic data...")
68
+
69
+ eml_files = {
70
+ "ACCESS": DOCS_DIR / "antibiotic_guidelines" / "EML export-ACCESS group.xlsx",
71
+ "RESERVE": DOCS_DIR / "antibiotic_guidelines" / "EML export-RESERVE group.xlsx",
72
+ "WATCH": DOCS_DIR / "antibiotic_guidelines" / "EML export-WATCH group.xlsx",
73
+ }
74
+
75
+ records = []
76
+ for category, filepath in eml_files.items():
77
+ if not filepath.exists():
78
+ print(f" Warning: {filepath} not found, skipping...")
79
+ continue
80
+
81
+ try:
82
+ # Use openpyxl directly with read_only=True for faster loading
83
+ import openpyxl
84
+ wb = openpyxl.load_workbook(filepath, read_only=True)
85
+ ws = wb.active
86
+
87
+ # Get headers from first row
88
+ headers = []
89
+ for cell in ws[1]:
90
+ headers.append(str(cell.value).strip().lower().replace(' ', '_') if cell.value else f'col_{len(headers)}')
91
+
92
+ # Process data rows
93
+ for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
94
+ row_dict = dict(zip(headers, row))
95
+
96
+ medicine = str(row_dict.get('medicine_name', row_dict.get('medicine', '')))
97
+ if not medicine or medicine == 'None' or medicine == 'nan':
98
+ continue
99
+
100
+ def safe_str(val):
101
+ if val is None or pd.isna(val):
102
+ return ''
103
+ return str(val)
104
+
105
+ records.append((
106
+ medicine,
107
+ category,
108
+ safe_str(row_dict.get('eml_section', '')),
109
+ safe_str(row_dict.get('formulations', '')),
110
+ safe_str(row_dict.get('indication', '')),
111
+ safe_str(row_dict.get('atc_codes', row_dict.get('atc_code', ''))),
112
+ safe_str(row_dict.get('combined_with', '')),
113
+ safe_str(row_dict.get('status', '')),
114
+ ))
115
+
116
+ wb.close()
117
+ print(f" Loaded {len([r for r in records if r[1] == category])} from {category}")
118
+
119
+ except Exception as e:
120
+ print(f" Warning: Error reading {filepath}: {e}")
121
+ continue
122
+
123
+ if records:
124
+ query = """
125
+ INSERT INTO eml_antibiotics
126
+ (medicine_name, who_category, eml_section, formulations,
127
+ indication, atc_codes, combined_with, status)
128
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
129
+ """
130
+ execute_many(query, records)
131
+ print(f" Imported {len(records)} EML antibiotic records total")
132
+
133
+ return len(records)
134
+
135
+
136
+ def import_atlas_susceptibility() -> int:
137
+ """Import ATLAS antimicrobial susceptibility data."""
138
+ print("Importing ATLAS susceptibility data...")
139
+
140
+ filepath = DOCS_DIR / "pathogen_resistance" / "ATLAS Susceptibility Data Export.xlsx"
141
+
142
+ if not filepath.exists():
143
+ print(f" Warning: {filepath} not found, skipping...")
144
+ return 0
145
+
146
+ # Read the raw data to find the header row and extract region
147
+ df_raw = pd.read_excel(filepath, sheet_name="Percent", header=None)
148
+
149
+ # Extract region from the title (row 1)
150
+ region = "Unknown"
151
+ for idx, row in df_raw.head(5).iterrows():
152
+ cell = str(row.iloc[0]) if pd.notna(row.iloc[0]) else ""
153
+ if "from" in cell.lower():
154
+ # Extract country from "Percentage Susceptibility from Argentina"
155
+ parts = cell.split("from")
156
+ if len(parts) > 1:
157
+ region = parts[1].strip()
158
+ break
159
+
160
+ # Find the header row (contains 'Antibacterial' or 'N')
161
+ header_row = 4 # Default
162
+ for idx, row in df_raw.head(10).iterrows():
163
+ if any('Antibacterial' in str(v) for v in row.values if pd.notna(v)):
164
+ header_row = idx
165
+ break
166
+
167
+ # Read with proper header
168
+ df = pd.read_excel(filepath, sheet_name="Percent", header=header_row)
169
+
170
+ # Standardize column names
171
+ df.columns = [str(col).strip().lower().replace(' ', '_').replace('.', '') for col in df.columns]
172
+
173
+ records = []
174
+ for _, row in df.iterrows():
175
+ antibiotic = str(row.get('antibacterial', ''))
176
+
177
+ # Skip empty or non-antibiotic rows
178
+ if not antibiotic or antibiotic == 'nan' or 'omitted' in antibiotic.lower():
179
+ continue
180
+ if 'in vitro' in antibiotic.lower() or 'table cells' in antibiotic.lower():
181
+ continue
182
+
183
+ # Get susceptibility values
184
+ n_value = row.get('n', None)
185
+ pct_s = row.get('susc', row.get('susceptible', None))
186
+ pct_i = row.get('int', row.get('intermediate', None))
187
+ pct_r = row.get('res', row.get('resistant', None))
188
+
189
+ # Use safe conversion functions
190
+ n_int = safe_int(n_value)
191
+ s_float = safe_float(pct_s)
192
+
193
+ if n_int is not None and s_float is not None:
194
+ records.append((
195
+ "General", # Species - will be refined if more data available
196
+ "", # Family
197
+ antibiotic,
198
+ s_float,
199
+ safe_float(pct_i),
200
+ safe_float(pct_r),
201
+ n_int,
202
+ 2024, # Year - from the data context
203
+ region,
204
+ "ATLAS"
205
+ ))
206
+
207
+ if records:
208
+ query = """
209
+ INSERT INTO atlas_susceptibility
210
+ (species, family, antibiotic, percent_susceptible,
211
+ percent_intermediate, percent_resistant, total_isolates,
212
+ year, region, source)
213
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
214
+ """
215
+ execute_many(query, records)
216
+ print(f" Imported {len(records)} ATLAS susceptibility records from {region}")
217
+
218
+ return len(records)
219
+
220
+
221
+ def import_mic_breakpoints() -> int:
222
+ """Import EUCAST MIC breakpoint tables."""
223
+ print("Importing MIC breakpoint data...")
224
+
225
+ filepath = DOCS_DIR / "mic_breakpoints" / "v_16.0__BreakpointTables.xlsx"
226
+
227
+ if not filepath.exists():
228
+ print(f" Warning: {filepath} not found, skipping...")
229
+ return 0
230
+
231
+ # Get all sheet names
232
+ xl = pd.ExcelFile(filepath)
233
+
234
+ # Skip non-pathogen sheets
235
+ skip_sheets = {'Content', 'Changes', 'Notes', 'Guidance', 'Dosages',
236
+ 'Technical uncertainty', 'PK PD breakpoints', 'PK PD cutoffs'}
237
+
238
+ records = []
239
+ for sheet_name in xl.sheet_names:
240
+ if sheet_name in skip_sheets:
241
+ continue
242
+
243
+ try:
244
+ df = pd.read_excel(filepath, sheet_name=sheet_name, header=None)
245
+
246
+ # Try to find antibiotic data - look for rows with MIC values
247
+ pathogen_group = sheet_name
248
+
249
+ # Simple heuristic: look for rows that might contain antibiotic names and MIC values
250
+ for idx, row in df.iterrows():
251
+ row_values = [str(v).strip() for v in row.values if pd.notna(v)]
252
+
253
+ # Look for rows that might be antibiotic entries
254
+ if len(row_values) >= 2:
255
+ potential_antibiotic = row_values[0]
256
+
257
+ # Skip header-like rows
258
+ if any(kw in potential_antibiotic.lower() for kw in
259
+ ['antibiotic', 'agent', 'note', 'disk', 'mic', 'breakpoint']):
260
+ continue
261
+
262
+ # Try to extract MIC values (numbers)
263
+ mic_values = []
264
+ for v in row_values[1:]:
265
+ try:
266
+ mic_values.append(float(v.replace('≤', '').replace('>', '').replace('<', '').strip()))
267
+ except (ValueError, AttributeError):
268
+ pass
269
+
270
+ if len(mic_values) >= 2 and len(potential_antibiotic) > 2:
271
+ records.append((
272
+ pathogen_group,
273
+ potential_antibiotic,
274
+ None, # route
275
+ mic_values[0] if len(mic_values) > 0 else None, # S breakpoint
276
+ mic_values[1] if len(mic_values) > 1 else None, # R breakpoint
277
+ None, # disk S
278
+ None, # disk R
279
+ None, # notes
280
+ "16.0"
281
+ ))
282
+ except Exception as e:
283
+ print(f" Warning: Could not parse sheet '{sheet_name}': {e}")
284
+ continue
285
+
286
+ if records:
287
+ query = """
288
+ INSERT INTO mic_breakpoints
289
+ (pathogen_group, antibiotic, route, mic_susceptible, mic_resistant,
290
+ disk_susceptible, disk_resistant, notes, eucast_version)
291
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
292
+ """
293
+ execute_many(query, records)
294
+ print(f" Imported {len(records)} MIC breakpoint records")
295
+
296
+ return len(records)
297
+
298
+
299
+ def import_drug_interactions(limit: int = None) -> int:
300
+ """Import drug-drug interaction database."""
301
+ print("Importing drug interactions data...")
302
+
303
+ filepath = DOCS_DIR / "drug_safety" / "db_drug_interactions.csv"
304
+
305
+ if not filepath.exists():
306
+ print(f" Warning: {filepath} not found, skipping...")
307
+ return 0
308
+
309
+ # Read CSV in chunks due to large size
310
+ chunk_size = 10000
311
+ total_records = 0
312
+
313
+ for chunk in pd.read_csv(filepath, chunksize=chunk_size):
314
+ # Standardize column names
315
+ chunk.columns = [col.strip().lower().replace(' ', '_') for col in chunk.columns]
316
+
317
+ records = []
318
+ for _, row in chunk.iterrows():
319
+ drug_1 = str(row.get('drug_1', row.get('drug1', row.iloc[0] if len(row) > 0 else '')))
320
+ drug_2 = str(row.get('drug_2', row.get('drug2', row.iloc[1] if len(row) > 1 else '')))
321
+ description = str(row.get('interaction_description', row.get('description',
322
+ row.get('interaction', row.iloc[2] if len(row) > 2 else ''))))
323
+
324
+ severity = classify_severity(description)
325
+
326
+ if drug_1 and drug_2:
327
+ records.append((drug_1, drug_2, description, severity))
328
+
329
+ if records:
330
+ query = """
331
+ INSERT INTO drug_interactions
332
+ (drug_1, drug_2, interaction_description, severity)
333
+ VALUES (?, ?, ?, ?)
334
+ """
335
+ execute_many(query, records)
336
+ total_records += len(records)
337
+
338
+ if limit and total_records >= limit:
339
+ break
340
+
341
+ print(f" Imported {total_records} drug interaction records")
342
+ return total_records
343
+
344
+
345
+ def import_all_data(interactions_limit: int = None) -> dict:
346
+ """Import all structured data into the database."""
347
+ print(f"\n{'='*50}")
348
+ print("Med-I-C Data Import")
349
+ print(f"{'='*50}\n")
350
+
351
+ # Initialize database
352
+ init_database()
353
+
354
+ # Clear existing data
355
+ with get_connection() as conn:
356
+ conn.execute("DELETE FROM eml_antibiotics")
357
+ conn.execute("DELETE FROM atlas_susceptibility")
358
+ conn.execute("DELETE FROM mic_breakpoints")
359
+ conn.execute("DELETE FROM drug_interactions")
360
+ conn.commit()
361
+ print("Cleared existing data\n")
362
+
363
+ # Import all data
364
+ results = {
365
+ "eml_antibiotics": import_eml_antibiotics(),
366
+ "atlas_susceptibility": import_atlas_susceptibility(),
367
+ "mic_breakpoints": import_mic_breakpoints(),
368
+ "drug_interactions": import_drug_interactions(limit=interactions_limit),
369
+ }
370
+
371
+ print(f"\n{'='*50}")
372
+ print("Import Summary:")
373
+ for table, count in results.items():
374
+ print(f" {table}: {count} records")
375
+ print(f"{'='*50}\n")
376
+
377
+ return results
378
+
379
+
380
+ if __name__ == "__main__":
381
+ # Import with a limit on interactions for faster demo
382
+ import_all_data(interactions_limit=50000)
src/db/schema.sql ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Med-I-C Database Schema
2
+ -- AMR-Guard: Infection Lifecycle Orchestrator
3
+
4
+ -- EML Antibiotic Classification Table
5
+ CREATE TABLE IF NOT EXISTS eml_antibiotics (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ medicine_name TEXT NOT NULL,
8
+ who_category TEXT NOT NULL, -- 'ACCESS', 'RESERVE', 'WATCH'
9
+ eml_section TEXT,
10
+ formulations TEXT,
11
+ indication TEXT,
12
+ atc_codes TEXT,
13
+ combined_with TEXT,
14
+ status TEXT,
15
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
16
+ );
17
+
18
+ CREATE INDEX IF NOT EXISTS idx_eml_medicine_name ON eml_antibiotics(medicine_name);
19
+ CREATE INDEX IF NOT EXISTS idx_eml_who_category ON eml_antibiotics(who_category);
20
+ CREATE INDEX IF NOT EXISTS idx_eml_atc_codes ON eml_antibiotics(atc_codes);
21
+
22
+ -- ATLAS Susceptibility Data (Percent)
23
+ CREATE TABLE IF NOT EXISTS atlas_susceptibility (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ species TEXT,
26
+ family TEXT,
27
+ antibiotic TEXT,
28
+ percent_susceptible REAL,
29
+ percent_intermediate REAL,
30
+ percent_resistant REAL,
31
+ total_isolates INTEGER,
32
+ year INTEGER,
33
+ region TEXT,
34
+ source TEXT,
35
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_atlas_species ON atlas_susceptibility(species);
39
+ CREATE INDEX IF NOT EXISTS idx_atlas_antibiotic ON atlas_susceptibility(antibiotic);
40
+ CREATE INDEX IF NOT EXISTS idx_atlas_family ON atlas_susceptibility(family);
41
+
42
+ -- MIC Breakpoints Table
43
+ CREATE TABLE IF NOT EXISTS mic_breakpoints (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ pathogen_group TEXT NOT NULL,
46
+ antibiotic TEXT NOT NULL,
47
+ route TEXT,
48
+ mic_susceptible REAL,
49
+ mic_resistant REAL,
50
+ disk_susceptible REAL,
51
+ disk_resistant REAL,
52
+ notes TEXT,
53
+ eucast_version TEXT DEFAULT '16.0',
54
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
55
+ );
56
+
57
+ CREATE INDEX IF NOT EXISTS idx_bp_pathogen ON mic_breakpoints(pathogen_group);
58
+ CREATE INDEX IF NOT EXISTS idx_bp_antibiotic ON mic_breakpoints(antibiotic);
59
+
60
+ -- Dosage Guidance Table
61
+ CREATE TABLE IF NOT EXISTS dosage_guidance (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ antibiotic TEXT NOT NULL,
64
+ standard_dose TEXT,
65
+ high_dose TEXT,
66
+ renal_adjustment TEXT,
67
+ notes TEXT,
68
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
69
+ );
70
+
71
+ CREATE INDEX IF NOT EXISTS idx_dosage_antibiotic ON dosage_guidance(antibiotic);
72
+
73
+ -- Drug Interactions Table
74
+ CREATE TABLE IF NOT EXISTS drug_interactions (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ drug_1 TEXT NOT NULL,
77
+ drug_2 TEXT NOT NULL,
78
+ interaction_description TEXT,
79
+ severity TEXT,
80
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
81
+ );
82
+
83
+ CREATE INDEX IF NOT EXISTS idx_di_drug_1 ON drug_interactions(drug_1);
84
+ CREATE INDEX IF NOT EXISTS idx_di_drug_2 ON drug_interactions(drug_2);
85
+ CREATE INDEX IF NOT EXISTS idx_di_severity ON drug_interactions(severity);
86
+
87
+ -- View for bidirectional drug interaction lookup
88
+ CREATE VIEW IF NOT EXISTS drug_interaction_lookup AS
89
+ SELECT id, drug_1, drug_2, interaction_description, severity FROM drug_interactions
90
+ UNION ALL
91
+ SELECT id, drug_2 as drug_1, drug_1 as drug_2, interaction_description, severity FROM drug_interactions;
92
+
93
+ -- Patient History Table (for demo purposes)
94
+ CREATE TABLE IF NOT EXISTS patient_history (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ patient_id TEXT NOT NULL,
97
+ infection_date DATE,
98
+ pathogen TEXT,
99
+ antibiotic TEXT,
100
+ mic_value REAL,
101
+ interpretation TEXT,
102
+ outcome TEXT,
103
+ notes TEXT,
104
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
105
+ );
106
+
107
+ CREATE INDEX IF NOT EXISTS idx_ph_patient ON patient_history(patient_id);
108
+ CREATE INDEX IF NOT EXISTS idx_ph_pathogen ON patient_history(pathogen);
src/db/vector_store.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ChromaDB vector store for unstructured document RAG."""
2
+
3
+ import chromadb
4
+ from chromadb.utils import embedding_functions
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ import hashlib
8
+
9
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
10
+ from pypdf import PdfReader
11
+
12
+ # Project paths
13
+ PROJECT_ROOT = Path(__file__).parent.parent.parent
14
+ DATA_DIR = PROJECT_ROOT / "data"
15
+ DOCS_DIR = PROJECT_ROOT / "docs"
16
+ CHROMA_DIR = DATA_DIR / "chroma"
17
+
18
+
19
+ def get_chroma_client() -> chromadb.PersistentClient:
20
+ """Get ChromaDB persistent client."""
21
+ CHROMA_DIR.mkdir(parents=True, exist_ok=True)
22
+ return chromadb.PersistentClient(path=str(CHROMA_DIR))
23
+
24
+
25
+ def get_embedding_function():
26
+ """Get the embedding function for ChromaDB."""
27
+ return embedding_functions.SentenceTransformerEmbeddingFunction(
28
+ model_name="all-MiniLM-L6-v2"
29
+ )
30
+
31
+
32
+ def extract_pdf_text(pdf_path: Path) -> str:
33
+ """Extract text from PDF file."""
34
+ reader = PdfReader(pdf_path)
35
+ text = ""
36
+ for page in reader.pages:
37
+ text += page.extract_text() + "\n\n"
38
+ return text
39
+
40
+
41
+ def chunk_text(text: str, chunk_size: int = 1000, chunk_overlap: int = 200) -> list[str]:
42
+ """Split text into chunks for embedding."""
43
+ splitter = RecursiveCharacterTextSplitter(
44
+ chunk_size=chunk_size,
45
+ chunk_overlap=chunk_overlap,
46
+ separators=["\n\n", "\n", ". ", " ", ""]
47
+ )
48
+ return splitter.split_text(text)
49
+
50
+
51
+ def generate_doc_id(text: str, index: int) -> str:
52
+ """Generate a unique document ID."""
53
+ hash_input = f"{text[:100]}_{index}"
54
+ return hashlib.md5(hash_input.encode()).hexdigest()
55
+
56
+
57
+ def init_idsa_guidelines_collection() -> chromadb.Collection:
58
+ """Initialize the IDSA treatment guidelines collection."""
59
+ client = get_chroma_client()
60
+ ef = get_embedding_function()
61
+
62
+ # Delete existing collection if exists
63
+ try:
64
+ client.delete_collection("idsa_treatment_guidelines")
65
+ except Exception:
66
+ pass
67
+
68
+ collection = client.create_collection(
69
+ name="idsa_treatment_guidelines",
70
+ embedding_function=ef,
71
+ metadata={
72
+ "source": "IDSA 2024 Guidance",
73
+ "doi": "10.1093/cid/ciae403",
74
+ "description": "Antimicrobial-Resistant Gram-Negative Infections Treatment Guidelines"
75
+ }
76
+ )
77
+
78
+ return collection
79
+
80
+
81
+ def init_mic_reference_collection() -> chromadb.Collection:
82
+ """Initialize the MIC reference documentation collection."""
83
+ client = get_chroma_client()
84
+ ef = get_embedding_function()
85
+
86
+ # Delete existing collection if exists
87
+ try:
88
+ client.delete_collection("mic_reference_docs")
89
+ except Exception:
90
+ pass
91
+
92
+ collection = client.create_collection(
93
+ name="mic_reference_docs",
94
+ embedding_function=ef,
95
+ metadata={
96
+ "source": "EUCAST Breakpoint Tables",
97
+ "version": "16.0",
98
+ "description": "MIC Breakpoint Reference Documentation"
99
+ }
100
+ )
101
+
102
+ return collection
103
+
104
+
105
+ def classify_chunk_pathogen(text: str) -> str:
106
+ """Classify which pathogen type a chunk relates to."""
107
+ text_lower = text.lower()
108
+
109
+ pathogen_keywords = {
110
+ "ESBL-E": ["esbl", "extended-spectrum beta-lactamase", "esbl-e", "esbl-producing"],
111
+ "CRE": ["carbapenem-resistant enterobacterales", "cre", "carbapenemase"],
112
+ "CRAB": ["acinetobacter baumannii", "crab", "carbapenem-resistant acinetobacter"],
113
+ "DTR-PA": ["pseudomonas aeruginosa", "dtr-p", "difficult-to-treat resistance"],
114
+ "S.maltophilia": ["stenotrophomonas maltophilia", "s. maltophilia"],
115
+ "AmpC-E": ["ampc", "ampc-e", "ampc-producing"],
116
+ }
117
+
118
+ for pathogen, keywords in pathogen_keywords.items():
119
+ for keyword in keywords:
120
+ if keyword in text_lower:
121
+ return pathogen
122
+
123
+ return "General"
124
+
125
+
126
+ def import_idsa_guidelines() -> int:
127
+ """Import IDSA guidelines PDF into ChromaDB."""
128
+ print("Importing IDSA guidelines into ChromaDB...")
129
+
130
+ pdf_path = DOCS_DIR / "antibiotic_guidelines" / "ciae403.pdf"
131
+
132
+ if not pdf_path.exists():
133
+ print(f" Warning: {pdf_path} not found, skipping...")
134
+ return 0
135
+
136
+ # Extract text from PDF
137
+ print(" Extracting text from PDF...")
138
+ text = extract_pdf_text(pdf_path)
139
+
140
+ # Chunk the text
141
+ print(" Chunking text...")
142
+ chunks = chunk_text(text)
143
+
144
+ # Initialize collection
145
+ collection = init_idsa_guidelines_collection()
146
+
147
+ # Prepare documents for insertion
148
+ documents = []
149
+ metadatas = []
150
+ ids = []
151
+
152
+ for i, chunk in enumerate(chunks):
153
+ documents.append(chunk)
154
+ metadatas.append({
155
+ "source": "ciae403.pdf",
156
+ "chunk_index": i,
157
+ "pathogen_type": classify_chunk_pathogen(chunk),
158
+ "page_estimate": i // 3 # Rough estimate
159
+ })
160
+ ids.append(generate_doc_id(chunk, i))
161
+
162
+ # Add to collection
163
+ print(f" Adding {len(documents)} chunks to collection...")
164
+ collection.add(
165
+ documents=documents,
166
+ metadatas=metadatas,
167
+ ids=ids
168
+ )
169
+
170
+ print(f" Imported {len(documents)} chunks from IDSA guidelines")
171
+ return len(documents)
172
+
173
+
174
+ def import_mic_reference() -> int:
175
+ """Import MIC breakpoint PDF into ChromaDB."""
176
+ print("Importing MIC reference PDF into ChromaDB...")
177
+
178
+ pdf_path = DOCS_DIR / "mic_breakpoints" / "v_16.0_Breakpoint_Tables.pdf"
179
+
180
+ if not pdf_path.exists():
181
+ print(f" Warning: {pdf_path} not found, skipping...")
182
+ return 0
183
+
184
+ # Extract text from PDF
185
+ print(" Extracting text from PDF...")
186
+ text = extract_pdf_text(pdf_path)
187
+
188
+ # Chunk the text
189
+ print(" Chunking text...")
190
+ chunks = chunk_text(text, chunk_size=800, chunk_overlap=150)
191
+
192
+ # Initialize collection
193
+ collection = init_mic_reference_collection()
194
+
195
+ # Prepare documents for insertion
196
+ documents = []
197
+ metadatas = []
198
+ ids = []
199
+
200
+ for i, chunk in enumerate(chunks):
201
+ documents.append(chunk)
202
+ metadatas.append({
203
+ "source": "v_16.0_Breakpoint_Tables.pdf",
204
+ "chunk_index": i,
205
+ "document_type": "mic_reference"
206
+ })
207
+ ids.append(generate_doc_id(chunk, i))
208
+
209
+ # Add to collection
210
+ print(f" Adding {len(documents)} chunks to collection...")
211
+ collection.add(
212
+ documents=documents,
213
+ metadatas=metadatas,
214
+ ids=ids
215
+ )
216
+
217
+ print(f" Imported {len(documents)} chunks from MIC reference")
218
+ return len(documents)
219
+
220
+
221
+ def get_collection(name: str) -> Optional[chromadb.Collection]:
222
+ """Get a collection by name."""
223
+ client = get_chroma_client()
224
+ ef = get_embedding_function()
225
+
226
+ try:
227
+ return client.get_collection(name=name, embedding_function=ef)
228
+ except Exception:
229
+ return None
230
+
231
+
232
+ def search_guidelines(
233
+ query: str,
234
+ n_results: int = 5,
235
+ pathogen_filter: str = None
236
+ ) -> list[dict]:
237
+ """Search the IDSA guidelines collection."""
238
+ collection = get_collection("idsa_treatment_guidelines")
239
+
240
+ if collection is None:
241
+ return []
242
+
243
+ where_filter = None
244
+ if pathogen_filter:
245
+ where_filter = {"pathogen_type": pathogen_filter}
246
+
247
+ results = collection.query(
248
+ query_texts=[query],
249
+ n_results=n_results,
250
+ where=where_filter,
251
+ include=["documents", "metadatas", "distances"]
252
+ )
253
+
254
+ # Format results
255
+ formatted = []
256
+ for i in range(len(results['documents'][0])):
257
+ formatted.append({
258
+ "content": results['documents'][0][i],
259
+ "metadata": results['metadatas'][0][i],
260
+ "distance": results['distances'][0][i]
261
+ })
262
+
263
+ return formatted
264
+
265
+
266
+ def search_mic_reference(query: str, n_results: int = 3) -> list[dict]:
267
+ """Search the MIC reference collection."""
268
+ collection = get_collection("mic_reference_docs")
269
+
270
+ if collection is None:
271
+ return []
272
+
273
+ results = collection.query(
274
+ query_texts=[query],
275
+ n_results=n_results,
276
+ include=["documents", "metadatas", "distances"]
277
+ )
278
+
279
+ # Format results
280
+ formatted = []
281
+ for i in range(len(results['documents'][0])):
282
+ formatted.append({
283
+ "content": results['documents'][0][i],
284
+ "metadata": results['metadatas'][0][i],
285
+ "distance": results['distances'][0][i]
286
+ })
287
+
288
+ return formatted
289
+
290
+
291
+ def import_all_vectors() -> dict:
292
+ """Import all PDFs into ChromaDB."""
293
+ print(f"\n{'='*50}")
294
+ print("ChromaDB Vector Import")
295
+ print(f"{'='*50}\n")
296
+
297
+ results = {
298
+ "idsa_guidelines": import_idsa_guidelines(),
299
+ "mic_reference": import_mic_reference(),
300
+ }
301
+
302
+ print(f"\n{'='*50}")
303
+ print("Vector Import Summary:")
304
+ for collection, count in results.items():
305
+ print(f" {collection}: {count} chunks")
306
+ print(f"{'='*50}\n")
307
+
308
+ return results
309
+
310
+
311
+ if __name__ == "__main__":
312
+ import_all_vectors()
src/tools/__init__.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Med-I-C Query Tools for AMR-Guard Workflow."""
2
+
3
+ from .antibiotic_tools import (
4
+ query_antibiotic_info,
5
+ get_antibiotics_by_category,
6
+ get_antibiotic_for_indication,
7
+ interpret_mic_value,
8
+ get_breakpoints_for_pathogen,
9
+ )
10
+
11
+ from .resistance_tools import (
12
+ query_resistance_pattern,
13
+ get_most_effective_antibiotics,
14
+ get_resistance_trend,
15
+ calculate_mic_trend,
16
+ get_pathogen_families,
17
+ get_pathogens_by_family,
18
+ )
19
+
20
+ from .safety_tools import (
21
+ check_drug_interactions,
22
+ check_single_interaction,
23
+ get_all_interactions_for_drug,
24
+ get_major_interactions_for_drug,
25
+ screen_antibiotic_safety,
26
+ get_interaction_statistics,
27
+ )
28
+
29
+ from .rag_tools import (
30
+ search_clinical_guidelines,
31
+ search_mic_reference_docs,
32
+ get_treatment_recommendation,
33
+ explain_mic_interpretation,
34
+ get_empirical_therapy_guidance,
35
+ )
36
+
37
+ __all__ = [
38
+ # Antibiotic tools
39
+ "query_antibiotic_info",
40
+ "get_antibiotics_by_category",
41
+ "get_antibiotic_for_indication",
42
+ "interpret_mic_value",
43
+ "get_breakpoints_for_pathogen",
44
+
45
+ # Resistance tools
46
+ "query_resistance_pattern",
47
+ "get_most_effective_antibiotics",
48
+ "get_resistance_trend",
49
+ "calculate_mic_trend",
50
+ "get_pathogen_families",
51
+ "get_pathogens_by_family",
52
+
53
+ # Safety tools
54
+ "check_drug_interactions",
55
+ "check_single_interaction",
56
+ "get_all_interactions_for_drug",
57
+ "get_major_interactions_for_drug",
58
+ "screen_antibiotic_safety",
59
+ "get_interaction_statistics",
60
+
61
+ # RAG tools
62
+ "search_clinical_guidelines",
63
+ "search_mic_reference_docs",
64
+ "get_treatment_recommendation",
65
+ "explain_mic_interpretation",
66
+ "get_empirical_therapy_guidance",
67
+ ]
src/tools/antibiotic_tools.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Antibiotic query tools for Med-I-C workflow."""
2
+
3
+ from typing import Optional
4
+ from src.db.database import execute_query
5
+
6
+
7
+ def query_antibiotic_info(
8
+ antibiotic_name: str,
9
+ include_category: bool = True,
10
+ include_formulations: bool = True
11
+ ) -> list[dict]:
12
+ """
13
+ Query EML antibiotic database for classification and details.
14
+
15
+ Args:
16
+ antibiotic_name: Name of the antibiotic (partial match supported)
17
+ include_category: Include WHO stewardship category
18
+ include_formulations: Include available formulations
19
+
20
+ Returns:
21
+ List of matching antibiotics with details
22
+
23
+ Used by: Agent 1, Agent 4
24
+ """
25
+ query = """
26
+ SELECT
27
+ medicine_name,
28
+ who_category,
29
+ eml_section,
30
+ formulations,
31
+ indication,
32
+ atc_codes,
33
+ combined_with,
34
+ status
35
+ FROM eml_antibiotics
36
+ WHERE LOWER(medicine_name) LIKE LOWER(?)
37
+ ORDER BY
38
+ CASE who_category
39
+ WHEN 'ACCESS' THEN 1
40
+ WHEN 'WATCH' THEN 2
41
+ WHEN 'RESERVE' THEN 3
42
+ ELSE 4
43
+ END
44
+ """
45
+
46
+ results = execute_query(query, (f"%{antibiotic_name}%",))
47
+
48
+ # Filter columns based on parameters
49
+ if not include_category or not include_formulations:
50
+ filtered_results = []
51
+ for r in results:
52
+ filtered = dict(r)
53
+ if not include_category:
54
+ filtered.pop('who_category', None)
55
+ if not include_formulations:
56
+ filtered.pop('formulations', None)
57
+ filtered_results.append(filtered)
58
+ return filtered_results
59
+
60
+ return results
61
+
62
+
63
+ def get_antibiotics_by_category(category: str) -> list[dict]:
64
+ """
65
+ Get all antibiotics in a specific WHO category.
66
+
67
+ Args:
68
+ category: WHO category ('ACCESS', 'WATCH', 'RESERVE')
69
+
70
+ Returns:
71
+ List of antibiotics in that category
72
+ """
73
+ query = """
74
+ SELECT medicine_name, indication, formulations, atc_codes
75
+ FROM eml_antibiotics
76
+ WHERE UPPER(who_category) = UPPER(?)
77
+ ORDER BY medicine_name
78
+ """
79
+
80
+ return execute_query(query, (category,))
81
+
82
+
83
+ def get_antibiotic_for_indication(indication_keyword: str) -> list[dict]:
84
+ """
85
+ Find antibiotics based on indication keywords.
86
+
87
+ Args:
88
+ indication_keyword: Keyword to search in indications
89
+
90
+ Returns:
91
+ List of matching antibiotics with indications
92
+ """
93
+ query = """
94
+ SELECT
95
+ medicine_name,
96
+ who_category,
97
+ indication,
98
+ formulations
99
+ FROM eml_antibiotics
100
+ WHERE LOWER(indication) LIKE LOWER(?)
101
+ ORDER BY
102
+ CASE who_category
103
+ WHEN 'ACCESS' THEN 1
104
+ WHEN 'WATCH' THEN 2
105
+ WHEN 'RESERVE' THEN 3
106
+ ELSE 4
107
+ END
108
+ """
109
+
110
+ return execute_query(query, (f"%{indication_keyword}%",))
111
+
112
+
113
+ def interpret_mic_value(
114
+ pathogen: str,
115
+ antibiotic: str,
116
+ mic_value: float,
117
+ route: str = None
118
+ ) -> dict:
119
+ """
120
+ Interpret MIC value against EUCAST breakpoints.
121
+
122
+ Args:
123
+ pathogen: Pathogen name or group
124
+ antibiotic: Antibiotic name
125
+ mic_value: MIC value in mg/L
126
+ route: Administration route (IV, Oral)
127
+
128
+ Returns:
129
+ Dict with interpretation (S/I/R), breakpoint values, clinical notes
130
+
131
+ Used by: Agent 2, Agent 3
132
+ """
133
+ query = """
134
+ SELECT
135
+ pathogen_group,
136
+ antibiotic,
137
+ mic_susceptible,
138
+ mic_resistant,
139
+ notes,
140
+ route
141
+ FROM mic_breakpoints
142
+ WHERE LOWER(pathogen_group) LIKE LOWER(?)
143
+ AND LOWER(antibiotic) LIKE LOWER(?)
144
+ LIMIT 1
145
+ """
146
+
147
+ results = execute_query(query, (f"%{pathogen}%", f"%{antibiotic}%"))
148
+
149
+ if not results:
150
+ return {
151
+ "interpretation": "UNKNOWN",
152
+ "message": f"No breakpoint found for {antibiotic} against {pathogen}",
153
+ "mic_value": mic_value,
154
+ "breakpoints": None
155
+ }
156
+
157
+ bp = results[0]
158
+ mic_s = bp.get('mic_susceptible')
159
+ mic_r = bp.get('mic_resistant')
160
+
161
+ # Determine interpretation
162
+ if mic_s is not None and mic_value <= mic_s:
163
+ interpretation = "SUSCEPTIBLE"
164
+ message = f"MIC ({mic_value} mg/L) ≤ S breakpoint ({mic_s} mg/L)"
165
+ elif mic_r is not None and mic_value > mic_r:
166
+ interpretation = "RESISTANT"
167
+ message = f"MIC ({mic_value} mg/L) > R breakpoint ({mic_r} mg/L)"
168
+ elif mic_s is not None and mic_r is not None:
169
+ interpretation = "INTERMEDIATE"
170
+ message = f"MIC ({mic_value} mg/L) between S ({mic_s}) and R ({mic_r}) breakpoints"
171
+ else:
172
+ interpretation = "UNKNOWN"
173
+ message = "Incomplete breakpoint data"
174
+
175
+ return {
176
+ "interpretation": interpretation,
177
+ "message": message,
178
+ "mic_value": mic_value,
179
+ "breakpoints": {
180
+ "susceptible": mic_s,
181
+ "resistant": mic_r
182
+ },
183
+ "pathogen_group": bp.get('pathogen_group'),
184
+ "notes": bp.get('notes')
185
+ }
186
+
187
+
188
+ def get_breakpoints_for_pathogen(pathogen: str) -> list[dict]:
189
+ """
190
+ Get all available breakpoints for a pathogen.
191
+
192
+ Args:
193
+ pathogen: Pathogen name or group
194
+
195
+ Returns:
196
+ List of antibiotic breakpoints for the pathogen
197
+ """
198
+ query = """
199
+ SELECT
200
+ antibiotic,
201
+ mic_susceptible,
202
+ mic_resistant,
203
+ route,
204
+ notes
205
+ FROM mic_breakpoints
206
+ WHERE LOWER(pathogen_group) LIKE LOWER(?)
207
+ ORDER BY antibiotic
208
+ """
209
+
210
+ return execute_query(query, (f"%{pathogen}%",))
src/tools/rag_tools.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RAG tools for querying clinical guidelines via ChromaDB."""
2
+
3
+ from typing import Optional
4
+ from src.db.vector_store import search_guidelines, search_mic_reference
5
+
6
+
7
+ def search_clinical_guidelines(
8
+ query: str,
9
+ pathogen_filter: str = None,
10
+ n_results: int = 5
11
+ ) -> list[dict]:
12
+ """
13
+ Semantic search over IDSA clinical guidelines.
14
+
15
+ Args:
16
+ query: Natural language query about treatment
17
+ pathogen_filter: Optional pathogen type filter
18
+ Options: 'ESBL-E', 'CRE', 'CRAB', 'DTR-PA', 'S.maltophilia', 'AmpC-E', 'General'
19
+ n_results: Number of results to return
20
+
21
+ Returns:
22
+ List of relevant guideline excerpts with metadata
23
+
24
+ Used by: Agent 1 (Empirical), Agent 4 (Justification)
25
+ """
26
+ results = search_guidelines(query, n_results, pathogen_filter)
27
+
28
+ # Format for agent consumption
29
+ formatted = []
30
+ for r in results:
31
+ formatted.append({
32
+ "content": r.get("content", ""),
33
+ "pathogen_type": r.get("metadata", {}).get("pathogen_type", "General"),
34
+ "source": r.get("metadata", {}).get("source", "IDSA Guidelines"),
35
+ "relevance_score": 1 - r.get("distance", 1) # Convert distance to similarity
36
+ })
37
+
38
+ return formatted
39
+
40
+
41
+ def search_mic_reference_docs(query: str, n_results: int = 3) -> list[dict]:
42
+ """
43
+ Search MIC breakpoint reference documentation.
44
+
45
+ Args:
46
+ query: Query about MIC interpretation or breakpoints
47
+ n_results: Number of results to return
48
+
49
+ Returns:
50
+ List of relevant reference excerpts
51
+ """
52
+ results = search_mic_reference(query, n_results)
53
+
54
+ formatted = []
55
+ for r in results:
56
+ formatted.append({
57
+ "content": r.get("content", ""),
58
+ "source": r.get("metadata", {}).get("source", "EUCAST Breakpoints"),
59
+ "relevance_score": 1 - r.get("distance", 1)
60
+ })
61
+
62
+ return formatted
63
+
64
+
65
+ def get_treatment_recommendation(
66
+ pathogen: str,
67
+ infection_site: str = None,
68
+ patient_factors: list[str] = None
69
+ ) -> dict:
70
+ """
71
+ Get treatment recommendation by searching guidelines.
72
+
73
+ Args:
74
+ pathogen: Identified or suspected pathogen
75
+ infection_site: Location of infection (e.g., "urinary", "respiratory")
76
+ patient_factors: List of patient factors (e.g., ["renal impairment", "pregnancy"])
77
+
78
+ Returns:
79
+ Treatment recommendation with guideline citations
80
+ """
81
+ # Build comprehensive query
82
+ query_parts = [f"treatment for {pathogen} infection"]
83
+
84
+ if infection_site:
85
+ query_parts.append(f"in {infection_site}")
86
+
87
+ if patient_factors:
88
+ query_parts.append(f"considering {', '.join(patient_factors)}")
89
+
90
+ query = " ".join(query_parts)
91
+
92
+ # Search guidelines
93
+ results = search_clinical_guidelines(query, n_results=5)
94
+
95
+ # Try to determine pathogen category
96
+ pathogen_category = None
97
+ pathogen_lower = pathogen.lower()
98
+
99
+ pathogen_mapping = {
100
+ "ESBL-E": ["esbl", "extended-spectrum", "e. coli", "klebsiella"],
101
+ "CRE": ["carbapenem-resistant", "cre", "carbapenemase"],
102
+ "CRAB": ["acinetobacter", "crab"],
103
+ "DTR-PA": ["pseudomonas", "dtr"],
104
+ "S.maltophilia": ["stenotrophomonas", "maltophilia"],
105
+ }
106
+
107
+ for category, keywords in pathogen_mapping.items():
108
+ for keyword in keywords:
109
+ if keyword in pathogen_lower:
110
+ pathogen_category = category
111
+ break
112
+
113
+ # Search with pathogen filter if category identified
114
+ if pathogen_category:
115
+ filtered_results = search_clinical_guidelines(
116
+ query, pathogen_filter=pathogen_category, n_results=3
117
+ )
118
+ if filtered_results:
119
+ results = filtered_results + results[:2] # Combine results
120
+
121
+ return {
122
+ "query": query,
123
+ "pathogen_category": pathogen_category or "General",
124
+ "recommendations": results[:5],
125
+ "note": "These recommendations are from IDSA 2024 guidelines. Always verify with current institutional protocols."
126
+ }
127
+
128
+
129
+ def explain_mic_interpretation(
130
+ pathogen: str,
131
+ antibiotic: str,
132
+ mic_value: float
133
+ ) -> dict:
134
+ """
135
+ Get detailed explanation for MIC interpretation from reference docs.
136
+
137
+ Args:
138
+ pathogen: Pathogen name
139
+ antibiotic: Antibiotic name
140
+ mic_value: The MIC value to interpret
141
+
142
+ Returns:
143
+ Detailed explanation with reference citations
144
+ """
145
+ query = f"MIC breakpoint interpretation for {antibiotic} against {pathogen}"
146
+
147
+ results = search_mic_reference_docs(query, n_results=3)
148
+
149
+ return {
150
+ "query": query,
151
+ "mic_value": mic_value,
152
+ "reference_excerpts": results,
153
+ "note": "Refer to current EUCAST v16.0 breakpoint tables for official interpretation."
154
+ }
155
+
156
+
157
+ def get_empirical_therapy_guidance(
158
+ infection_type: str,
159
+ risk_factors: list[str] = None
160
+ ) -> dict:
161
+ """
162
+ Get empirical therapy guidance for an infection type.
163
+
164
+ Args:
165
+ infection_type: Type of infection (e.g., "UTI", "pneumonia", "sepsis")
166
+ risk_factors: List of risk factors (e.g., ["prior MRSA", "recent antibiotics"])
167
+
168
+ Returns:
169
+ Empirical therapy recommendations
170
+ """
171
+ query_parts = [f"empirical therapy for {infection_type}"]
172
+
173
+ if risk_factors:
174
+ query_parts.append(f"with risk factors: {', '.join(risk_factors)}")
175
+
176
+ query = " ".join(query_parts)
177
+
178
+ results = search_clinical_guidelines(query, n_results=5)
179
+
180
+ return {
181
+ "infection_type": infection_type,
182
+ "risk_factors": risk_factors or [],
183
+ "recommendations": results,
184
+ "note": "Empirical therapy should be de-escalated based on culture results."
185
+ }
src/tools/resistance_tools.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Resistance pattern and trend analysis tools for Med-I-C workflow."""
2
+
3
+ from typing import Optional
4
+ from src.db.database import execute_query
5
+
6
+
7
+ def query_resistance_pattern(
8
+ pathogen: str,
9
+ antibiotic: str = None,
10
+ region: str = None,
11
+ year: int = None
12
+ ) -> list[dict]:
13
+ """
14
+ Query ATLAS susceptibility data for resistance patterns.
15
+
16
+ Args:
17
+ pathogen: Pathogen name (e.g., "E. coli", "K. pneumoniae")
18
+ antibiotic: Optional specific antibiotic to check
19
+ region: Optional geographic region filter
20
+ year: Optional year filter (defaults to most recent)
21
+
22
+ Returns:
23
+ List of susceptibility records with percentages
24
+
25
+ Used by: Agent 1 (Empirical), Agent 3 (Trend Analysis)
26
+ """
27
+ conditions = ["LOWER(species) LIKE LOWER(?)"]
28
+ params = [f"%{pathogen}%"]
29
+
30
+ if antibiotic:
31
+ conditions.append("LOWER(antibiotic) LIKE LOWER(?)")
32
+ params.append(f"%{antibiotic}%")
33
+
34
+ if region:
35
+ conditions.append("LOWER(region) LIKE LOWER(?)")
36
+ params.append(f"%{region}%")
37
+
38
+ if year:
39
+ conditions.append("year = ?")
40
+ params.append(year)
41
+
42
+ where_clause = " AND ".join(conditions)
43
+
44
+ query = f"""
45
+ SELECT
46
+ species,
47
+ family,
48
+ antibiotic,
49
+ percent_susceptible,
50
+ percent_intermediate,
51
+ percent_resistant,
52
+ total_isolates,
53
+ year,
54
+ region
55
+ FROM atlas_susceptibility
56
+ WHERE {where_clause}
57
+ ORDER BY year DESC, percent_susceptible DESC
58
+ LIMIT 50
59
+ """
60
+
61
+ return execute_query(query, tuple(params))
62
+
63
+
64
+ def get_most_effective_antibiotics(
65
+ pathogen: str,
66
+ min_susceptibility: float = 80.0,
67
+ limit: int = 10
68
+ ) -> list[dict]:
69
+ """
70
+ Find antibiotics with highest susceptibility for a pathogen.
71
+
72
+ Args:
73
+ pathogen: Pathogen name
74
+ min_susceptibility: Minimum susceptibility percentage (default 80%)
75
+ limit: Maximum number of results
76
+
77
+ Returns:
78
+ List of effective antibiotics sorted by susceptibility
79
+ """
80
+ query = """
81
+ SELECT
82
+ antibiotic,
83
+ AVG(percent_susceptible) as avg_susceptibility,
84
+ SUM(total_isolates) as total_samples,
85
+ MAX(year) as latest_year
86
+ FROM atlas_susceptibility
87
+ WHERE LOWER(species) LIKE LOWER(?)
88
+ AND percent_susceptible >= ?
89
+ GROUP BY antibiotic
90
+ ORDER BY avg_susceptibility DESC
91
+ LIMIT ?
92
+ """
93
+
94
+ return execute_query(query, (f"%{pathogen}%", min_susceptibility, limit))
95
+
96
+
97
+ def get_resistance_trend(
98
+ pathogen: str,
99
+ antibiotic: str
100
+ ) -> list[dict]:
101
+ """
102
+ Get resistance trend over time for pathogen-antibiotic combination.
103
+
104
+ Args:
105
+ pathogen: Pathogen name
106
+ antibiotic: Antibiotic name
107
+
108
+ Returns:
109
+ List of yearly susceptibility data
110
+ """
111
+ query = """
112
+ SELECT
113
+ year,
114
+ AVG(percent_susceptible) as avg_susceptibility,
115
+ AVG(percent_resistant) as avg_resistance,
116
+ SUM(total_isolates) as total_samples
117
+ FROM atlas_susceptibility
118
+ WHERE LOWER(species) LIKE LOWER(?)
119
+ AND LOWER(antibiotic) LIKE LOWER(?)
120
+ AND year IS NOT NULL
121
+ GROUP BY year
122
+ ORDER BY year ASC
123
+ """
124
+
125
+ return execute_query(query, (f"%{pathogen}%", f"%{antibiotic}%"))
126
+
127
+
128
+ def calculate_mic_trend(
129
+ historical_mics: list[dict],
130
+ current_mic: float = None
131
+ ) -> dict:
132
+ """
133
+ Calculate resistance velocity and MIC trend from historical data.
134
+
135
+ Args:
136
+ historical_mics: List of historical MIC readings [{"date": ..., "mic_value": ...}, ...]
137
+ current_mic: Optional current MIC value (if not in historical_mics)
138
+
139
+ Returns:
140
+ Dict with trend analysis, resistance_velocity, risk_level
141
+
142
+ Used by: Agent 3 (Trend Analyst)
143
+
144
+ Logic:
145
+ - If MIC increases by 4x (two-step dilution), flag HIGH risk
146
+ - If MIC increases by 2x (one-step dilution), flag MODERATE risk
147
+ - Otherwise, LOW risk
148
+ """
149
+ if not historical_mics:
150
+ return {
151
+ "risk_level": "UNKNOWN",
152
+ "message": "No historical MIC data available",
153
+ "trend": None,
154
+ "velocity": None
155
+ }
156
+
157
+ # Sort by date if available
158
+ sorted_mics = sorted(
159
+ historical_mics,
160
+ key=lambda x: x.get('date', '0')
161
+ )
162
+
163
+ mic_values = [m['mic_value'] for m in sorted_mics if m.get('mic_value')]
164
+
165
+ if current_mic:
166
+ mic_values.append(current_mic)
167
+
168
+ if len(mic_values) < 2:
169
+ return {
170
+ "risk_level": "UNKNOWN",
171
+ "message": "Insufficient MIC history (need at least 2 values)",
172
+ "trend": None,
173
+ "velocity": None,
174
+ "values": mic_values
175
+ }
176
+
177
+ baseline_mic = mic_values[0]
178
+ latest_mic = mic_values[-1]
179
+
180
+ # Avoid division by zero
181
+ if baseline_mic == 0:
182
+ baseline_mic = 0.001
183
+
184
+ ratio = latest_mic / baseline_mic
185
+
186
+ # Calculate velocity (fold change per time point)
187
+ velocity = ratio ** (1 / (len(mic_values) - 1)) if len(mic_values) > 1 else 1
188
+
189
+ # Determine trend direction
190
+ if ratio > 1.5:
191
+ trend = "INCREASING"
192
+ elif ratio < 0.67:
193
+ trend = "DECREASING"
194
+ else:
195
+ trend = "STABLE"
196
+
197
+ # Determine risk level
198
+ if ratio >= 4:
199
+ risk_level = "HIGH"
200
+ alert = "MIC CREEP DETECTED - Two-step dilution increase. High risk of treatment failure even if currently 'Susceptible'."
201
+ elif ratio >= 2:
202
+ risk_level = "MODERATE"
203
+ alert = "MIC trending upward (one-step dilution increase). Monitor closely and consider alternative agents."
204
+ elif trend == "INCREASING":
205
+ risk_level = "LOW"
206
+ alert = "Slight MIC increase observed. Continue current therapy with monitoring."
207
+ else:
208
+ risk_level = "LOW"
209
+ alert = "MIC stable or decreasing. Current therapy appears effective."
210
+
211
+ return {
212
+ "risk_level": risk_level,
213
+ "alert": alert,
214
+ "trend": trend,
215
+ "velocity": round(velocity, 2),
216
+ "ratio": round(ratio, 2),
217
+ "baseline_mic": baseline_mic,
218
+ "current_mic": latest_mic,
219
+ "data_points": len(mic_values),
220
+ "values": mic_values
221
+ }
222
+
223
+
224
+ def get_pathogen_families() -> list[dict]:
225
+ """Get list of unique pathogen families in the database."""
226
+ query = """
227
+ SELECT DISTINCT family, COUNT(DISTINCT species) as species_count
228
+ FROM atlas_susceptibility
229
+ WHERE family IS NOT NULL AND family != ''
230
+ GROUP BY family
231
+ ORDER BY species_count DESC
232
+ """
233
+ return execute_query(query)
234
+
235
+
236
+ def get_pathogens_by_family(family: str) -> list[dict]:
237
+ """Get all pathogens in a specific family."""
238
+ query = """
239
+ SELECT DISTINCT species
240
+ FROM atlas_susceptibility
241
+ WHERE LOWER(family) LIKE LOWER(?)
242
+ ORDER BY species
243
+ """
244
+ return execute_query(query, (f"%{family}%",))
src/tools/safety_tools.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Drug safety and interaction tools for Med-I-C workflow."""
2
+
3
+ from typing import Optional
4
+ from src.db.database import execute_query
5
+
6
+
7
+ def check_drug_interactions(
8
+ target_drug: str,
9
+ patient_medications: list[str],
10
+ severity_filter: str = None
11
+ ) -> list[dict]:
12
+ """
13
+ Check for interactions between target drug and patient's medications.
14
+
15
+ Args:
16
+ target_drug: Antibiotic being considered
17
+ patient_medications: List of patient's current medications
18
+ severity_filter: Optional filter ('major', 'moderate', 'minor')
19
+
20
+ Returns:
21
+ List of interaction dicts with severity and description
22
+
23
+ Used by: Agent 4 (Safety Check)
24
+ """
25
+ if not patient_medications:
26
+ return []
27
+
28
+ # Build query with proper parameter handling
29
+ placeholders = ','.join(['?' for _ in patient_medications])
30
+
31
+ conditions = [f"LOWER(drug_2) IN ({placeholders})"]
32
+ params = [med.lower() for med in patient_medications]
33
+
34
+ # Add target drug condition
35
+ conditions.append("LOWER(drug_1) LIKE LOWER(?)")
36
+ params.append(f"%{target_drug}%")
37
+
38
+ if severity_filter:
39
+ conditions.append("severity = ?")
40
+ params.append(severity_filter)
41
+
42
+ where_clause = " AND ".join(conditions)
43
+
44
+ query = f"""
45
+ SELECT
46
+ drug_1,
47
+ drug_2,
48
+ interaction_description,
49
+ severity
50
+ FROM drug_interaction_lookup
51
+ WHERE {where_clause}
52
+ ORDER BY
53
+ CASE severity
54
+ WHEN 'major' THEN 1
55
+ WHEN 'moderate' THEN 2
56
+ WHEN 'minor' THEN 3
57
+ ELSE 4
58
+ END
59
+ """
60
+
61
+ return execute_query(query, tuple(params))
62
+
63
+
64
+ def check_single_interaction(drug_1: str, drug_2: str) -> Optional[dict]:
65
+ """
66
+ Check for interaction between two specific drugs.
67
+
68
+ Args:
69
+ drug_1: First drug name
70
+ drug_2: Second drug name
71
+
72
+ Returns:
73
+ Interaction details or None if no interaction found
74
+ """
75
+ query = """
76
+ SELECT
77
+ drug_1,
78
+ drug_2,
79
+ interaction_description,
80
+ severity
81
+ FROM drug_interaction_lookup
82
+ WHERE (LOWER(drug_1) LIKE LOWER(?) AND LOWER(drug_2) LIKE LOWER(?))
83
+ LIMIT 1
84
+ """
85
+
86
+ results = execute_query(query, (f"%{drug_1}%", f"%{drug_2}%"))
87
+ return results[0] if results else None
88
+
89
+
90
+ def get_all_interactions_for_drug(drug: str) -> list[dict]:
91
+ """
92
+ Get all known interactions for a specific drug.
93
+
94
+ Args:
95
+ drug: Drug name to check
96
+
97
+ Returns:
98
+ List of all interactions involving this drug
99
+ """
100
+ query = """
101
+ SELECT
102
+ drug_1,
103
+ drug_2,
104
+ interaction_description,
105
+ severity
106
+ FROM drug_interaction_lookup
107
+ WHERE LOWER(drug_1) LIKE LOWER(?)
108
+ ORDER BY
109
+ CASE severity
110
+ WHEN 'major' THEN 1
111
+ WHEN 'moderate' THEN 2
112
+ WHEN 'minor' THEN 3
113
+ ELSE 4
114
+ END
115
+ LIMIT 100
116
+ """
117
+
118
+ return execute_query(query, (f"%{drug}%",))
119
+
120
+
121
+ def get_major_interactions_for_drug(drug: str) -> list[dict]:
122
+ """
123
+ Get only major interactions for a specific drug.
124
+
125
+ Args:
126
+ drug: Drug name to check
127
+
128
+ Returns:
129
+ List of major severity interactions
130
+ """
131
+ query = """
132
+ SELECT
133
+ drug_1,
134
+ drug_2,
135
+ interaction_description
136
+ FROM drug_interaction_lookup
137
+ WHERE LOWER(drug_1) LIKE LOWER(?)
138
+ AND severity = 'major'
139
+ LIMIT 50
140
+ """
141
+
142
+ return execute_query(query, (f"%{drug}%",))
143
+
144
+
145
+ def screen_antibiotic_safety(
146
+ antibiotic: str,
147
+ patient_medications: list[str],
148
+ patient_allergies: list[str] = None
149
+ ) -> dict:
150
+ """
151
+ Comprehensive safety screening for an antibiotic choice.
152
+
153
+ Args:
154
+ antibiotic: Proposed antibiotic
155
+ patient_medications: List of current medications
156
+ patient_allergies: List of known allergies (optional)
157
+
158
+ Returns:
159
+ Safety assessment with interactions and alerts
160
+
161
+ Used by: Agent 4 (Clinical Pharmacologist)
162
+ """
163
+ safety_report = {
164
+ "antibiotic": antibiotic,
165
+ "safe_to_use": True,
166
+ "alerts": [],
167
+ "interactions": [],
168
+ "allergy_warnings": []
169
+ }
170
+
171
+ # Check drug interactions
172
+ interactions = check_drug_interactions(antibiotic, patient_medications)
173
+
174
+ if interactions:
175
+ safety_report["interactions"] = interactions
176
+
177
+ # Check for major interactions
178
+ major = [i for i in interactions if i.get('severity') == 'major']
179
+ moderate = [i for i in interactions if i.get('severity') == 'moderate']
180
+
181
+ if major:
182
+ safety_report["safe_to_use"] = False
183
+ safety_report["alerts"].append({
184
+ "level": "CRITICAL",
185
+ "message": f"Found {len(major)} major drug interaction(s). Review required before prescribing."
186
+ })
187
+
188
+ if moderate:
189
+ safety_report["alerts"].append({
190
+ "level": "WARNING",
191
+ "message": f"Found {len(moderate)} moderate drug interaction(s). Consider dose adjustment or monitoring."
192
+ })
193
+
194
+ # Check allergies (basic check for cross-reactivity)
195
+ if patient_allergies:
196
+ antibiotic_lower = antibiotic.lower()
197
+
198
+ # Common antibiotic class cross-reactivity patterns
199
+ cross_reactivity = {
200
+ "penicillin": ["amoxicillin", "ampicillin", "piperacillin", "cephalosporin"],
201
+ "cephalosporin": ["ceftriaxone", "cefotaxime", "ceftazidime", "cefepime"],
202
+ "sulfa": ["sulfamethoxazole", "trimethoprim-sulfamethoxazole", "bactrim"],
203
+ "fluoroquinolone": ["ciprofloxacin", "levofloxacin", "moxifloxacin"],
204
+ }
205
+
206
+ for allergy in patient_allergies:
207
+ allergy_lower = allergy.lower()
208
+
209
+ # Direct match
210
+ if allergy_lower in antibiotic_lower:
211
+ safety_report["safe_to_use"] = False
212
+ safety_report["allergy_warnings"].append({
213
+ "level": "CRITICAL",
214
+ "message": f"Patient has documented allergy to {allergy}. CONTRAINDICATED."
215
+ })
216
+
217
+ # Cross-reactivity check
218
+ for allergen, related in cross_reactivity.items():
219
+ if allergen in allergy_lower:
220
+ for related_drug in related:
221
+ if related_drug in antibiotic_lower:
222
+ safety_report["alerts"].append({
223
+ "level": "WARNING",
224
+ "message": f"Potential cross-reactivity: Patient allergic to {allergy}, {antibiotic} is in related class."
225
+ })
226
+
227
+ # Summary
228
+ if safety_report["safe_to_use"]:
229
+ safety_report["summary"] = "No critical safety concerns identified."
230
+ else:
231
+ safety_report["summary"] = "SAFETY CONCERNS IDENTIFIED - Review required before prescribing."
232
+
233
+ return safety_report
234
+
235
+
236
+ def get_interaction_statistics() -> dict:
237
+ """Get statistics about the drug interaction database."""
238
+ queries = {
239
+ "total": "SELECT COUNT(*) as count FROM drug_interactions",
240
+ "major": "SELECT COUNT(*) as count FROM drug_interactions WHERE severity = 'major'",
241
+ "moderate": "SELECT COUNT(*) as count FROM drug_interactions WHERE severity = 'moderate'",
242
+ "minor": "SELECT COUNT(*) as count FROM drug_interactions WHERE severity = 'minor'",
243
+ }
244
+
245
+ stats = {}
246
+ for key, query in queries.items():
247
+ result = execute_query(query)
248
+ stats[key] = result[0]['count'] if result else 0
249
+
250
+ return stats
uv.lock CHANGED
@@ -2041,6 +2041,7 @@ dependencies = [
2041
  { name = "langchain-text-splitters" },
2042
  { name = "langgraph" },
2043
  { name = "openpyxl" },
 
2044
  { name = "pillow" },
2045
  { name = "pydantic" },
2046
  { name = "pypdf" },
@@ -2065,6 +2066,7 @@ requires-dist = [
2065
  { name = "langchain-text-splitters" },
2066
  { name = "langgraph", specifier = ">=0.0.15" },
2067
  { name = "openpyxl" },
 
2068
  { name = "pillow" },
2069
  { name = "pydantic", specifier = ">=2.0" },
2070
  { name = "pypdf" },
 
2041
  { name = "langchain-text-splitters" },
2042
  { name = "langgraph" },
2043
  { name = "openpyxl" },
2044
+ { name = "pandas" },
2045
  { name = "pillow" },
2046
  { name = "pydantic" },
2047
  { name = "pypdf" },
 
2066
  { name = "langchain-text-splitters" },
2067
  { name = "langgraph", specifier = ">=0.0.15" },
2068
  { name = "openpyxl" },
2069
+ { name = "pandas", specifier = ">=2.0.0" },
2070
  { name = "pillow" },
2071
  { name = "pydantic", specifier = ">=2.0" },
2072
  { name = "pypdf" },