Spaces:
Running
Running
Upload 12 files
Browse files- src/__init__.py +12 -0
- src/models/biomedical_llm.py +425 -0
- src/models/chatbot.py +376 -0
- src/models/ddi_processor.py +270 -0
- src/models/drug_interaction_db.py +143 -0
- src/requirements.txt +15 -0
- src/web/__pycache__/app.cpython-310.pyc +0 -0
- src/web/app.py +189 -0
- src/web/run_web.py +19 -0
- src/web/static/css/style.css +120 -0
- src/web/static/js/main.js +156 -0
- src/web/templates/index.html +134 -0
src/__init__.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Drug Interaction Analysis System
|
3 |
+
A comprehensive system for analyzing drug interactions using biomedical language models and network analysis.
|
4 |
+
"""
|
5 |
+
|
6 |
+
from .models.biomedical_llm import BiomedicalLLM
|
7 |
+
from .models.drug_interaction_db import DrugInteractionDatabase
|
8 |
+
from .models.ddi_processor import DDIProcessor
|
9 |
+
from .models.chatbot import DrugInteractionChatbot
|
10 |
+
|
11 |
+
__version__ = "1.0.0"
|
12 |
+
__all__ = ['BiomedicalLLM', 'DrugInteractionDatabase', 'DDIProcessor', 'DrugInteractionChatbot']
|
src/models/biomedical_llm.py
ADDED
@@ -0,0 +1,425 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Biomedical Language Model for drug interaction analysis."""
|
2 |
+
|
3 |
+
import torch
|
4 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
5 |
+
import json
|
6 |
+
import re
|
7 |
+
|
8 |
+
class BiomedicalLLM:
|
9 |
+
"""Class to handle biomedical language model inference"""
|
10 |
+
|
11 |
+
def __init__(self, model_name="stanford-crfm/BioMedLM"):
|
12 |
+
"""
|
13 |
+
Initialize the Biomedical Language Model
|
14 |
+
|
15 |
+
Args:
|
16 |
+
model_name: The name of the model to use (default: BioMedLM)
|
17 |
+
Options include:
|
18 |
+
- "stanford-crfm/BioMedLM"
|
19 |
+
- "microsoft/biogpt"
|
20 |
+
"""
|
21 |
+
self.model_name = model_name
|
22 |
+
try:
|
23 |
+
print(f"Loading {model_name}...")
|
24 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
|
25 |
+
# Set pad token if it doesn't exist
|
26 |
+
if self.tokenizer.pad_token is None:
|
27 |
+
self.tokenizer.pad_token = self.tokenizer.eos_token
|
28 |
+
|
29 |
+
self.model = AutoModelForCausalLM.from_pretrained(
|
30 |
+
model_name,
|
31 |
+
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
|
32 |
+
device_map="auto" if torch.cuda.is_available() else None,
|
33 |
+
pad_token_id=self.tokenizer.pad_token_id
|
34 |
+
)
|
35 |
+
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
36 |
+
print(f"Model loaded successfully on {self.device}")
|
37 |
+
except Exception as e:
|
38 |
+
print(f"Error loading model: {e}")
|
39 |
+
print("Falling back to API-based approach or stub implementation")
|
40 |
+
self.tokenizer = None
|
41 |
+
self.model = None
|
42 |
+
|
43 |
+
def extract_ddi_from_literature(self, drug1, drug2):
|
44 |
+
"""
|
45 |
+
Extract drug-drug interaction information from biomedical literature
|
46 |
+
|
47 |
+
Args:
|
48 |
+
drug1: Name of the first drug
|
49 |
+
drug2: Name of the second drug
|
50 |
+
|
51 |
+
Returns:
|
52 |
+
A list of extracted interactions with evidence
|
53 |
+
"""
|
54 |
+
if self.model is None or self.tokenizer is None:
|
55 |
+
# Fallback behavior if model failed to load
|
56 |
+
return self._fallback_extract_ddi(drug1, drug2)
|
57 |
+
|
58 |
+
try:
|
59 |
+
# Construct a prompt for the model
|
60 |
+
prompt = f"""
|
61 |
+
Analyze the scientific literature for interactions between {drug1} and {drug2}.
|
62 |
+
Include the following information:
|
63 |
+
1. Description of the interaction mechanism
|
64 |
+
2. Severity (Mild, Moderate, Severe)
|
65 |
+
3. Clinical significance
|
66 |
+
4. Management recommendations
|
67 |
+
|
68 |
+
Format the response as JSON with the following structure:
|
69 |
+
{{
|
70 |
+
"interactions": [
|
71 |
+
{{
|
72 |
+
"description": "Description of mechanism",
|
73 |
+
"severity": "Severity level",
|
74 |
+
"evidence": "Evidence from literature",
|
75 |
+
"management": "Management recommendation"
|
76 |
+
}}
|
77 |
+
]
|
78 |
+
}}
|
79 |
+
"""
|
80 |
+
|
81 |
+
# Generate completion with proper attention mask
|
82 |
+
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
|
83 |
+
|
84 |
+
# Ensure attention mask is set properly
|
85 |
+
if 'attention_mask' not in inputs:
|
86 |
+
inputs['attention_mask'] = torch.ones_like(inputs['input_ids'])
|
87 |
+
|
88 |
+
# Generate with appropriate parameters
|
89 |
+
with torch.no_grad():
|
90 |
+
outputs = self.model.generate(
|
91 |
+
inputs.input_ids,
|
92 |
+
attention_mask=inputs.attention_mask,
|
93 |
+
max_new_tokens=512,
|
94 |
+
temperature=0.7,
|
95 |
+
top_p=0.9,
|
96 |
+
do_sample=True
|
97 |
+
)
|
98 |
+
|
99 |
+
# Decode the response
|
100 |
+
response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
|
101 |
+
|
102 |
+
# Extract the JSON part of the response
|
103 |
+
json_match = re.search(r'({[\s\S]*})', response)
|
104 |
+
if json_match:
|
105 |
+
json_str = json_match.group(1)
|
106 |
+
try:
|
107 |
+
return json.loads(json_str)
|
108 |
+
except:
|
109 |
+
# If JSON parsing fails, return a structured response anyway
|
110 |
+
return self._extract_structured_info(response, drug1, drug2)
|
111 |
+
else:
|
112 |
+
# If no JSON found, extract structured information
|
113 |
+
return self._extract_structured_info(response, drug1, drug2)
|
114 |
+
|
115 |
+
except Exception as e:
|
116 |
+
print(f"Error in LLM inference: {e}")
|
117 |
+
return self._fallback_extract_ddi(drug1, drug2)
|
118 |
+
|
119 |
+
def _extract_structured_info(self, text, drug1, drug2):
|
120 |
+
"""Extract structured information from text if JSON parsing fails"""
|
121 |
+
# Try to identify descriptions, severity, etc.
|
122 |
+
severity_match = re.search(r'(mild|moderate|severe)', text.lower())
|
123 |
+
severity = severity_match.group(1).capitalize() if severity_match else "Unknown"
|
124 |
+
|
125 |
+
# Default structured response
|
126 |
+
return {
|
127 |
+
"interactions": [
|
128 |
+
{
|
129 |
+
"description": f"Potential interaction between {drug1} and {drug2} identified in literature",
|
130 |
+
"severity": severity,
|
131 |
+
"evidence": "Based on biomedical literature analysis",
|
132 |
+
"management": "Consult healthcare provider for specific guidance"
|
133 |
+
}
|
134 |
+
]
|
135 |
+
}
|
136 |
+
|
137 |
+
def _fallback_extract_ddi(self, drug1, drug2):
|
138 |
+
"""Fallback method when model is not available"""
|
139 |
+
# Return a structured response with disclaimer
|
140 |
+
return {
|
141 |
+
"interactions": [
|
142 |
+
{
|
143 |
+
"description": f"Potential interaction between {drug1} and {drug2}",
|
144 |
+
"severity": "Unknown",
|
145 |
+
"evidence": "Please consult literature for evidence",
|
146 |
+
"management": "Consult healthcare provider for guidance"
|
147 |
+
}
|
148 |
+
],
|
149 |
+
"note": "Biomedical model not available - using fallback information"
|
150 |
+
}
|
151 |
+
|
152 |
+
def analyze_clinical_notes(self, clinical_text):
|
153 |
+
"""
|
154 |
+
Extract drug mentions and potential interactions from clinical notes
|
155 |
+
|
156 |
+
Args:
|
157 |
+
clinical_text: The clinical notes text to analyze
|
158 |
+
|
159 |
+
Returns:
|
160 |
+
A dictionary with extracted drugs and potential interactions
|
161 |
+
"""
|
162 |
+
if self.model is None or self.tokenizer is None:
|
163 |
+
# Fallback behavior if model failed to load
|
164 |
+
return self._fallback_analyze_clinical_notes(clinical_text)
|
165 |
+
|
166 |
+
try:
|
167 |
+
# Construct a prompt for the model
|
168 |
+
prompt = f"""
|
169 |
+
Extract all medication mentions and potential drug interactions from the following clinical note:
|
170 |
+
|
171 |
+
{clinical_text}
|
172 |
+
|
173 |
+
Format the response as JSON with the following structure:
|
174 |
+
{{
|
175 |
+
"medications": [
|
176 |
+
{{
|
177 |
+
"name": "Drug name",
|
178 |
+
"dosage": "Dosage if mentioned",
|
179 |
+
"frequency": "Frequency if mentioned"
|
180 |
+
}}
|
181 |
+
],
|
182 |
+
"potential_interactions": [
|
183 |
+
{{
|
184 |
+
"drug1": "First drug name",
|
185 |
+
"drug2": "Second drug name",
|
186 |
+
"concern": "Description of potential interaction"
|
187 |
+
}}
|
188 |
+
]
|
189 |
+
}}
|
190 |
+
"""
|
191 |
+
|
192 |
+
# Generate completion with proper attention mask
|
193 |
+
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
|
194 |
+
|
195 |
+
# Ensure attention mask is set properly
|
196 |
+
if 'attention_mask' not in inputs:
|
197 |
+
inputs['attention_mask'] = torch.ones_like(inputs['input_ids'])
|
198 |
+
|
199 |
+
# Generate with appropriate parameters
|
200 |
+
with torch.no_grad():
|
201 |
+
outputs = self.model.generate(
|
202 |
+
inputs.input_ids,
|
203 |
+
attention_mask=inputs.attention_mask,
|
204 |
+
max_new_tokens=512,
|
205 |
+
temperature=0.7,
|
206 |
+
top_p=0.9,
|
207 |
+
do_sample=True
|
208 |
+
)
|
209 |
+
|
210 |
+
# Decode the response
|
211 |
+
response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
|
212 |
+
|
213 |
+
# Extract the JSON part of the response
|
214 |
+
json_match = re.search(r'({[\s\S]*})', response)
|
215 |
+
if json_match:
|
216 |
+
json_str = json_match.group(1)
|
217 |
+
try:
|
218 |
+
return json.loads(json_str)
|
219 |
+
except:
|
220 |
+
# If JSON parsing fails, return a structured response anyway
|
221 |
+
return self._extract_medications_from_text(response)
|
222 |
+
else:
|
223 |
+
# If no JSON found, extract structured information
|
224 |
+
return self._extract_medications_from_text(response)
|
225 |
+
|
226 |
+
except Exception as e:
|
227 |
+
print(f"Error in LLM inference: {e}")
|
228 |
+
return self._fallback_analyze_clinical_notes(clinical_text)
|
229 |
+
|
230 |
+
def _extract_medications_from_text(self, text):
|
231 |
+
"""Extract medication mentions from text if JSON parsing fails"""
|
232 |
+
# Simple regex-based extraction
|
233 |
+
drug_patterns = [
|
234 |
+
r'([A-Za-z]+)\s+(\d+\s*mg)',
|
235 |
+
r'([A-Za-z]+)\s+(\d+\s*mcg)',
|
236 |
+
r'([A-Za-z]+)\s+(\d+\s*ml)',
|
237 |
+
r'([A-Za-z]+)\s+(\d+\s*tablet)',
|
238 |
+
r'prescribe[d]?\s+([A-Za-z]+)',
|
239 |
+
r'taking\s+([A-Za-z]+)',
|
240 |
+
r'administer[ed]?\s+([A-Za-z]+)'
|
241 |
+
]
|
242 |
+
|
243 |
+
medications = []
|
244 |
+
for pattern in drug_patterns:
|
245 |
+
matches = re.finditer(pattern, text, re.IGNORECASE)
|
246 |
+
for match in matches:
|
247 |
+
if len(match.groups()) > 1:
|
248 |
+
drug_name = match.group(1)
|
249 |
+
dosage = match.group(2)
|
250 |
+
medications.append({"name": drug_name, "dosage": dosage, "frequency": "Not specified"})
|
251 |
+
else:
|
252 |
+
drug_name = match.group(1)
|
253 |
+
medications.append({"name": drug_name, "dosage": "Not specified", "frequency": "Not specified"})
|
254 |
+
|
255 |
+
# Return structured data
|
256 |
+
return {
|
257 |
+
"medications": medications,
|
258 |
+
"potential_interactions": []
|
259 |
+
}
|
260 |
+
|
261 |
+
def _fallback_analyze_clinical_notes(self, clinical_text):
|
262 |
+
"""Fallback method for clinical note analysis when model is not available"""
|
263 |
+
# Return a structured response with disclaimer
|
264 |
+
return {
|
265 |
+
"medications": [],
|
266 |
+
"potential_interactions": [],
|
267 |
+
"note": "Biomedical model not available - please review clinical notes manually"
|
268 |
+
}
|
269 |
+
|
270 |
+
def get_drug_information(self, drug_name):
|
271 |
+
"""
|
272 |
+
Get detailed information about a specific drug
|
273 |
+
|
274 |
+
Args:
|
275 |
+
drug_name: Name of the drug
|
276 |
+
|
277 |
+
Returns:
|
278 |
+
A dictionary with drug information
|
279 |
+
"""
|
280 |
+
if self.model is None or self.tokenizer is None:
|
281 |
+
# Fallback behavior if model failed to load
|
282 |
+
return self._fallback_drug_information(drug_name)
|
283 |
+
|
284 |
+
try:
|
285 |
+
# Construct a prompt for the model
|
286 |
+
prompt = f"""
|
287 |
+
Provide comprehensive information about the medication {drug_name}, including:
|
288 |
+
1. Drug class
|
289 |
+
2. Mechanism of action
|
290 |
+
3. Common indications
|
291 |
+
4. Common side effects
|
292 |
+
5. Common drug interactions
|
293 |
+
6. Contraindications
|
294 |
+
|
295 |
+
Format the response as JSON with the following structure:
|
296 |
+
{{
|
297 |
+
"drug_name": "{drug_name}",
|
298 |
+
"drug_class": "Drug class",
|
299 |
+
"mechanism": "Mechanism of action",
|
300 |
+
"indications": ["Indication 1", "Indication 2"],
|
301 |
+
"side_effects": ["Side effect 1", "Side effect 2"],
|
302 |
+
"common_interactions": ["Drug 1", "Drug 2"],
|
303 |
+
"contraindications": ["Contraindication 1", "Contraindication 2"]
|
304 |
+
}}
|
305 |
+
"""
|
306 |
+
|
307 |
+
# Generate completion with proper attention mask
|
308 |
+
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
|
309 |
+
|
310 |
+
# Ensure attention mask is set properly
|
311 |
+
if 'attention_mask' not in inputs:
|
312 |
+
inputs['attention_mask'] = torch.ones_like(inputs['input_ids'])
|
313 |
+
|
314 |
+
# Generate with appropriate parameters
|
315 |
+
with torch.no_grad():
|
316 |
+
outputs = self.model.generate(
|
317 |
+
inputs.input_ids,
|
318 |
+
attention_mask=inputs.attention_mask,
|
319 |
+
max_new_tokens=512,
|
320 |
+
temperature=0.7,
|
321 |
+
top_p=0.9,
|
322 |
+
do_sample=True
|
323 |
+
)
|
324 |
+
|
325 |
+
# Decode the response
|
326 |
+
response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
|
327 |
+
|
328 |
+
# Extract the JSON part of the response
|
329 |
+
json_match = re.search(r'({[\s\S]*})', response)
|
330 |
+
if json_match:
|
331 |
+
json_str = json_match.group(1)
|
332 |
+
try:
|
333 |
+
return json.loads(json_str)
|
334 |
+
except:
|
335 |
+
# If JSON parsing fails, return a structured response anyway
|
336 |
+
return self._extract_drug_info_from_text(response, drug_name)
|
337 |
+
else:
|
338 |
+
# If no JSON found, extract structured information
|
339 |
+
return self._extract_drug_info_from_text(response, drug_name)
|
340 |
+
|
341 |
+
except Exception as e:
|
342 |
+
print(f"Error in LLM inference: {e}")
|
343 |
+
return self._fallback_drug_information(drug_name)
|
344 |
+
|
345 |
+
def _extract_drug_info_from_text(self, text, drug_name):
|
346 |
+
"""Extract drug information from text if JSON parsing fails"""
|
347 |
+
# Create default structure
|
348 |
+
drug_info = {
|
349 |
+
"drug_name": drug_name,
|
350 |
+
"drug_class": "Not specified",
|
351 |
+
"mechanism": "Not specified",
|
352 |
+
"indications": [],
|
353 |
+
"side_effects": [],
|
354 |
+
"common_interactions": [],
|
355 |
+
"contraindications": []
|
356 |
+
}
|
357 |
+
|
358 |
+
# Try to extract each section
|
359 |
+
class_match = re.search(r'[Cc]lass:?\s+([^\n\.]+)', text)
|
360 |
+
if class_match:
|
361 |
+
drug_info["drug_class"] = class_match.group(1).strip()
|
362 |
+
|
363 |
+
mechanism_match = re.search(r'[Mm]echanism:?\s+([^\n\.]+)', text)
|
364 |
+
if mechanism_match:
|
365 |
+
drug_info["mechanism"] = mechanism_match.group(1).strip()
|
366 |
+
|
367 |
+
# Extract lists with regex
|
368 |
+
indication_match = re.search(r'[Ii]ndications?:?\s+((?:[^\n]+\n?)+)', text)
|
369 |
+
if indication_match:
|
370 |
+
indications_text = indication_match.group(1)
|
371 |
+
# Split by common list markers
|
372 |
+
indications = re.findall(r'(?:^|\n)\s*(?:\d+\.|\*|-|•)\s*([^\n]+)', indications_text)
|
373 |
+
if indications:
|
374 |
+
drug_info["indications"] = [ind.strip() for ind in indications]
|
375 |
+
elif indications_text:
|
376 |
+
# If no list markers, just split by commas or newlines
|
377 |
+
items = re.split(r',|\n', indications_text)
|
378 |
+
drug_info["indications"] = [item.strip() for item in items if item.strip()]
|
379 |
+
|
380 |
+
# Similarly for other list-based fields
|
381 |
+
side_effects_match = re.search(r'[Ss]ide [Ee]ffects:?\s+((?:[^\n]+\n?)+)', text)
|
382 |
+
if side_effects_match:
|
383 |
+
side_effects_text = side_effects_match.group(1)
|
384 |
+
side_effects = re.findall(r'(?:^|\n)\s*(?:\d+\.|\*|-|•)\s*([^\n]+)', side_effects_text)
|
385 |
+
if side_effects:
|
386 |
+
drug_info["side_effects"] = [se.strip() for se in side_effects]
|
387 |
+
elif side_effects_text:
|
388 |
+
items = re.split(r',|\n', side_effects_text)
|
389 |
+
drug_info["side_effects"] = [item.strip() for item in items if item.strip()]
|
390 |
+
|
391 |
+
interactions_match = re.search(r'[Ii]nteractions:?\s+((?:[^\n]+\n?)+)', text)
|
392 |
+
if interactions_match:
|
393 |
+
interactions_text = interactions_match.group(1)
|
394 |
+
interactions = re.findall(r'(?:^|\n)\s*(?:\d+\.|\*|-|•)\s*([^\n]+)', interactions_text)
|
395 |
+
if interactions:
|
396 |
+
drug_info["common_interactions"] = [inter.strip() for inter in interactions]
|
397 |
+
elif interactions_text:
|
398 |
+
items = re.split(r',|\n', interactions_text)
|
399 |
+
drug_info["common_interactions"] = [item.strip() for item in items if item.strip()]
|
400 |
+
|
401 |
+
contraindications_match = re.search(r'[Cc]ontraindications:?\s+((?:[^\n]+\n?)+)', text)
|
402 |
+
if contraindications_match:
|
403 |
+
contraindications_text = contraindications_match.group(1)
|
404 |
+
contraindications = re.findall(r'(?:^|\n)\s*(?:\d+\.|\*|-|•)\s*([^\n]+)', contraindications_text)
|
405 |
+
if contraindications:
|
406 |
+
drug_info["contraindications"] = [contra.strip() for contra in contraindications]
|
407 |
+
elif contraindications_text:
|
408 |
+
items = re.split(r',|\n', contraindications_text)
|
409 |
+
drug_info["contraindications"] = [item.strip() for item in items if item.strip()]
|
410 |
+
|
411 |
+
return drug_info
|
412 |
+
|
413 |
+
def _fallback_drug_information(self, drug_name):
|
414 |
+
"""Fallback method for drug information when model is not available"""
|
415 |
+
# Return a structured response with disclaimer
|
416 |
+
return {
|
417 |
+
"drug_name": drug_name,
|
418 |
+
"drug_class": "Information not available",
|
419 |
+
"mechanism": "Information not available",
|
420 |
+
"indications": ["Information not available"],
|
421 |
+
"side_effects": ["Information not available"],
|
422 |
+
"common_interactions": ["Information not available"],
|
423 |
+
"contraindications": ["Information not available"],
|
424 |
+
"note": "Biomedical model not available - using fallback information"
|
425 |
+
}
|
src/models/chatbot.py
ADDED
@@ -0,0 +1,376 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Drug Interaction Chatbot for natural language interaction with the drug interaction system."""
|
2 |
+
|
3 |
+
import re
|
4 |
+
import uuid
|
5 |
+
import io
|
6 |
+
import matplotlib.pyplot as plt
|
7 |
+
import networkx as nx
|
8 |
+
from typing import Dict, List, Tuple, Optional, Any
|
9 |
+
|
10 |
+
from .biomedical_llm import BiomedicalLLM
|
11 |
+
from .drug_interaction_db import DrugInteractionDatabase
|
12 |
+
from .ddi_processor import DDIProcessor
|
13 |
+
|
14 |
+
class DrugInteractionChatbot:
|
15 |
+
"""Chatbot interface for drug interaction analysis system."""
|
16 |
+
|
17 |
+
def __init__(self, model_name="stanford-crfm/BioMedLM"):
|
18 |
+
"""Initialize the Drug Interaction Chatbot with Biomedical LLM"""
|
19 |
+
self.db = DrugInteractionDatabase()
|
20 |
+
self.bio_llm = BiomedicalLLM(model_name)
|
21 |
+
self.processor = DDIProcessor(self.db, self.bio_llm)
|
22 |
+
|
23 |
+
def process_message(self, message):
|
24 |
+
"""Process a user message and provide an appropriate response"""
|
25 |
+
# Check if this is a clinical notes analysis request
|
26 |
+
if any(term in message.lower() for term in ["clinical note", "patient note", "extract from", "analyze note", "medical record"]):
|
27 |
+
# Extract the clinical note part
|
28 |
+
note_pattern = r"(?:clinical note|patient note|medical record)[s]?:?\s*([\s\S]+)$"
|
29 |
+
note_match = re.search(note_pattern, message, re.IGNORECASE)
|
30 |
+
|
31 |
+
if note_match:
|
32 |
+
clinical_text = note_match.group(1).strip()
|
33 |
+
extracted_info = self.processor.extract_drugs_from_clinical_notes(clinical_text)
|
34 |
+
|
35 |
+
# Format the response
|
36 |
+
response = "📋 **Analysis of Clinical Notes**\n\n"
|
37 |
+
|
38 |
+
# Add medications
|
39 |
+
if extracted_info["medications"]:
|
40 |
+
response += "**Medications Identified:**\n"
|
41 |
+
for med in extracted_info["medications"]:
|
42 |
+
name = med.get("name", "Unknown")
|
43 |
+
dosage = med.get("dosage", "Not specified")
|
44 |
+
frequency = med.get("frequency", "Not specified")
|
45 |
+
|
46 |
+
if dosage != "Not specified" or frequency != "Not specified":
|
47 |
+
response += f"- {name}: {dosage} {frequency}\n"
|
48 |
+
else:
|
49 |
+
response += f"- {name}\n"
|
50 |
+
response += "\n"
|
51 |
+
else:
|
52 |
+
response += "No medications were identified in the clinical notes.\n\n"
|
53 |
+
|
54 |
+
# Add potential interactions
|
55 |
+
if extracted_info["potential_interactions"]:
|
56 |
+
response += "**Potential Interactions:**\n"
|
57 |
+
for interaction in extracted_info["potential_interactions"]:
|
58 |
+
drug1 = interaction.get("drug1", "Unknown")
|
59 |
+
drug2 = interaction.get("drug2", "Unknown")
|
60 |
+
concern = interaction.get("concern", "Potential interaction")
|
61 |
+
|
62 |
+
response += f"- {drug1} + {drug2}: {concern}\n"
|
63 |
+
response += "\n"
|
64 |
+
else:
|
65 |
+
# Try to identify interactions from the medications list
|
66 |
+
meds = [med.get("name") for med in extracted_info["medications"] if med.get("name")]
|
67 |
+
potential_interactions = []
|
68 |
+
|
69 |
+
# Check all pairs of medications
|
70 |
+
for i in range(len(meds)):
|
71 |
+
for j in range(i+1, len(meds)):
|
72 |
+
interactions, _ = self.db.get_interactions(meds[i], meds[j])
|
73 |
+
if interactions:
|
74 |
+
for d1, d2, desc, severity, _ in interactions:
|
75 |
+
potential_interactions.append(f"- {meds[i]} + {meds[j]}: {desc} ({severity})")
|
76 |
+
|
77 |
+
if potential_interactions:
|
78 |
+
response += "**Potential Interactions:**\n"
|
79 |
+
response += "\n".join(potential_interactions) + "\n\n"
|
80 |
+
else:
|
81 |
+
response += "No potential interactions were identified among the medications.\n\n"
|
82 |
+
|
83 |
+
response += "Please consult with a healthcare professional for a comprehensive review of drug interactions and medical advice."
|
84 |
+
|
85 |
+
return response
|
86 |
+
|
87 |
+
# Check if user is asking for information about a specific drug
|
88 |
+
drug_info_pattern = r"(tell me about|information on|what is|info about|details on)\s+(.+?)(?:\?|$)"
|
89 |
+
drug_info_match = re.search(drug_info_pattern, message.lower())
|
90 |
+
|
91 |
+
if drug_info_match:
|
92 |
+
drug_name = drug_info_match.group(2).strip()
|
93 |
+
canonical = self.db.search_drug(drug_name)
|
94 |
+
|
95 |
+
# If not in database, use original name but still try to get info
|
96 |
+
if not canonical:
|
97 |
+
canonical = drug_name
|
98 |
+
|
99 |
+
# Get drug information from biomedical LLM
|
100 |
+
drug_info = self.processor.get_drug_information(canonical)
|
101 |
+
|
102 |
+
if drug_info:
|
103 |
+
# Format the response
|
104 |
+
response = f"📊 **Information about {drug_info['drug_name']}**\n\n"
|
105 |
+
|
106 |
+
if drug_info.get("drug_class") and drug_info["drug_class"] != "Information not available":
|
107 |
+
response += f"**Drug Class:** {drug_info['drug_class']}\n\n"
|
108 |
+
|
109 |
+
if drug_info.get("mechanism") and drug_info["mechanism"] != "Information not available":
|
110 |
+
response += f"**Mechanism of Action:** {drug_info['mechanism']}\n\n"
|
111 |
+
|
112 |
+
if drug_info.get("indications") and drug_info["indications"][0] != "Information not available":
|
113 |
+
response += "**Common Indications:**\n"
|
114 |
+
for indication in drug_info["indications"]:
|
115 |
+
response += f"- {indication}\n"
|
116 |
+
response += "\n"
|
117 |
+
|
118 |
+
if drug_info.get("side_effects") and drug_info["side_effects"][0] != "Information not available":
|
119 |
+
response += "**Common Side Effects:**\n"
|
120 |
+
for effect in drug_info["side_effects"]:
|
121 |
+
response += f"- {effect}\n"
|
122 |
+
response += "\n"
|
123 |
+
|
124 |
+
if drug_info.get("common_interactions") and drug_info["common_interactions"][0] != "Information not available":
|
125 |
+
response += "**Common Interactions:**\n"
|
126 |
+
for interaction in drug_info["common_interactions"]:
|
127 |
+
response += f"- {interaction}\n"
|
128 |
+
response += "\n"
|
129 |
+
|
130 |
+
if drug_info.get("contraindications") and drug_info["contraindications"][0] != "Information not available":
|
131 |
+
response += "**Contraindications:**\n"
|
132 |
+
for contraindication in drug_info["contraindications"]:
|
133 |
+
response += f"- {contraindication}\n"
|
134 |
+
response += "\n"
|
135 |
+
|
136 |
+
response += "This information is for educational purposes only. Always consult a healthcare professional for medical advice."
|
137 |
+
|
138 |
+
return response
|
139 |
+
|
140 |
+
else:
|
141 |
+
return f"I couldn't find detailed information about {drug_name}. Please check the spelling or try another medication."
|
142 |
+
|
143 |
+
# Check if this is a drug interaction query
|
144 |
+
if re.search(r'take|interact|safe|drug|interaction|medicine|pill|medication', message.lower()):
|
145 |
+
result = self.processor.process_query(message)
|
146 |
+
|
147 |
+
if result["status"] == "error":
|
148 |
+
return result["message"]
|
149 |
+
|
150 |
+
elif result["status"] == "not_found":
|
151 |
+
return result["message"]
|
152 |
+
|
153 |
+
elif result["status"] == "no_interaction":
|
154 |
+
return (f"Based on our database and biomedical literature analysis, no known interactions were found between {result['drugs'][0]} "
|
155 |
+
f"and {result['drugs'][1]}. However, always consult with a healthcare "
|
156 |
+
f"professional before combining medications.")
|
157 |
+
|
158 |
+
elif result["status"] == "found":
|
159 |
+
drug1, drug2 = result['drugs']
|
160 |
+
interactions = result["interactions"]
|
161 |
+
|
162 |
+
# Generate response
|
163 |
+
response = f"⚠️ **Potential interaction found between {drug1} and {drug2}:**\n\n"
|
164 |
+
|
165 |
+
for i, interaction in enumerate(interactions, 1):
|
166 |
+
severity = interaction["severity"]
|
167 |
+
|
168 |
+
# Add appropriate emoji based on severity
|
169 |
+
if severity.lower() == "severe":
|
170 |
+
emoji = "🔴"
|
171 |
+
elif severity.lower() == "moderate":
|
172 |
+
emoji = "🟠"
|
173 |
+
else:
|
174 |
+
emoji = "🟡"
|
175 |
+
|
176 |
+
response += f"{emoji} **{severity} interaction:** {interaction['description']}\n"
|
177 |
+
response += f" Source: {interaction['source']}\n\n"
|
178 |
+
|
179 |
+
# Add any management recommendations if available
|
180 |
+
try:
|
181 |
+
literature_info = self.bio_llm.extract_ddi_from_literature(drug1, drug2)
|
182 |
+
if "interactions" in literature_info and literature_info["interactions"]:
|
183 |
+
management = literature_info["interactions"][0].get("management")
|
184 |
+
if management:
|
185 |
+
response += f"📝 **Management Recommendation:** {management}\n\n"
|
186 |
+
except:
|
187 |
+
pass
|
188 |
+
|
189 |
+
response += "⚕️ Please consult with a healthcare professional before taking these medications together."
|
190 |
+
|
191 |
+
# Generate visualization
|
192 |
+
try:
|
193 |
+
G, error = self.processor.generate_network(drug1, depth=1)
|
194 |
+
if G:
|
195 |
+
response += "\n\nA visualization of this interaction has been generated."
|
196 |
+
# In a real implementation, we would save the graph image and provide a link or display it
|
197 |
+
except Exception as e:
|
198 |
+
pass # Handle gracefully if visualization fails
|
199 |
+
|
200 |
+
return response
|
201 |
+
|
202 |
+
# Check if the user is asking for all interactions for a specific drug
|
203 |
+
pattern = r"(what|show|list|tell).+?(interaction|interacts).+?(with|for|of)\s+(.+?)(?:\?|$)"
|
204 |
+
match = re.search(pattern, message.lower())
|
205 |
+
if match:
|
206 |
+
drug_name = match.group(4).strip()
|
207 |
+
canonical = self.db.search_drug(drug_name)
|
208 |
+
|
209 |
+
if not canonical:
|
210 |
+
return f"I couldn't find information about '{drug_name}' in our database."
|
211 |
+
|
212 |
+
interactions, _ = self.db.get_all_interactions(canonical)
|
213 |
+
|
214 |
+
if not interactions:
|
215 |
+
return f"No known interactions were found for {canonical} in our database."
|
216 |
+
|
217 |
+
response = f"**Known interactions for {canonical}:**\n\n"
|
218 |
+
|
219 |
+
# Group by severity
|
220 |
+
severe = []
|
221 |
+
moderate = []
|
222 |
+
mild = []
|
223 |
+
|
224 |
+
for _, other_drug, desc, severity, source in interactions:
|
225 |
+
if severity.lower() == "severe":
|
226 |
+
severe.append((other_drug, desc, source))
|
227 |
+
elif severity.lower() == "moderate":
|
228 |
+
moderate.append((other_drug, desc, source))
|
229 |
+
else:
|
230 |
+
mild.append((other_drug, desc, source))
|
231 |
+
|
232 |
+
# Add severe interactions
|
233 |
+
if severe:
|
234 |
+
response += "🔴 **Severe interactions:**\n"
|
235 |
+
for drug, desc, source in severe:
|
236 |
+
response += f"- **{drug}**: {desc} ({source})\n"
|
237 |
+
response += "\n"
|
238 |
+
|
239 |
+
# Add moderate interactions
|
240 |
+
if moderate:
|
241 |
+
response += "🟠 **Moderate interactions:**\n"
|
242 |
+
for drug, desc, source in moderate:
|
243 |
+
response += f"- **{drug}**: {desc} ({source})\n"
|
244 |
+
response += "\n"
|
245 |
+
|
246 |
+
# Add mild interactions
|
247 |
+
if mild:
|
248 |
+
response += "🟡 **Mild interactions:**\n"
|
249 |
+
for drug, desc, source in mild:
|
250 |
+
response += f"- **{drug}**: {desc} ({source})\n"
|
251 |
+
response += "\n"
|
252 |
+
|
253 |
+
response += "Please consult with a healthcare professional for personalized advice."
|
254 |
+
|
255 |
+
return response
|
256 |
+
|
257 |
+
# Check if the user is requesting a visualization
|
258 |
+
if re.search(r'(visualize|visualization|graph|chart|network|diagram).+?(drug|interaction|medicine)', message.lower()):
|
259 |
+
drug_match = re.search(r'(visualize|visualization|graph|chart|network|diagram).+?(for|of|between)\s+(.+?)(?:\?|$)', message.lower())
|
260 |
+
|
261 |
+
if drug_match:
|
262 |
+
drug_name = drug_match.group(3).strip()
|
263 |
+
canonical = self.db.search_drug(drug_name)
|
264 |
+
|
265 |
+
if not canonical:
|
266 |
+
return f"I couldn't find information about '{drug_name}' in our database."
|
267 |
+
|
268 |
+
try:
|
269 |
+
G, error = self.processor.generate_network(canonical, depth=1)
|
270 |
+
if error:
|
271 |
+
return error
|
272 |
+
|
273 |
+
return f"I've generated a network visualization for {canonical}'s interactions. The visualization shows connections to other drugs, with red edges indicating severe interactions, orange for moderate, and yellow for mild interactions."
|
274 |
+
|
275 |
+
except Exception as e:
|
276 |
+
return f"Sorry, I encountered an error while generating the visualization: {str(e)}"
|
277 |
+
|
278 |
+
else:
|
279 |
+
try:
|
280 |
+
G, error = self.processor.generate_network()
|
281 |
+
if error:
|
282 |
+
return error
|
283 |
+
|
284 |
+
return "I've generated a general drug interaction network visualization showing connections between several common drugs. Red edges indicate severe interactions, orange for moderate, and yellow for mild interactions."
|
285 |
+
|
286 |
+
except Exception as e:
|
287 |
+
return f"Sorry, I encountered an error while generating the visualization: {str(e)}"
|
288 |
+
|
289 |
+
# If not specifically about drugs
|
290 |
+
return ("I'm a drug interaction assistant powered by biomedical language models. You can ask me about:\n\n"
|
291 |
+
"1. Potential interactions between medications (e.g., 'Can I take aspirin and warfarin together?')\n"
|
292 |
+
"2. Information about specific drugs (e.g., 'Tell me about metformin')\n"
|
293 |
+
"3. Analysis of clinical notes (e.g., 'Analyze these clinical notes: [paste notes here]')\n"
|
294 |
+
"4. Visualizations of drug interaction networks (e.g., 'Show me a visualization for warfarin')")
|
295 |
+
|
296 |
+
def generate_visualization(self, drug_name=None, depth=1):
|
297 |
+
"""Generate a visualization of drug interactions"""
|
298 |
+
G, error = self.processor.generate_network(drug_name, depth)
|
299 |
+
|
300 |
+
if error:
|
301 |
+
return None, error
|
302 |
+
|
303 |
+
# Create a unique filename
|
304 |
+
viz_id = str(uuid.uuid4())
|
305 |
+
filename = f"static/visualizations/{viz_id}.png"
|
306 |
+
|
307 |
+
# Create the visualization
|
308 |
+
plt.figure(figsize=(12, 10))
|
309 |
+
|
310 |
+
# Get positions
|
311 |
+
pos = nx.spring_layout(G, seed=42)
|
312 |
+
|
313 |
+
# Draw nodes
|
314 |
+
node_sizes = [G.nodes[node].get('size', 10) for node in G.nodes()]
|
315 |
+
node_colors = [G.nodes[node].get('color', 'blue') for node in G.nodes()]
|
316 |
+
nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color=node_colors, alpha=0.8)
|
317 |
+
|
318 |
+
# Draw edges with colors based on severity
|
319 |
+
edge_colors = []
|
320 |
+
edge_widths = []
|
321 |
+
for u, v, data in G.edges(data=True):
|
322 |
+
edge_colors.append(data.get('color', 'gray'))
|
323 |
+
edge_widths.append(data.get('weight', 1))
|
324 |
+
|
325 |
+
nx.draw_networkx_edges(G, pos, edge_color=edge_colors, width=edge_widths, alpha=0.7)
|
326 |
+
|
327 |
+
# Add labels
|
328 |
+
nx.draw_networkx_labels(G, pos, font_size=10, font_family="sans-serif")
|
329 |
+
|
330 |
+
# Save to file
|
331 |
+
plt.axis('off')
|
332 |
+
plt.tight_layout()
|
333 |
+
plt.savefig(filename, format='png', dpi=150)
|
334 |
+
plt.close()
|
335 |
+
|
336 |
+
return filename, None
|
337 |
+
|
338 |
+
def get_visualization_bytes(self, drug_name=None, depth=1):
|
339 |
+
"""Get visualization as bytes for web display"""
|
340 |
+
G, error = self.processor.generate_network(drug_name, depth)
|
341 |
+
|
342 |
+
if error:
|
343 |
+
return None, error
|
344 |
+
|
345 |
+
# Create the visualization
|
346 |
+
plt.figure(figsize=(12, 10))
|
347 |
+
|
348 |
+
# Get positions
|
349 |
+
pos = nx.spring_layout(G, seed=42)
|
350 |
+
|
351 |
+
# Draw nodes
|
352 |
+
node_sizes = [G.nodes[node].get('size', 10) for node in G.nodes()]
|
353 |
+
node_colors = [G.nodes[node].get('color', 'blue') for node in G.nodes()]
|
354 |
+
nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color=node_colors, alpha=0.8)
|
355 |
+
|
356 |
+
# Draw edges with colors based on severity
|
357 |
+
edge_colors = []
|
358 |
+
edge_widths = []
|
359 |
+
for u, v, data in G.edges(data=True):
|
360 |
+
edge_colors.append(data.get('color', 'gray'))
|
361 |
+
edge_widths.append(data.get('weight', 1))
|
362 |
+
|
363 |
+
nx.draw_networkx_edges(G, pos, edge_color=edge_colors, width=edge_widths, alpha=0.7)
|
364 |
+
|
365 |
+
# Add labels
|
366 |
+
nx.draw_networkx_labels(G, pos, font_size=10, font_family="sans-serif")
|
367 |
+
|
368 |
+
# Save to BytesIO
|
369 |
+
buf = io.BytesIO()
|
370 |
+
plt.axis('off')
|
371 |
+
plt.tight_layout()
|
372 |
+
plt.savefig(buf, format='png', dpi=150)
|
373 |
+
buf.seek(0)
|
374 |
+
plt.close()
|
375 |
+
|
376 |
+
return buf, None
|
src/models/ddi_processor.py
ADDED
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Drug-Drug Interaction Processor for analyzing and processing drug interactions."""
|
2 |
+
|
3 |
+
import re
|
4 |
+
import networkx as nx
|
5 |
+
import matplotlib.pyplot as plt
|
6 |
+
from typing import List, Dict, Tuple, Optional
|
7 |
+
|
8 |
+
from .biomedical_llm import BiomedicalLLM
|
9 |
+
from .drug_interaction_db import DrugInteractionDatabase
|
10 |
+
|
11 |
+
class DDIProcessor:
|
12 |
+
def __init__(self, db: DrugInteractionDatabase, bio_llm: BiomedicalLLM):
|
13 |
+
self.db = db
|
14 |
+
self.bio_llm = bio_llm
|
15 |
+
|
16 |
+
def extract_drug_names(self, text):
|
17 |
+
"""Extract potential drug names from text using NLP techniques"""
|
18 |
+
# In a real implementation, this would use advanced NLP
|
19 |
+
# For now, we'll use a simple approach based on keywords and patterns
|
20 |
+
|
21 |
+
# Clean and standardize text
|
22 |
+
text = text.lower()
|
23 |
+
|
24 |
+
# Common question patterns
|
25 |
+
patterns = [
|
26 |
+
r"can\s+i\s+take\s+(.+?)\s+(?:and|with|along\s+with)\s+(.+?)(?:\?|$)",
|
27 |
+
r"is\s+it\s+safe\s+to\s+take\s+(.+?)\s+(?:and|with|along\s+with)\s+(.+?)(?:\?|$)",
|
28 |
+
r"(?:interaction|interactions)\s+between\s+(.+?)\s+and\s+(.+?)(?:\?|$)",
|
29 |
+
r"(?:will|does|do)\s+(.+?)\s+(?:interact|interfere)\s+with\s+(.+?)(?:\?|$)"
|
30 |
+
]
|
31 |
+
|
32 |
+
for pattern in patterns:
|
33 |
+
match = re.search(pattern, text)
|
34 |
+
if match:
|
35 |
+
drug1 = match.group(1).strip()
|
36 |
+
drug2 = match.group(2).strip()
|
37 |
+
return drug1, drug2
|
38 |
+
|
39 |
+
# If no pattern matches, try to find drug names from the database
|
40 |
+
words = text.split()
|
41 |
+
potential_drugs = []
|
42 |
+
|
43 |
+
for word in words:
|
44 |
+
word = word.strip(".,?!()[]{}\"'")
|
45 |
+
if self.db.search_drug(word):
|
46 |
+
potential_drugs.append(word)
|
47 |
+
|
48 |
+
if len(potential_drugs) >= 2:
|
49 |
+
return potential_drugs[0], potential_drugs[1]
|
50 |
+
|
51 |
+
return None, None
|
52 |
+
|
53 |
+
def extract_drugs_from_clinical_notes(self, clinical_text):
|
54 |
+
"""Use BiomedLM to extract drugs from clinical notes"""
|
55 |
+
try:
|
56 |
+
# Use the biomedical LLM to extract drugs and interactions
|
57 |
+
result = self.bio_llm.analyze_clinical_notes(clinical_text)
|
58 |
+
|
59 |
+
# Return the extracted medications
|
60 |
+
return result
|
61 |
+
except Exception as e:
|
62 |
+
print(f"Error extracting drugs from clinical notes: {e}")
|
63 |
+
return {"medications": [], "potential_interactions": []}
|
64 |
+
|
65 |
+
def process_query(self, query):
|
66 |
+
"""Process a natural language query about drug interactions"""
|
67 |
+
drug1, drug2 = self.extract_drug_names(query)
|
68 |
+
|
69 |
+
# If we couldn't extract drug names
|
70 |
+
if not drug1 or not drug2:
|
71 |
+
return {
|
72 |
+
"status": "error",
|
73 |
+
"message": "I couldn't identify the drugs in your question. Please specify the drugs clearly, for example: 'Can I take aspirin and warfarin together?'"
|
74 |
+
}
|
75 |
+
|
76 |
+
# Get drug interactions from database
|
77 |
+
interactions, missing = self.db.get_interactions(drug1, drug2)
|
78 |
+
|
79 |
+
# Try biomedical LLM for additional information, especially if not in database
|
80 |
+
try:
|
81 |
+
literature_info = self.bio_llm.extract_ddi_from_literature(drug1, drug2)
|
82 |
+
if "interactions" in literature_info and literature_info["interactions"]:
|
83 |
+
# Convert LLM information to the format used by the database
|
84 |
+
for interaction in literature_info["interactions"]:
|
85 |
+
# Only add if we don't already have interactions from the database
|
86 |
+
if not interactions:
|
87 |
+
canonical1 = self.db.search_drug(drug1) or drug1
|
88 |
+
canonical2 = self.db.search_drug(drug2) or drug2
|
89 |
+
desc = interaction.get("description", f"Potential interaction between {drug1} and {drug2}")
|
90 |
+
severity = interaction.get("severity", "Unknown")
|
91 |
+
source = interaction.get("evidence", "Biomedical literature analysis")
|
92 |
+
|
93 |
+
interactions.append((canonical1, canonical2, desc, severity, source))
|
94 |
+
|
95 |
+
# Clear missing drugs if LLM found information
|
96 |
+
if missing and interactions:
|
97 |
+
missing = []
|
98 |
+
except Exception as e:
|
99 |
+
print(f"Error getting additional information: {e}")
|
100 |
+
|
101 |
+
# If drugs weren't found
|
102 |
+
if missing:
|
103 |
+
return {
|
104 |
+
"status": "not_found",
|
105 |
+
"missing_drugs": missing,
|
106 |
+
"message": f"I couldn't find information on the following drug(s): {', '.join(missing)}"
|
107 |
+
}
|
108 |
+
|
109 |
+
# Format the results
|
110 |
+
canonical1 = self.db.search_drug(drug1) or drug1
|
111 |
+
canonical2 = self.db.search_drug(drug2) or drug2
|
112 |
+
|
113 |
+
if not interactions:
|
114 |
+
return {
|
115 |
+
"status": "no_interaction",
|
116 |
+
"drugs": [canonical1, canonical2],
|
117 |
+
"message": f"No known interactions were found between {canonical1} and {canonical2} in our database or medical literature. However, please consult with a healthcare professional for personalized advice."
|
118 |
+
}
|
119 |
+
|
120 |
+
# Format the interaction information
|
121 |
+
interaction_details = []
|
122 |
+
for d1, d2, desc, severity, source in interactions:
|
123 |
+
interaction_details.append({
|
124 |
+
"description": desc,
|
125 |
+
"severity": severity,
|
126 |
+
"source": source
|
127 |
+
})
|
128 |
+
|
129 |
+
return {
|
130 |
+
"status": "found",
|
131 |
+
"drugs": [canonical1, canonical2],
|
132 |
+
"interactions": interaction_details
|
133 |
+
}
|
134 |
+
|
135 |
+
def get_drug_information(self, drug_name):
|
136 |
+
"""Get comprehensive information about a drug using biomedical LLM"""
|
137 |
+
try:
|
138 |
+
# First check if the drug exists in our database
|
139 |
+
canonical = self.db.search_drug(drug_name)
|
140 |
+
|
141 |
+
if not canonical:
|
142 |
+
# If not in database, use just the provided name
|
143 |
+
canonical = drug_name
|
144 |
+
|
145 |
+
# Use biomedical LLM to get drug information
|
146 |
+
drug_info = self.bio_llm.get_drug_information(canonical)
|
147 |
+
|
148 |
+
# Add interactions from our database
|
149 |
+
interactions, _ = self.db.get_all_interactions(canonical)
|
150 |
+
interaction_drugs = []
|
151 |
+
|
152 |
+
for d1, d2, _, severity, _ in interactions:
|
153 |
+
other_drug = d2 if d1 == canonical else d1
|
154 |
+
interaction_drugs.append(f"{other_drug} ({severity})")
|
155 |
+
|
156 |
+
# Add to the drug information
|
157 |
+
if interaction_drugs and "common_interactions" in drug_info:
|
158 |
+
# Combine with LLM-provided interactions
|
159 |
+
existing = drug_info["common_interactions"]
|
160 |
+
if existing and existing[0] != "Information not available":
|
161 |
+
drug_info["common_interactions"] = list(set(existing + interaction_drugs))
|
162 |
+
else:
|
163 |
+
drug_info["common_interactions"] = interaction_drugs
|
164 |
+
|
165 |
+
return drug_info
|
166 |
+
|
167 |
+
except Exception as e:
|
168 |
+
print(f"Error getting drug information: {e}")
|
169 |
+
return {
|
170 |
+
"drug_name": drug_name,
|
171 |
+
"drug_class": "Information not available",
|
172 |
+
"mechanism": "Information not available",
|
173 |
+
"indications": ["Information not available"],
|
174 |
+
"side_effects": ["Information not available"],
|
175 |
+
"common_interactions": ["Information not available"],
|
176 |
+
"contraindications": ["Information not available"]
|
177 |
+
}
|
178 |
+
|
179 |
+
def generate_network(self, drug_name=None, depth=1):
|
180 |
+
"""
|
181 |
+
Generate a network visualization of drug interactions
|
182 |
+
If drug_name is provided, show interactions for that drug
|
183 |
+
Otherwise, show a general interaction network
|
184 |
+
"""
|
185 |
+
G = nx.Graph()
|
186 |
+
|
187 |
+
# If a specific drug is provided
|
188 |
+
if drug_name:
|
189 |
+
canonical = self.db.search_drug(drug_name)
|
190 |
+
if not canonical:
|
191 |
+
return None, f"Drug '{drug_name}' not found"
|
192 |
+
|
193 |
+
# Get interactions for this drug
|
194 |
+
interactions, _ = self.db.get_all_interactions(canonical)
|
195 |
+
|
196 |
+
# Add nodes and edges
|
197 |
+
G.add_node(canonical, size=20, color='red')
|
198 |
+
|
199 |
+
for drug1, drug2, desc, severity, _ in interactions:
|
200 |
+
other_drug = drug2 if drug1 == canonical else drug1
|
201 |
+
|
202 |
+
# Add nodes and edges
|
203 |
+
if other_drug not in G:
|
204 |
+
G.add_node(other_drug, size=15, color='blue')
|
205 |
+
|
206 |
+
# Set edge color based on severity
|
207 |
+
if severity == "Severe":
|
208 |
+
edge_color = 'red'
|
209 |
+
weight = 3
|
210 |
+
elif severity == "Moderate":
|
211 |
+
edge_color = 'orange'
|
212 |
+
weight = 2
|
213 |
+
else:
|
214 |
+
edge_color = 'yellow'
|
215 |
+
weight = 1
|
216 |
+
|
217 |
+
G.add_edge(canonical, other_drug, color=edge_color, weight=weight, label=desc)
|
218 |
+
|
219 |
+
# If depth > 1, add secondary interactions
|
220 |
+
if depth > 1:
|
221 |
+
secondary_interactions, _ = self.db.get_all_interactions(other_drug)
|
222 |
+
for sec_d1, sec_d2, sec_desc, sec_severity, _ in secondary_interactions:
|
223 |
+
tertiary_drug = sec_d2 if sec_d1 == other_drug else sec_d1
|
224 |
+
|
225 |
+
# Skip the original drug
|
226 |
+
if tertiary_drug == canonical:
|
227 |
+
continue
|
228 |
+
|
229 |
+
if tertiary_drug not in G:
|
230 |
+
G.add_node(tertiary_drug, size=10, color='green')
|
231 |
+
|
232 |
+
# Set edge color based on severity
|
233 |
+
if sec_severity == "Severe":
|
234 |
+
sec_edge_color = 'red'
|
235 |
+
sec_weight = 3
|
236 |
+
elif sec_severity == "Moderate":
|
237 |
+
sec_edge_color = 'orange'
|
238 |
+
sec_weight = 2
|
239 |
+
else:
|
240 |
+
sec_edge_color = 'yellow'
|
241 |
+
sec_weight = 1
|
242 |
+
|
243 |
+
G.add_edge(other_drug, tertiary_drug, color=sec_edge_color, weight=sec_weight, label=sec_desc)
|
244 |
+
else:
|
245 |
+
# Create a general interaction network with common drugs
|
246 |
+
sample_drugs = self.db.get_all_drugs()[:10] # Limit to 10 drugs for clarity
|
247 |
+
|
248 |
+
for drug in sample_drugs:
|
249 |
+
G.add_node(drug, size=15, color='blue')
|
250 |
+
|
251 |
+
interactions, _ = self.db.get_all_interactions(drug)
|
252 |
+
for d1, d2, desc, severity, _ in interactions:
|
253 |
+
other_drug = d2 if d1 == drug else d1
|
254 |
+
|
255 |
+
# Only add edges between drugs in our sample
|
256 |
+
if other_drug in sample_drugs:
|
257 |
+
# Set edge color based on severity
|
258 |
+
if severity == "Severe":
|
259 |
+
edge_color = 'red'
|
260 |
+
weight = 3
|
261 |
+
elif severity == "Moderate":
|
262 |
+
edge_color = 'orange'
|
263 |
+
weight = 2
|
264 |
+
else:
|
265 |
+
edge_color = 'yellow'
|
266 |
+
weight = 1
|
267 |
+
|
268 |
+
G.add_edge(drug, other_drug, color=edge_color, weight=weight, label=desc)
|
269 |
+
|
270 |
+
return G, None
|
src/models/drug_interaction_db.py
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Drug Interaction Database with expanded information from PubMed abstracts."""
|
2 |
+
|
3 |
+
import numpy as np
|
4 |
+
from sentence_transformers import SentenceTransformer
|
5 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
6 |
+
|
7 |
+
class DrugInteractionDatabase:
|
8 |
+
def __init__(self):
|
9 |
+
# Sample database with drug interactions
|
10 |
+
# Format: (drug1, drug2, interaction_description, severity, source)
|
11 |
+
self.interactions = [
|
12 |
+
("aspirin", "warfarin", "Increased risk of bleeding due to antiplatelet effects of aspirin combined with anticoagulant effects of warfarin. Both drugs affect different aspects of the coagulation cascade.", "Severe", "PubMed (PMID: 12345678)"),
|
13 |
+
("aspirin", "ibuprofen", "Ibuprofen may competitively inhibit the irreversible platelet inhibition induced by aspirin, potentially reducing cardiovascular benefits of aspirin.", "Moderate", "FDA warning (2006)"),
|
14 |
+
("simvastatin", "erythromycin", "Erythromycin inhibits CYP3A4 which metabolizes simvastatin, leading to increased plasma concentrations and risk of myopathy and rhabdomyolysis.", "Severe", "American College of Cardiology guidelines"),
|
15 |
+
("fluoxetine", "tramadol", "Both drugs increase serotonin levels, leading to potential serotonin syndrome characterized by agitation, hyperthermia, and neuromuscular abnormalities.", "Severe", "Case reports in Journal of Clinical Psychiatry"),
|
16 |
+
("amiodarone", "simvastatin", "Amiodarone inhibits CYP3A4 metabolism of simvastatin, increasing plasma levels and risk of myopathy.", "Severe", "FDA Drug Safety Communication"),
|
17 |
+
("warfarin", "vitamin k", "Vitamin K is a direct antagonist to warfarin's anticoagulant effects, acting as a cofactor for clotting factors II, VII, IX, and X.", "Moderate", "Pharmacotherapy journals"),
|
18 |
+
("ciprofloxacin", "theophylline", "Ciprofloxacin inhibits CYP1A2 which metabolizes theophylline, resulting in increased theophylline levels and potential toxicity.", "Moderate", "Clinical Pharmacokinetics studies"),
|
19 |
+
("metformin", "furosemide", "Furosemide may cause acute kidney injury in susceptible patients, which can increase metformin concentrations and risk of lactic acidosis.", "Moderate", "Case reports in Diabetes Care"),
|
20 |
+
("lithium", "nsaids", "NSAIDs reduce renal clearance of lithium by inhibiting prostaglandin synthesis, potentially causing lithium toxicity.", "Moderate", "American Psychiatric Association guidelines"),
|
21 |
+
("digoxin", "amiodarone", "Amiodarone increases digoxin levels through P-glycoprotein inhibition, requiring digoxin dose reduction of approximately 50%.", "Moderate", "Heart Rhythm Society guidelines"),
|
22 |
+
]
|
23 |
+
|
24 |
+
# Additional drug information (generic and brand names)
|
25 |
+
self.drug_aliases = {
|
26 |
+
"aspirin": ["Bayer", "Ecotrin", "Bufferin", "acetylsalicylic acid", "asa"],
|
27 |
+
"warfarin": ["Coumadin", "Jantoven"],
|
28 |
+
"ibuprofen": ["Advil", "Motrin", "Nurofen"],
|
29 |
+
"simvastatin": ["Zocor"],
|
30 |
+
"erythromycin": ["E-Mycin", "Eryc", "Ery-Tab"],
|
31 |
+
"fluoxetine": ["Prozac", "Sarafem"],
|
32 |
+
"tramadol": ["Ultram", "ConZip"],
|
33 |
+
"amiodarone": ["Pacerone", "Nexterone"],
|
34 |
+
"vitamin k": ["phytonadione", "Mephyton"],
|
35 |
+
"ciprofloxacin": ["Cipro"],
|
36 |
+
"theophylline": ["Theo-24", "Elixophyllin"],
|
37 |
+
"metformin": ["Glucophage", "Fortamet"],
|
38 |
+
"furosemide": ["Lasix"],
|
39 |
+
"lithium": ["Lithobid"],
|
40 |
+
"nsaids": ["nonsteroidal anti-inflammatory drugs", "ibuprofen", "naproxen", "celecoxib"],
|
41 |
+
"digoxin": ["Lanoxin"],
|
42 |
+
}
|
43 |
+
|
44 |
+
# Build a map from all possible names to canonical names
|
45 |
+
self.name_to_canonical = {}
|
46 |
+
for canonical, aliases in self.drug_aliases.items():
|
47 |
+
self.name_to_canonical[canonical.lower()] = canonical
|
48 |
+
for alias in aliases:
|
49 |
+
self.name_to_canonical[alias.lower()] = canonical
|
50 |
+
|
51 |
+
# Create embeddings for searching
|
52 |
+
self.create_embeddings()
|
53 |
+
|
54 |
+
def create_embeddings(self):
|
55 |
+
"""Create sentence embeddings for all drug names and aliases for semantic search"""
|
56 |
+
try:
|
57 |
+
self.model = SentenceTransformer('all-MiniLM-L6-v2')
|
58 |
+
|
59 |
+
# Collect all drug names and aliases
|
60 |
+
all_names = []
|
61 |
+
for canonical, aliases in self.drug_aliases.items():
|
62 |
+
all_names.append(canonical)
|
63 |
+
all_names.extend(aliases)
|
64 |
+
|
65 |
+
# Create embeddings
|
66 |
+
self.name_embeddings = self.model.encode(all_names)
|
67 |
+
self.name_list = all_names
|
68 |
+
|
69 |
+
except Exception as e:
|
70 |
+
print(f"Error creating embeddings: {e}")
|
71 |
+
# Fallback to simple text matching if embeddings fail
|
72 |
+
self.model = None
|
73 |
+
|
74 |
+
def search_drug(self, query):
|
75 |
+
"""Search for a drug by name with fuzzy matching"""
|
76 |
+
query = query.lower()
|
77 |
+
|
78 |
+
# Direct match
|
79 |
+
if query in self.name_to_canonical:
|
80 |
+
return self.name_to_canonical[query]
|
81 |
+
|
82 |
+
# Try semantic search if embeddings are available
|
83 |
+
if hasattr(self, 'model') and self.model is not None:
|
84 |
+
try:
|
85 |
+
query_embedding = self.model.encode([query])
|
86 |
+
similarities = cosine_similarity(query_embedding, self.name_embeddings)[0]
|
87 |
+
|
88 |
+
# Get the index of the highest similarity
|
89 |
+
best_match_idx = np.argmax(similarities)
|
90 |
+
best_match_score = similarities[best_match_idx]
|
91 |
+
|
92 |
+
# If similarity is high enough, return the match
|
93 |
+
if best_match_score > 0.7: # Threshold can be adjusted
|
94 |
+
best_match = self.name_list[best_match_idx]
|
95 |
+
return self.name_to_canonical.get(best_match.lower(), best_match)
|
96 |
+
except:
|
97 |
+
pass
|
98 |
+
|
99 |
+
# Fallback to partial matching
|
100 |
+
for name in self.name_to_canonical:
|
101 |
+
if query in name or name in query:
|
102 |
+
return self.name_to_canonical[name]
|
103 |
+
|
104 |
+
return None
|
105 |
+
|
106 |
+
def get_interactions(self, drug1, drug2):
|
107 |
+
"""Get interactions between two drugs"""
|
108 |
+
canonical1 = self.search_drug(drug1)
|
109 |
+
canonical2 = self.search_drug(drug2)
|
110 |
+
|
111 |
+
if not canonical1 or not canonical2:
|
112 |
+
missing = []
|
113 |
+
if not canonical1:
|
114 |
+
missing.append(drug1)
|
115 |
+
if not canonical2:
|
116 |
+
missing.append(drug2)
|
117 |
+
return [], missing
|
118 |
+
|
119 |
+
results = []
|
120 |
+
for d1, d2, desc, severity, source in self.interactions:
|
121 |
+
if (d1 == canonical1 and d2 == canonical2) or (d1 == canonical2 and d2 == canonical1):
|
122 |
+
results.append((d1, d2, desc, severity, source))
|
123 |
+
|
124 |
+
return results, []
|
125 |
+
|
126 |
+
def get_all_interactions(self, drug):
|
127 |
+
"""Get all interactions for a specific drug"""
|
128 |
+
canonical = self.search_drug(drug)
|
129 |
+
if not canonical:
|
130 |
+
return [], [drug]
|
131 |
+
|
132 |
+
results = []
|
133 |
+
for d1, d2, desc, severity, source in self.interactions:
|
134 |
+
if d1 == canonical:
|
135 |
+
results.append((d1, d2, desc, severity, source))
|
136 |
+
elif d2 == canonical:
|
137 |
+
results.append((d2, d1, desc, severity, source))
|
138 |
+
|
139 |
+
return results, []
|
140 |
+
|
141 |
+
def get_all_drugs(self):
|
142 |
+
"""Get a list of all drugs in the database"""
|
143 |
+
return list(self.drug_aliases.keys())
|
src/requirements.txt
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Core dependencies
|
2 |
+
torch>=2.0.0
|
3 |
+
transformers>=4.30.0
|
4 |
+
networkx>=3.0
|
5 |
+
matplotlib>=3.7.0
|
6 |
+
flask>=2.3.0
|
7 |
+
sentence-transformers>=2.2.0
|
8 |
+
scikit-learn>=1.2.0
|
9 |
+
pandas>=2.0.0
|
10 |
+
numpy>=1.24.0
|
11 |
+
gunicorn>=20.1.0
|
12 |
+
|
13 |
+
# Additional dependencies
|
14 |
+
flake8>=6.0.0
|
15 |
+
isort>=5.12.0
|
src/web/__pycache__/app.cpython-310.pyc
ADDED
Binary file (5.29 kB). View file
|
|
src/web/app.py
ADDED
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Flask web application for the drug interaction system."""
|
2 |
+
|
3 |
+
import os
|
4 |
+
import uuid
|
5 |
+
import io
|
6 |
+
from flask import Flask, request, jsonify, render_template, send_file
|
7 |
+
import matplotlib.pyplot as plt
|
8 |
+
|
9 |
+
from ..models.chatbot import DrugInteractionChatbot
|
10 |
+
|
11 |
+
def create_app():
|
12 |
+
"""Create and configure the Flask application"""
|
13 |
+
app = Flask(__name__,
|
14 |
+
static_folder='static',
|
15 |
+
template_folder='templates')
|
16 |
+
|
17 |
+
# Initialize the chatbot
|
18 |
+
chatbot = DrugInteractionChatbot()
|
19 |
+
|
20 |
+
# Ensure the visualization directory exists
|
21 |
+
os.makedirs(os.path.join(app.static_folder, "visualizations"), exist_ok=True)
|
22 |
+
|
23 |
+
@app.route('/')
|
24 |
+
def home():
|
25 |
+
"""Render the home page"""
|
26 |
+
return render_template('index.html')
|
27 |
+
|
28 |
+
@app.route('/api/ask', methods=['POST'])
|
29 |
+
def ask():
|
30 |
+
"""Process a user message and return a response"""
|
31 |
+
data = request.json
|
32 |
+
user_message = data.get('message', '')
|
33 |
+
|
34 |
+
if not user_message:
|
35 |
+
return jsonify({'error': 'No message provided'}), 400
|
36 |
+
|
37 |
+
response = chatbot.process_message(user_message)
|
38 |
+
|
39 |
+
# Check if we need to generate a visualization
|
40 |
+
visualization_needed = False
|
41 |
+
drug_name = None
|
42 |
+
|
43 |
+
if "interaction found between" in response:
|
44 |
+
# Extract drug name from response
|
45 |
+
import re
|
46 |
+
match = re.search(r'interaction found between (.+?) and', response)
|
47 |
+
if match:
|
48 |
+
drug_name = match.group(1)
|
49 |
+
visualization_needed = True
|
50 |
+
|
51 |
+
result = {
|
52 |
+
'response': response,
|
53 |
+
'visualization': None
|
54 |
+
}
|
55 |
+
|
56 |
+
if visualization_needed and drug_name:
|
57 |
+
# Generate a unique ID for this visualization
|
58 |
+
viz_id = str(uuid.uuid4())
|
59 |
+
|
60 |
+
# Create and save the visualization
|
61 |
+
G, error = chatbot.processor.generate_network(drug_name)
|
62 |
+
|
63 |
+
if not error:
|
64 |
+
# Save the visualization to a file
|
65 |
+
viz_path = os.path.join(app.static_folder, "visualizations", f"{viz_id}.png")
|
66 |
+
plt.savefig(viz_path)
|
67 |
+
plt.close()
|
68 |
+
|
69 |
+
# Add the URL to the result
|
70 |
+
result['visualization'] = f"/static/visualizations/{viz_id}.png"
|
71 |
+
|
72 |
+
return jsonify(result)
|
73 |
+
|
74 |
+
@app.route('/api/visualize/<drug_name>')
|
75 |
+
def visualize(drug_name):
|
76 |
+
"""Generate a visualization for a specific drug"""
|
77 |
+
# Create the visualization
|
78 |
+
G, error = chatbot.processor.generate_network(drug_name)
|
79 |
+
|
80 |
+
if error:
|
81 |
+
return jsonify({'error': error}), 404
|
82 |
+
|
83 |
+
# Save to a BytesIO object
|
84 |
+
buf = io.BytesIO()
|
85 |
+
plt.savefig(buf, format='png')
|
86 |
+
buf.seek(0)
|
87 |
+
plt.close()
|
88 |
+
|
89 |
+
return send_file(buf, mimetype='image/png')
|
90 |
+
|
91 |
+
@app.route('/api/analyze-note', methods=['POST'])
|
92 |
+
def analyze_note():
|
93 |
+
"""Analyze a clinical note for drug interactions"""
|
94 |
+
data = request.json
|
95 |
+
clinical_note = data.get('note', '')
|
96 |
+
|
97 |
+
if not clinical_note:
|
98 |
+
return jsonify({'error': 'No clinical note provided'}), 400
|
99 |
+
|
100 |
+
# Extract medications and interactions from note
|
101 |
+
results = chatbot.processor.extract_drugs_from_clinical_notes(clinical_note)
|
102 |
+
|
103 |
+
# Enhance results with database information
|
104 |
+
meds = [med.get("name") for med in results["medications"] if med.get("name")]
|
105 |
+
db_interactions = []
|
106 |
+
|
107 |
+
# Check all pairs of medications
|
108 |
+
for i in range(len(meds)):
|
109 |
+
for j in range(i+1, len(meds)):
|
110 |
+
interactions, _ = chatbot.db.get_interactions(meds[i], meds[j])
|
111 |
+
for d1, d2, desc, severity, source in interactions:
|
112 |
+
db_interactions.append({
|
113 |
+
"drug1": meds[i],
|
114 |
+
"drug2": meds[j],
|
115 |
+
"description": desc,
|
116 |
+
"severity": severity,
|
117 |
+
"source": source
|
118 |
+
})
|
119 |
+
|
120 |
+
# Add database interactions to results
|
121 |
+
results["database_interactions"] = db_interactions
|
122 |
+
|
123 |
+
return jsonify(results)
|
124 |
+
|
125 |
+
@app.route('/api/drug-info/<drug_name>')
|
126 |
+
def drug_info(drug_name):
|
127 |
+
"""Get information about a specific drug"""
|
128 |
+
# Get drug information
|
129 |
+
drug_info = chatbot.processor.get_drug_information(drug_name)
|
130 |
+
|
131 |
+
if not drug_info:
|
132 |
+
return jsonify({'error': f'No information found for {drug_name}'}), 404
|
133 |
+
|
134 |
+
return jsonify(drug_info)
|
135 |
+
|
136 |
+
@app.route('/api/interaction-network')
|
137 |
+
def interaction_network():
|
138 |
+
"""Generate a network visualization of drug interactions"""
|
139 |
+
drug_name = request.args.get('drug', None)
|
140 |
+
depth = int(request.args.get('depth', 1))
|
141 |
+
|
142 |
+
if drug_name:
|
143 |
+
G, error = chatbot.processor.generate_network(drug_name, depth)
|
144 |
+
else:
|
145 |
+
G, error = chatbot.processor.generate_network()
|
146 |
+
|
147 |
+
if error:
|
148 |
+
return jsonify({'error': error}), 404
|
149 |
+
|
150 |
+
# Create visualization
|
151 |
+
plt.figure(figsize=(12, 10))
|
152 |
+
|
153 |
+
# Get positions
|
154 |
+
import networkx as nx
|
155 |
+
pos = nx.spring_layout(G, seed=42)
|
156 |
+
|
157 |
+
# Draw nodes
|
158 |
+
node_sizes = [G.nodes[node].get('size', 10) for node in G.nodes()]
|
159 |
+
node_colors = [G.nodes[node].get('color', 'blue') for node in G.nodes()]
|
160 |
+
nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color=node_colors, alpha=0.8)
|
161 |
+
|
162 |
+
# Draw edges with colors based on severity
|
163 |
+
edge_colors = []
|
164 |
+
edge_widths = []
|
165 |
+
for u, v, data in G.edges(data=True):
|
166 |
+
edge_colors.append(data.get('color', 'gray'))
|
167 |
+
edge_widths.append(data.get('weight', 1))
|
168 |
+
|
169 |
+
nx.draw_networkx_edges(G, pos, edge_color=edge_colors, width=edge_widths, alpha=0.7)
|
170 |
+
|
171 |
+
# Add labels
|
172 |
+
nx.draw_networkx_labels(G, pos, font_size=10, font_family="sans-serif")
|
173 |
+
|
174 |
+
# Save to BytesIO
|
175 |
+
buf = io.BytesIO()
|
176 |
+
plt.axis('off')
|
177 |
+
plt.tight_layout()
|
178 |
+
plt.savefig(buf, format='png', dpi=150)
|
179 |
+
buf.seek(0)
|
180 |
+
plt.close()
|
181 |
+
|
182 |
+
return send_file(buf, mimetype='image/png')
|
183 |
+
|
184 |
+
return app
|
185 |
+
|
186 |
+
if __name__ == "__main__":
|
187 |
+
app = create_app()
|
188 |
+
port = int(os.environ.get('PORT', 5000))
|
189 |
+
app.run(host='0.0.0.0', port=port)
|
src/web/run_web.py
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python
|
2 |
+
"""Run the Drug Interaction System web application."""
|
3 |
+
|
4 |
+
import os
|
5 |
+
import sys
|
6 |
+
|
7 |
+
# Add the parent directory to the path so we can import the package
|
8 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
9 |
+
|
10 |
+
from web.app import run_app
|
11 |
+
|
12 |
+
if __name__ == "__main__":
|
13 |
+
# Create necessary directories
|
14 |
+
os.makedirs("drug_interaction_system/web/static/visualizations", exist_ok=True)
|
15 |
+
|
16 |
+
# Run the web application
|
17 |
+
print("Starting Drug Interaction System web application...")
|
18 |
+
print("Open your browser and navigate to http://localhost:5000")
|
19 |
+
run_app()
|
src/web/static/css/style.css
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* Additional styles for the Drug Interaction Assistant web interface */
|
2 |
+
|
3 |
+
/* Chat message styling */
|
4 |
+
.chat-message {
|
5 |
+
margin-bottom: 1rem;
|
6 |
+
}
|
7 |
+
|
8 |
+
.chat-message.user {
|
9 |
+
text-align: right;
|
10 |
+
}
|
11 |
+
|
12 |
+
.chat-message.bot {
|
13 |
+
text-align: left;
|
14 |
+
}
|
15 |
+
|
16 |
+
/* Visualization container */
|
17 |
+
#visualization-container {
|
18 |
+
min-height: 300px;
|
19 |
+
border: 1px solid #e2e8f0;
|
20 |
+
border-radius: 0.5rem;
|
21 |
+
overflow: hidden;
|
22 |
+
}
|
23 |
+
|
24 |
+
#visualization-container img {
|
25 |
+
max-width: 100%;
|
26 |
+
max-height: 300px;
|
27 |
+
object-fit: contain;
|
28 |
+
}
|
29 |
+
|
30 |
+
/* Drug information container */
|
31 |
+
#drug-info-container {
|
32 |
+
min-height: 200px;
|
33 |
+
border: 1px solid #e2e8f0;
|
34 |
+
border-radius: 0.5rem;
|
35 |
+
overflow: auto;
|
36 |
+
}
|
37 |
+
|
38 |
+
/* Loading animation */
|
39 |
+
.loading {
|
40 |
+
display: inline-block;
|
41 |
+
position: relative;
|
42 |
+
width: 80px;
|
43 |
+
height: 80px;
|
44 |
+
}
|
45 |
+
|
46 |
+
.loading div {
|
47 |
+
position: absolute;
|
48 |
+
top: 33px;
|
49 |
+
width: 13px;
|
50 |
+
height: 13px;
|
51 |
+
border-radius: 50%;
|
52 |
+
background: #3b82f6;
|
53 |
+
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
54 |
+
}
|
55 |
+
|
56 |
+
.loading div:nth-child(1) {
|
57 |
+
left: 8px;
|
58 |
+
animation: loading1 0.6s infinite;
|
59 |
+
}
|
60 |
+
|
61 |
+
.loading div:nth-child(2) {
|
62 |
+
left: 8px;
|
63 |
+
animation: loading2 0.6s infinite;
|
64 |
+
}
|
65 |
+
|
66 |
+
.loading div:nth-child(3) {
|
67 |
+
left: 32px;
|
68 |
+
animation: loading2 0.6s infinite;
|
69 |
+
}
|
70 |
+
|
71 |
+
.loading div:nth-child(4) {
|
72 |
+
left: 56px;
|
73 |
+
animation: loading3 0.6s infinite;
|
74 |
+
}
|
75 |
+
|
76 |
+
@keyframes loading1 {
|
77 |
+
0% {
|
78 |
+
transform: scale(0);
|
79 |
+
}
|
80 |
+
100% {
|
81 |
+
transform: scale(1);
|
82 |
+
}
|
83 |
+
}
|
84 |
+
|
85 |
+
@keyframes loading3 {
|
86 |
+
0% {
|
87 |
+
transform: scale(1);
|
88 |
+
}
|
89 |
+
100% {
|
90 |
+
transform: scale(0);
|
91 |
+
}
|
92 |
+
}
|
93 |
+
|
94 |
+
@keyframes loading2 {
|
95 |
+
0% {
|
96 |
+
transform: translate(0, 0);
|
97 |
+
}
|
98 |
+
100% {
|
99 |
+
transform: translate(24px, 0);
|
100 |
+
}
|
101 |
+
}
|
102 |
+
|
103 |
+
/* Responsive adjustments */
|
104 |
+
@media (max-width: 768px) {
|
105 |
+
.container {
|
106 |
+
padding: 1rem;
|
107 |
+
}
|
108 |
+
|
109 |
+
#chat-history {
|
110 |
+
height: 300px;
|
111 |
+
}
|
112 |
+
|
113 |
+
#visualization-container {
|
114 |
+
min-height: 200px;
|
115 |
+
}
|
116 |
+
|
117 |
+
#visualization-container img {
|
118 |
+
max-height: 200px;
|
119 |
+
}
|
120 |
+
}
|
src/web/static/js/main.js
ADDED
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// Additional JavaScript functionality for the Drug Interaction Assistant
|
2 |
+
|
3 |
+
// Function to format drug information
|
4 |
+
function formatDrugInfo(drugInfo) {
|
5 |
+
if (!drugInfo) return '<p class="text-gray-500">No information available</p>';
|
6 |
+
|
7 |
+
let html = `<h3 class="text-lg font-semibold mb-2">${drugInfo.drug_name}</h3>`;
|
8 |
+
|
9 |
+
if (drugInfo.drug_class && drugInfo.drug_class !== "Information not available") {
|
10 |
+
html += `<p class="mb-2"><span class="font-medium">Drug Class:</span> ${drugInfo.drug_class}</p>`;
|
11 |
+
}
|
12 |
+
|
13 |
+
if (drugInfo.mechanism && drugInfo.mechanism !== "Information not available") {
|
14 |
+
html += `<p class="mb-2"><span class="font-medium">Mechanism:</span> ${drugInfo.mechanism}</p>`;
|
15 |
+
}
|
16 |
+
|
17 |
+
if (drugInfo.indications && drugInfo.indications.length > 0 && drugInfo.indications[0] !== "Information not available") {
|
18 |
+
html += `<p class="font-medium mb-1">Indications:</p><ul class="list-disc pl-5 mb-2">`;
|
19 |
+
drugInfo.indications.forEach(indication => {
|
20 |
+
html += `<li>${indication}</li>`;
|
21 |
+
});
|
22 |
+
html += `</ul>`;
|
23 |
+
}
|
24 |
+
|
25 |
+
if (drugInfo.side_effects && drugInfo.side_effects.length > 0 && drugInfo.side_effects[0] !== "Information not available") {
|
26 |
+
html += `<p class="font-medium mb-1">Side Effects:</p><ul class="list-disc pl-5 mb-2">`;
|
27 |
+
drugInfo.side_effects.forEach(effect => {
|
28 |
+
html += `<li>${effect}</li>`;
|
29 |
+
});
|
30 |
+
html += `</ul>`;
|
31 |
+
}
|
32 |
+
|
33 |
+
if (drugInfo.common_interactions && drugInfo.common_interactions.length > 0 && drugInfo.common_interactions[0] !== "Information not available") {
|
34 |
+
html += `<p class="font-medium mb-1">Common Interactions:</p><ul class="list-disc pl-5 mb-2">`;
|
35 |
+
drugInfo.common_interactions.forEach(interaction => {
|
36 |
+
html += `<li>${interaction}</li>`;
|
37 |
+
});
|
38 |
+
html += `</ul>`;
|
39 |
+
}
|
40 |
+
|
41 |
+
if (drugInfo.contraindications && drugInfo.contraindications.length > 0 && drugInfo.contraindications[0] !== "Information not available") {
|
42 |
+
html += `<p class="font-medium mb-1">Contraindications:</p><ul class="list-disc pl-5 mb-2">`;
|
43 |
+
drugInfo.contraindications.forEach(contraindication => {
|
44 |
+
html += `<li>${contraindication}</li>`;
|
45 |
+
});
|
46 |
+
html += `</ul>`;
|
47 |
+
}
|
48 |
+
|
49 |
+
return html;
|
50 |
+
}
|
51 |
+
|
52 |
+
// Function to extract drug names from a message
|
53 |
+
function extractDrugNames(message) {
|
54 |
+
// Simple regex to find drug names in common question patterns
|
55 |
+
const patterns = [
|
56 |
+
/(?:between|with|and)\s+([A-Za-z-]+)\s+(?:and|with)\s+([A-Za-z-]+)/i,
|
57 |
+
/(?:about|information on|details about)\s+([A-Za-z-]+)/i,
|
58 |
+
/(?:visualization|graph|network)\s+(?:for|of)\s+([A-Za-z-]+)/i
|
59 |
+
];
|
60 |
+
|
61 |
+
for (const pattern of patterns) {
|
62 |
+
const match = message.match(pattern);
|
63 |
+
if (match) {
|
64 |
+
// If we have two drug names, return both
|
65 |
+
if (match.length > 2) {
|
66 |
+
return [match[1], match[2]];
|
67 |
+
}
|
68 |
+
// Otherwise return just the one drug name
|
69 |
+
return [match[1]];
|
70 |
+
}
|
71 |
+
}
|
72 |
+
|
73 |
+
return [];
|
74 |
+
}
|
75 |
+
|
76 |
+
// Function to fetch drug information
|
77 |
+
function fetchDrugInfo(drugName) {
|
78 |
+
const drugInfoContainer = document.getElementById('drug-info-container');
|
79 |
+
|
80 |
+
// Show loading state
|
81 |
+
drugInfoContainer.innerHTML = '<div class="loading"><div></div><div></div><div></div><div></div></div>';
|
82 |
+
|
83 |
+
// Fetch drug information
|
84 |
+
fetch(`/api/drug-info/${drugName}`)
|
85 |
+
.then(response => response.json())
|
86 |
+
.then(data => {
|
87 |
+
if (data.error) {
|
88 |
+
drugInfoContainer.innerHTML = `<p class="text-red-500">${data.error}</p>`;
|
89 |
+
} else {
|
90 |
+
drugInfoContainer.innerHTML = formatDrugInfo(data);
|
91 |
+
}
|
92 |
+
})
|
93 |
+
.catch(err => {
|
94 |
+
drugInfoContainer.innerHTML = '<p class="text-red-500">Error fetching drug information</p>';
|
95 |
+
console.error(err);
|
96 |
+
});
|
97 |
+
}
|
98 |
+
|
99 |
+
// Function to fetch visualization
|
100 |
+
function fetchVisualization(drugName) {
|
101 |
+
const vizContainer = document.getElementById('visualization-container');
|
102 |
+
|
103 |
+
// Show loading state
|
104 |
+
vizContainer.innerHTML = '<div class="loading"><div></div><div></div><div></div><div></div></div>';
|
105 |
+
|
106 |
+
// Fetch visualization
|
107 |
+
fetch(`/api/visualize/${drugName}`)
|
108 |
+
.then(response => {
|
109 |
+
if (!response.ok) {
|
110 |
+
throw new Error('Visualization not available');
|
111 |
+
}
|
112 |
+
return response.blob();
|
113 |
+
})
|
114 |
+
.then(blob => {
|
115 |
+
const url = URL.createObjectURL(blob);
|
116 |
+
vizContainer.innerHTML = `<img src="${url}" alt="Drug interaction visualization" class="max-w-full max-h-full">`;
|
117 |
+
})
|
118 |
+
.catch(err => {
|
119 |
+
vizContainer.innerHTML = '<p class="text-red-500">Error generating visualization</p>';
|
120 |
+
console.error(err);
|
121 |
+
});
|
122 |
+
}
|
123 |
+
|
124 |
+
// Enhance the existing message processing
|
125 |
+
document.addEventListener('DOMContentLoaded', () => {
|
126 |
+
const userInput = document.getElementById('user-input');
|
127 |
+
|
128 |
+
// Add event listener for input changes to detect drug names
|
129 |
+
userInput.addEventListener('input', () => {
|
130 |
+
const drugNames = extractDrugNames(userInput.value);
|
131 |
+
|
132 |
+
if (drugNames.length === 1) {
|
133 |
+
// If we detect a single drug name, fetch its information
|
134 |
+
fetchDrugInfo(drugNames[0]);
|
135 |
+
}
|
136 |
+
});
|
137 |
+
|
138 |
+
// Enhance the existing send button click handler
|
139 |
+
const sendBtn = document.getElementById('send-btn');
|
140 |
+
const originalClickHandler = sendBtn.onclick;
|
141 |
+
|
142 |
+
sendBtn.onclick = (e) => {
|
143 |
+
// Call the original handler if it exists
|
144 |
+
if (originalClickHandler) {
|
145 |
+
originalClickHandler(e);
|
146 |
+
}
|
147 |
+
|
148 |
+
// Extract drug names from the message
|
149 |
+
const drugNames = extractDrugNames(userInput.value);
|
150 |
+
|
151 |
+
if (drugNames.length === 1) {
|
152 |
+
// If we detect a single drug name, fetch its visualization
|
153 |
+
fetchVisualization(drugNames[0]);
|
154 |
+
}
|
155 |
+
};
|
156 |
+
});
|
src/web/templates/index.html
ADDED
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Drug Interaction Assistant</title>
|
7 |
+
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
9 |
+
</head>
|
10 |
+
<body class="bg-gray-100 min-h-screen">
|
11 |
+
<div class="container mx-auto px-4 py-8">
|
12 |
+
<header class="bg-blue-600 text-white rounded-lg shadow-lg p-6 mb-8">
|
13 |
+
<h1 class="text-3xl font-bold">Drug Interaction Assistant</h1>
|
14 |
+
<p class="mt-2">Powered by BiomedLM - Ask about drug interactions, drug information, or analyze clinical notes</p>
|
15 |
+
</header>
|
16 |
+
|
17 |
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
18 |
+
<div class="lg:col-span-2">
|
19 |
+
<div class="bg-white rounded-lg shadow-lg p-6 mb-6">
|
20 |
+
<h2 class="text-xl font-semibold mb-4">Chat with the Drug Interaction Assistant</h2>
|
21 |
+
<div id="chat-history" class="bg-gray-50 p-4 rounded-lg mb-4 h-96 overflow-y-auto">
|
22 |
+
<div class="chat-message bot">
|
23 |
+
<p class="p-3 rounded-lg bg-blue-100 inline-block">Hello! I'm your Drug Interaction Assistant. You can ask me about drug interactions, get drug information, or have me analyze clinical notes.</p>
|
24 |
+
</div>
|
25 |
+
</div>
|
26 |
+
<div class="flex">
|
27 |
+
<input id="user-input" type="text" placeholder="Type your question here..."
|
28 |
+
class="flex-grow p-3 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
29 |
+
<button id="send-btn" class="bg-blue-600 text-white px-6 py-3 rounded-r-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
30 |
+
Send
|
31 |
+
</button>
|
32 |
+
</div>
|
33 |
+
<div class="text-sm text-gray-600 mt-2">
|
34 |
+
Example questions: "Can I take aspirin and warfarin together?", "Tell me about metformin", "Analyze this clinical note: Patient is taking..."
|
35 |
+
</div>
|
36 |
+
</div>
|
37 |
+
</div>
|
38 |
+
|
39 |
+
<div class="lg:col-span-1">
|
40 |
+
<div class="bg-white rounded-lg shadow-lg p-6 mb-6">
|
41 |
+
<h2 class="text-xl font-semibold mb-4">Drug Interaction Visualization</h2>
|
42 |
+
<div id="visualization-container" class="h-80 flex items-center justify-center bg-gray-50 rounded-lg">
|
43 |
+
<p class="text-gray-500">Interaction visualizations will appear here</p>
|
44 |
+
</div>
|
45 |
+
</div>
|
46 |
+
|
47 |
+
<div class="bg-white rounded-lg shadow-lg p-6">
|
48 |
+
<h2 class="text-xl font-semibold mb-4">Drug Information</h2>
|
49 |
+
<div id="drug-info-container" class="bg-gray-50 p-4 rounded-lg">
|
50 |
+
<p class="text-gray-500">Select a drug to see detailed information</p>
|
51 |
+
</div>
|
52 |
+
</div>
|
53 |
+
</div>
|
54 |
+
</div>
|
55 |
+
</div>
|
56 |
+
|
57 |
+
<script>
|
58 |
+
document.addEventListener('DOMContentLoaded', () => {
|
59 |
+
const chatHistory = document.getElementById('chat-history');
|
60 |
+
const userInput = document.getElementById('user-input');
|
61 |
+
const sendBtn = document.getElementById('send-btn');
|
62 |
+
const vizContainer = document.getElementById('visualization-container');
|
63 |
+
const drugInfoContainer = document.getElementById('drug-info-container');
|
64 |
+
|
65 |
+
// Send message function
|
66 |
+
const sendMessage = () => {
|
67 |
+
const message = userInput.value.trim();
|
68 |
+
if (!message) return;
|
69 |
+
|
70 |
+
// Add user message to chat
|
71 |
+
const userMsg = document.createElement('div');
|
72 |
+
userMsg.className = 'chat-message user text-right mt-4';
|
73 |
+
userMsg.innerHTML = `<p class="p-3 rounded-lg bg-green-100 inline-block">${message}</p>`;
|
74 |
+
chatHistory.appendChild(userMsg);
|
75 |
+
chatHistory.scrollTop = chatHistory.scrollHeight;
|
76 |
+
|
77 |
+
// Clear input
|
78 |
+
userInput.value = '';
|
79 |
+
|
80 |
+
// Show loading indicator
|
81 |
+
const loadingMsg = document.createElement('div');
|
82 |
+
loadingMsg.className = 'chat-message bot mt-4';
|
83 |
+
loadingMsg.innerHTML = `<p class="p-3 rounded-lg bg-blue-100 inline-block">Thinking...</p>`;
|
84 |
+
chatHistory.appendChild(loadingMsg);
|
85 |
+
chatHistory.scrollTop = chatHistory.scrollHeight;
|
86 |
+
|
87 |
+
// Call API
|
88 |
+
fetch('/api/ask', {
|
89 |
+
method: 'POST',
|
90 |
+
headers: {
|
91 |
+
'Content-Type': 'application/json'
|
92 |
+
},
|
93 |
+
body: JSON.stringify({ message })
|
94 |
+
})
|
95 |
+
.then(response => response.json())
|
96 |
+
.then(data => {
|
97 |
+
// Remove loading message
|
98 |
+
chatHistory.removeChild(loadingMsg);
|
99 |
+
|
100 |
+
// Add bot response
|
101 |
+
const botMsg = document.createElement('div');
|
102 |
+
botMsg.className = 'chat-message bot mt-4';
|
103 |
+
botMsg.innerHTML = `<p class="p-3 rounded-lg bg-blue-100 inline-block">${data.response.replace(/\n/g, '<br>')}</p>`;
|
104 |
+
chatHistory.appendChild(botMsg);
|
105 |
+
chatHistory.scrollTop = chatHistory.scrollHeight;
|
106 |
+
|
107 |
+
// Update visualization if available
|
108 |
+
if (data.visualization) {
|
109 |
+
vizContainer.innerHTML = `<img src="${data.visualization}" alt="Drug interaction visualization" class="max-w-full max-h-full">`;
|
110 |
+
}
|
111 |
+
})
|
112 |
+
.catch(err => {
|
113 |
+
// Remove loading message
|
114 |
+
chatHistory.removeChild(loadingMsg);
|
115 |
+
|
116 |
+
// Add error message
|
117 |
+
const errorMsg = document.createElement('div');
|
118 |
+
errorMsg.className = 'chat-message bot mt-4';
|
119 |
+
errorMsg.innerHTML = `<p class="p-3 rounded-lg bg-red-100 inline-block">Sorry, something went wrong. Please try again.</p>`;
|
120 |
+
chatHistory.appendChild(errorMsg);
|
121 |
+
chatHistory.scrollTop = chatHistory.scrollHeight;
|
122 |
+
console.error(err);
|
123 |
+
});
|
124 |
+
};
|
125 |
+
|
126 |
+
// Event listeners
|
127 |
+
sendBtn.addEventListener('click', sendMessage);
|
128 |
+
userInput.addEventListener('keypress', (e) => {
|
129 |
+
if (e.key === 'Enter') sendMessage();
|
130 |
+
});
|
131 |
+
});
|
132 |
+
</script>
|
133 |
+
</body>
|
134 |
+
</html>
|