Add ChromaDB vector store for unstructured document retrieval and import functionality
Browse files- .gitignore +3 -0
- app.py +451 -0
- docs/KNOWLEDGE_STORAGE_STRATEGY.md +611 -0
- pyproject.toml +2 -1
- setup_demo.py +57 -0
- src/__init__.py +1 -0
- src/agents.py +16 -0
- src/agents/__init__.py +0 -0
- src/db/__init__.py +34 -0
- src/db/database.py +61 -0
- src/db/import_data.py +382 -0
- src/db/schema.sql +108 -0
- src/db/vector_store.py +312 -0
- src/tools/__init__.py +67 -0
- src/tools/antibiotic_tools.py +210 -0
- src/tools/rag_tools.py +185 -0
- src/tools/resistance_tools.py +244 -0
- src/tools/safety_tools.py +250 -0
- uv.lock +2 -0
.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 = "
|
| 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" },
|