alimonis4 commited on
Commit
24c5eb2
Β·
verified Β·
1 Parent(s): b56b54d

Upload 19 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,18 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ data/Antibiotic[[:space:]]Guidelines[[:space:]](2020)_0.pdf filter=lfs diff=lfs merge=lfs -text
37
+ data/dengue.pdf filter=lfs diff=lfs merge=lfs -text
38
+ data/EAU-Guidelines-on-Urological-Infections-2024.pdf filter=lfs diff=lfs merge=lfs -text
39
+ data/executive_summary.pdf filter=lfs diff=lfs merge=lfs -text
40
+ data/General[[:space:]]Fever[[:space:]]+[[:space:]]Viral[[:space:]]URI.pdf filter=lfs diff=lfs merge=lfs -text
41
+ data/Gyan-Vahini-11-Safe-Drugs-Use-In-Pregnancy-Lactation-Nov-25.pdf filter=lfs diff=lfs merge=lfs -text
42
+ data/influenza.pdf filter=lfs diff=lfs merge=lfs -text
43
+ data/malaria.pdf filter=lfs diff=lfs merge=lfs -text
44
+ data/practice-guidelines-for-the-diagnosis-and-management-of-skin-and-soft-tissue-infections-2014-update-by-the-infectious-diseases-society-of-america.pdf filter=lfs diff=lfs merge=lfs -text
45
+ data/Surgical[[:space:]]Infection[[:space:]]Society[[:space:]]2020[[:space:]]updated[[:space:]]guidelines[[:space:]]on[[:space:]]the[[:space:]]managem.pdf filter=lfs diff=lfs merge=lfs -text
46
+ data/The_WHO_AWaRe_Access_Watch_Reserve_antib.pdf filter=lfs diff=lfs merge=lfs -text
47
+ data/Treatment_Guidelines_2019_Final.pdf filter=lfs diff=lfs merge=lfs -text
48
+ data/typhoid.pdf filter=lfs diff=lfs merge=lfs -text
49
+ data/WHO-Bacterial-Priority-2024.pdf filter=lfs diff=lfs merge=lfs -text
50
+ data/WHO-MHP-HPS-EML-2023.02-eng.pdf filter=lfs diff=lfs merge=lfs -text
chatbot.py ADDED
@@ -0,0 +1,1240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import sqlite3
4
+ import requests
5
+ from datetime import datetime
6
+ from flask import Flask, request, jsonify, render_template, g
7
+ from flask_cors import CORS
8
+ from threading import Lock
9
+
10
+ # OpenMed NER β€” symptom entity extraction
11
+ try:
12
+ from transformers import pipeline
13
+ openmed_ner = pipeline(
14
+ "token-classification",
15
+ model="OpenMed/OpenMed-NER-DiseaseDetect-BioMed-335M",
16
+ aggregation_strategy="simple"
17
+ )
18
+ OPENMED_AVAILABLE = True
19
+ print("βœ… OpenMed NER loaded")
20
+ except Exception as e:
21
+ openmed_ner = None
22
+ OPENMED_AVAILABLE = False
23
+ print(f"⚠️ OpenMed NER not available: {e}")
24
+
25
+ # Load .env file
26
+ try:
27
+ from dotenv import load_dotenv
28
+ load_dotenv(dotenv_path=".env") # ← yeh ek word change karo
29
+ except ImportError:
30
+ pass
31
+
32
+ from pypdf import PdfReader
33
+ from langchain_community.vectorstores import FAISS
34
+ from langchain_community.embeddings import HuggingFaceEmbeddings
35
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
36
+ from langchain_core.documents import Document
37
+
38
+ from gtts import gTTS
39
+ import io
40
+ import uuid
41
+
42
+
43
+ app = Flask(__name__)
44
+ CORS(app)
45
+ rag_lock = Lock()
46
+ VECTORSTORE = None
47
+ DATABASE = "safecure.db"
48
+
49
+
50
+ # ==============================
51
+ # DATABASE SETUP
52
+ # ==============================
53
+ def get_db():
54
+ db = getattr(g, '_database', None)
55
+ if db is None:
56
+ db = g._database = sqlite3.connect(DATABASE)
57
+ db.row_factory = sqlite3.Row
58
+ return db
59
+
60
+ @app.teardown_appcontext
61
+ def close_connection(exception):
62
+ db = getattr(g, '_database', None)
63
+ if db is not None:
64
+ db.close()
65
+
66
+ def init_db():
67
+ with app.app_context():
68
+ db = sqlite3.connect(DATABASE)
69
+ db.execute('''
70
+ CREATE TABLE IF NOT EXISTS patients (
71
+ id TEXT PRIMARY KEY,
72
+ timestamp TEXT NOT NULL,
73
+ condition TEXT,
74
+ allergies TEXT,
75
+ medications TEXT,
76
+ age TEXT,
77
+ assessment TEXT,
78
+ antibiotic_necessity TEXT,
79
+ first_line TEXT,
80
+ second_line TEXT,
81
+ contraindications TEXT,
82
+ recommended_tests TEXT,
83
+ additional_info_needed TEXT,
84
+ raw_response TEXT
85
+ )
86
+ ''')
87
+ db.commit()
88
+ db.close()
89
+
90
+ def save_to_db(patient_id, data, parsed):
91
+ db = get_db()
92
+ # Migrate old schema if needed
93
+ try:
94
+ db.execute("SELECT recommended_tests FROM patients LIMIT 1")
95
+ except Exception:
96
+ for col in ["age TEXT", "recommended_tests TEXT", "additional_info_needed TEXT"]:
97
+ try:
98
+ db.execute(f"ALTER TABLE patients ADD COLUMN {col}")
99
+ except Exception:
100
+ pass
101
+ db.commit()
102
+
103
+ db.execute('''
104
+ INSERT INTO patients
105
+ (id, timestamp, condition, allergies, medications, age, assessment,
106
+ antibiotic_necessity, first_line, second_line, contraindications,
107
+ recommended_tests, additional_info_needed, raw_response)
108
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
109
+ ''', (
110
+ patient_id,
111
+ datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
112
+ data.get('condition', ''),
113
+ data.get('allergies', ''),
114
+ data.get('medications', ''),
115
+ data.get('age', ''),
116
+ parsed.get('assessment', ''),
117
+ parsed.get('antibiotic_necessity', ''),
118
+ parsed.get('first_line', ''),
119
+ parsed.get('second_line', ''),
120
+ parsed.get('contraindications', ''),
121
+ parsed.get('recommended_tests', ''),
122
+ parsed.get('additional_info_needed', ''),
123
+ parsed.get('raw', '')
124
+ ))
125
+ db.commit()
126
+
127
+
128
+ # ==============================
129
+ # PARSE RESPONSE
130
+ # ==============================
131
+ def parse_response(response_text):
132
+ response_text = re.sub(r'\*+', '', response_text)
133
+ response_text = re.sub(r'#+\s*', '', response_text)
134
+
135
+ sections = {
136
+ 'assessment': '',
137
+ 'antibiotic_necessity': '',
138
+ 'first_line': '',
139
+ 'second_line': '',
140
+ 'contraindications': '',
141
+ 'recommended_tests': '',
142
+ 'additional_info_needed': '',
143
+ 'raw': response_text
144
+ }
145
+
146
+ # Multiple header variations mapped to same section key
147
+ section_map = [
148
+ (["clinical assessment:", "assessment:"], 'assessment'),
149
+ (["antibiotic necessity:", "antibiotic:"], 'antibiotic_necessity'),
150
+ (["first-line therapy:", "first-line:", "first line therapy:", "first line:"], 'first_line'),
151
+ (["second-line alternatives:", "second-line:", "second line alternatives:", "second line:"], 'second_line'),
152
+ (["contraindications & precautions:", "contraindications:", "precautions:"], 'contraindications'),
153
+ (["recommended tests:", "tests:", "investigations:"], 'recommended_tests'),
154
+ (["additional information needed:", "additional information:", "additional info:"], 'additional_info_needed'),
155
+ ]
156
+
157
+ lines = response_text.split('\n')
158
+ current = None
159
+ buffer = []
160
+
161
+ def flush(key):
162
+ if key and buffer:
163
+ sections[key] = ' | '.join(buffer).strip()
164
+ buffer.clear()
165
+
166
+ for line in lines:
167
+ line = line.strip()
168
+ if not line:
169
+ continue
170
+
171
+ matched = False
172
+ line_lower = line.lower()
173
+ for headers, key in section_map:
174
+ for header in headers:
175
+ if line_lower.startswith(header):
176
+ flush(current)
177
+ current = key
178
+ val = line[len(header):].strip()
179
+ if val:
180
+ buffer.append(val)
181
+ matched = True
182
+ break
183
+ if matched:
184
+ break
185
+
186
+ if not matched and current:
187
+ item = line.lstrip('-β€’0123456789.').strip()
188
+ if item:
189
+ buffer.append(item)
190
+
191
+ flush(current)
192
+ return sections
193
+
194
+
195
+ # ==============================
196
+ # LOAD PDFs
197
+ # ==============================
198
+ def load_pdfs(folder="data"):
199
+ docs = []
200
+ if not os.path.exists(folder):
201
+ os.makedirs(folder)
202
+ return docs
203
+ for file in os.listdir(folder):
204
+ if file.endswith(".pdf"):
205
+ try:
206
+ reader = PdfReader(os.path.join(folder, file))
207
+ for page in reader.pages:
208
+ text = page.extract_text()
209
+ if text:
210
+ docs.append(Document(page_content=text))
211
+ except Exception as e:
212
+ print(f"Error loading {file}: {e}")
213
+ return docs
214
+
215
+
216
+ # ==============================
217
+ # INIT RAG
218
+ # ==============================
219
+ def initialize_rag():
220
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
221
+ if os.path.exists("faiss_index"):
222
+ return FAISS.load_local("faiss_index", embeddings, allow_dangerous_deserialization=True)
223
+ docs = load_pdfs()
224
+ if not docs:
225
+ return FAISS.from_texts(["No clinical guidelines loaded."], embeddings)
226
+ splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
227
+ chunks = splitter.split_documents(docs)
228
+ vs = FAISS.from_documents(chunks, embeddings)
229
+ vs.save_local("faiss_index")
230
+ return vs
231
+
232
+ def get_vectorstore():
233
+ global VECTORSTORE
234
+ if VECTORSTORE is None:
235
+ with rag_lock:
236
+ if VECTORSTORE is None:
237
+ VECTORSTORE = initialize_rag()
238
+ return VECTORSTORE
239
+
240
+
241
+ # ==============================
242
+ # LLM CALL
243
+ # ==============================
244
+ def call_llm(llm_prompt):
245
+ import os
246
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
247
+ try:
248
+ res = requests.post(
249
+ "https://api.groq.com/openai/v1/chat/completions",
250
+ headers={"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"},
251
+ json={
252
+ "model": "llama-3.1-8b-instant",
253
+ "messages": [
254
+ {
255
+ "role": "system",
256
+ "content": (
257
+ "You are a senior physician and clinical decision support system. "
258
+ "Think like a board-certified doctor: diagnose precisely, treat conservatively, escalate when needed. "
259
+ "STRICT OUTPUT RULES: "
260
+ "Each section = maximum 2 lines. Do NOT repeat symptoms in assessment. "
261
+ "No full sentences β€” use structured labels only. No preamble. No conclusion. No filler text. "
262
+ "Do NOT use **, ##, or bullet dashes. Plain text only. "
263
+ "Antibiotics ONLY when bacterial infection is clearly and strongly indicated. "
264
+ "Patient safety is absolute β€” never suggest unsafe, contraindicated, or unnecessary drugs."
265
+ )
266
+ },
267
+ {"role": "user", "content": llm_prompt}
268
+ ],
269
+ "max_tokens": 600,
270
+ "temperature": 0.15
271
+ },
272
+ timeout=45
273
+ )
274
+ if res.status_code == 200:
275
+ return res.json()["choices"][0]["message"]["content"]
276
+ else:
277
+ return f"ERROR: {res.text[:200]}"
278
+ except Exception as e:
279
+ print(f"❌ GROQ EXCEPTION: {str(e)}")
280
+ return f"ERROR: {str(e)}"
281
+
282
+
283
+ # ==============================
284
+ # SAFETY FILTER
285
+ # ==============================
286
+ def safety_filter(response):
287
+ # Remove any dosage numbers that sneak through
288
+ response = re.sub(r'\b\d+\s?(mg|g|ml|mcg|IU|kg)\b', '', response, flags=re.IGNORECASE)
289
+ response = re.sub(r'\b\d+\s?times?\s?(daily|a day)\b', '', response, flags=re.IGNORECASE)
290
+ response = re.sub(r'every\s+\d+\s+hours?', '', response, flags=re.IGNORECASE)
291
+ response = re.sub(r'for\s+\d+\s+days?', '', response, flags=re.IGNORECASE)
292
+ response = re.sub(r'once\s+daily|twice\s+daily|three\s+times\s+daily', '', response, flags=re.IGNORECASE)
293
+ # Clean up extra whitespace left behind
294
+ response = re.sub(r' +', ' ', response)
295
+ response = re.sub(r' ,', ',', response)
296
+ response = re.sub(r' \)', ')', response)
297
+ # Pregnancy warnings
298
+ if "pregnan" in response.lower():
299
+ response = re.sub(
300
+ r"\b(tetracycline|doxycycline|ciprofloxacin|levofloxacin|ibuprofen|naproxen|aspirin|trimethoprim|methotrexate|warfarin)\b",
301
+ lambda m: f"{m.group()} [AVOID IN PREGNANCY]",
302
+ response, flags=re.IGNORECASE
303
+ )
304
+ return response
305
+
306
+
307
+ # ==============================
308
+ # RESPONSE VALIDATION
309
+ # ==============================
310
+ def validate_response(response):
311
+ if not response or response.startswith("ERROR"):
312
+ return False
313
+ cleaned = re.sub(r'\*+', '', response).lower()
314
+
315
+ # Each list = acceptable variations of same section header
316
+ required_sections = [
317
+ ["clinical assessment", "assessment:", "diagnosis:"],
318
+ ["antibiotic necessity", "antibiotic:", "antibiotics:"],
319
+ ["first-line therapy", "first-line", "first line", "first_line"],
320
+ ["second-line", "second line", "second_line", "alternatives"],
321
+ ["contraindication", "precaution", "avoid:"],
322
+ ["recommended tests", "tests:", "investigations"],
323
+ ["additional information", "additional info", "info needed"],
324
+ ]
325
+
326
+ matched = 0
327
+ for section_variants in required_sections:
328
+ if any(variant in cleaned for variant in section_variants):
329
+ matched += 1
330
+
331
+ # Pass if at least 4 out of 7 sections found (lenient β€” avoids false fallback)
332
+ return matched >= 4
333
+
334
+
335
+ # ==============================
336
+ # RULE ENGINE
337
+ # ==============================
338
+ SYMPTOM_RULES = [
339
+ {
340
+ "name": "Malaria",
341
+ "keywords": ["chills", "rigors", "cyclical fever", "sweating", "malaria", "shivering"],
342
+ "required_any": ["fever", "chills", "sweating", "rigors"],
343
+ "antibiotic": "NO β€” antimalarial needed, not antibiotic",
344
+ "first_line": ["Artemether-Lumefantrine (Falciparum Malaria)", "Paracetamol (Fever)", "ORS (Hydration)"],
345
+ "second_line": ["IV Artesunate (Severe Malaria β€” hospital only)", "Chloroquine (Vivax Malaria)"],
346
+ "tests": ["Malaria RDT β€” rapid confirmation", "Blood smear β€” species identification", "CBC β€” severity check"],
347
+ "avoid": ["Aspirin β€” bleeding risk", "Ibuprofen β€” avoid in malaria", "Antibiotics β€” not indicated"],
348
+ },
349
+ {
350
+ "name": "Dengue Fever",
351
+ "keywords": ["dengue", "bone pain", "eye pain", "retro-orbital", "rash", "platelet", "breakbone"],
352
+ "required_any": ["fever", "rash", "bone pain", "joint pain", "eye pain"],
353
+ "antibiotic": "NO β€” viral disease, antibiotics strictly contraindicated",
354
+ "first_line": ["Paracetamol (Fever and Pain)", "ORS (Hydration)", "Rest"],
355
+ "second_line": ["IV Fluids (if severe dengue β€” hospital)"],
356
+ "tests": ["NS1 Antigen β€” early dengue confirmation", "CBC with Platelet β€” thrombocytopenia check", "Dengue IgM/IgG β€” serology"],
357
+ "avoid": ["Ibuprofen β€” severe bleeding risk", "Aspirin β€” bleeding risk", "Diclofenac β€” avoid", "Antibiotics β€” not indicated"],
358
+ },
359
+ {
360
+ "name": "Typhoid Fever",
361
+ "keywords": ["typhoid", "abdominal pain", "constipation", "rose spots", "enteric"],
362
+ "required_any": ["fever", "abdominal pain", "nausea", "vomiting"],
363
+ "antibiotic": "YES β€” bacterial infection, antibiotic required",
364
+ "first_line": ["Azithromycin (Typhoid β€” safest oral)", "Paracetamol (Fever)"],
365
+ "second_line": ["Ceftriaxone (Severe Typhoid β€” IV)", "Ciprofloxacin (if sensitive β€” avoid in pregnancy/children)"],
366
+ "tests": ["Widal Test β€” typhoid serology", "Blood Culture β€” gold standard", "CBC β€” infection screen"],
367
+ "avoid": ["NSAIDs β€” GI bleed risk in typhoid", "Ciprofloxacin β€” avoid in pregnancy and children"],
368
+ },
369
+ {
370
+ "name": "Urinary Tract Infection (UTI)",
371
+ "keywords": ["burning urination", "frequent urination", "dysuria", "uti", "urinary", "cloudy urine", "pelvic pain"],
372
+ "required_any": ["burning", "urination", "dysuria", "pelvic pain", "urinary"],
373
+ "antibiotic": "YES β€” bacterial infection confirmed",
374
+ "first_line": ["Nitrofurantoin (UTI β€” safest first line)", "Paracetamol (Pain relief)"],
375
+ "second_line": ["Cefixime (if resistance suspected)", "Co-amoxiclav (complicated UTI)"],
376
+ "tests": ["Urine Routine/Microscopy β€” UTI confirmation", "Urine Culture β€” sensitivity testing"],
377
+ "avoid": ["Trimethoprim β€” avoid in pregnancy and elderly", "Fluoroquinolones β€” avoid in pregnancy"],
378
+ },
379
+ {
380
+ "name": "Pyelonephritis (Upper UTI)",
381
+ "keywords": ["flank pain", "loin pain", "back pain", "rigors", "vomiting", "high fever"],
382
+ "required_any": ["burning urination", "dysuria", "urinary", "frequent urination"],
383
+ "antibiotic": "YES β€” upper UTI requires systemic antibiotic",
384
+ "first_line": ["Co-amoxiclav (Pyelonephritis β€” oral)", "Paracetamol (Fever)"],
385
+ "second_line": ["Ceftriaxone IV (Severe β€” hospital only)", "Ciprofloxacin (if sensitivities allow β€” avoid in pregnancy)"],
386
+ "tests": ["Urine Culture & Sensitivity β€” mandatory", "CBC β€” severity assessment", "Renal Function Tests β€” kidney involvement", "Ultrasound KUB β€” structural assessment"],
387
+ "avoid": ["Nitrofurantoin β€” NOT effective for upper UTI", "Trimethoprim β€” resistance risk"],
388
+ },
389
+ {
390
+ "name": "Community Acquired Pneumonia",
391
+ "keywords": ["pneumonia", "productive cough", "chest pain", "breathlessness", "sputum"],
392
+ "required_any": ["cough", "chest pain", "breathlessness", "fever", "sputum"],
393
+ "antibiotic": "YES β€” bacterial pneumonia likely",
394
+ "first_line": ["Amoxicillin (Pneumonia β€” first line)", "Paracetamol (Fever)", "ORS (Hydration)"],
395
+ "second_line": ["Azithromycin (Penicillin allergy or atypical)", "Amoxicillin-Clavulanate (Severe)"],
396
+ "tests": ["Chest X-Ray β€” consolidation confirmation", "CBC β€” infection severity", "Sputum Culture β€” pathogen identification"],
397
+ "avoid": ["NSAIDs β€” avoid if bleeding risk"],
398
+ },
399
+ {
400
+ "name": "Influenza / Viral Fever",
401
+ "keywords": ["flu", "influenza", "myalgia", "body ache", "fatigue", "runny nose", "sore throat", "viral"],
402
+ "required_any": ["fever", "body ache", "fatigue", "cough", "sore throat"],
403
+ "antibiotic": "NO β€” viral infection, antibiotics not indicated",
404
+ "first_line": ["Paracetamol (Fever and Body Ache)", "ORS (Hydration)", "Rest", "Vitamin C (Immune support)"],
405
+ "second_line": ["Oseltamivir (Within 48 hours of onset only)"],
406
+ "tests": ["CBC β€” rule out secondary bacterial infection", "Rapid Influenza Test β€” if available"],
407
+ "avoid": ["Aspirin β€” Reye's syndrome risk", "Antibiotics β€” not indicated"],
408
+ },
409
+ {
410
+ "name": "Cellulitis",
411
+ "keywords": ["cellulitis", "skin redness", "skin warmth", "skin infection", "erythema"],
412
+ "required_any": ["redness", "swelling", "warmth", "skin"],
413
+ "antibiotic": "YES β€” bacterial skin infection",
414
+ "first_line": ["Flucloxacillin (Cellulitis β€” first line)", "Paracetamol (Pain and Fever)"],
415
+ "second_line": ["Clindamycin (Penicillin allergy)", "Co-amoxiclav (Polymicrobial)"],
416
+ "tests": ["CBC β€” infection severity", "CRP β€” inflammatory marker"],
417
+ "avoid": ["Penicillin β€” if allergy reported"],
418
+ },
419
+ {
420
+ "name": "Otitis Media",
421
+ "keywords": ["ear pain", "earache", "ear discharge", "otitis", "ear infection"],
422
+ "required_any": ["ear pain", "earache", "ear"],
423
+ "antibiotic": "CONDITIONAL β€” observe 48-72hrs first, antibiotics if not improving",
424
+ "first_line": ["Paracetamol (Pain relief β€” watchful waiting 48-72hrs)", "Warm compress"],
425
+ "second_line": ["Amoxicillin (If not improving after 72hrs)"],
426
+ "tests": ["Otoscopy β€” tympanic membrane assessment"],
427
+ "avoid": ["Aspirin β€” children", "Ciprofloxacin ear drops β€” only for perforated drum"],
428
+ },
429
+ {
430
+ "name": "Gastroenteritis / Food Poisoning",
431
+ "keywords": ["diarrhea", "vomiting", "abdominal cramps", "loose stools", "food poisoning", "outside food", "gastroenteritis", "nausea"],
432
+ "required_any": ["vomiting", "diarrhea", "abdominal cramps", "loose stools", "nausea"],
433
+ "antibiotic": "NO β€” usually viral or self-limiting bacterial, antibiotics rarely needed",
434
+ "first_line": ["ORS (Rehydration β€” priority)", "Paracetamol (Fever)", "Rest", "Zinc (if child)"],
435
+ "second_line": ["Not applicable β€” supportive care sufficient in most cases"],
436
+ "tests": ["Stool Routine β€” only if bloody diarrhea or fever > 3 days", "Stool Culture β€” if Salmonella/Shigella suspected"],
437
+ "avoid": ["Antibiotics β€” not indicated for routine gastroenteritis", "Ibuprofen β€” GI irritation risk"],
438
+ },
439
+ {
440
+ "name": "Acute Sinusitis (Viral)",
441
+ "keywords": ["facial pain", "nasal congestion", "sinus", "sinusitis", "pressure around eyes", "blocked nose"],
442
+ "required_any": ["facial pain", "nasal congestion", "headache", "blocked nose", "sinus"],
443
+ "antibiotic": "NO β€” >90% viral, antibiotics not indicated in first 10 days",
444
+ "first_line": ["Paracetamol (Pain and Fever)", "Saline nasal rinse (Congestion)", "Steam inhalation", "Rest"],
445
+ "second_line": ["Amoxicillin (Only if symptoms worsen after 10 days or severe bacterial signs)"],
446
+ "tests": ["None required β€” clinical diagnosis sufficient for mild sinusitis"],
447
+ "avoid": ["Antibiotics in first 10 days β€” viral cause likely", "Decongestant sprays > 3 days β€” rebound congestion"],
448
+ },
449
+ {
450
+ "name": "Tonsillitis / Pharyngitis",
451
+ "keywords": ["sore throat", "throat pain", "difficulty swallowing", "tonsil", "pharyngitis", "strep"],
452
+ "required_any": ["sore throat", "throat pain", "difficulty swallowing"],
453
+ "antibiotic": "CONDITIONAL β€” viral in 70% of cases; antibiotic only if bacterial strep suspected",
454
+ "first_line": ["Paracetamol (Pain and Fever)", "Salt water gargles", "Rest", "ORS (Hydration)"],
455
+ "second_line": ["Amoxicillin (If Strep throat confirmed or strongly suspected β€” avoid if penicillin allergy)", "Azithromycin (Penicillin allergy)"],
456
+ "tests": ["Throat Swab Culture β€” only if bacterial strep strongly suspected", "Rapid Strep Test β€” if available"],
457
+ "avoid": ["Aspirin β€” children (Reye's syndrome)", "Amoxicillin β€” if penicillin allergy or EBV suspected (causes rash)"],
458
+ },
459
+ {
460
+ "name": "Tuberculosis (TB)",
461
+ "keywords": ["weight loss", "night sweats", "prolonged fever", "weeks", "tuberculosis", "tb", "haemoptysis", "blood in sputum"],
462
+ "required_any": ["cough", "weight loss", "night sweats", "fever", "fatigue"],
463
+ "antibiotic": "YES β€” anti-TB therapy required (specialist-supervised)",
464
+ "first_line": ["Refer to specialist β€” TB requires DOTS therapy (specialist-supervised)", "Paracetamol (Fever)"],
465
+ "second_line": ["DOTS Regimen (HRZE β€” Isoniazid, Rifampicin, Pyrazinamide, Ethambutol β€” specialist only)"],
466
+ "tests": ["Chest X-Ray β€” bilateral infiltrates/cavitation", "Sputum AFB Smear β€” TB confirmation", "Mantoux Test β€” TB exposure", "CBNAAT/GeneXpert β€” rapid TB and drug resistance"],
467
+ "avoid": ["Self-medication β€” incomplete treatment causes drug resistance", "Fluoroquinolones without TB ruled out β€” masks TB"],
468
+ },
469
+ {
470
+ "name": "Sepsis / Septic Shock",
471
+ "keywords": ["confusion", "low blood pressure", "hypotension", "fast breathing", "sepsis", "septic shock", "cold clammy", "organ failure"],
472
+ "required_any": ["fever", "confusion", "low blood pressure", "fast breathing", "weakness"],
473
+ "antibiotic": "YES β€” broad-spectrum IV antibiotics required immediately",
474
+ "first_line": ["EMERGENCY: Immediate hospital referral required", "IV Piperacillin-Tazobactam (Broad-spectrum β€” hospital only)", "IV Normal Saline bolus (Fluid resuscitation)", "Norepinephrine (Vasopressor β€” ICU only if BP unresponsive)"],
475
+ "second_line": ["IV Meropenem (If resistant organisms suspected)", "IV Vancomycin (If MRSA suspected)"],
476
+ "tests": ["Blood Culture x2 β€” before antibiotics if possible", "CBC β€” infection severity", "Lactate β€” sepsis severity marker", "Renal Function Tests β€” organ involvement", "CRP/Procalcitonin β€” sepsis confirmation"],
477
+ "avoid": ["Delay in antibiotics β€” every hour delay increases mortality", "Oral antibiotics β€” IV route required"],
478
+ },
479
+ ]
480
+
481
+ CRITICAL_KEYWORDS = [
482
+ "altered consciousness", "confusion", "seizure", "fits",
483
+ "difficulty breathing", "severe breathlessness", "can't breathe",
484
+ "stiff neck", "photophobia", "neck stiffness",
485
+ "uncontrolled bleeding", "coughing blood", "blood in stool",
486
+ "crushing chest pain", "chest pain with sweating",
487
+ "unconscious", "not passing urine",
488
+ "high fever", "fever more than 5 days", "fever not responding to medication",
489
+ "severe dehydration", "unable to swallow", "persistent vomiting",
490
+ "severe headache with neck stiffness", "rash with fever",
491
+ # Septic shock triggers
492
+ "low blood pressure", "hypotension", "fast breathing", "rapid breathing",
493
+ "septic shock", "sepsis", "organ failure", "cold clammy",
494
+ ]
495
+
496
+ def run_rule_engine(condition_text, allergies_text, pregnancy_status="Not mentioned", diabetes="Not mentioned", age="Not specified"):
497
+ text = condition_text.lower()
498
+ allergy_text = allergies_text.lower()
499
+ is_pregnant = pregnancy_status.lower() not in ["not mentioned", "no", "none", ""]
500
+ is_diabetic = diabetes.lower() not in ["not mentioned", "no", "none", ""]
501
+ is_child = any(w in text for w in ["child", "baby", "infant", "toddler", "kid"]) or \
502
+ (age.isdigit() and int(age) < 18)
503
+
504
+ is_critical = any(kw in text for kw in CRITICAL_KEYWORDS)
505
+ matched_rules = []
506
+
507
+ for rule in SYMPTOM_RULES:
508
+ keyword_hit = any(kw in text for kw in rule["keywords"])
509
+ required_hit = any(kw in text for kw in rule["required_any"])
510
+ if keyword_hit and required_hit:
511
+ matched_rules.append(rule)
512
+
513
+ # PYELONEPHRITIS PRIORITY: fever + vomiting + back/flank pain β†’ override plain UTI
514
+ pyelonephritis_triggers = ["fever", "vomiting", "back pain", "flank pain", "loin pain", "rigors", "high fever"]
515
+ has_upper_uti_flags = any(kw in text for kw in pyelonephritis_triggers)
516
+ matched_names = [r["name"] for r in matched_rules]
517
+ if "Pyelonephritis (Upper UTI)" in matched_names and "Urinary Tract Infection (UTI)" in matched_names and has_upper_uti_flags:
518
+ matched_rules = [r for r in matched_rules if r["name"] != "Urinary Tract Infection (UTI)"]
519
+
520
+ allergy_warnings = []
521
+ if "penicillin" in allergy_text:
522
+ allergy_warnings.append("Penicillin allergy β€” avoid Amoxicillin, Flucloxacillin, Co-amoxiclav")
523
+ if "sulfa" in allergy_text or "sulphonamide" in allergy_text:
524
+ allergy_warnings.append("Sulfa allergy β€” avoid Trimethoprim-Sulfamethoxazole")
525
+ if "aspirin" in allergy_text or "nsaid" in allergy_text:
526
+ allergy_warnings.append("NSAID/Aspirin allergy β€” avoid all NSAIDs")
527
+ if "metformin" in allergy_text or is_diabetic:
528
+ allergy_warnings.append("Diabetic patient β€” flag Ciprofloxacin interaction risk; prefer safer alternatives")
529
+ allergy_warnings.append("Diabetic + UTI β€” Nitrofurantoin preferred over Fluoroquinolones")
530
+
531
+ # Pregnancy-specific warnings β€” only if explicitly mentioned
532
+ if is_pregnant:
533
+ allergy_warnings.append("PREGNANCY NOTED β€” avoid Fluoroquinolones, Tetracyclines, Trimethoprim, NSAIDs, Clarithromycin")
534
+ allergy_warnings.append("PREGNANCY + UTI β€” Nitrofurantoin safe only in 1st/2nd trimester; prefer Cefalexin if trimester unknown")
535
+ allergy_warnings.append("PREGNANCY + Pneumonia β€” Azithromycin (Category B) preferred; Clarithromycin (Category C) β€” AVOID")
536
+
537
+ # Child-specific warnings
538
+ if is_child:
539
+ allergy_warnings.append("CHILD PATIENT β€” avoid Aspirin (Reye's syndrome risk); avoid Fluoroquinolones under 18")
540
+ allergy_warnings.append("CHILD PATIENT β€” weight-based dosing needed; flag in Additional Information Needed")
541
+
542
+ return {"is_critical": is_critical, "matched_rules": matched_rules, "allergy_warnings": allergy_warnings}
543
+
544
+
545
+ # ==============================
546
+ # CORE CLINICAL ENGINE
547
+ # ==============================
548
+ def clinical_engine(data):
549
+ vs = get_vectorstore()
550
+ query = f"Symptoms: {data.get('condition')} Allergies: {data.get('allergies')} Medications: {data.get('medications')}"
551
+ try:
552
+ docs = vs.as_retriever(search_kwargs={"k": 3}).invoke(query)
553
+ context = "\n\n".join(d.page_content[:500] for d in docs)
554
+ except Exception:
555
+ context = "General medical guidelines apply."
556
+
557
+ condition = data.get('condition', '')
558
+ allergies = data.get('allergies', 'None')
559
+ medications = data.get('medications', 'None')
560
+ age = data.get('age', 'Not specified')
561
+ pregnancy_status = data.get('pregnancy', 'Not mentioned')
562
+ diabetes = data.get('diabetes', 'Not mentioned')
563
+ renal_issues = data.get('renal_issues', 'Not mentioned')
564
+
565
+ # Run rule engine β€” pass all patient factors
566
+ rules = run_rule_engine(condition, allergies, pregnancy_status, diabetes, age)
567
+ matched = rules["matched_rules"]
568
+ is_critical = rules["is_critical"]
569
+ allergy_warnings = rules["allergy_warnings"]
570
+
571
+ # Septic Shock override β€” if critical AND confusion + hypotension present β†’ force emergency
572
+ septic_flags = ["confusion", "low blood pressure", "hypotension", "fast breathing", "sepsis", "septic"]
573
+ septic_hit = sum(1 for kw in septic_flags if kw in condition.lower())
574
+ is_septic_shock = septic_hit >= 2
575
+
576
+ # Build rule hints for LLM
577
+ rule_hint = ""
578
+ if is_critical:
579
+ rule_hint += "CRITICAL EMERGENCY DETECTED β€” Recommend immediate hospital referral.\n"
580
+ if is_septic_shock:
581
+ rule_hint += (
582
+ "SEPTIC SHOCK PATTERN DETECTED β€” MANDATORY RULES:\n"
583
+ " 1. Clinical Assessment MUST start with: EMERGENCY: Immediate hospital referral required.\n"
584
+ " 2. First-Line MUST include: IV Broad-Spectrum Antibiotics (Piperacillin-Tazobactam or Meropenem β€” hospital only)\n"
585
+ " 3. First-Line MUST include: IV Fluid Resuscitation (Normal Saline bolus)\n"
586
+ " 4. First-Line MUST include: Vasopressors if BP not responding (Norepinephrine β€” ICU only)\n"
587
+ " 5. Additional Information Needed MUST say: EMERGENCY β€” ICU admission required immediately.\n"
588
+ )
589
+ if matched:
590
+ rule_hint += "\nRule Engine Pre-Analysis (HIGH CONFIDENCE β€” follow unless contradicted):\n"
591
+ for r in matched:
592
+ rule_hint += f" Disease: {r['name']}\n"
593
+ rule_hint += f" Antibiotic: {r['antibiotic']}\n"
594
+ rule_hint += f" First-Line: {', '.join(r['first_line'][:3])}\n"
595
+ rule_hint += f" Key Tests: {', '.join(r['tests'][:3])}\n"
596
+ rule_hint += f" Avoid: {', '.join(r['avoid'][:3])}\n\n"
597
+ if allergy_warnings:
598
+ rule_hint += "Allergy Alerts:\n" + "\n".join(f" - {w}" for w in allergy_warnings) + "\n"
599
+ if not matched:
600
+ rule_hint += "No strong pattern match. Use clinical reasoning based on symptoms only.\n"
601
+
602
+ llm_prompt = f"""
603
+ You are a senior physician and clinical decision support system (CDS).
604
+
605
+ IMPORTANT: The Rule Engine below has pre-analyzed this case. You MUST follow it unless you have strong clinical reason to deviate.
606
+
607
+ ========================
608
+ RULE ENGINE OUTPUT
609
+ ========================
610
+ {rule_hint}
611
+ ========================
612
+ INPUT DATA
613
+ ========================
614
+ Symptoms: {condition}
615
+ Allergies: {allergies}
616
+ Current Medications: {medications}
617
+ Age: {age}
618
+ Pregnancy Status: {pregnancy_status}
619
+ Diabetes: {diabetes}
620
+ Renal Issues: {renal_issues}
621
+
622
+ Clinical Guidelines (PDF Context):
623
+ {context}
624
+
625
+ ========================
626
+ REASONING FRAMEWORK (POORQA)
627
+ ========================
628
+
629
+ Step 1: Pattern Recognition
630
+ - Analyze symptom combinations carefully
631
+ - Identify if symptoms strongly match a known clinical pattern
632
+ - If a strong pattern exists β†’ prefer a specific diagnosis
633
+ - If no clear pattern β†’ use general diagnosis cautiously
634
+
635
+ Step 2: Differential Diagnosis
636
+ - List top 1–3 most likely diseases
637
+ - Rank them by probability (most likely first)
638
+ - Do NOT include unrelated diseases
639
+
640
+ Step 3: Antibiotic Decision
641
+ - Decide: YES / NO / ALTERNATIVE (e.g., antiviral/antiparasitic)
642
+ - Antibiotics ONLY if strong bacterial evidence
643
+ - If unclear β†’ DO NOT give antibiotics
644
+
645
+ Step 4: Treatment Selection
646
+ - First-line = safest effective option
647
+ - Second-line = only if needed
648
+ - Treatment MUST match primary diagnosis ONLY
649
+ - DO NOT mix treatments from different diseases
650
+
651
+ Step 5: Safety Check (CRITICAL)
652
+ - Check allergies
653
+ - Check drug safety
654
+ - Avoid:
655
+ - Unnecessary antibiotics
656
+ - NSAIDs in suspected bleeding-risk conditions
657
+ - Steroids unless clearly indicated
658
+ - Prefer safest drug (e.g., paracetamol over NSAIDs when uncertain)
659
+
660
+ Step 6: Test Justification
661
+ - Mild case + clear clinical pattern β†’ 0 tests needed, write "None required"
662
+ - Moderate suspicion β†’ 1–2 targeted confirmatory tests only
663
+ - Severe or uncertain diagnosis β†’ targeted panel only, no blanket ordering
664
+ - DO NOT suggest CBC routinely for every case
665
+ - Each test MUST have a specific clinical reason stated after a dash
666
+
667
+ Step 7: Final Validation
668
+ Before answering, ensure:
669
+ - No hallucinated symptoms added
670
+ - No unsafe drug suggested
671
+ - No missing critical test
672
+ - No vague diagnosis if specific possible
673
+
674
+ ========================
675
+ STRICT RULES
676
+ ========================
677
+ - NEVER assume symptoms not provided
678
+ - NEVER give antibiotics without clear bacterial indication
679
+ - NEVER use vague diagnosis if a strong clinical pattern exists
680
+ - NEVER suggest contraindicated or harmful drugs
681
+ - ALWAYS prioritize patient safety above all else
682
+ - ALWAYS be clinically logical and consistent
683
+ - Viral fever / dengue / malaria / flu β†’ antibiotics ABSOLUTELY PROHIBITED
684
+ - Mild URI or cold < 3 days β†’ supportive care ONLY, no antibiotics
685
+ - If bacterial infection is UNCLEAR β†’ do NOT give antibiotics; recommend 48hr monitoring
686
+ - If CRITICAL emergency (chest pain + sweating, unconscious, heavy bleeding, can't breathe, stiff neck) β†’
687
+ Clinical Assessment MUST start with "EMERGENCY: Immediate hospital referral required."
688
+ - Consider patient age, allergies, comorbidities (diabetes, renal issues) in every recommendation
689
+ - Include pregnancy contraindications ONLY if pregnancy_status is explicitly mentioned by user β€” do NOT assume
690
+ - Elderly or renal impairment β†’ avoid Nitrofurantoin for upper UTI; flag renally-cleared drug risks
691
+ - Diabetic patients β†’ flag Ciprofloxacin interaction risk where relevant
692
+
693
+ ========================
694
+ OUTPUT FORMAT (STRICT)
695
+ ========================
696
+
697
+ Use EXACTLY these section headers in this order β€” do not rename, skip, or reorder:
698
+
699
+ Clinical Assessment:
700
+ [Top 2–3 differential diagnoses ranked by probability. Format each on its own line:
701
+ 1. Most Likely: [Disease] β€” [one clinical reason from given symptoms only]
702
+ 2. Also Consider: [Disease] β€” [one clinical reason]
703
+ 3. Less Likely: [Disease] β€” [only if genuinely relevant]
704
+ Do NOT repeat symptoms. Do NOT invent symptoms not provided.]
705
+
706
+ Antibiotic Necessity:
707
+ [YES / NO / ANTIMALARIAL / ANTIVIRAL β€” one short reason. Must match primary diagnosis.]
708
+
709
+ First-Line Therapy:
710
+ [One drug per line. Format: DrugName (Condition). No doses.
711
+ If NO antibiotic: supportive care only β€” Paracetamol, ORS, Rest, Vitamin C as appropriate.
712
+ If YES antibiotic: safest guideline-based antibiotic first.]
713
+
714
+ Second-Line Alternatives:
715
+ [If antibiotic YES: 1–2 guideline-based alternatives, one per line.
716
+ If antibiotic NO: write β€” Not applicable β€” [reason e.g. viral infection]
717
+ NEVER write an antibiotic here when Antibiotic Necessity = NO.]
718
+
719
+ Contraindications & Precautions:
720
+ [Drug to avoid β€” reason. One per line.
721
+ Check allergies and current medications.
722
+ Pregnancy warnings ONLY if pregnancy was explicitly mentioned.
723
+ Write None if nothing applicable.]
724
+
725
+ Recommended Tests:
726
+ [TestName β€” specific reason. One per line.
727
+ Mild case with clear diagnosis β†’ write: None required β€” clinical diagnosis sufficient
728
+ Moderate/severe or uncertain β†’ targeted confirmatory tests only, no blanket panels.]
729
+
730
+ Additional Information Needed:
731
+ [EMERGENCY cases β†’ first line MUST be: EMERGENCY: Immediate hospital referral required.
732
+ Clear mild diagnosis β†’ write: None
733
+ Otherwise β†’ max 2 missing details that would change management.]
734
+
735
+ ========================
736
+ MANDATORY CONSISTENCY RULES
737
+ ========================
738
+ - Malaria / Dengue / Viral Fever / Influenza / Common Cold β†’ Antibiotic MUST be NO
739
+ - If Antibiotic = NO β†’ zero antibiotics in First-Line AND Second-Line
740
+ - Treatment must match primary diagnosis only β€” no mixing
741
+ - Never add symptoms not given by user
742
+ - Never leave a section empty β€” use None or Not applicable
743
+ - Never use ** ## or bullet dashes β€” plain text only
744
+ - Follow exact section headers above
745
+
746
+ PYELONEPHRITIS vs UTI (CRITICAL):
747
+ - Fever + vomiting + back pain/flank pain + urinary symptoms β†’ PRIMARY = Pyelonephritis
748
+ - Nitrofurantoin PROHIBITED in Pyelonephritis β€” ineffective in kidney tissue
749
+ - Pyelonephritis β†’ Co-amoxiclav oral or Ceftriaxone IV (severe)
750
+ - Simple UTI (no fever, no systemic symptoms) β†’ Nitrofurantoin acceptable
751
+
752
+ PREGNANCY SAFETY (only if explicitly mentioned):
753
+ - Nitrofurantoin β†’ trimester unknown β†’ prefer Cefalexin instead
754
+ - Azithromycin β†’ Category B β†’ safe, preferred for pneumonia
755
+ - Clarithromycin β†’ Category C β†’ AVOID, flag in contraindications
756
+ - Fluoroquinolones, Tetracyclines, Trimethoprim, NSAIDs β†’ AVOID
757
+
758
+ PNEUMONIA ANTIBIOTIC DECISION:
759
+ - High fever + chest pain + breathlessness + productive cough β†’ bacterial β†’ antibiotic YES
760
+ - Fatigue + body ache + mild cough only β†’ viral first β†’ no antibiotic without X-Ray confirmation
761
+ - Always recommend Chest X-Ray as first test for suspected pneumonia
762
+
763
+ SEPTIC SHOCK (MANDATORY):
764
+ - Fever + confusion + low BP + fast breathing β†’ Septic Shock
765
+ - Assessment MUST start: EMERGENCY: Immediate hospital referral required.
766
+ - First-Line MUST include IV antibiotics + IV fluids + vasopressors if BP unresponsive
767
+ - Additional Info MUST say: EMERGENCY β€” ICU admission required immediately
768
+
769
+ DIABETIC PATIENTS:
770
+ - Flag Ciprofloxacin interaction risk if patient is diabetic on relevant medications
771
+ - Prefer safer alternatives where possible
772
+
773
+ CHILD PATIENTS:
774
+ - Avoid Aspirin β€” Reye's syndrome risk
775
+ - Avoid Fluoroquinolones in children under 18
776
+ - Dose adjustments may be needed β€” flag this in Additional Information Needed
777
+ """
778
+
779
+ response = None
780
+ last_raw = None
781
+ for i in range(3):
782
+ raw = call_llm(llm_prompt)
783
+ if raw and not raw.startswith("ERROR"):
784
+ last_raw = raw
785
+ if validate_response(raw):
786
+ response = raw
787
+ break
788
+ print(f"⚠️ Retry {i+1}: validation failed")
789
+ else:
790
+ import time
791
+ time.sleep(10) # 10 second wait before retry
792
+
793
+ # Agar validation pass nahi hua lekin LLM ne kuch meaningful diya β†’ use karo
794
+ if not response:
795
+ if last_raw and len(last_raw.strip()) > 100:
796
+ print("⚠️ Using last LLM response despite validation failure")
797
+ response = last_raw
798
+ else:
799
+ response = """Clinical Assessment:
800
+ Unable to determine diagnosis. Please consult a doctor immediately.
801
+
802
+ Antibiotic Necessity:
803
+ NO β€” Insufficient information to make a safe antibiotic decision.
804
+
805
+ First-Line Therapy:
806
+ Paracetamol (fever and pain relief) | ORS (hydration) | Rest
807
+
808
+ Second-Line Alternatives:
809
+ Not applicable β€” insufficient clinical information
810
+
811
+ Contraindications & Precautions:
812
+ Do not self-medicate without professional medical evaluation.
813
+
814
+ Recommended Tests:
815
+ Complete Blood Count (CBC) β€” Basic infection screening
816
+
817
+ Additional Information Needed:
818
+ Please provide full symptom history, duration, and severity to enable proper diagnosis.
819
+ """
820
+
821
+ response = safety_filter(response)
822
+ return response
823
+
824
+
825
+ # ==============================
826
+ # API ENDPOINTS
827
+ # ==============================
828
+ @app.route("/analyze", methods=["POST"])
829
+ def analyze():
830
+ try:
831
+ data = request.get_json(force=True, silent=True)
832
+ if not data:
833
+ return jsonify({"status": "error", "message": "Invalid JSON"}), 400
834
+
835
+ # βœ… FIX: Empty symptoms check β€” fallback se bachao
836
+ condition = data.get('condition', '').strip()
837
+ if not condition:
838
+ return jsonify({
839
+ "status": "error",
840
+ "message": "Please enter symptoms before analyzing. The Symptoms / Condition field cannot be empty."
841
+ }), 400
842
+ result = clinical_engine(data)
843
+ parsed = parse_response(result)
844
+
845
+
846
+ db_temp = get_db()
847
+ count = db_temp.execute("SELECT COUNT(*) FROM patients").fetchone()[0]
848
+ year = datetime.now().strftime("%Y")
849
+ patient_id = f"P-{year}{str(count + 1).zfill(2)}"
850
+
851
+ result = clinical_engine(data)
852
+ parsed = parse_response(result)
853
+ save_to_db(patient_id, data, parsed)
854
+
855
+ def to_array(text):
856
+ if not text:
857
+ return []
858
+ items = []
859
+ for part in text.split(' | '):
860
+ part = part.strip().lstrip('-β€’').strip()
861
+ if part:
862
+ items.append(part)
863
+ return items
864
+ summary_text = f"""
865
+ Clinical assessment: {parsed.get("assessment", "")}.
866
+ Antibiotic necessity: {parsed.get("antibiotic_necessity", "")}.
867
+ First line therapy: {parsed.get("first_line", "")}.
868
+ Recommended tests: {parsed.get("recommended_tests", "")}.
869
+ """
870
+ return jsonify({
871
+ "status": "success",
872
+ "patient_id": patient_id,
873
+ "clinical_assessment": to_array(parsed.get("assessment", "")),
874
+ "antibiotic_necessity": parsed.get("antibiotic_necessity", "").strip(),
875
+ "first_line_therapy": to_array(parsed.get("first_line", "")),
876
+ "second_line_alternatives": to_array(parsed.get("second_line", "")),
877
+ "contraindications": [x for x in to_array(parsed.get("contraindications", "")) if x.lower() != "none"],
878
+ "recommended_tests": to_array(parsed.get("recommended_tests", "")),
879
+ "additional_info_needed": [x for x in to_array(parsed.get("additional_info_needed", "")) if x.lower() != "none"],
880
+ })
881
+ except Exception as e:
882
+ return jsonify({"status": "error", "message": str(e)})
883
+
884
+
885
+ @app.route("/patients", methods=["GET"])
886
+ def get_patients():
887
+ try:
888
+ db = get_db()
889
+ rows = db.execute("SELECT * FROM patients ORDER BY timestamp DESC").fetchall()
890
+ patients = [dict(row) for row in rows]
891
+ return jsonify({"status": "success", "patients": patients})
892
+ except Exception as e:
893
+ return jsonify({"status": "error", "message": str(e)})
894
+
895
+
896
+
897
+
898
+ # ==============================
899
+ # SMART TTS β€” OpenAI β†’ ElevenLabs β†’ gTTS fallback
900
+ # ==============================
901
+ def generate_tts(text, lang="en"):
902
+ """
903
+ Tries TTS providers in order of quality:
904
+ 1. OpenAI TTS (onyx = deep calm male, nova = female)
905
+ 2. ElevenLabs
906
+ 3. gTTS (fallback)
907
+ Returns: (filepath, filename) or raises Exception
908
+ """
909
+ os.makedirs("static", exist_ok=True)
910
+ filename = f"tts_{uuid.uuid4().hex}.mp3"
911
+ path = os.path.join("static", filename)
912
+
913
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip()
914
+ ELEVENLABS_KEY = os.getenv("ELEVENLABS_API_KEY", "").strip()
915
+ ELEVENLABS_VOICE = os.getenv("ELEVENLABS_VOICE_ID", "ErXwobaYiN019PkySvjV")
916
+
917
+ print(f"πŸ”‘ TTS: OpenAI key present = {bool(OPENAI_API_KEY)}, ElevenLabs = {bool(ELEVENLABS_KEY)}")
918
+
919
+ # ── 1. OpenAI TTS (best quality, natural, no pauses) ──
920
+ if OPENAI_API_KEY:
921
+ try:
922
+ voice = "onyx" # Deep, calm male β€” Jarvis feel
923
+ if lang == "hi":
924
+ voice = "onyx" # onyx handles Hindi well too
925
+ resp = requests.post(
926
+ "https://api.openai.com/v1/audio/speech",
927
+ headers={
928
+ "Authorization": f"Bearer {OPENAI_API_KEY}",
929
+ "Content-Type": "application/json"
930
+ },
931
+ json={
932
+ "model": "tts-1-hd", # HD = higher quality, smoother
933
+ "input": text,
934
+ "voice": voice,
935
+ "speed": 0.92, # Calm, measured doctor pace
936
+ "response_format": "mp3"
937
+ },
938
+ timeout=20
939
+ )
940
+ if resp.status_code == 200:
941
+ with open(path, "wb") as f:
942
+ f.write(resp.content)
943
+ print("βœ… TTS: OpenAI")
944
+ return path, filename
945
+ else:
946
+ print(f"OpenAI TTS error {resp.status_code}: {resp.text[:100]}")
947
+ except Exception as e:
948
+ print(f"OpenAI TTS exception: {e}")
949
+
950
+ # ── 2. ElevenLabs (most natural, best for Hindi too) ──
951
+ if ELEVENLABS_KEY:
952
+ try:
953
+ resp = requests.post(
954
+ f"https://api.elevenlabs.io/v1/text-to-speech/{ELEVENLABS_VOICE}",
955
+ headers={
956
+ "xi-api-key": ELEVENLABS_KEY,
957
+ "Content-Type": "application/json"
958
+ },
959
+ json={
960
+ "text": text,
961
+ "model_id": "eleven_multilingual_v2",
962
+ "voice_settings": {
963
+ "stability": 0.55,
964
+ "similarity_boost": 0.80,
965
+ "style": 0.20,
966
+ "use_speaker_boost": True
967
+ }
968
+ },
969
+ timeout=20
970
+ )
971
+ if resp.status_code == 200:
972
+ with open(path, "wb") as f:
973
+ f.write(resp.content)
974
+ print("βœ… TTS: ElevenLabs")
975
+ return path, filename
976
+ else:
977
+ print(f"ElevenLabs TTS error {resp.status_code}: {resp.text[:100]}")
978
+ except Exception as e:
979
+ print(f"ElevenLabs TTS exception: {e}")
980
+
981
+ # ── 3. gTTS fallback (basic but works) ──
982
+ try:
983
+ tts_lang_code = "hi" if lang == "hi" else "en"
984
+ tts_obj = gTTS(text=text, lang=tts_lang_code, slow=False)
985
+ tts_obj.save(path)
986
+ print("⚠️ TTS: gTTS fallback")
987
+ return path, filename
988
+ except Exception as e:
989
+ raise Exception(f"All TTS providers failed. Last error: {e}")
990
+
991
+
992
+ @app.route("/tts", methods=["POST"])
993
+ def tts():
994
+ try:
995
+ data = request.get_json()
996
+ text = data.get("text", "").strip()
997
+ lang = data.get("lang", "en")
998
+ if not text:
999
+ return jsonify({"error": "Empty text"}), 400
1000
+
1001
+ path, filename = generate_tts(text, lang)
1002
+ return jsonify({"audio_url": f"/static/{filename}"})
1003
+
1004
+ except Exception as e:
1005
+ return jsonify({"error": str(e)})
1006
+
1007
+
1008
+ # ==============================
1009
+ # CONVERSATIONAL DOCTOR ENDPOINT
1010
+ # ==============================
1011
+ DOCTOR_SYSTEM_PROMPT = """You are Dr. Safecure β€” an intelligent AI doctor, like Jarvis or Siri but for medicine.
1012
+ You talk like a calm, confident, real human doctor. Natural. Direct. Never robotic or stiff.
1013
+ You have clinical guidelines from PDFs available as context β€” use them for all recommendations.
1014
+
1015
+ YOUR VOICE & STYLE:
1016
+ - Sound like a real doctor talking to a patient face to face. Warm but not over-the-top.
1017
+ - Short natural sentences. Like how a person actually speaks.
1018
+ - NO filler: never say "I understand your concern", "That sounds difficult", "Great question", "Certainly" etc.
1019
+ - NO markdown: no **, ##, dashes as bullets. Plain text only during conversation.
1020
+ - Respond in the SAME language the patient speaks (Hindi or English).
1021
+
1022
+ CONSULTATION FLOW:
1023
+ 1. Greet once, naturally. Ask the main problem.
1024
+ 2. Ask ONE follow-up question at a time. Max 3 follow-ups total. Be brief.
1025
+ 3. Once you have enough info (symptom + duration + allergies/current meds) β†’ immediately give FINAL ASSESSMENT.
1026
+ 4. Use the special final assessment format below (EXACTLY).
1027
+
1028
+ EMERGENCY OVERRIDE: If patient says chest pain with sweating, can't breathe, unconscious, heavy bleeding β†’ say:
1029
+ "This sounds like an emergency. Please call an ambulance or go to the nearest hospital right now. Don't wait."
1030
+ Then stop. Nothing else.
1031
+
1032
+ FINAL ASSESSMENT FORMAT (use EXACTLY when you have enough info):
1033
+ When ready to give your final assessment, output it in this exact format β€” each section on its own line with the label followed by a colon and the content. Do not use bullets or dashes:
1034
+
1035
+ DIAGNOSIS: [Most likely condition and brief reason why]
1036
+ FIRST LINE: [Primary medicines β€” drug names only, no doses, each separated by comma]
1037
+ SECOND LINE: [Alternative medicines if first line fails or is contraindicated β€” or write "Not needed"]
1038
+ TESTS: [Recommended tests β€” each separated by comma, with brief reason after a dash]
1039
+ AVOID: [Medicines or things to avoid β€” or write "None"]
1040
+ NOTE: [One short sentence of important advice for the patient]
1041
+
1042
+ ANTIBIOTIC RULES (NON-NEGOTIABLE):
1043
+ - Viral fever, dengue, malaria, flu, cold, sore throat β†’ NEVER prescribe antibiotics under any circumstance
1044
+ - Antibiotics ONLY when bacterial infection is strongly and clearly suspected
1045
+ - Mild URI / cough < 3 days β†’ supportive care ONLY (Paracetamol, ORS, Rest)
1046
+ - If bacterial vs viral is unclear β†’ say "Monitor for 48 hours β€” antibiotics not needed yet"
1047
+ - Include pregnancy contraindications ONLY if patient explicitly mentions being pregnant
1048
+
1049
+ RULES:
1050
+
1051
+ Step 1: Pattern Recognition
1052
+ - Analyze symptom combinations carefully
1053
+ - Identify if symptoms strongly match a known clinical pattern
1054
+ - If a strong pattern exists β†’ prefer a specific diagnosis
1055
+ - If no clear pattern β†’ use general diagnosis cautiously
1056
+
1057
+ Step 2: Differential Diagnosis
1058
+ - List top 1 (or 2 if needed) most likely diseases
1059
+ - Rank them by probability (most likely first)
1060
+ - Do NOT include unrelated diseases
1061
+
1062
+ Step 3: Antibiotic Decision
1063
+ - Decide: YES / NO / ALTERNATIVE (e.g., antiviral/antiparasitic)
1064
+ - Antibiotics ONLY if strong bacterial evidence
1065
+ - If unclear β†’ DO NOT give antibiotics
1066
+
1067
+ Step 4: Treatment Selection
1068
+ - First-line = safest effective option
1069
+ - Second-line = only if needed
1070
+ - Treatment MUST match primary diagnosis ONLY
1071
+ - DO NOT mix treatments from different diseases
1072
+
1073
+ Step 5: Safety Check (CRITICAL)
1074
+ - Check allergies
1075
+ - Check drug safety
1076
+ - Avoid:
1077
+ - Unnecessary antibiotics
1078
+ - NSAIDs in suspected bleeding-risk conditions
1079
+ - Steroids unless clearly indicated
1080
+ - Prefer safest drug (e.g., paracetamol over NSAIDs when uncertain)
1081
+
1082
+ Step 6: Test Justification
1083
+ - Suggest tests ONLY if clinically needed
1084
+ - If strong suspicion of specific disease β†’ MUST suggest confirmatory test
1085
+ - DO NOT skip tests in moderate/high suspicion cases
1086
+
1087
+ Step 7: Final Validation
1088
+ Before answering, ensure:
1089
+ - No hallucinated symptoms added
1090
+ - No unsafe drug suggested
1091
+ - No missing critical test
1092
+ - No vague diagnosis if specific possible
1093
+
1094
+ ========================
1095
+ STRICT RULES
1096
+ ========================
1097
+
1098
+ - NEVER assume symptoms not provided
1099
+ - NEVER give antibiotics without clear indication
1100
+ - NEVER use vague diagnosis if a strong pattern exists
1101
+ - NEVER suggest harmful or contraindicated drugs
1102
+ - ALWAYS prioritize patient safety
1103
+ - ALWAYS be clinically logical and consistent
1104
+ ========================
1105
+ MANDATORY CONSISTENCY RULES
1106
+ ========================
1107
+
1108
+ - If diagnosis is Malaria/Dengue/Viral β†’ Antibiotic MUST be NO
1109
+ - If Antibiotic = NO β†’ ZERO antibiotics in First-Line or Second-Line
1110
+ - Treatment MUST match primary diagnosis
1111
+ - Do NOT add symptoms that were not given
1112
+ - Do NOT leave any section empty
1113
+ - NEVER use ** or ## or bullet points in output"""
1114
+
1115
+ @app.route("/chat", methods=["POST"])
1116
+ def chat():
1117
+ try:
1118
+ data = request.get_json(force=True, silent=True)
1119
+ if not data:
1120
+ return jsonify({"status": "error", "message": "Invalid JSON"}), 400
1121
+
1122
+ conversation_history = data.get("history", [])
1123
+ user_message = data.get("message", "").strip()
1124
+ lang = data.get("lang", "en")
1125
+
1126
+ if not user_message:
1127
+ return jsonify({"status": "error", "message": "Empty message"}), 400
1128
+
1129
+ # Build messages array for Groq
1130
+ messages = [{"role": "system", "content": DOCTOR_SYSTEM_PROMPT}]
1131
+
1132
+ # RAG context retrieve karo
1133
+ rag_context = "No clinical guidelines available."
1134
+ try:
1135
+ all_user_text = " ".join(
1136
+ t["content"] for t in conversation_history if t.get("role") == "user"
1137
+ ) + " " + user_message
1138
+ vs = get_vectorstore()
1139
+ docs = vs.as_retriever(search_kwargs={"k": 4}).invoke(all_user_text)
1140
+ rag_context = "\n\n".join(d.page_content[:400] for d in docs)
1141
+ except Exception as e:
1142
+ print(f"RAG error in chat: {e}")
1143
+
1144
+ # RAG system context inject karo
1145
+ messages.append({
1146
+ "role": "system",
1147
+ "content": f"CLINICAL GUIDELINES FROM PDF (use these for recommendations):\n{rag_context}"
1148
+ })
1149
+
1150
+ # Pehle conversation history add karo
1151
+ for turn in conversation_history:
1152
+ if turn.get("role") in ("user", "assistant"):
1153
+ messages.append({"role": turn["role"], "content": turn["content"]})
1154
+
1155
+ # OpenMed NER β€” entities extract karo user message se
1156
+ openmed_context = ""
1157
+ if OPENMED_AVAILABLE and openmed_ner:
1158
+ try:
1159
+ entities = openmed_ner(user_message)
1160
+ detected = list(set([
1161
+ e['word'].strip() for e in entities
1162
+ if e['score'] > 0.7 and len(e['word'].strip()) > 2
1163
+ ]))
1164
+ if detected:
1165
+ openmed_context = f"\n[OpenMed detected clinical entities: {', '.join(detected)}]"
1166
+ print(f"πŸ”¬ OpenMed entities: {detected}")
1167
+ except Exception as e:
1168
+ print(f"OpenMed NER error: {e}")
1169
+
1170
+ # Enriched user message aakhir mein append karo
1171
+ enriched_message = user_message + openmed_context
1172
+ messages.append({"role": "user", "content": enriched_message})
1173
+
1174
+ # Groq API call
1175
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
1176
+ res = requests.post(
1177
+ "https://api.groq.com/openai/v1/chat/completions",
1178
+ headers={"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"},
1179
+ json={
1180
+ "model": "llama-3.1-8b-instant",
1181
+ "messages": messages,
1182
+ "max_tokens": 480,
1183
+ "temperature": 0.2,
1184
+ },
1185
+ timeout=30
1186
+ )
1187
+
1188
+ if res.status_code != 200:
1189
+ return jsonify({"status": "error", "message": f"LLM error: {res.text[:200]}"}), 500
1190
+
1191
+ reply = res.json()["choices"][0]["message"]["content"]
1192
+ reply = safety_filter(reply)
1193
+
1194
+ # TTS generate karo
1195
+ tts_url = None
1196
+ try:
1197
+ tts_text = reply
1198
+ import re as _re
1199
+ tts_text = _re.sub(r'(DIAGNOSIS|FIRST LINE|SECOND LINE|TESTS|AVOID|NOTE):\s*', '', tts_text)
1200
+ _, fname = generate_tts(tts_text.strip(), lang)
1201
+ tts_url = f"/static/{fname}"
1202
+ except Exception as e:
1203
+ print(f"TTS error in chat: {e}")
1204
+
1205
+ return jsonify({
1206
+ "status": "success",
1207
+ "reply": reply,
1208
+ "audio_url": tts_url
1209
+ })
1210
+
1211
+ except Exception as e:
1212
+ return jsonify({"status": "error", "message": str(e)})
1213
+
1214
+
1215
+ @app.route("/health", methods=["GET"])
1216
+ def health():
1217
+ return jsonify({"status": "healthy"})
1218
+
1219
+
1220
+ @app.route("/")
1221
+ def home():
1222
+ return render_template("chatbot.html")
1223
+
1224
+
1225
+ @app.route("/database")
1226
+ def database_page():
1227
+ return render_template("history.html")
1228
+
1229
+
1230
+ # ==============================
1231
+ # RUN
1232
+ # ==============================
1233
+
1234
+ if __name__ == '__main__':
1235
+ print("πŸš€ Initializing Safecure AI...")
1236
+ init_db()
1237
+ get_vectorstore()
1238
+ print("βœ… Ready! Visit http://localhost:5000")
1239
+ port = int(os.environ.get('PORT', 5000))
1240
+ app.run(host='0.0.0.0', port=port,debug=False)
data/Antibiotic Guidelines (2020)_0.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1add4500e58dca4fd99838123f47a922a6a3feff11a7a607b9f27b46ef75ef24
3
+ size 526445
data/EAU-Guidelines-on-Urological-Infections-2024.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4f5be8b7509e9338818b639c712b118410ea7c67ee4fb37aaaa0f5287b9a006f
3
+ size 827122
data/General Fever + Viral URI.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e17581cf0829bea6dd4711dda95a727cc298a4ffbd16d37fa2bb89a7b4b0fb65
3
+ size 3103071
data/Gyan-Vahini-11-Safe-Drugs-Use-In-Pregnancy-Lactation-Nov-25.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:88e75853f479033ee319ce24001cd2fa32a8286ef2ebf129fd7dc3e0c845c8d8
3
+ size 3708230
data/Surgical Infection Society 2020 updated guidelines on the managem.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c39a3159d734ed0b0a9c3f48b38ee59a98e92db9e6759d9d7cf39382a3788b8f
3
+ size 311212
data/The_WHO_AWaRe_Access_Watch_Reserve_antib.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:37bfad4478fa0f8f25b9f5ee43c2b5e94703c53bd870837f7be8273fbd437714
3
+ size 48829332
data/Treatment_Guidelines_2019_Final.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:74e988ffe0603263ce131d043a187302d5845fbc4ff30aa74f9190ce99ff5016
3
+ size 4917213
data/WHO-Bacterial-Priority-2024.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5d883081dced7d404532d06e2a1a30c16c9528324c21c8b64f83230814a8e845
3
+ size 3560208
data/WHO-MHP-HPS-EML-2023.02-eng.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9617e1dee5f9772ec1154d4c4453c761fe53483c821525b9d7d9dee66a4850a1
3
+ size 926649
data/dengue.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b0ca16915451b163f47c7bbb2574423e7ecb873c7bb481e1a480e4d2d97d1067
3
+ size 6533834
data/executive_summary.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d79f88501db721412ed2282d1f7bcf3de30ed81f54f766188fd7dc2e4fe827f2
3
+ size 657667
data/influenza.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c845b39c8d6add9d5be54fb1ad7e7f1d806a3bb42d2f7465a147cd43dbab1f9b
3
+ size 3396859
data/malaria.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c5f43e97e43c1c79a344ed121d4daf8b98efc080a2dc56bab2c48a629f53b14e
3
+ size 2852047
data/practice-guidelines-for-the-diagnosis-and-management-of-skin-and-soft-tissue-infections-2014-update-by-the-infectious-diseases-society-of-america.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1a6b9ca7797fc9c81d68d993b1539201584b4d14d16d9ec7424d0386d8d5d4ad
3
+ size 1048509
data/typhoid.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d4f1f16ed8f847064953909afdad5048b735b64964d6f3a00db87a8f44201c51
3
+ size 230615
requirements.txt ADDED
Binary file (3.66 kB). View file
 
templates/chatbot.html ADDED
The diff for this file is too large to render. See raw diff
 
templates/history.html ADDED
@@ -0,0 +1,978 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Safecure AI | Patient Database</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link
11
+ href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300;14..32,400;14..32,500;14..32,600;14..32,700;14..32,800&display=swap"
12
+ rel="stylesheet">
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
14
+ <style>
15
+ * {
16
+ margin: 0;
17
+ padding: 0;
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ body {
22
+ font-family: 'Inter', sans-serif;
23
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
24
+ min-height: 100vh;
25
+ overflow-x: hidden;
26
+ }
27
+
28
+ .animated-bg {
29
+ position: fixed;
30
+ top: 0;
31
+ left: 0;
32
+ width: 100%;
33
+ height: 100%;
34
+ z-index: 0;
35
+ overflow: hidden;
36
+ }
37
+
38
+ .circles {
39
+ position: absolute;
40
+ top: 0;
41
+ left: 0;
42
+ width: 100%;
43
+ height: 100%;
44
+ overflow: hidden;
45
+ }
46
+
47
+ .circles li {
48
+ position: absolute;
49
+ display: block;
50
+ list-style: none;
51
+ width: 20px;
52
+ height: 20px;
53
+ background: rgba(255, 255, 255, 0.1);
54
+ animation: animate 25s linear infinite;
55
+ bottom: -150px;
56
+ border-radius: 50%;
57
+ }
58
+
59
+ @keyframes animate {
60
+ 0% {
61
+ transform: translateY(0) rotate(0deg);
62
+ opacity: 1;
63
+ }
64
+
65
+ 100% {
66
+ transform: translateY(-1000px) rotate(720deg);
67
+ opacity: 0;
68
+ }
69
+ }
70
+
71
+ .circles li:nth-child(1) {
72
+ left: 25%;
73
+ width: 80px;
74
+ height: 80px;
75
+ animation-delay: 0s;
76
+ }
77
+
78
+ .circles li:nth-child(2) {
79
+ left: 10%;
80
+ width: 20px;
81
+ height: 20px;
82
+ animation-delay: 2s;
83
+ animation-duration: 12s;
84
+ }
85
+
86
+ .circles li:nth-child(3) {
87
+ left: 70%;
88
+ width: 20px;
89
+ height: 20px;
90
+ animation-delay: 4s;
91
+ }
92
+
93
+ .circles li:nth-child(4) {
94
+ left: 40%;
95
+ width: 60px;
96
+ height: 60px;
97
+ animation-delay: 0s;
98
+ animation-duration: 18s;
99
+ }
100
+
101
+ .circles li:nth-child(5) {
102
+ left: 65%;
103
+ width: 20px;
104
+ height: 20px;
105
+ animation-delay: 0s;
106
+ }
107
+
108
+ .circles li:nth-child(6) {
109
+ left: 75%;
110
+ width: 110px;
111
+ height: 110px;
112
+ animation-delay: 3s;
113
+ }
114
+
115
+ .circles li:nth-child(7) {
116
+ left: 35%;
117
+ width: 150px;
118
+ height: 150px;
119
+ animation-delay: 7s;
120
+ }
121
+
122
+ .circles li:nth-child(8) {
123
+ left: 50%;
124
+ width: 25px;
125
+ height: 25px;
126
+ animation-delay: 15s;
127
+ animation-duration: 45s;
128
+ }
129
+
130
+ .circles li:nth-child(9) {
131
+ left: 20%;
132
+ width: 15px;
133
+ height: 15px;
134
+ animation-delay: 2s;
135
+ animation-duration: 35s;
136
+ }
137
+
138
+ .circles li:nth-child(10) {
139
+ left: 85%;
140
+ width: 150px;
141
+ height: 150px;
142
+ animation-delay: 0s;
143
+ animation-duration: 11s;
144
+ }
145
+
146
+ .container {
147
+ position: relative;
148
+ z-index: 1;
149
+ max-width: 1300px;
150
+ margin: 0 auto;
151
+ padding: 20px;
152
+ }
153
+
154
+ .glass-card {
155
+ background: rgba(255, 255, 255, 0.97);
156
+ backdrop-filter: blur(10px);
157
+ border-radius: 30px;
158
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
159
+ overflow: hidden;
160
+ animation: slideIn 0.6s ease-out;
161
+ }
162
+
163
+ @keyframes slideIn {
164
+ from {
165
+ opacity: 0;
166
+ transform: translateY(30px);
167
+ }
168
+
169
+ to {
170
+ opacity: 1;
171
+ transform: translateY(0);
172
+ }
173
+ }
174
+
175
+ .header {
176
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
177
+ padding: 25px 30px;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: space-between;
181
+ position: relative;
182
+ overflow: hidden;
183
+ }
184
+
185
+ .header::before {
186
+ content: '';
187
+ position: absolute;
188
+ top: -50%;
189
+ left: -50%;
190
+ width: 200%;
191
+ height: 200%;
192
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
193
+ animation: rotate 20s linear infinite;
194
+ }
195
+
196
+ @keyframes rotate {
197
+ from {
198
+ transform: rotate(0deg);
199
+ }
200
+
201
+ to {
202
+ transform: rotate(360deg);
203
+ }
204
+ }
205
+
206
+ .header-left {
207
+ position: relative;
208
+ z-index: 1;
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 15px;
212
+ }
213
+
214
+ .header-left i {
215
+ font-size: 2.5rem;
216
+ color: white;
217
+ }
218
+
219
+ .header-left h1 {
220
+ color: white;
221
+ font-size: 1.8rem;
222
+ font-weight: 800;
223
+ }
224
+
225
+ .header-left p {
226
+ color: rgba(255, 255, 255, 0.85);
227
+ font-size: 0.9rem;
228
+ }
229
+
230
+ .back-btn {
231
+ position: relative;
232
+ z-index: 1;
233
+ background: rgba(255, 255, 255, 0.2);
234
+ border: 2px solid rgba(255, 255, 255, 0.5);
235
+ color: white;
236
+ padding: 10px 20px;
237
+ border-radius: 50px;
238
+ font-size: 0.9rem;
239
+ font-weight: 600;
240
+ cursor: pointer;
241
+ font-family: 'Inter', sans-serif;
242
+ transition: all 0.3s;
243
+ text-decoration: none;
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 8px;
247
+ }
248
+
249
+ .back-btn:hover {
250
+ background: rgba(255, 255, 255, 0.35);
251
+ transform: translateY(-2px);
252
+ }
253
+
254
+ /* Stats Bar */
255
+ .stats-bar {
256
+ display: flex;
257
+ gap: 20px;
258
+ padding: 25px 30px;
259
+ background: linear-gradient(135deg, #f8f9ff, #f0f0ff);
260
+ border-bottom: 1px solid #e8e8f0;
261
+ flex-wrap: wrap;
262
+ }
263
+
264
+ .stat-card {
265
+ flex: 1;
266
+ min-width: 150px;
267
+ background: white;
268
+ border-radius: 15px;
269
+ padding: 15px 20px;
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 12px;
273
+ box-shadow: 0 3px 15px rgba(102, 126, 234, 0.1);
274
+ border-left: 4px solid #667eea;
275
+ }
276
+
277
+ .stat-card i {
278
+ font-size: 1.8rem;
279
+ color: #667eea;
280
+ }
281
+
282
+ .stat-card .stat-num {
283
+ font-size: 1.5rem;
284
+ font-weight: 800;
285
+ color: #333;
286
+ }
287
+
288
+ .stat-card .stat-label {
289
+ font-size: 0.75rem;
290
+ color: #888;
291
+ font-weight: 500;
292
+ }
293
+
294
+ /* Search & Filter */
295
+ .controls {
296
+ padding: 20px 30px;
297
+ display: flex;
298
+ gap: 15px;
299
+ flex-wrap: wrap;
300
+ border-bottom: 1px solid #f0f0f0;
301
+ }
302
+
303
+ .search-box {
304
+ flex: 1;
305
+ min-width: 200px;
306
+ position: relative;
307
+ }
308
+
309
+ .search-box i {
310
+ position: absolute;
311
+ left: 14px;
312
+ top: 50%;
313
+ transform: translateY(-50%);
314
+ color: #999;
315
+ }
316
+
317
+ .search-box input {
318
+ width: 100%;
319
+ padding: 11px 15px 11px 40px;
320
+ border: 2px solid #e8e8f0;
321
+ border-radius: 12px;
322
+ font-family: 'Inter', sans-serif;
323
+ font-size: 0.9rem;
324
+ transition: all 0.3s;
325
+ }
326
+
327
+ .search-box input:focus {
328
+ outline: none;
329
+ border-color: #667eea;
330
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
331
+ }
332
+
333
+ .filter-select {
334
+ padding: 11px 15px;
335
+ border: 2px solid #e8e8f0;
336
+ border-radius: 12px;
337
+ font-family: 'Inter', sans-serif;
338
+ font-size: 0.9rem;
339
+ color: #555;
340
+ background: white;
341
+ cursor: pointer;
342
+ transition: all 0.3s;
343
+ }
344
+
345
+ .filter-select:focus {
346
+ outline: none;
347
+ border-color: #667eea;
348
+ }
349
+
350
+ .refresh-btn {
351
+ padding: 11px 20px;
352
+ background: linear-gradient(135deg, #667eea, #764ba2);
353
+ color: white;
354
+ border: none;
355
+ border-radius: 12px;
356
+ font-family: 'Inter', sans-serif;
357
+ font-size: 0.9rem;
358
+ font-weight: 600;
359
+ cursor: pointer;
360
+ display: flex;
361
+ align-items: center;
362
+ gap: 8px;
363
+ transition: all 0.3s;
364
+ }
365
+
366
+ .refresh-btn:hover {
367
+ transform: translateY(-2px);
368
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
369
+ }
370
+
371
+ /* Table */
372
+ .table-container {
373
+ padding: 0 30px 30px;
374
+ overflow-x: auto;
375
+ }
376
+
377
+ table {
378
+ width: 100%;
379
+ border-collapse: collapse;
380
+ margin-top: 15px;
381
+ }
382
+
383
+ thead tr {
384
+ background: linear-gradient(135deg, #667eea, #764ba2);
385
+ color: white;
386
+ }
387
+
388
+ thead th {
389
+ padding: 14px 16px;
390
+ text-align: left;
391
+ font-size: 0.85rem;
392
+ font-weight: 600;
393
+ letter-spacing: 0.5px;
394
+ white-space: nowrap;
395
+ }
396
+
397
+ thead th:first-child {
398
+ border-radius: 12px 0 0 0;
399
+ }
400
+
401
+ thead th:last-child {
402
+ border-radius: 0 12px 0 0;
403
+ }
404
+
405
+ tbody tr {
406
+ border-bottom: 1px solid #f0f0f0;
407
+ transition: all 0.2s;
408
+ animation: rowSlide 0.4s ease-out;
409
+ }
410
+
411
+ @keyframes rowSlide {
412
+ from {
413
+ opacity: 0;
414
+ transform: translateX(-10px);
415
+ }
416
+
417
+ to {
418
+ opacity: 1;
419
+ transform: translateX(0);
420
+ }
421
+ }
422
+
423
+ tbody tr:hover {
424
+ background: #f8f8ff;
425
+ transform: scale(1.002);
426
+ }
427
+
428
+ tbody td {
429
+ padding: 14px 16px;
430
+ font-size: 0.85rem;
431
+ color: #444;
432
+ vertical-align: top;
433
+ max-width: 220px;
434
+ }
435
+
436
+ .patient-id-cell {
437
+ font-weight: 700;
438
+ color: #667eea;
439
+ font-family: monospace;
440
+ font-size: 0.9rem;
441
+ white-space: nowrap;
442
+ }
443
+
444
+ .antibiotic-badge {
445
+ display: inline-block;
446
+ padding: 4px 12px;
447
+ border-radius: 20px;
448
+ font-size: 0.75rem;
449
+ font-weight: 700;
450
+ letter-spacing: 0.5px;
451
+ }
452
+
453
+ .badge-yes {
454
+ background: #ffe0e0;
455
+ color: #d32f2f;
456
+ }
457
+
458
+ .badge-no {
459
+ background: #e0f5e9;
460
+ color: #2e7d32;
461
+ }
462
+
463
+ .truncate {
464
+ max-width: 200px;
465
+ overflow: hidden;
466
+ text-overflow: ellipsis;
467
+ white-space: nowrap;
468
+ cursor: pointer;
469
+ }
470
+
471
+ .truncate:hover {
472
+ white-space: normal;
473
+ overflow: visible;
474
+ }
475
+
476
+ .timestamp {
477
+ color: #999;
478
+ font-size: 0.78rem;
479
+ white-space: nowrap;
480
+ }
481
+
482
+ /* Modal */
483
+ .modal-overlay {
484
+ position: fixed;
485
+ inset: 0;
486
+ background: rgba(0, 0, 0, 0.6);
487
+ backdrop-filter: blur(8px);
488
+ z-index: 2000;
489
+ display: none;
490
+ justify-content: center;
491
+ align-items: center;
492
+ padding: 20px;
493
+ }
494
+
495
+ .modal-overlay.active {
496
+ display: flex;
497
+ }
498
+
499
+ .modal {
500
+ background: white;
501
+ border-radius: 20px;
502
+ max-width: 650px;
503
+ width: 100%;
504
+ max-height: 85vh;
505
+ overflow-y: auto;
506
+ animation: modalIn 0.3s ease-out;
507
+ }
508
+
509
+ @keyframes modalIn {
510
+ from {
511
+ opacity: 0;
512
+ transform: scale(0.9);
513
+ }
514
+
515
+ to {
516
+ opacity: 1;
517
+ transform: scale(1);
518
+ }
519
+ }
520
+
521
+ .modal-header {
522
+ background: linear-gradient(135deg, #667eea, #764ba2);
523
+ padding: 20px 25px;
524
+ border-radius: 20px 20px 0 0;
525
+ display: flex;
526
+ align-items: center;
527
+ justify-content: space-between;
528
+ }
529
+
530
+ .modal-header h3 {
531
+ color: white;
532
+ font-size: 1.1rem;
533
+ display: flex;
534
+ align-items: center;
535
+ gap: 10px;
536
+ }
537
+
538
+ .modal-close {
539
+ background: rgba(255, 255, 255, 0.2);
540
+ border: none;
541
+ color: white;
542
+ width: 32px;
543
+ height: 32px;
544
+ border-radius: 50%;
545
+ font-size: 1rem;
546
+ cursor: pointer;
547
+ transition: all 0.2s;
548
+ }
549
+
550
+ .modal-close:hover {
551
+ background: rgba(255, 255, 255, 0.4);
552
+ }
553
+
554
+ .modal-body {
555
+ padding: 25px;
556
+ }
557
+
558
+ .modal-section {
559
+ background: #f8f9ff;
560
+ border-radius: 12px;
561
+ padding: 15px;
562
+ margin-bottom: 12px;
563
+ border-left: 4px solid #667eea;
564
+ }
565
+
566
+ .modal-section h4 {
567
+ color: #667eea;
568
+ font-size: 0.85rem;
569
+ font-weight: 700;
570
+ margin-bottom: 6px;
571
+ text-transform: uppercase;
572
+ letter-spacing: 0.5px;
573
+ }
574
+
575
+ .modal-section p {
576
+ font-size: 0.9rem;
577
+ color: #444;
578
+ line-height: 1.5;
579
+ }
580
+
581
+ /* Empty state */
582
+ .empty-state {
583
+ text-align: center;
584
+ padding: 60px 20px;
585
+ color: #aaa;
586
+ }
587
+
588
+ .empty-state i {
589
+ font-size: 4rem;
590
+ margin-bottom: 15px;
591
+ display: block;
592
+ }
593
+
594
+ .empty-state p {
595
+ font-size: 1rem;
596
+ }
597
+
598
+ /* Loading */
599
+ .loading {
600
+ text-align: center;
601
+ padding: 40px;
602
+ }
603
+
604
+ .loading-spinner {
605
+ width: 40px;
606
+ height: 40px;
607
+ border: 4px solid #f0f0ff;
608
+ border-top: 4px solid #667eea;
609
+ border-radius: 50%;
610
+ animation: spin 0.8s linear infinite;
611
+ margin: 0 auto 15px;
612
+ }
613
+
614
+ @keyframes spin {
615
+ to {
616
+ transform: rotate(360deg);
617
+ }
618
+ }
619
+
620
+ @media (max-width: 768px) {
621
+ .stats-bar {
622
+ gap: 10px;
623
+ }
624
+
625
+ .controls {
626
+ flex-direction: column;
627
+ }
628
+
629
+ .table-container {
630
+ padding: 0 15px 20px;
631
+ }
632
+
633
+ thead th,
634
+ tbody td {
635
+ padding: 10px 12px;
636
+ }
637
+ }
638
+ </style>
639
+ </head>
640
+
641
+ <body>
642
+
643
+ <div class="animated-bg">
644
+ <ul class="circles">
645
+ <li></li>
646
+ <li></li>
647
+ <li></li>
648
+ <li></li>
649
+ <li></li>
650
+ <li></li>
651
+ <li></li>
652
+ <li></li>
653
+ <li></li>
654
+ <li></li>
655
+ </ul>
656
+ </div>
657
+
658
+ <div class="container">
659
+ <div class="glass-card">
660
+
661
+ <!-- Header -->
662
+ <div class="header">
663
+ <div class="header-left">
664
+ <i class="bi bi-database-fill-gear"></i>
665
+ <div>
666
+ <h1>Patient Database</h1>
667
+ <p>Safecure AI β€” Clinical Records</p>
668
+ </div>
669
+ </div>
670
+ <a href="/" class="back-btn">
671
+ <i class="bi bi-arrow-left"></i> Back to Analysis
672
+ </a>
673
+ </div>
674
+
675
+ <!-- Stats -->
676
+ <div class="stats-bar" id="statsBar">
677
+ <div class="stat-card">
678
+ <i class="bi bi-people-fill"></i>
679
+ <div>
680
+ <div class="stat-num" id="totalCount">β€”</div>
681
+ <div class="stat-label">Total Patients</div>
682
+ </div>
683
+ </div>
684
+ <div class="stat-card" style="border-left-color:#d32f2f">
685
+ <i class="bi bi-capsule-pill" style="color:#d32f2f"></i>
686
+ <div>
687
+ <div class="stat-num" id="antibioticYes">β€”</div>
688
+ <div class="stat-label">Antibiotic Prescribed</div>
689
+ </div>
690
+ </div>
691
+ <div class="stat-card" style="border-left-color:#2e7d32">
692
+ <i class="bi bi-shield-check" style="color:#2e7d32"></i>
693
+ <div>
694
+ <div class="stat-num" id="antibioticNo">β€”</div>
695
+ <div class="stat-label">Antibiotic Not Required</div>
696
+ </div>
697
+ </div>
698
+ <div class="stat-card" style="border-left-color:#f57c00">
699
+ <i class="bi bi-clock-history" style="color:#f57c00"></i>
700
+ <div>
701
+ <div class="stat-num" id="todayCount">β€”</div>
702
+ <div class="stat-label">Today's Cases</div>
703
+ </div>
704
+ </div>
705
+ </div>
706
+
707
+ <!-- Controls -->
708
+ <div class="controls">
709
+ <div class="search-box">
710
+ <i class="bi bi-search"></i>
711
+ <input type="text" id="searchInput" placeholder="Search by Patient ID, symptoms, diagnosis..."
712
+ oninput="filterTable()">
713
+ </div>
714
+ <select class="filter-select" id="antibioticFilter" onchange="filterTable()">
715
+ <option value="">All Cases</option>
716
+ <option value="yes">Antibiotic: YES</option>
717
+ <option value="no">Antibiotic: NO</option>
718
+ </select>
719
+ <button class="refresh-btn" onclick="loadPatients()">
720
+ <i class="bi bi-arrow-clockwise"></i> Refresh
721
+ </button>
722
+ <button class="refresh-btn" style="background:linear-gradient(135deg,#28a745,#20c997);"
723
+ onclick="downloadCSV()">
724
+ <i class="bi bi-filetype-csv"></i> CSV
725
+ </button>
726
+ <button class="refresh-btn" style="background:linear-gradient(135deg,#217346,#1e6e3e);"
727
+ onclick="downloadExcel()">
728
+ <i class="bi bi-file-earmark-excel-fill"></i> Excel
729
+ </button>
730
+ </div>
731
+
732
+ <!-- Table -->
733
+ <div class="table-container">
734
+ <div id="loadingState" class="loading">
735
+ <div class="loading-spinner"></div>
736
+ <p style="color:#999">Loading patient records...</p>
737
+ </div>
738
+
739
+ <table id="patientsTable" style="display:none">
740
+ <thead>
741
+ <tr>
742
+ <th><i class="bi bi-person-badge"></i> Patient ID</th>
743
+ <th><i class="bi bi-clock"></i> Timestamp</th>
744
+ <th><i class="bi bi-activity"></i> Symptoms</th>
745
+ <th><i class="bi bi-clipboard2-pulse"></i> Diagnosis</th>
746
+ <th><i class="bi bi-capsule"></i> Antibiotics</th>
747
+ <th><i class="bi bi-star"></i> First-Line</th>
748
+ <th><i class="bi bi-eye"></i> Details</th>
749
+ </tr>
750
+ </thead>
751
+ <tbody id="tableBody"></tbody>
752
+ </table>
753
+
754
+ <div id="emptyState" class="empty-state" style="display:none">
755
+ <i class="bi bi-database-x"></i>
756
+ <p>No patient records found.</p>
757
+ </div>
758
+ </div>
759
+
760
+ </div>
761
+ </div>
762
+
763
+ <!-- Modal -->
764
+ <div class="modal-overlay" id="modalOverlay" onclick="closeModal(event)">
765
+ <div class="modal" id="modal">
766
+ <div class="modal-header">
767
+ <h3><i class="bi bi-file-medical-fill"></i> <span id="modalTitle">Patient Details</span></h3>
768
+ <button class="modal-close" onclick="closeModalDirect()">βœ•</button>
769
+ </div>
770
+ <div class="modal-body" id="modalBody"></div>
771
+ </div>
772
+ </div>
773
+
774
+ <script>
775
+ let allPatients = [];
776
+
777
+ async function loadPatients() {
778
+ document.getElementById('loadingState').style.display = 'block';
779
+ document.getElementById('patientsTable').style.display = 'none';
780
+ document.getElementById('emptyState').style.display = 'none';
781
+
782
+ try {
783
+ const res = await fetch('/patients');
784
+ const data = await res.json();
785
+
786
+ if (data.status === 'success') {
787
+ allPatients = data.patients;
788
+ updateStats(allPatients);
789
+ renderTable(allPatients);
790
+ }
791
+ } catch (e) {
792
+ document.getElementById('loadingState').innerHTML = `<p style="color:#e74c3c">❌ Failed to load. Is the server running?</p>`;
793
+ }
794
+ }
795
+
796
+ function updateStats(patients) {
797
+ const today = new Date().toISOString().split('T')[0];
798
+ const yesCount = patients.filter(p => p.antibiotic_necessity && p.antibiotic_necessity.toUpperCase().startsWith('YES')).length;
799
+ const noCount = patients.filter(p => p.antibiotic_necessity && p.antibiotic_necessity.toUpperCase().startsWith('NO')).length;
800
+ const todayCount = patients.filter(p => p.timestamp && p.timestamp.startsWith(today)).length;
801
+
802
+ document.getElementById('totalCount').textContent = patients.length;
803
+ document.getElementById('antibioticYes').textContent = yesCount;
804
+ document.getElementById('antibioticNo').textContent = noCount;
805
+ document.getElementById('todayCount').textContent = todayCount;
806
+ }
807
+
808
+ function renderTable(patients) {
809
+ document.getElementById('loadingState').style.display = 'none';
810
+
811
+ if (!patients.length) {
812
+ document.getElementById('emptyState').style.display = 'block';
813
+ return;
814
+ }
815
+
816
+ document.getElementById('patientsTable').style.display = 'table';
817
+ const tbody = document.getElementById('tableBody');
818
+ tbody.innerHTML = '';
819
+
820
+ patients.forEach(p => {
821
+ const antiYes = p.antibiotic_necessity && p.antibiotic_necessity.toUpperCase().startsWith('YES');
822
+ const badge = antiYes
823
+ ? `<span class="antibiotic-badge badge-yes">YES</span>`
824
+ : `<span class="antibiotic-badge badge-no">NO</span>`;
825
+
826
+ const diagnosis = (p.assessment || '').split('.')[0];
827
+ const firstLine = (p.first_line || 'Not required').split(' ')[0] + (p.first_line && p.first_line.length > 20 ? '...' : '');
828
+
829
+ const row = document.createElement('tr');
830
+ const idx = allPatients.indexOf(p);
831
+ row.innerHTML = `
832
+ <td class="patient-id-cell">${p.id}</td>
833
+ <td class="timestamp">${p.timestamp}</td>
834
+ <td><div class="truncate" title="${escHtml(p.condition)}">${escHtml(p.condition || 'β€”')}</div></td>
835
+ <td><div class="truncate" title="${escHtml(p.assessment)}">${escHtml(diagnosis || 'β€”')}</div></td>
836
+ <td>${badge}</td>
837
+ <td><div class="truncate" title="${escHtml(p.first_line)}">${escHtml(firstLine || 'β€”')}</div></td>
838
+ <td>
839
+ <button onclick='showModal(${idx})' style="
840
+ background: linear-gradient(135deg,#667eea,#764ba2);
841
+ color:white; border:none; border-radius:8px;
842
+ padding:6px 14px; cursor:pointer; font-size:0.8rem;
843
+ font-family:Inter,sans-serif; font-weight:600; transition:all 0.2s;">
844
+ <i class="bi bi-eye"></i> View
845
+ </button>
846
+ </td>
847
+ `;
848
+ tbody.appendChild(row);
849
+ });
850
+ }
851
+
852
+ function filterTable() {
853
+ const search = document.getElementById('searchInput').value.toLowerCase();
854
+ const abFilter = document.getElementById('antibioticFilter').value.toLowerCase();
855
+
856
+ const filtered = allPatients.filter(p => {
857
+ const matchSearch = !search ||
858
+ (p.id || '').toLowerCase().includes(search) ||
859
+ (p.condition || '').toLowerCase().includes(search) ||
860
+ (p.assessment || '').toLowerCase().includes(search) ||
861
+ (p.allergies || '').toLowerCase().includes(search);
862
+
863
+ const matchAb = !abFilter ||
864
+ (abFilter === 'yes' && p.antibiotic_necessity && p.antibiotic_necessity.toUpperCase().startsWith('YES')) ||
865
+ (abFilter === 'no' && p.antibiotic_necessity && p.antibiotic_necessity.toUpperCase().startsWith('NO'));
866
+
867
+ return matchSearch && matchAb;
868
+ });
869
+
870
+ renderTable(filtered);
871
+ }
872
+
873
+ function showModal(idx) {
874
+ const p = allPatients[idx];
875
+ document.getElementById('modalTitle').textContent = `Patient ${p.id}`;
876
+ document.getElementById('modalBody').innerHTML = `
877
+ <div class="modal-section">
878
+ <h4>πŸͺͺ Patient Info</h4>
879
+ <p><strong>ID:</strong> ${p.id} &nbsp;|&nbsp; <strong>Date:</strong> ${p.timestamp}</p>
880
+ </div>
881
+ <div class="modal-section">
882
+ <h4>🩺 Symptoms / Condition</h4>
883
+ <p>${escHtml(p.condition || 'β€”')}</p>
884
+ </div>
885
+ <div class="modal-section">
886
+ <h4>⚠️ Allergies</h4>
887
+ <p>${escHtml(p.allergies || 'None reported')}</p>
888
+ </div>
889
+ <div class="modal-section">
890
+ <h4>πŸ’Š Current Medications</h4>
891
+ <p>${escHtml(p.medications || 'None reported')}</p>
892
+ </div>
893
+ <div class="modal-section">
894
+ <h4>πŸ”¬ Clinical Assessment</h4>
895
+ <p>${escHtml(p.assessment || 'β€”')}</p>
896
+ </div>
897
+ <div class="modal-section" style="border-left-color:${p.antibiotic_necessity && p.antibiotic_necessity.toUpperCase().startsWith('YES') ? '#d32f2f' : '#2e7d32'}">
898
+ <h4>πŸ’‰ Antibiotic Necessity</h4>
899
+ <p>${escHtml(p.antibiotic_necessity || 'β€”')}</p>
900
+ </div>
901
+ <div class="modal-section">
902
+ <h4>⭐ First-Line Therapy</h4>
903
+ <p>${escHtml(p.first_line || 'Not required')}</p>
904
+ </div>
905
+ <div class="modal-section">
906
+ <h4>πŸ”„ Second-Line Alternatives</h4>
907
+ <p>${escHtml(p.second_line || 'Not required')}</p>
908
+ </div>
909
+ <div class="modal-section">
910
+ <h4>🚫 Contraindications & Precautions</h4>
911
+ <p>${escHtml(p.contraindications || 'β€”')}</p>
912
+ </div>
913
+ `;
914
+ document.getElementById('modalOverlay').classList.add('active');
915
+ }
916
+
917
+ function closeModal(e) {
918
+ if (e.target === document.getElementById('modalOverlay')) closeModalDirect();
919
+ }
920
+
921
+ function closeModalDirect() {
922
+ document.getElementById('modalOverlay').classList.remove('active');
923
+ }
924
+
925
+ function downloadCSV() {
926
+ if (!allPatients.length) return alert('No data to download.');
927
+ const headers = ['Patient ID', 'Timestamp', 'Symptoms', 'Allergies', 'Medications', 'Diagnosis', 'Antibiotics', 'First-Line', 'Second-Line', 'Contraindications'];
928
+ const rows = allPatients.map(p => [
929
+ p.id, p.timestamp, p.condition, p.allergies, p.medications,
930
+ p.assessment, p.antibiotic_necessity, p.first_line, p.second_line, p.contraindications
931
+ ].map(v => `"${(v || '').replace(/"/g, '""')}"`));
932
+
933
+ const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
934
+ const blob = new Blob([csv], { type: 'text/csv' });
935
+ const a = document.createElement('a');
936
+ a.href = URL.createObjectURL(blob);
937
+ a.download = `safecure_patients_${new Date().toISOString().split('T')[0]}.csv`;
938
+ a.click();
939
+ }
940
+
941
+ function downloadExcel() {
942
+ if (!allPatients.length) return alert('No data to download.');
943
+ const headers = ['Patient ID', 'Timestamp', 'Symptoms', 'Allergies', 'Medications', 'Diagnosis', 'Antibiotics', 'First-Line', 'Second-Line', 'Contraindications'];
944
+ const rows = allPatients.map(p => [
945
+ p.id, p.timestamp, p.condition, p.allergies, p.medications,
946
+ p.assessment, p.antibiotic_necessity, p.first_line, p.second_line, p.contraindications
947
+ ]);
948
+
949
+ // Build XML-based Excel (xls)
950
+ let xml = `<?xml version="1.0"?>
951
+ <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
952
+ xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
953
+ <Worksheet ss:Name="Patients"><Table>`;
954
+
955
+ xml += '<Row>' + headers.map(h => `<Cell><Data ss:Type="String">${h}</Data></Cell>`).join('') + '</Row>';
956
+ rows.forEach(row => {
957
+ xml += '<Row>' + row.map(v => `<Cell><Data ss:Type="String">${(v || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</Data></Cell>`).join('') + '</Row>';
958
+ });
959
+
960
+ xml += '</Table></Worksheet></Workbook>';
961
+ const blob = new Blob([xml], { type: 'application/vnd.ms-excel' });
962
+ const a = document.createElement('a');
963
+ a.href = URL.createObjectURL(blob);
964
+ a.download = `safecure_patients_${new Date().toISOString().split('T')[0]}.xls`;
965
+ a.click();
966
+ }
967
+
968
+ function escHtml(str) {
969
+ if (!str) return '';
970
+ return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[m]));
971
+ }
972
+
973
+ // Load on start
974
+ loadPatients();
975
+ </script>
976
+ </body>
977
+
978
+ </html>