dylanglenister commited on
Commit
d6a79b9
Β·
1 Parent(s): 7fbfb89

Merge branch 'main' of hf.co:spaces/MedAI-COS30018/MedicalDiagnosisSystem into hf_main

Browse files
.dockerignore CHANGED
@@ -1,7 +1,14 @@
1
  .git
2
  .gitignore
 
3
  .env
4
  .venv
 
5
  __pycache__
6
  *.pyc
 
 
 
 
 
7
  key
 
1
  .git
2
  .gitignore
3
+
4
  .env
5
  .venv
6
+
7
  __pycache__
8
  *.pyc
9
+
10
+ *.md
11
+ LICENSE
12
+
13
+ tests
14
  key
schemas/account_validator.json CHANGED
@@ -6,8 +6,7 @@
6
  "name",
7
  "role",
8
  "created_at",
9
- "updated_at",
10
- "last_seen"
11
  ],
12
  "properties": {
13
  "name": {
 
6
  "name",
7
  "role",
8
  "created_at",
9
+ "updated_at"
 
10
  ],
11
  "properties": {
12
  "name": {
src/api/routes/emr.py CHANGED
@@ -3,10 +3,13 @@
3
  from datetime import datetime, timezone
4
  from typing import List, Optional
5
 
6
- from fastapi import APIRouter, Depends, HTTPException
 
7
 
8
- from src.models.emr import EMRResponse, EMRSearchRequest, EMRUpdateRequest
9
  from src.services.service import EMRService
 
 
10
  from src.core.state import AppState, get_state
11
  from src.utils.logger import logger
12
 
@@ -17,7 +20,7 @@ router = APIRouter(prefix="/emr", tags=["EMR"])
17
  async def check_emr_exists(message_id: str):
18
  """Check if EMR extraction has already been done for a message."""
19
  try:
20
- from data.repositories.emr import check_emr_exists
21
  exists = check_emr_exists(message_id)
22
  return {
23
  "message_id": message_id,
@@ -88,11 +91,11 @@ async def extract_emr_from_message(
88
  patient = get_patient_by_id(patient_id)
89
  if patient:
90
  patient_context = {
91
- "name": patient.get("name"),
92
- "age": patient.get("age"),
93
- "sex": patient.get("sex"),
94
- "medications": patient.get("medications", []),
95
- "past_assessment_summary": patient.get("past_assessment_summary")
96
  }
97
  except Exception as e:
98
  logger().warning(f"Could not fetch patient context: {e}")
@@ -278,11 +281,11 @@ async def bulk_extract_emr(
278
  patient = get_patient_by_id(extraction['patient_id'])
279
  if patient:
280
  patient_context = {
281
- "name": patient.get("name"),
282
- "age": patient.get("age"),
283
- "sex": patient.get("sex"),
284
- "medications": patient.get("medications", []),
285
- "past_assessment_summary": patient.get("past_assessment_summary")
286
  }
287
  except Exception as e:
288
  logger().warning(f"Could not fetch patient context for extraction {i}: {e}")
@@ -324,3 +327,300 @@ async def bulk_extract_emr(
324
  except Exception as e:
325
  logger().error(f"Error in bulk EMR extraction: {e}")
326
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from datetime import datetime, timezone
4
  from typing import List, Optional
5
 
6
+ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
7
+ from fastapi.responses import JSONResponse
8
 
9
+ from src.models.emr import EMRResponse, EMRSearchRequest, EMRUpdateRequest, ExtractedData
10
  from src.services.service import EMRService
11
+ from src.services.extractor import EMRExtractor
12
+ from src.data.emr_update import EMRUpdateService
13
  from src.core.state import AppState, get_state
14
  from src.utils.logger import logger
15
 
 
20
  async def check_emr_exists(message_id: str):
21
  """Check if EMR extraction has already been done for a message."""
22
  try:
23
+ from src.data.repositories.emr import check_emr_exists
24
  exists = check_emr_exists(message_id)
25
  return {
26
  "message_id": message_id,
 
91
  patient = get_patient_by_id(patient_id)
92
  if patient:
93
  patient_context = {
94
+ "name": patient.name,
95
+ "age": patient.age,
96
+ "sex": patient.sex,
97
+ "medications": patient.medications or [],
98
+ "past_assessment_summary": patient.past_assessment_summary
99
  }
100
  except Exception as e:
101
  logger().warning(f"Could not fetch patient context: {e}")
 
281
  patient = get_patient_by_id(extraction['patient_id'])
282
  if patient:
283
  patient_context = {
284
+ "name": patient.name,
285
+ "age": patient.age,
286
+ "sex": patient.sex,
287
+ "medications": patient.medications or [],
288
+ "past_assessment_summary": patient.past_assessment_summary
289
  }
290
  except Exception as e:
291
  logger().warning(f"Could not fetch patient context for extraction {i}: {e}")
 
327
  except Exception as e:
328
  logger().error(f"Error in bulk EMR extraction: {e}")
329
  raise HTTPException(status_code=500, detail=str(e))
330
+
331
+
332
+ def get_emr_extractor(state: AppState = Depends(get_state)) -> EMRExtractor:
333
+ """Get EMR extractor instance."""
334
+ return EMRExtractor(state.gemini_rotator)
335
+
336
+
337
+ def get_emr_update_service() -> EMRUpdateService:
338
+ """Get EMR update service instance."""
339
+ return EMRUpdateService()
340
+
341
+
342
+ @router.post("/upload-document", response_model=dict)
343
+ async def upload_and_analyze_document(
344
+ patient_id: str = Form(...),
345
+ file: UploadFile = File(...),
346
+ emr_extractor: EMRExtractor = Depends(get_emr_extractor),
347
+ emr_update_service: EMRUpdateService = Depends(get_emr_update_service)
348
+ ):
349
+ """Upload and analyze a medical document to extract EMR data."""
350
+ try:
351
+ # Validate patient ID
352
+ if not patient_id or not patient_id.strip():
353
+ raise HTTPException(status_code=400, detail="Patient ID is required")
354
+
355
+ # Validate file
356
+ if not file or not file.filename:
357
+ raise HTTPException(status_code=400, detail="No file provided")
358
+
359
+ # Check file size (limit to 10MB)
360
+ file_content = await file.read()
361
+ if len(file_content) > 10 * 1024 * 1024: # 10MB
362
+ raise HTTPException(status_code=400, detail="File size exceeds 10MB limit")
363
+
364
+ # Check file type
365
+ allowed_extensions = {'.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png', '.tiff'}
366
+ file_extension = '.' + file.filename.split('.')[-1].lower() if '.' in file.filename else ''
367
+ if file_extension not in allowed_extensions:
368
+ raise HTTPException(
369
+ status_code=400,
370
+ detail=f"Unsupported file type. Allowed types: {', '.join(allowed_extensions)}"
371
+ )
372
+
373
+ logger().info(f"Document upload requested for patient {patient_id}, file: {file.filename}")
374
+
375
+ # Get patient context if available
376
+ patient_context = None
377
+ try:
378
+ from src.data.repositories.patient import get_patient_by_id
379
+ patient = get_patient_by_id(patient_id)
380
+ if patient:
381
+ patient_context = {
382
+ "name": patient.name,
383
+ "age": patient.age,
384
+ "sex": patient.sex,
385
+ "medications": patient.medications or [],
386
+ "past_assessment_summary": patient.past_assessment_summary
387
+ }
388
+ except Exception as e:
389
+ logger().warning(f"Could not fetch patient context: {e}")
390
+
391
+ # Analyze the document
392
+ extracted_data, confidence_score = await emr_extractor.analyze_document(
393
+ file_content=file_content,
394
+ filename=file.filename,
395
+ patient_context=patient_context
396
+ )
397
+
398
+ # Save to database
399
+ emr_id = await emr_update_service.save_document_analysis(
400
+ patient_id=patient_id,
401
+ filename=file.filename,
402
+ file_content=file_content,
403
+ extracted_data=extracted_data,
404
+ confidence_score=confidence_score
405
+ )
406
+
407
+ return {
408
+ "emr_id": emr_id,
409
+ "filename": file.filename,
410
+ "confidence_score": confidence_score,
411
+ "extracted_data": {
412
+ "overview": extracted_data.notes.split("Document Overview: ")[-1] if "Document Overview:" in extracted_data.notes else "",
413
+ "diagnosis": extracted_data.diagnosis or [],
414
+ "symptoms": extracted_data.symptoms or [],
415
+ "medications": [
416
+ {
417
+ "name": med.name,
418
+ "dosage": med.dosage,
419
+ "frequency": med.frequency,
420
+ "duration": med.duration
421
+ }
422
+ for med in extracted_data.medications or []
423
+ ],
424
+ "vital_signs": {
425
+ "blood_pressure": extracted_data.vital_signs.blood_pressure if extracted_data.vital_signs else None,
426
+ "heart_rate": extracted_data.vital_signs.heart_rate if extracted_data.vital_signs else None,
427
+ "temperature": extracted_data.vital_signs.temperature if extracted_data.vital_signs else None,
428
+ "respiratory_rate": extracted_data.vital_signs.respiratory_rate if extracted_data.vital_signs else None,
429
+ "oxygen_saturation": extracted_data.vital_signs.oxygen_saturation if extracted_data.vital_signs else None
430
+ } if extracted_data.vital_signs else None,
431
+ "lab_results": [
432
+ {
433
+ "test_name": lab.test_name,
434
+ "value": lab.value,
435
+ "unit": lab.unit,
436
+ "reference_range": lab.reference_range
437
+ }
438
+ for lab in extracted_data.lab_results or []
439
+ ],
440
+ "procedures": extracted_data.procedures or [],
441
+ "notes": extracted_data.notes or ""
442
+ },
443
+ "message": "Document analyzed and EMR data extracted successfully"
444
+ }
445
+
446
+ except HTTPException:
447
+ raise
448
+ except Exception as e:
449
+ logger().error(f"Error in document upload and analysis: {e}")
450
+ raise HTTPException(status_code=500, detail=str(e))
451
+
452
+
453
+ @router.post("/preview-document", response_model=dict)
454
+ async def preview_document_analysis(
455
+ patient_id: str = Form(...),
456
+ file: UploadFile = File(...),
457
+ emr_extractor: EMRExtractor = Depends(get_emr_extractor)
458
+ ):
459
+ """Upload and analyze a medical document to preview extracted data before saving."""
460
+ try:
461
+ # Validate patient ID
462
+ if not patient_id or not patient_id.strip():
463
+ raise HTTPException(status_code=400, detail="Patient ID is required")
464
+
465
+ # Validate file
466
+ if not file or not file.filename:
467
+ raise HTTPException(status_code=400, detail="No file provided")
468
+
469
+ # Check file size (limit to 10MB)
470
+ file_content = await file.read()
471
+ if len(file_content) > 10 * 1024 * 1024: # 10MB
472
+ raise HTTPException(status_code=400, detail="File size exceeds 10MB limit")
473
+
474
+ # Check file type
475
+ allowed_extensions = {'.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png', '.tiff'}
476
+ file_extension = '.' + file.filename.split('.')[-1].lower() if '.' in file.filename else ''
477
+ if file_extension not in allowed_extensions:
478
+ raise HTTPException(
479
+ status_code=400,
480
+ detail=f"Unsupported file type. Allowed types: {', '.join(allowed_extensions)}"
481
+ )
482
+
483
+ logger().info(f"Document preview requested for patient {patient_id}, file: {file.filename}")
484
+
485
+ # Get patient context if available
486
+ patient_context = None
487
+ try:
488
+ from src.data.repositories.patient import get_patient_by_id
489
+ patient = get_patient_by_id(patient_id)
490
+ if patient:
491
+ patient_context = {
492
+ "name": patient.name,
493
+ "age": patient.age,
494
+ "sex": patient.sex,
495
+ "medications": patient.medications or [],
496
+ "past_assessment_summary": patient.past_assessment_summary
497
+ }
498
+ except Exception as e:
499
+ logger().warning(f"Could not fetch patient context: {e}")
500
+
501
+ # Analyze the document
502
+ extracted_data, confidence_score = await emr_extractor.analyze_document(
503
+ file_content=file_content,
504
+ filename=file.filename,
505
+ patient_context=patient_context
506
+ )
507
+
508
+ return {
509
+ "filename": file.filename,
510
+ "confidence_score": confidence_score,
511
+ "extracted_data": {
512
+ "overview": extracted_data.notes.split("Document Overview: ")[-1] if "Document Overview:" in extracted_data.notes else "",
513
+ "diagnosis": extracted_data.diagnosis or [],
514
+ "symptoms": extracted_data.symptoms or [],
515
+ "medications": [
516
+ {
517
+ "name": med.name,
518
+ "dosage": med.dosage,
519
+ "frequency": med.frequency,
520
+ "duration": med.duration
521
+ }
522
+ for med in extracted_data.medications or []
523
+ ],
524
+ "vital_signs": {
525
+ "blood_pressure": extracted_data.vital_signs.blood_pressure if extracted_data.vital_signs else None,
526
+ "heart_rate": extracted_data.vital_signs.heart_rate if extracted_data.vital_signs else None,
527
+ "temperature": extracted_data.vital_signs.temperature if extracted_data.vital_signs else None,
528
+ "respiratory_rate": extracted_data.vital_signs.respiratory_rate if extracted_data.vital_signs else None,
529
+ "oxygen_saturation": extracted_data.vital_signs.oxygen_saturation if extracted_data.vital_signs else None
530
+ } if extracted_data.vital_signs else None,
531
+ "lab_results": [
532
+ {
533
+ "test_name": lab.test_name,
534
+ "value": lab.value,
535
+ "unit": lab.unit,
536
+ "reference_range": lab.reference_range
537
+ }
538
+ for lab in extracted_data.lab_results or []
539
+ ],
540
+ "procedures": extracted_data.procedures or [],
541
+ "notes": extracted_data.notes or ""
542
+ },
543
+ "message": "Document analyzed successfully. Review the data before saving."
544
+ }
545
+
546
+ except HTTPException:
547
+ raise
548
+ except Exception as e:
549
+ logger().error(f"Error in document preview: {e}")
550
+ raise HTTPException(status_code=500, detail=str(e))
551
+
552
+
553
+ @router.post("/save-document-analysis", response_model=dict)
554
+ async def save_document_analysis(
555
+ patient_id: str = Form(...),
556
+ filename: str = Form(...),
557
+ extracted_data: str = Form(...), # JSON string
558
+ confidence_score: float = Form(...),
559
+ emr_update_service: EMRUpdateService = Depends(get_emr_update_service)
560
+ ):
561
+ """Save document analysis results to EMR database."""
562
+ try:
563
+ import json
564
+
565
+ # Validate inputs
566
+ if not patient_id or not patient_id.strip():
567
+ raise HTTPException(status_code=400, detail="Patient ID is required")
568
+ if not filename or not filename.strip():
569
+ raise HTTPException(status_code=400, detail="Filename is required")
570
+ if not extracted_data or not extracted_data.strip():
571
+ raise HTTPException(status_code=400, detail="Extracted data is required")
572
+
573
+ # Parse extracted data
574
+ try:
575
+ data_dict = json.loads(extracted_data)
576
+ except json.JSONDecodeError as e:
577
+ raise HTTPException(status_code=400, detail=f"Invalid JSON in extracted data: {e}")
578
+
579
+ # Convert to ExtractedData object
580
+ extracted_data_obj = ExtractedData(
581
+ diagnosis=data_dict.get('diagnosis', []),
582
+ symptoms=data_dict.get('symptoms', []),
583
+ medications=[
584
+ {
585
+ "name": med.get('name', ''),
586
+ "dosage": med.get('dosage'),
587
+ "frequency": med.get('frequency'),
588
+ "duration": med.get('duration')
589
+ }
590
+ for med in data_dict.get('medications', [])
591
+ ],
592
+ vital_signs=data_dict.get('vital_signs'),
593
+ lab_results=[
594
+ {
595
+ "test_name": lab.get('test_name', ''),
596
+ "value": lab.get('value', ''),
597
+ "unit": lab.get('unit'),
598
+ "reference_range": lab.get('reference_range')
599
+ }
600
+ for lab in data_dict.get('lab_results', [])
601
+ ],
602
+ procedures=data_dict.get('procedures', []),
603
+ notes=data_dict.get('notes', '') + (f"\n\nDocument Overview: {data_dict.get('overview', '')}" if data_dict.get('overview') else '')
604
+ )
605
+
606
+ logger().info(f"Saving document analysis for patient {patient_id}, file: {filename}")
607
+
608
+ # Save to database (without file content for preview saves)
609
+ emr_id = await emr_update_service.save_document_analysis(
610
+ patient_id=patient_id,
611
+ filename=filename,
612
+ file_content=b"", # Empty for preview saves
613
+ extracted_data=extracted_data_obj,
614
+ confidence_score=confidence_score
615
+ )
616
+
617
+ return {
618
+ "emr_id": emr_id,
619
+ "message": "Document analysis saved to EMR successfully"
620
+ }
621
+
622
+ except HTTPException:
623
+ raise
624
+ except Exception as e:
625
+ logger().error(f"Error saving document analysis: {e}")
626
+ raise HTTPException(status_code=500, detail=str(e))
src/api/routes/migration.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/api/routes/migration.py
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, status
4
+ from typing import Dict, Any
5
+
6
+ from src.core.state import AppState, get_state
7
+ from src.data.migration import run_database_migration
8
+ from src.utils.logger import logger
9
+
10
+ router = APIRouter(prefix="/migration", tags=["Migration"])
11
+
12
+ @router.post("/fix-database", response_model=Dict[str, Any])
13
+ async def fix_database_records(
14
+ state: AppState = Depends(get_state)
15
+ ):
16
+ """
17
+ Fix existing database records that have missing or invalid required fields.
18
+ This endpoint ensures all records conform to current Pydantic model requirements.
19
+ """
20
+ logger().info("Migration endpoint called - starting database fix")
21
+
22
+ try:
23
+ result = run_database_migration()
24
+
25
+ if result['success']:
26
+ return {
27
+ "message": "Database migration completed successfully",
28
+ "stats": result
29
+ }
30
+ else:
31
+ raise HTTPException(
32
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
33
+ detail=f"Migration failed: {result.get('error', 'Unknown error')}"
34
+ )
35
+
36
+ except Exception as e:
37
+ logger().error(f"Migration endpoint error: {e}")
38
+ raise HTTPException(
39
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
40
+ detail=f"Migration failed: {str(e)}"
41
+ )
src/api/routes/session.py CHANGED
@@ -8,6 +8,7 @@ from src.core.state import AppState, get_state
8
  from src.models.session import (ChatRequest, ChatResponse, Message, Session,
9
  SessionCreateRequest)
10
  from src.services.medical_response import generate_medical_response
 
11
  from src.utils.logger import logger
12
 
13
  router = APIRouter(prefix="/session", tags=["Session & Chat"])
@@ -85,6 +86,22 @@ async def post_chat_message(
85
  """
86
  logger().info(f"POST /session/{session_id}/messages")
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  # 1. Get Enhanced Context
89
  try:
90
  medical_context = await state.memory_manager.get_enhanced_context(
@@ -105,12 +122,27 @@ async def post_chat_message(
105
  user_role="Medical Professional",
106
  user_specialty="",
107
  rotator=state.gemini_rotator,
108
- medical_context=medical_context
 
109
  )
110
  except Exception as e:
111
  logger().error(f"Error generating medical response: {e}")
112
  raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to generate AI response.")
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  # 3. Process and Store the Exchange
115
  summary = await state.memory_manager.process_medical_exchange(
116
  session_id=session_id,
 
8
  from src.models.session import (ChatRequest, ChatResponse, Message, Session,
9
  SessionCreateRequest)
10
  from src.services.medical_response import generate_medical_response
11
+ from src.services.guard import SafetyGuard
12
  from src.utils.logger import logger
13
 
14
  router = APIRouter(prefix="/session", tags=["Session & Chat"])
 
86
  """
87
  logger().info(f"POST /session/{session_id}/messages")
88
 
89
+ # 0. Safety Guard: Validate user query
90
+ try:
91
+ safety_guard = SafetyGuard(state.nvidia_rotator)
92
+ is_safe, safety_reason = safety_guard.check_user_query(req.message)
93
+ if not is_safe:
94
+ logger().warning(f"Safety guard blocked user query: {safety_reason}")
95
+ raise HTTPException(
96
+ status_code=status.HTTP_400_BAD_REQUEST,
97
+ detail=f"Query blocked for safety reasons: {safety_reason}"
98
+ )
99
+ logger().info(f"User query passed safety validation: {safety_reason}")
100
+ except Exception as e:
101
+ logger().error(f"Safety guard error: {e}")
102
+ # Fail open for now - allow query through if guard fails
103
+ logger().warning("Safety guard failed, allowing query through")
104
+
105
  # 1. Get Enhanced Context
106
  try:
107
  medical_context = await state.memory_manager.get_enhanced_context(
 
122
  user_role="Medical Professional",
123
  user_specialty="",
124
  rotator=state.gemini_rotator,
125
+ medical_context=medical_context,
126
+ nvidia_rotator=state.nvidia_rotator
127
  )
128
  except Exception as e:
129
  logger().error(f"Error generating medical response: {e}")
130
  raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to generate AI response.")
131
 
132
+ # 2.5. Safety Guard: Validate AI response
133
+ try:
134
+ is_safe, safety_reason = safety_guard.check_model_answer(req.message, response_text)
135
+ if not is_safe:
136
+ logger().warning(f"Safety guard blocked AI response: {safety_reason}")
137
+ # Replace with safe fallback response
138
+ response_text = "I apologize, but I cannot provide a response to that query as it may contain unsafe content. Please consult with a qualified healthcare professional for medical advice."
139
+ else:
140
+ logger().info(f"AI response passed safety validation: {safety_reason}")
141
+ except Exception as e:
142
+ logger().error(f"Safety guard error for response: {e}")
143
+ # Fail open for now - allow response through if guard fails
144
+ logger().warning("Safety guard failed for response, allowing through")
145
+
146
  # 3. Process and Store the Exchange
147
  summary = await state.memory_manager.process_medical_exchange(
148
  session_id=session_id,
src/config/settings.py CHANGED
@@ -1,4 +1,5 @@
1
  # src/config/settings.py
 
2
 
3
  class Settings:
4
  """Application-wide settings."""
@@ -7,6 +8,11 @@ class Settings:
7
  DEFAULT_TOP_K: int = 5
8
  SEMANTIC_CONTEXT_SIZE: int = 17
9
  SIMILARITY_THRESHOLD: float = 0.15
 
 
 
 
 
10
 
11
  # Create singleton instance
12
  settings = Settings()
 
1
  # src/config/settings.py
2
+ import os
3
 
4
  class Settings:
5
  """Application-wide settings."""
 
8
  DEFAULT_TOP_K: int = 5
9
  SEMANTIC_CONTEXT_SIZE: int = 17
10
  SIMILARITY_THRESHOLD: float = 0.15
11
+
12
+ # Safety Guard settings
13
+ SAFETY_GUARD_ENABLED: bool = os.getenv("SAFETY_GUARD_ENABLED", "true").lower() == "true"
14
+ SAFETY_GUARD_TIMEOUT: int = int(os.getenv("SAFETY_GUARD_TIMEOUT", "30"))
15
+ SAFETY_GUARD_FAIL_OPEN: bool = os.getenv("SAFETY_GUARD_FAIL_OPEN", "true").lower() == "true"
16
 
17
  # Create singleton instance
18
  settings = Settings()
src/data/emr_update.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/data/emr_update.py
2
+
3
+ import os
4
+ import uuid
5
+ from datetime import datetime
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from src.data.connection import get_database
9
+ from src.models.emr import ExtractedData
10
+ from src.utils.logger import logger
11
+
12
+
13
+ class EMRUpdateService:
14
+ """Service for updating EMR records with document analysis results."""
15
+
16
+ def __init__(self):
17
+ self.db = get_database()
18
+
19
+ async def save_document_analysis(
20
+ self,
21
+ patient_id: str,
22
+ filename: str,
23
+ file_content: bytes,
24
+ extracted_data: ExtractedData,
25
+ confidence_score: float,
26
+ original_message: str = None
27
+ ) -> str:
28
+ """
29
+ Save document analysis results to the EMR database.
30
+
31
+ Args:
32
+ patient_id: The ID of the patient
33
+ filename: The name of the uploaded file
34
+ file_content: The binary content of the file
35
+ extracted_data: The extracted medical data
36
+ confidence_score: The confidence score of the analysis
37
+ original_message: Optional original message (for chat-based entries)
38
+
39
+ Returns:
40
+ The EMR ID of the created record
41
+ """
42
+ try:
43
+ # Generate unique EMR ID
44
+ emr_id = str(uuid.uuid4())
45
+
46
+ # Prepare the EMR record
47
+ emr_record = {
48
+ "emr_id": emr_id,
49
+ "patient_id": patient_id,
50
+ "original_message": original_message or f"Document upload: {filename}",
51
+ "extracted_data": {
52
+ "diagnosis": extracted_data.diagnosis or [],
53
+ "symptoms": extracted_data.symptoms or [],
54
+ "medications": [
55
+ {
56
+ "name": med.name,
57
+ "dosage": med.dosage,
58
+ "frequency": med.frequency,
59
+ "duration": med.duration
60
+ }
61
+ for med in extracted_data.medications or []
62
+ ],
63
+ "vital_signs": {
64
+ "blood_pressure": extracted_data.vital_signs.blood_pressure if extracted_data.vital_signs else None,
65
+ "heart_rate": extracted_data.vital_signs.heart_rate if extracted_data.vital_signs else None,
66
+ "temperature": extracted_data.vital_signs.temperature if extracted_data.vital_signs else None,
67
+ "respiratory_rate": extracted_data.vital_signs.respiratory_rate if extracted_data.vital_signs else None,
68
+ "oxygen_saturation": extracted_data.vital_signs.oxygen_saturation if extracted_data.vital_signs else None
69
+ } if extracted_data.vital_signs else None,
70
+ "lab_results": [
71
+ {
72
+ "test_name": lab.test_name,
73
+ "value": lab.value,
74
+ "unit": lab.unit,
75
+ "reference_range": lab.reference_range
76
+ }
77
+ for lab in extracted_data.lab_results or []
78
+ ],
79
+ "procedures": extracted_data.procedures or [],
80
+ "notes": extracted_data.notes or ""
81
+ },
82
+ "confidence_score": confidence_score,
83
+ "source": "document_upload",
84
+ "filename": filename,
85
+ "created_at": datetime.utcnow(),
86
+ "updated_at": datetime.utcnow()
87
+ }
88
+
89
+ # Save to database
90
+ result = await self.db.emr_records.insert_one(emr_record)
91
+
92
+ if result.inserted_id:
93
+ logger().info(f"Successfully saved document analysis for patient {patient_id}, EMR ID: {emr_id}")
94
+ return emr_id
95
+ else:
96
+ raise Exception("Failed to insert EMR record")
97
+
98
+ except Exception as e:
99
+ logger().error(f"Error saving document analysis: {e}")
100
+ raise
101
+
102
+ async def update_emr_record(
103
+ self,
104
+ emr_id: str,
105
+ extracted_data: ExtractedData,
106
+ confidence_score: float = None
107
+ ) -> bool:
108
+ """
109
+ Update an existing EMR record with new extracted data.
110
+
111
+ Args:
112
+ emr_id: The EMR record ID to update
113
+ extracted_data: The updated extracted medical data
114
+ confidence_score: Optional new confidence score
115
+
116
+ Returns:
117
+ True if update was successful, False otherwise
118
+ """
119
+ try:
120
+ # Prepare update data
121
+ update_data = {
122
+ "extracted_data": {
123
+ "diagnosis": extracted_data.diagnosis or [],
124
+ "symptoms": extracted_data.symptoms or [],
125
+ "medications": [
126
+ {
127
+ "name": med.name,
128
+ "dosage": med.dosage,
129
+ "frequency": med.frequency,
130
+ "duration": med.duration
131
+ }
132
+ for med in extracted_data.medications or []
133
+ ],
134
+ "vital_signs": {
135
+ "blood_pressure": extracted_data.vital_signs.blood_pressure if extracted_data.vital_signs else None,
136
+ "heart_rate": extracted_data.vital_signs.heart_rate if extracted_data.vital_signs else None,
137
+ "temperature": extracted_data.vital_signs.temperature if extracted_data.vital_signs else None,
138
+ "respiratory_rate": extracted_data.vital_signs.respiratory_rate if extracted_data.vital_signs else None,
139
+ "oxygen_saturation": extracted_data.vital_signs.oxygen_saturation if extracted_data.vital_signs else None
140
+ } if extracted_data.vital_signs else None,
141
+ "lab_results": [
142
+ {
143
+ "test_name": lab.test_name,
144
+ "value": lab.value,
145
+ "unit": lab.unit,
146
+ "reference_range": lab.reference_range
147
+ }
148
+ for lab in extracted_data.lab_results or []
149
+ ],
150
+ "procedures": extracted_data.procedures or [],
151
+ "notes": extracted_data.notes or ""
152
+ },
153
+ "updated_at": datetime.utcnow()
154
+ }
155
+
156
+ if confidence_score is not None:
157
+ update_data["confidence_score"] = confidence_score
158
+
159
+ # Update the record
160
+ result = await self.db.emr_records.update_one(
161
+ {"emr_id": emr_id},
162
+ {"$set": update_data}
163
+ )
164
+
165
+ if result.modified_count > 0:
166
+ logger().info(f"Successfully updated EMR record {emr_id}")
167
+ return True
168
+ else:
169
+ logger().warning(f"No EMR record found with ID {emr_id}")
170
+ return False
171
+
172
+ except Exception as e:
173
+ logger().error(f"Error updating EMR record {emr_id}: {e}")
174
+ raise
175
+
176
+ async def get_emr_record(self, emr_id: str) -> Optional[Dict[str, Any]]:
177
+ """
178
+ Retrieve an EMR record by ID.
179
+
180
+ Args:
181
+ emr_id: The EMR record ID
182
+
183
+ Returns:
184
+ The EMR record if found, None otherwise
185
+ """
186
+ try:
187
+ record = await self.db.emr_records.find_one({"emr_id": emr_id})
188
+ if record:
189
+ # Convert ObjectId to string for JSON serialization
190
+ record["_id"] = str(record["_id"])
191
+ return record
192
+
193
+ except Exception as e:
194
+ logger().error(f"Error retrieving EMR record {emr_id}: {e}")
195
+ raise
196
+
197
+ async def delete_emr_record(self, emr_id: str) -> bool:
198
+ """
199
+ Delete an EMR record.
200
+
201
+ Args:
202
+ emr_id: The EMR record ID to delete
203
+
204
+ Returns:
205
+ True if deletion was successful, False otherwise
206
+ """
207
+ try:
208
+ result = await self.db.emr_records.delete_one({"emr_id": emr_id})
209
+
210
+ if result.deleted_count > 0:
211
+ logger().info(f"Successfully deleted EMR record {emr_id}")
212
+ return True
213
+ else:
214
+ logger().warning(f"No EMR record found with ID {emr_id}")
215
+ return False
216
+
217
+ except Exception as e:
218
+ logger().error(f"Error deleting EMR record {emr_id}: {e}")
219
+ raise
220
+
221
+ async def get_patient_emr_records(
222
+ self,
223
+ patient_id: str,
224
+ limit: int = 100,
225
+ skip: int = 0
226
+ ) -> List[Dict[str, Any]]:
227
+ """
228
+ Retrieve EMR records for a specific patient.
229
+
230
+ Args:
231
+ patient_id: The patient ID
232
+ limit: Maximum number of records to return
233
+ skip: Number of records to skip
234
+
235
+ Returns:
236
+ List of EMR records
237
+ """
238
+ try:
239
+ cursor = self.db.emr_records.find(
240
+ {"patient_id": patient_id}
241
+ ).sort("created_at", -1).skip(skip).limit(limit)
242
+
243
+ records = []
244
+ async for record in cursor:
245
+ # Convert ObjectId to string for JSON serialization
246
+ record["_id"] = str(record["_id"])
247
+ records.append(record)
248
+
249
+ return records
250
+
251
+ except Exception as e:
252
+ logger().error(f"Error retrieving EMR records for patient {patient_id}: {e}")
253
+ raise
254
+
255
+ async def get_patient_emr_statistics(self, patient_id: str) -> Dict[str, Any]:
256
+ """
257
+ Get EMR statistics for a patient.
258
+
259
+ Args:
260
+ patient_id: The patient ID
261
+
262
+ Returns:
263
+ Dictionary containing EMR statistics
264
+ """
265
+ try:
266
+ pipeline = [
267
+ {"$match": {"patient_id": patient_id}},
268
+ {
269
+ "$group": {
270
+ "_id": None,
271
+ "total_entries": {"$sum": 1},
272
+ "avg_confidence": {"$avg": "$confidence_score"},
273
+ "diagnosis_count": {
274
+ "$sum": {
275
+ "$cond": [
276
+ {"$gt": [{"$size": {"$ifNull": ["$extracted_data.diagnosis", []]}}, 0]},
277
+ 1,
278
+ 0
279
+ ]
280
+ }
281
+ },
282
+ "medication_count": {
283
+ "$sum": {
284
+ "$cond": [
285
+ {"$gt": [{"$size": {"$ifNull": ["$extracted_data.medications", []]}}, 0]},
286
+ 1,
287
+ 0
288
+ ]
289
+ }
290
+ }
291
+ }
292
+ }
293
+ ]
294
+
295
+ result = await self.db.emr_records.aggregate(pipeline).to_list(1)
296
+
297
+ if result:
298
+ stats = result[0]
299
+ return {
300
+ "total_entries": stats.get("total_entries", 0),
301
+ "avg_confidence": stats.get("avg_confidence", 0.0),
302
+ "diagnosis_count": stats.get("diagnosis_count", 0),
303
+ "medication_count": stats.get("medication_count", 0)
304
+ }
305
+ else:
306
+ return {
307
+ "total_entries": 0,
308
+ "avg_confidence": 0.0,
309
+ "diagnosis_count": 0,
310
+ "medication_count": 0
311
+ }
312
+
313
+ except Exception as e:
314
+ logger().error(f"Error retrieving EMR statistics for patient {patient_id}: {e}")
315
+ raise
src/data/migration.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/data/migration.py
2
+ """
3
+ Database Migration Module
4
+
5
+ This module provides functions to fix existing database records that have
6
+ missing or invalid required fields to ensure they conform to current Pydantic model requirements.
7
+ """
8
+
9
+ from datetime import datetime, timezone
10
+ from typing import Dict, Any
11
+
12
+ from bson import ObjectId
13
+ from pymongo.errors import ConnectionFailure, PyMongoError
14
+
15
+ from src.data.connection import get_collection, Collections
16
+ from src.models.account import Account
17
+ from src.models.patient import Patient
18
+ from src.utils.logger import logger
19
+
20
+ # Valid roles for accounts
21
+ VALID_ROLES = [
22
+ "Doctor",
23
+ "Healthcare Prof",
24
+ "Nurse",
25
+ "Caregiver",
26
+ "Physician",
27
+ "Medical Student",
28
+ "Other"
29
+ ]
30
+
31
+ def fix_account_records() -> Dict[str, int]:
32
+ """Fix account records with missing or invalid required fields."""
33
+ logger().info("πŸ”§ Starting Account records migration...")
34
+
35
+ collection = get_collection(Collections.ACCOUNT)
36
+ now = datetime.now(timezone.utc)
37
+
38
+ stats = {
39
+ 'total_checked': 0,
40
+ 'fixed': 0,
41
+ 'errors': 0
42
+ }
43
+
44
+ try:
45
+ # Find all accounts
46
+ cursor = collection.find({})
47
+
48
+ for doc in cursor:
49
+ stats['total_checked'] += 1
50
+ doc_id = doc['_id']
51
+
52
+ try:
53
+ # Check what needs to be fixed
54
+ updates = {}
55
+ needs_fix = False
56
+
57
+ # Fix missing or None role
58
+ if not doc.get('role') or doc.get('role') is None:
59
+ updates['role'] = 'Other' # Default role
60
+ needs_fix = True
61
+ logger().info(f" πŸ“ Account {doc_id}: Setting role to 'Other' (was: {doc.get('role')})")
62
+
63
+ # Fix missing or None name
64
+ if not doc.get('name') or doc.get('name') is None:
65
+ updates['name'] = f"User {str(doc_id)[:8]}" # Generate a name
66
+ needs_fix = True
67
+ logger().info(f" πŸ“ Account {doc_id}: Setting name to '{updates['name']}' (was: {doc.get('name')})")
68
+
69
+ # Fix missing timestamps
70
+ if not doc.get('created_at'):
71
+ updates['created_at'] = now
72
+ needs_fix = True
73
+ logger().info(f" πŸ“ Account {doc_id}: Setting created_at to {now}")
74
+
75
+ if not doc.get('updated_at'):
76
+ updates['updated_at'] = now
77
+ needs_fix = True
78
+ logger().info(f" πŸ“ Account {doc_id}: Setting updated_at to {now}")
79
+
80
+ # Apply updates if needed
81
+ if needs_fix:
82
+ collection.update_one(
83
+ {"_id": doc_id},
84
+ {"$set": updates}
85
+ )
86
+ stats['fixed'] += 1
87
+
88
+ # Validate the record can be parsed by Pydantic
89
+ updated_doc = collection.find_one({"_id": doc_id})
90
+ Account.model_validate(updated_doc)
91
+
92
+ except Exception as e:
93
+ stats['errors'] += 1
94
+ logger().error(f" ❌ Error fixing account {doc_id}: {e}")
95
+
96
+ except (ConnectionFailure, PyMongoError) as e:
97
+ logger().error(f"❌ Database error while fixing accounts: {e}")
98
+ raise
99
+
100
+ logger().info(f"βœ… Account migration completed: {stats['fixed']} records fixed, {stats['errors']} errors")
101
+ return stats
102
+
103
+ def fix_patient_records() -> Dict[str, int]:
104
+ """Fix patient records with missing or invalid required fields."""
105
+ logger().info("πŸ”§ Starting Patient records migration...")
106
+
107
+ collection = get_collection(Collections.PATIENT)
108
+ now = datetime.now(timezone.utc)
109
+
110
+ stats = {
111
+ 'total_checked': 0,
112
+ 'fixed': 0,
113
+ 'errors': 0
114
+ }
115
+
116
+ try:
117
+ # Find all patients
118
+ cursor = collection.find({})
119
+
120
+ for doc in cursor:
121
+ stats['total_checked'] += 1
122
+ doc_id = doc['_id']
123
+
124
+ try:
125
+ # Check what needs to be fixed
126
+ updates = {}
127
+ needs_fix = False
128
+
129
+ # Fix missing or None name
130
+ if not doc.get('name') or doc.get('name') is None:
131
+ updates['name'] = f"Patient {str(doc_id)[:8]}" # Generate a name
132
+ needs_fix = True
133
+ logger().info(f" πŸ“ Patient {doc_id}: Setting name to '{updates['name']}' (was: {doc.get('name')})")
134
+
135
+ # Fix missing or None age
136
+ if not doc.get('age') or doc.get('age') is None:
137
+ updates['age'] = 30 # Default age
138
+ needs_fix = True
139
+ logger().info(f" πŸ“ Patient {doc_id}: Setting age to 30 (was: {doc.get('age')})")
140
+
141
+ # Fix missing or None sex
142
+ if not doc.get('sex') or doc.get('sex') is None:
143
+ updates['sex'] = 'Other' # Default sex
144
+ needs_fix = True
145
+ logger().info(f" πŸ“ Patient {doc_id}: Setting sex to 'Other' (was: {doc.get('sex')})")
146
+
147
+ # Fix missing or None ethnicity
148
+ if not doc.get('ethnicity') or doc.get('ethnicity') is None:
149
+ updates['ethnicity'] = 'Not Specified' # Default ethnicity
150
+ needs_fix = True
151
+ logger().info(f" πŸ“ Patient {doc_id}: Setting ethnicity to 'Not Specified' (was: {doc.get('ethnicity')})")
152
+
153
+ # Fix missing timestamps
154
+ if not doc.get('created_at'):
155
+ updates['created_at'] = now
156
+ needs_fix = True
157
+ logger().info(f" πŸ“ Patient {doc_id}: Setting created_at to {now}")
158
+
159
+ if not doc.get('updated_at'):
160
+ updates['updated_at'] = now
161
+ needs_fix = True
162
+ logger().info(f" πŸ“ Patient {doc_id}: Setting updated_at to {now}")
163
+
164
+ # Apply updates if needed
165
+ if needs_fix:
166
+ collection.update_one(
167
+ {"_id": doc_id},
168
+ {"$set": updates}
169
+ )
170
+ stats['fixed'] += 1
171
+
172
+ # Validate the record can be parsed by Pydantic
173
+ updated_doc = collection.find_one({"_id": doc_id})
174
+ Patient.model_validate(updated_doc)
175
+
176
+ except Exception as e:
177
+ stats['errors'] += 1
178
+ logger().error(f" ❌ Error fixing patient {doc_id}: {e}")
179
+
180
+ except (ConnectionFailure, PyMongoError) as e:
181
+ logger().error(f"❌ Database error while fixing patients: {e}")
182
+ raise
183
+
184
+ logger().info(f"βœ… Patient migration completed: {stats['fixed']} records fixed, {stats['errors']} errors")
185
+ return stats
186
+
187
+ def run_database_migration() -> Dict[str, Any]:
188
+ """Run the complete database migration to fix all records."""
189
+ logger().info("πŸš€ Starting Database Migration")
190
+ logger().info("=" * 50)
191
+
192
+ try:
193
+ # Fix account records
194
+ account_stats = fix_account_records()
195
+
196
+ logger().info("=" * 50)
197
+
198
+ # Fix patient records
199
+ patient_stats = fix_patient_records()
200
+
201
+ logger().info("=" * 50)
202
+ logger().info("πŸ“Š MIGRATION SUMMARY")
203
+ logger().info("=" * 50)
204
+
205
+ logger().info(f"πŸ“‹ Accounts:")
206
+ logger().info(f" - Total checked: {account_stats['total_checked']}")
207
+ logger().info(f" - Fixed: {account_stats['fixed']}")
208
+ logger().info(f" - Errors: {account_stats['errors']}")
209
+
210
+ logger().info(f"πŸ‘₯ Patients:")
211
+ logger().info(f" - Total checked: {patient_stats['total_checked']}")
212
+ logger().info(f" - Fixed: {patient_stats['fixed']}")
213
+ logger().info(f" - Errors: {patient_stats['errors']}")
214
+
215
+ total_fixed = account_stats['fixed'] + patient_stats['fixed']
216
+ total_errors = account_stats['errors'] + patient_stats['errors']
217
+
218
+ logger().info(f"βœ… Total records fixed: {total_fixed}")
219
+ if total_errors > 0:
220
+ logger().info(f"⚠️ Total errors: {total_errors}")
221
+
222
+ logger().info("πŸŽ‰ Database migration completed successfully!")
223
+
224
+ return {
225
+ 'account_stats': account_stats,
226
+ 'patient_stats': patient_stats,
227
+ 'total_fixed': total_fixed,
228
+ 'total_errors': total_errors,
229
+ 'success': True
230
+ }
231
+
232
+ except Exception as e:
233
+ logger().error(f"❌ Migration failed with error: {e}")
234
+ return {
235
+ 'success': False,
236
+ 'error': str(e)
237
+ }
src/data/repositories/account.py CHANGED
@@ -191,11 +191,23 @@ def search_accounts(
191
 
192
  try:
193
  collection = get_collection(collection_name)
 
 
194
  cursor = collection.find({
195
  "name": {"$regex": pattern}
196
  }).sort("name", ASCENDING).limit(limit)
197
 
198
- accounts = [Account.model_validate(doc) for doc in cursor]
 
 
 
 
 
 
 
 
 
 
199
  logger().info(f"Found {len(accounts)} accounts matching query")
200
  return accounts
201
  except (ConnectionFailure, PyMongoError) as e:
@@ -210,9 +222,25 @@ def get_all_accounts(
210
  """Gets all accounts, returning a list of Pydantic Account objects."""
211
  try:
212
  collection = get_collection(collection_name)
213
- cursor = collection.find().sort("name", ASCENDING).limit(limit)
214
-
215
- accounts = [Account.model_validate(doc) for doc in cursor]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  logger().info(f"Retrieved {len(accounts)} accounts")
217
  return accounts
218
  except (ConnectionFailure, PyMongoError) as e:
 
191
 
192
  try:
193
  collection = get_collection(collection_name)
194
+ now = datetime.now(timezone.utc)
195
+
196
  cursor = collection.find({
197
  "name": {"$regex": pattern}
198
  }).sort("name", ASCENDING).limit(limit)
199
 
200
+ accounts = []
201
+ for doc in cursor:
202
+ # Update last_seen for this account
203
+ collection.update_one(
204
+ {"_id": doc["_id"]},
205
+ {"$set": {"last_seen": now}}
206
+ )
207
+ # Add the updated timestamp to the document for validation
208
+ doc["last_seen"] = now
209
+ accounts.append(Account.model_validate(doc))
210
+
211
  logger().info(f"Found {len(accounts)} accounts matching query")
212
  return accounts
213
  except (ConnectionFailure, PyMongoError) as e:
 
222
  """Gets all accounts, returning a list of Pydantic Account objects."""
223
  try:
224
  collection = get_collection(collection_name)
225
+ now = datetime.now(timezone.utc)
226
+
227
+ # Update last_seen for all accounts and retrieve them
228
+ cursor = collection.find()
229
+ accounts = []
230
+ for doc in cursor:
231
+ # Update last_seen for this account
232
+ collection.update_one(
233
+ {"_id": doc["_id"]},
234
+ {"$set": {"last_seen": now}}
235
+ )
236
+ # Add the updated timestamp to the document for validation
237
+ doc["last_seen"] = now
238
+ accounts.append(Account.model_validate(doc))
239
+
240
+ # Sort by name and limit
241
+ accounts.sort(key=lambda x: x.name)
242
+ accounts = accounts[:limit]
243
+
244
  logger().info(f"Retrieved {len(accounts)} accounts")
245
  return accounts
246
  except (ConnectionFailure, PyMongoError) as e:
src/main.py CHANGED
@@ -27,6 +27,7 @@ except Exception as e:
27
  from src.api.routes import account as account_route
28
  from src.api.routes import audio as audio_route
29
  from src.api.routes import emr as emr_route
 
30
  from src.api.routes import patient as patients_route
31
  from src.api.routes import session as session_route
32
  from src.api.routes import static as static_route
@@ -86,6 +87,18 @@ def startup_event(state: AppState):
86
  medical_memory_repo.init()
87
  medical_record_repo.init()
88
  emr_repo.init()
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  def shutdown_event():
91
  """Cleanup on shutdown"""
@@ -132,6 +145,7 @@ app.include_router(summarise_route.router)
132
  app.include_router(system_route.router)
133
  app.include_router(audio_route.router)
134
  app.include_router(emr_route.router)
 
135
 
136
  @app.get("/api/info")
137
  async def get_api_info():
 
27
  from src.api.routes import account as account_route
28
  from src.api.routes import audio as audio_route
29
  from src.api.routes import emr as emr_route
30
+ from src.api.routes import migration as migration_route
31
  from src.api.routes import patient as patients_route
32
  from src.api.routes import session as session_route
33
  from src.api.routes import static as static_route
 
87
  medical_memory_repo.init()
88
  medical_record_repo.init()
89
  emr_repo.init()
90
+
91
+ # Run database migration to fix any existing records with missing required fields
92
+ try:
93
+ from src.data.migration import run_database_migration
94
+ logger(tag="startup").info("Running database migration...")
95
+ migration_result = run_database_migration()
96
+ if migration_result['success']:
97
+ logger(tag="startup").info(f"Database migration completed: {migration_result['total_fixed']} records fixed")
98
+ else:
99
+ logger(tag="startup").warning(f"Database migration failed: {migration_result.get('error', 'Unknown error')}")
100
+ except Exception as e:
101
+ logger(tag="startup").warning(f"Database migration error: {e}")
102
 
103
  def shutdown_event():
104
  """Cleanup on shutdown"""
 
145
  app.include_router(system_route.router)
146
  app.include_router(audio_route.router)
147
  app.include_router(emr_route.router)
148
+ app.include_router(migration_route.router)
149
 
150
  @app.get("/api/info")
151
  async def get_api_info():
src/models/account.py CHANGED
@@ -14,7 +14,7 @@ class Account(BaseMongoModel):
14
  specialty: str | None = None
15
  created_at: datetime
16
  updated_at: datetime
17
- last_seen: datetime
18
 
19
  class AccountCreateRequest(BaseModel):
20
  """A Pydantic model for an account creation request from the API."""
 
14
  specialty: str | None = None
15
  created_at: datetime
16
  updated_at: datetime
17
+ last_seen: datetime | None = None
18
 
19
  class AccountCreateRequest(BaseModel):
20
  """A Pydantic model for an account creation request from the API."""
src/services/extractor.py CHANGED
@@ -2,6 +2,8 @@
2
 
3
  import json
4
  import re
 
 
5
  from typing import Any, Dict, List, Optional, Tuple
6
 
7
  from src.models.emr import ExtractedData, LabResult, Medication, VitalSigns
@@ -192,7 +194,7 @@ Return the JSON followed by the confidence score on a new line."""
192
  vital_signs=vital_signs,
193
  lab_results=lab_results,
194
  procedures=data.get('procedures', []),
195
- notes=data.get('notes')
196
  )
197
 
198
  return extracted_data, confidence
@@ -258,3 +260,141 @@ Return the JSON followed by the confidence score on a new line."""
258
  vital_signs['oxygen_saturation'] = o2_match.group(1)
259
 
260
  return vital_signs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  import json
4
  import re
5
+ import base64
6
+ import mimetypes
7
  from typing import Any, Dict, List, Optional, Tuple
8
 
9
  from src.models.emr import ExtractedData, LabResult, Medication, VitalSigns
 
194
  vital_signs=vital_signs,
195
  lab_results=lab_results,
196
  procedures=data.get('procedures', []),
197
+ notes=data.get('notes', '') + (f"\n\nDocument Overview: {data.get('overview', '')}" if data.get('overview') else '')
198
  )
199
 
200
  return extracted_data, confidence
 
260
  vital_signs['oxygen_saturation'] = o2_match.group(1)
261
 
262
  return vital_signs
263
+
264
+ async def analyze_document(self, file_content: bytes, filename: str, patient_context: Optional[Dict[str, Any]] = None) -> Tuple[ExtractedData, float]:
265
+ """
266
+ Analyze a medical document (PDF, image, or text) and extract structured medical data.
267
+
268
+ Args:
269
+ file_content: The binary content of the uploaded file
270
+ filename: The name of the uploaded file
271
+ patient_context: Optional patient context information
272
+
273
+ Returns:
274
+ Tuple of (ExtractedData, confidence_score)
275
+ """
276
+ try:
277
+ # Determine file type and prepare content for Gemini
278
+ mime_type, _ = mimetypes.guess_type(filename)
279
+
280
+ if not mime_type:
281
+ logger().warning(f"Unknown file type for {filename}")
282
+ return ExtractedData(), 0.0
283
+
284
+ # Encode file content to base64
285
+ file_base64 = base64.b64encode(file_content).decode('utf-8')
286
+
287
+ # Build the prompt for document analysis
288
+ prompt = self._build_document_analysis_prompt(file_base64, mime_type, filename, patient_context)
289
+
290
+ # Get response from Gemini
291
+ response = await self._call_gemini_api(prompt)
292
+
293
+ # Parse the response
294
+ extracted_data, confidence = self._parse_gemini_response(response)
295
+
296
+ logger().info(f"Successfully analyzed document {filename} with confidence {confidence:.2f}")
297
+ return extracted_data, confidence
298
+
299
+ except Exception as e:
300
+ logger().error(f"Error analyzing document {filename}: {e}")
301
+ # Return empty data with low confidence
302
+ return ExtractedData(), 0.0
303
+
304
+ def _build_document_analysis_prompt(self, file_base64: str, mime_type: str, filename: str, patient_context: Optional[Dict[str, Any]] = None) -> str:
305
+ """Build the prompt for Gemini AI to analyze medical documents."""
306
+
307
+ context_info = ""
308
+ if patient_context:
309
+ context_info = f"""
310
+ Patient Context:
311
+ - Name: {patient_context.get('name', 'Unknown')}
312
+ - Age: {patient_context.get('age', 'Unknown')}
313
+ - Sex: {patient_context.get('sex', 'Unknown')}
314
+ - Current Medications: {', '.join(patient_context.get('medications', []))}
315
+ - Past Assessment Summary: {patient_context.get('past_assessment_summary', 'None')}
316
+ """
317
+
318
+ # Determine the content type for Gemini
319
+ if mime_type.startswith('image/'):
320
+ content_type = "image"
321
+ elif mime_type == 'application/pdf':
322
+ content_type = "pdf"
323
+ elif mime_type in ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']:
324
+ content_type = "document"
325
+ else:
326
+ content_type = "text"
327
+
328
+ prompt = f"""You are a medical AI assistant specialized in analyzing medical documents and extracting structured clinical information.
329
+
330
+ {context_info}
331
+
332
+ Please analyze the following medical document and extract all relevant clinical information in the specified JSON format.
333
+
334
+ Document Information:
335
+ - Filename: {filename}
336
+ - Content Type: {content_type}
337
+ - MIME Type: {mime_type}
338
+
339
+ Document Content (Base64 encoded):
340
+ {file_base64}
341
+
342
+ Extract the following information and return ONLY a valid JSON object with this exact structure:
343
+
344
+ {{
345
+ "overview": "Brief summary of the document content and main findings",
346
+ "diagnosis": ["list of diagnoses mentioned or identified"],
347
+ "symptoms": ["list of symptoms described"],
348
+ "medications": [
349
+ {{
350
+ "name": "medication name",
351
+ "dosage": "dosage if mentioned",
352
+ "frequency": "frequency if mentioned",
353
+ "duration": "duration if mentioned"
354
+ }}
355
+ ],
356
+ "vital_signs": {{
357
+ "blood_pressure": "value if mentioned",
358
+ "heart_rate": "value if mentioned",
359
+ "temperature": "value if mentioned",
360
+ "respiratory_rate": "value if mentioned",
361
+ "oxygen_saturation": "value if mentioned"
362
+ }},
363
+ "lab_results": [
364
+ {{
365
+ "test_name": "test name",
366
+ "value": "test value",
367
+ "unit": "unit if mentioned",
368
+ "reference_range": "normal range if mentioned"
369
+ }}
370
+ ],
371
+ "procedures": ["list of procedures mentioned or performed"],
372
+ "notes": "additional clinical notes and observations"
373
+ }}
374
+
375
+ Guidelines for Document Analysis:
376
+ 1. Carefully read and analyze the entire document content
377
+ 2. Extract information that is explicitly mentioned or clearly documented
378
+ 3. Use medical terminology appropriately and maintain accuracy
379
+ 4. If a field has no relevant information, use an empty array [] or null
380
+ 5. For medications, include all prescribed, recommended, or mentioned medications
381
+ 6. Extract vital signs only if specific values are documented
382
+ 7. Include lab results only if specific test values are provided
383
+ 8. Be thorough but conservative - prioritize accuracy over completeness
384
+ 9. For images, focus on visible text, charts, and medical data
385
+ 10. For PDFs and documents, analyze all text content systematically
386
+ 11. Return ONLY the JSON object, no additional text or explanation
387
+
388
+ Confidence Assessment:
389
+ After the JSON, provide a confidence score (0.0-1.0) based on:
390
+ - Document clarity and readability
391
+ - Specificity of medical information
392
+ - Presence of measurable values (vitals, lab results)
393
+ - Overall clinical relevance and completeness
394
+ - Document type and quality
395
+
396
+ Format: CONFIDENCE: 0.85
397
+
398
+ Return the JSON followed by the confidence score on a new line."""
399
+
400
+ return prompt
src/services/guard.py ADDED
@@ -0,0 +1,518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import requests
4
+ import logging
5
+ from typing import Tuple, List, Dict
6
+
7
+ from src.config.settings import settings
8
+ from src.utils.rotator import APIKeyRotator
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class SafetyGuard:
14
+ """
15
+ Wrapper around NVIDIA Llama Guard (meta/llama-guard-4-12b) hosted at
16
+ https://integrate.api.nvidia.com/v1/chat/completions
17
+
18
+ Exposes helpers to validate:
19
+ - user input safety
20
+ - model output safety (in context of the user question)
21
+ """
22
+ NVIDIA_GUARD = os.getenv("NVIDIA_GUARD", "meta/llama-guard-4-12b")
23
+
24
+ def __init__(self, nvidia_rotator: APIKeyRotator = None):
25
+ self.nvidia_rotator = nvidia_rotator or APIKeyRotator("NVIDIA_API_", max_slots=5)
26
+ if not self.nvidia_rotator.get_key():
27
+ raise ValueError("No NVIDIA API keys found. Set NVIDIA_API_1, NVIDIA_API_2, etc. environment variables")
28
+ self.base_url = "https://integrate.api.nvidia.com/v1/chat/completions"
29
+ self.model = NVIDIA_GUARD
30
+ self.timeout_s = settings.SAFETY_GUARD_TIMEOUT
31
+ self.enabled = settings.SAFETY_GUARD_ENABLED
32
+ self.fail_open = settings.SAFETY_GUARD_FAIL_OPEN
33
+
34
+ @staticmethod
35
+ def _chunk_text(text: str, chunk_size: int = 2800, overlap: int = 200) -> List[str]:
36
+ """Chunk long text to keep request payloads small enough for the guard.
37
+ Uses character-based approximation with small overlap.
38
+ """
39
+ if not text:
40
+ return [""]
41
+ n = len(text)
42
+ if n <= chunk_size:
43
+ return [text]
44
+ chunks: List[str] = []
45
+ start = 0
46
+ while start < n:
47
+ end = min(start + chunk_size, n)
48
+ chunks.append(text[start:end])
49
+ if end == n:
50
+ break
51
+ start = max(0, end - overlap)
52
+ return chunks
53
+
54
+ def _call_guard(self, messages: List[Dict], max_tokens: int = 512) -> str:
55
+ # Enhance messages with medical context if detected
56
+ enhanced_messages = self._enhance_messages_with_context(messages)
57
+
58
+ api_key = self.nvidia_rotator.get_key()
59
+ if not api_key:
60
+ logger.error("[SafetyGuard] No NVIDIA API key available")
61
+ return ""
62
+
63
+ headers = {
64
+ "Authorization": f"Bearer {api_key}",
65
+ "Content-Type": "application/json",
66
+ "Accept": "application/json",
67
+ }
68
+ # Try OpenAI-compatible schema first
69
+ payload_chat = {
70
+ "model": self.model,
71
+ "messages": enhanced_messages,
72
+ "temperature": 0.2,
73
+ "top_p": 0.7,
74
+ "max_tokens": max_tokens,
75
+ "stream": False,
76
+ }
77
+ # Alternative schema (some NVIDIA deployments require message content objects)
78
+ alt_messages = []
79
+ for m in enhanced_messages:
80
+ content = m.get("content", "")
81
+ if isinstance(content, str):
82
+ content = [{"type": "text", "text": content}]
83
+ alt_messages.append({"role": m.get("role", "user"), "content": content})
84
+ payload_alt = {
85
+ "model": self.model,
86
+ "messages": alt_messages,
87
+ "temperature": 0.2,
88
+ "top_p": 0.7,
89
+ "max_tokens": max_tokens,
90
+ "stream": False,
91
+ }
92
+ # Attempt primary, then fallback
93
+ for payload in (payload_chat, payload_alt):
94
+ try:
95
+ resp = requests.post(self.base_url, headers=headers, json=payload, timeout=self.timeout_s)
96
+ if resp.status_code in {401, 403, 429} or 500 <= resp.status_code < 600:
97
+ # Rotate API key and retry
98
+ logger.warning(f"[SafetyGuard] HTTP {resp.status_code}, rotating API key")
99
+ self.nvidia_rotator.rotate()
100
+ api_key = self.nvidia_rotator.get_key()
101
+ if api_key:
102
+ headers["Authorization"] = f"Bearer {api_key}"
103
+ continue
104
+ else:
105
+ logger.error("[SafetyGuard] No more API keys available")
106
+ return ""
107
+ elif resp.status_code >= 400:
108
+ # Log server message for debugging payload issues
109
+ try:
110
+ logger.error(f"[SafetyGuard] HTTP {resp.status_code}: {resp.text[:400]}")
111
+ except Exception:
112
+ pass
113
+ resp.raise_for_status()
114
+ data = resp.json()
115
+ content = (
116
+ data.get("choices", [{}])[0]
117
+ .get("message", {})
118
+ .get("content", "")
119
+ .strip()
120
+ )
121
+ if content:
122
+ return content
123
+ except Exception as e:
124
+ # Try next payload shape
125
+ logger.error(f"[SafetyGuard] Guard API call failed: {e}")
126
+ continue
127
+ # All attempts failed
128
+ return ""
129
+
130
+ @staticmethod
131
+ def _parse_guard_reply(text: str) -> Tuple[bool, str]:
132
+ """Parse guard reply; expect 'SAFE' or 'UNSAFE: <reason>' (case-insensitive)."""
133
+ if not text:
134
+ # Fail-open: treat as SAFE if guard unavailable to avoid false blocks
135
+ return True, "guard_unavailable"
136
+ t = text.strip()
137
+ upper = t.upper()
138
+ if upper.startswith("SAFE") and not upper.startswith("SAFEGUARD"):
139
+ return True, ""
140
+ if upper.startswith("UNSAFE"):
141
+ # Extract reason after the first colon if present
142
+ parts = t.split(":", 1)
143
+ reason = parts[1].strip() if len(parts) > 1 else "policy violation"
144
+ return False, reason
145
+ # Fallback: treat unknown response as unsafe
146
+ return False, t[:180]
147
+
148
+ def _is_medical_query(self, query: str) -> bool:
149
+ """Check if query is clearly medical in nature using comprehensive patterns."""
150
+ if not query:
151
+ return False
152
+
153
+ query_lower = query.lower()
154
+
155
+ # Medical keyword categories
156
+ medical_categories = {
157
+ 'symptoms': [
158
+ 'symptom', 'pain', 'ache', 'hurt', 'sore', 'tender', 'stiff', 'numb',
159
+ 'headache', 'migraine', 'fever', 'cough', 'cold', 'flu', 'sneeze',
160
+ 'nausea', 'vomit', 'diarrhea', 'constipation', 'bloating', 'gas',
161
+ 'dizziness', 'vertigo', 'fatigue', 'weakness', 'tired', 'exhausted',
162
+ 'shortness of breath', 'wheezing', 'chest pain', 'heart palpitations',
163
+ 'joint pain', 'muscle pain', 'back pain', 'neck pain', 'stomach pain',
164
+ 'abdominal pain', 'pelvic pain', 'menstrual pain', 'cramps'
165
+ ],
166
+ 'conditions': [
167
+ 'disease', 'condition', 'disorder', 'syndrome', 'illness', 'sickness',
168
+ 'infection', 'inflammation', 'allergy', 'asthma', 'diabetes', 'hypertension',
169
+ 'depression', 'anxiety', 'stress', 'panic', 'phobia', 'ocd', 'ptsd',
170
+ 'adhd', 'autism', 'dementia', 'alzheimer', 'parkinson', 'epilepsy',
171
+ 'cancer', 'tumor', 'cancerous', 'malignant', 'benign', 'metastasis',
172
+ 'heart disease', 'stroke', 'heart attack', 'coronary', 'arrhythmia',
173
+ 'pneumonia', 'bronchitis', 'copd', 'emphysema', 'tuberculosis',
174
+ 'migraine', 'headache', 'chronic migraine', 'cluster headache',
175
+ 'tension headache', 'sinus headache', 'cure', 'treat', 'treatment'
176
+ ],
177
+ 'treatments': [
178
+ 'treatment', 'therapy', 'medication', 'medicine', 'drug', 'pill', 'tablet',
179
+ 'injection', 'vaccine', 'immunization', 'surgery', 'operation', 'procedure',
180
+ 'chemotherapy', 'radiation', 'physical therapy', 'occupational therapy',
181
+ 'psychotherapy', 'counseling', 'rehabilitation', 'recovery', 'healing',
182
+ 'prescription', 'dosage', 'side effects', 'contraindications'
183
+ ],
184
+ 'body_parts': [
185
+ 'head', 'brain', 'eye', 'ear', 'nose', 'mouth', 'throat', 'neck',
186
+ 'chest', 'heart', 'lung', 'liver', 'kidney', 'stomach', 'intestine',
187
+ 'back', 'spine', 'joint', 'muscle', 'bone', 'skin', 'hair', 'nail',
188
+ 'arm', 'leg', 'hand', 'foot', 'finger', 'toe', 'pelvis', 'genital'
189
+ ],
190
+ 'medical_context': [
191
+ 'doctor', 'physician', 'nurse', 'specialist', 'surgeon', 'dentist',
192
+ 'medical', 'health', 'healthcare', 'hospital', 'clinic', 'emergency',
193
+ 'ambulance', 'paramedic', 'pharmacy', 'pharmacist', 'lab', 'test',
194
+ 'diagnosis', 'prognosis', 'examination', 'checkup', 'screening',
195
+ 'patient', 'case', 'history', 'medical history', 'family history'
196
+ ],
197
+ 'life_stages': [
198
+ 'pregnancy', 'pregnant', 'baby', 'infant', 'newborn', 'child', 'pediatric',
199
+ 'teenager', 'adolescent', 'adult', 'elderly', 'senior', 'geriatric',
200
+ 'menopause', 'puberty', 'aging', 'birth', 'delivery', 'miscarriage'
201
+ ],
202
+ 'vital_signs': [
203
+ 'blood pressure', 'heart rate', 'pulse', 'temperature', 'fever',
204
+ 'respiratory rate', 'oxygen saturation', 'weight', 'height', 'bmi',
205
+ 'blood sugar', 'glucose', 'cholesterol', 'hemoglobin', 'white blood cell'
206
+ ]
207
+ }
208
+
209
+ # Check for medical keywords
210
+ for category, keywords in medical_categories.items():
211
+ if any(keyword in query_lower for keyword in keywords):
212
+ return True
213
+
214
+ # Check for medical question patterns
215
+ medical_patterns = [
216
+ r'\b(what|how|why|when|where)\s+(causes?|treats?|prevents?|symptoms?|signs?)\b',
217
+ r'\b(is|are)\s+(.*?)\s+(dangerous|serious|harmful|safe|normal)\b',
218
+ r'\b(should|can|may|might)\s+(i|you|we)\s+(take|use|do|avoid)\b',
219
+ r'\b(diagnosis|diagnosed|symptoms|treatment|medicine|drug)\b',
220
+ r'\b(medical|health|doctor|physician|hospital|clinic)\b',
221
+ r'\b(pain|hurt|ache|sore|fever|cough|headache)\b',
222
+ r'\b(which\s+medication|best\s+medication|how\s+to\s+cure|without\s+medications)\b',
223
+ r'\b(chronic\s+migraine|migraine\s+treatment|migraine\s+cure)\b',
224
+ r'\b(cure|treat|heal|relief|remedy|solution)\b'
225
+ ]
226
+
227
+ for pattern in medical_patterns:
228
+ if re.search(pattern, query_lower):
229
+ return True
230
+
231
+ return False
232
+
233
+ def check_user_query(self, user_query: str) -> Tuple[bool, str]:
234
+ """Validate the user query is safe to process with medical context awareness."""
235
+ if not self.enabled:
236
+ logger.info("[SafetyGuard] Safety guard disabled, allowing query through")
237
+ return True, "guard_disabled"
238
+
239
+ text = user_query or ""
240
+
241
+ # For medical queries, be more permissive
242
+ if self._is_medical_query(text):
243
+ logger.info("[SafetyGuard] Medical query detected, skipping strict validation")
244
+ return True, "medical_query"
245
+
246
+ # If too long, validate each chunk; any UNSAFE makes overall UNSAFE
247
+ for part in self._chunk_text(text):
248
+ messages = [{"role": "user", "content": part}]
249
+ reply = self._call_guard(messages, max_tokens=64)
250
+ ok, reason = self._parse_guard_reply(reply)
251
+ if not ok:
252
+ return False, reason
253
+ return True, ""
254
+
255
+ def _detect_harmful_content(self, text: str) -> Tuple[bool, str]:
256
+ """Detect harmful content using sophisticated pattern matching."""
257
+ if not text:
258
+ return True, ""
259
+
260
+ text_lower = text.lower()
261
+
262
+ # First check if this is clearly medical content - be more permissive
263
+ if self._is_medical_query(text):
264
+ # For medical content, only check for truly dangerous patterns
265
+ dangerous_medical_patterns = {
266
+ 'suicide_self_harm': [
267
+ r'\b(kill\s+yourself|suicide|end\s+your\s+life|take\s+your\s+life)\b',
268
+ r'\b(self\s*harm|self\s*injury|cut\s+yourself|hurt\s+yourself)\b',
269
+ r'\b(overdose|poison\s+yourself|hang\s+yourself)\b'
270
+ ],
271
+ 'dangerous_medical_advice': [
272
+ r'\b(overdose\s+on|take\s+too\s+much|excessive\s+dosage)\b',
273
+ r'\b(mix\s+drugs|combine\s+medications|contraindicated)\b',
274
+ r'\b(stop\s+taking\s+prescribed|discontinue\s+medication)\b',
275
+ r'\b(don\'t\s+call\s+911|avoid\s+emergency\s+room|ignore\s+severe\s+symptoms)\b'
276
+ ],
277
+ 'illegal_drugs': [
278
+ r'\b(cocaine|heroin|methamphetamine|ecstasy|lsd|marijuana\s+abuse)\b',
279
+ r'\b(prescription\s+abuse|drug\s+dealing|illegal\s+substances)\b'
280
+ ]
281
+ }
282
+
283
+ # Check only dangerous medical patterns
284
+ for category, patterns in dangerous_medical_patterns.items():
285
+ for pattern in patterns:
286
+ if re.search(pattern, text_lower):
287
+ return False, f"contains {category}: {pattern}"
288
+
289
+ # Allow medical content through
290
+ return True, "medical_content"
291
+
292
+ # For non-medical content, use stricter patterns
293
+ critical_patterns = {
294
+ 'suicide_self_harm': [
295
+ r'\b(kill\s+yourself|suicide|end\s+your\s+life|take\s+your\s+life)\b',
296
+ r'\b(self\s*harm|self\s*injury|cut\s+yourself|hurt\s+yourself)\b',
297
+ r'\b(overdose|poison\s+yourself|hang\s+yourself)\b'
298
+ ],
299
+ 'violence': [
300
+ r'\b(kill\s+someone|murder|assassinate|violence|harm\s+others)\b',
301
+ r'\b(bomb|explosive|weapon|gun|knife)\b',
302
+ r'\b(attack\s+(someone|people|others|innocent))\b' # More specific attack pattern
303
+ ],
304
+ 'illegal_drugs': [
305
+ r'\b(cocaine|heroin|methamphetamine|ecstasy|lsd|marijuana\s+abuse)\b',
306
+ r'\b(prescription\s+abuse|drug\s+dealing|illegal\s+substances)\b'
307
+ ],
308
+ 'dangerous_medical': [
309
+ r'\b(overdose\s+on|take\s+too\s+much|excessive\s+dosage)\b',
310
+ r'\b(mix\s+drugs|combine\s+medications|contraindicated)\b',
311
+ r'\b(stop\s+taking\s+prescribed|discontinue\s+medication)\b'
312
+ ]
313
+ }
314
+
315
+ # Check critical patterns
316
+ for category, patterns in critical_patterns.items():
317
+ for pattern in patterns:
318
+ if re.search(pattern, text_lower):
319
+ return False, f"contains {category}: {pattern}"
320
+
321
+ # Context-aware medical safety checks
322
+ medical_safety_patterns = {
323
+ 'dosage_warnings': [
324
+ r'\b(take\s+more\s+than\s+prescribed|exceed\s+recommended\s+dose)\b',
325
+ r'\b(double\s+up\s+on\s+medication|take\s+extra\s+pills)\b'
326
+ ],
327
+ 'emergency_advice': [
328
+ r'\b(don\'t\s+call\s+911|avoid\s+emergency\s+room|ignore\s+severe\s+symptoms)\b',
329
+ r'\b(self\s*treat\s+emergency|handle\s+at\s+home\s+when\s+critical)\b'
330
+ ]
331
+ }
332
+
333
+ # Check medical safety patterns
334
+ for category, patterns in medical_safety_patterns.items():
335
+ for pattern in patterns:
336
+ if re.search(pattern, text_lower):
337
+ return False, f"contains dangerous medical advice: {pattern}"
338
+
339
+ return True, ""
340
+
341
+ def _enhance_messages_with_context(self, messages: List[Dict]) -> List[Dict]:
342
+ """Enhance messages with medical context awareness for better guard performance."""
343
+ if not messages:
344
+ return messages
345
+
346
+ # Check if any message contains medical content
347
+ has_medical_content = any(
348
+ self._is_medical_query(msg.get("content", "")) for msg in messages
349
+ )
350
+
351
+ if has_medical_content:
352
+ # Add medical context to the system prompt
353
+ enhanced_messages = []
354
+ for i, msg in enumerate(messages):
355
+ if i == 0 and msg.get("role") == "user":
356
+ # Add medical context to first user message
357
+ original_content = msg.get("content", "")
358
+ enhanced_content = f"""MEDICAL CONTEXT: This is a medical consultation. The user is asking about health-related topics.
359
+
360
+ ORIGINAL MESSAGE: {original_content}
361
+
362
+ Please evaluate this message for safety, keeping in mind that:
363
+ - Medical questions and health concerns are legitimate
364
+ - General medical information sharing is acceptable
365
+ - Only block content that is clearly harmful, dangerous, or illegal
366
+ - Medical advice should be educational, not prescriptive"""
367
+
368
+ enhanced_messages.append({
369
+ "role": msg["role"],
370
+ "content": enhanced_content
371
+ })
372
+ else:
373
+ enhanced_messages.append(msg)
374
+ return enhanced_messages
375
+
376
+ return messages
377
+
378
+ def _assess_risk_level(self, text: str) -> Tuple[str, float]:
379
+ """Assess the risk level of content using multiple indicators."""
380
+ if not text:
381
+ return "low", 0.0
382
+
383
+ text_lower = text.lower()
384
+
385
+ # If this is medical content, be more lenient
386
+ if self._is_medical_query(text):
387
+ # For medical content, only flag truly dangerous patterns
388
+ dangerous_medical_indicators = {
389
+ 'high': [
390
+ 'suicide', 'kill yourself', 'end your life', 'self harm',
391
+ 'overdose', 'poison yourself', 'illegal drugs', 'violence'
392
+ ],
393
+ 'medium': [
394
+ 'prescription abuse', 'excessive dosage', 'mix drugs',
395
+ 'stop taking prescribed', 'ignore severe symptoms'
396
+ ]
397
+ }
398
+
399
+ risk_score = 0.0
400
+ for level, indicators in dangerous_medical_indicators.items():
401
+ for indicator in indicators:
402
+ if indicator in text_lower:
403
+ if level == 'high':
404
+ risk_score += 3.0
405
+ elif level == 'medium':
406
+ risk_score += 1.5
407
+
408
+ # Normalize score for medical content (more lenient)
409
+ risk_score = min(risk_score / 15.0, 1.0)
410
+
411
+ if risk_score >= 0.6:
412
+ return "high", risk_score
413
+ elif risk_score >= 0.2:
414
+ return "medium", risk_score
415
+ else:
416
+ return "low", risk_score
417
+
418
+ # For non-medical content, use original risk assessment
419
+ risk_indicators = {
420
+ 'high': [
421
+ 'suicide', 'kill yourself', 'end your life', 'self harm',
422
+ 'overdose', 'poison', 'illegal drugs', 'violence', 'harm others'
423
+ ],
424
+ 'medium': [
425
+ 'prescription abuse', 'excessive dosage', 'mix drugs',
426
+ 'stop medication', 'ignore symptoms', 'avoid doctor'
427
+ ],
428
+ 'low': [
429
+ 'pain', 'headache', 'fever', 'cough', 'treatment',
430
+ 'medicine', 'doctor', 'hospital', 'symptoms'
431
+ ]
432
+ }
433
+
434
+ risk_score = 0.0
435
+ for level, indicators in risk_indicators.items():
436
+ for indicator in indicators:
437
+ if indicator in text_lower:
438
+ if level == 'high':
439
+ risk_score += 3.0
440
+ elif level == 'medium':
441
+ risk_score += 1.5
442
+ else:
443
+ risk_score += 0.5
444
+
445
+ # Normalize score
446
+ risk_score = min(risk_score / 10.0, 1.0)
447
+
448
+ if risk_score >= 0.7:
449
+ return "high", risk_score
450
+ elif risk_score >= 0.3:
451
+ return "medium", risk_score
452
+ else:
453
+ return "low", risk_score
454
+
455
+ def check_model_answer(self, user_query: str, model_answer: str) -> Tuple[bool, str]:
456
+ """Validate the model's answer is safe with medical context awareness."""
457
+ if not self.enabled:
458
+ logger.info("[SafetyGuard] Safety guard disabled, allowing response through")
459
+ return True, "guard_disabled"
460
+
461
+ uq = user_query or ""
462
+ ans = model_answer or ""
463
+
464
+ # Assess risk level first
465
+ risk_level, risk_score = self._assess_risk_level(ans)
466
+ logger.info(f"[SafetyGuard] Risk assessment: {risk_level} (score: {risk_score:.2f})")
467
+
468
+ # Always check for harmful content first
469
+ is_safe, reason = self._detect_harmful_content(ans)
470
+ if not is_safe:
471
+ return False, reason
472
+
473
+ # For high-risk content, always use strict validation
474
+ if risk_level == "high":
475
+ logger.warning("[SafetyGuard] High-risk content detected, using strict validation")
476
+ user_parts = self._chunk_text(uq, chunk_size=2000)
477
+ user_context = user_parts[0] if user_parts else ""
478
+ for ans_part in self._chunk_text(ans):
479
+ messages = [
480
+ {"role": "user", "content": user_context},
481
+ {"role": "assistant", "content": ans_part},
482
+ ]
483
+ reply = self._call_guard(messages, max_tokens=96)
484
+ ok, reason = self._parse_guard_reply(reply)
485
+ if not ok:
486
+ return False, reason
487
+ return True, "high_risk_validated"
488
+
489
+ # For medical queries and answers, use relaxed validation
490
+ if self._is_medical_query(uq) or self._is_medical_query(ans):
491
+ logger.info("[SafetyGuard] Medical content detected, using relaxed validation")
492
+ return True, "medical_content"
493
+
494
+ # For medium-risk non-medical content, use guard validation
495
+ if risk_level == "medium":
496
+ logger.info("[SafetyGuard] Medium-risk content detected, using guard validation")
497
+ user_parts = self._chunk_text(uq, chunk_size=2000)
498
+ user_context = user_parts[0] if user_parts else ""
499
+ for ans_part in self._chunk_text(ans):
500
+ messages = [
501
+ {"role": "user", "content": user_context},
502
+ {"role": "assistant", "content": ans_part},
503
+ ]
504
+ reply = self._call_guard(messages, max_tokens=96)
505
+ ok, reason = self._parse_guard_reply(reply)
506
+ if not ok:
507
+ return False, reason
508
+ return True, "medium_risk_validated"
509
+
510
+ # For low-risk content, allow through
511
+ logger.info("[SafetyGuard] Low-risk content detected, allowing through")
512
+ return True, "low_risk"
513
+
514
+
515
+ # Global instance (optional convenience) - will be initialized with rotator when needed
516
+ safety_guard = None
517
+
518
+
src/services/medical_response.py CHANGED
@@ -3,6 +3,7 @@
3
  from src.core import prompt_builder
4
  from src.data.medical_kb import search_medical_kb
5
  from src.services.gemini import gemini_chat
 
6
  from src.utils.logger import logger
7
  from src.utils.rotator import APIKeyRotator
8
 
@@ -12,7 +13,8 @@ async def generate_medical_response(
12
  user_role: str,
13
  user_specialty: str,
14
  rotator: APIKeyRotator,
15
- medical_context: str = ""
 
16
  ) -> str:
17
  """Generates an intelligent, contextual medical response using Gemini AI."""
18
  prompt = prompt_builder.medical_response_prompt(user_role, user_specialty, medical_context, user_message)
@@ -25,6 +27,22 @@ async def generate_medical_response(
25
  if "disclaimer" not in response_text.lower() and "consult" not in response_text.lower():
26
  response_text += "\n\n⚠️ **Important Disclaimer:** This information is for educational purposes only and should not replace professional medical advice, diagnosis, or treatment. Always consult with qualified healthcare professionals."
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  logger().info(f"Gemini response generated, length: {len(response_text)} chars")
29
  return response_text
30
 
 
3
  from src.core import prompt_builder
4
  from src.data.medical_kb import search_medical_kb
5
  from src.services.gemini import gemini_chat
6
+ from src.services.guard import SafetyGuard
7
  from src.utils.logger import logger
8
  from src.utils.rotator import APIKeyRotator
9
 
 
13
  user_role: str,
14
  user_specialty: str,
15
  rotator: APIKeyRotator,
16
+ medical_context: str = "",
17
+ nvidia_rotator: APIKeyRotator = None
18
  ) -> str:
19
  """Generates an intelligent, contextual medical response using Gemini AI."""
20
  prompt = prompt_builder.medical_response_prompt(user_role, user_specialty, medical_context, user_message)
 
27
  if "disclaimer" not in response_text.lower() and "consult" not in response_text.lower():
28
  response_text += "\n\n⚠️ **Important Disclaimer:** This information is for educational purposes only and should not replace professional medical advice, diagnosis, or treatment. Always consult with qualified healthcare professionals."
29
 
30
+ # Safety Guard: Validate the generated response
31
+ if nvidia_rotator:
32
+ try:
33
+ safety_guard = SafetyGuard(nvidia_rotator)
34
+ is_safe, safety_reason = safety_guard.check_model_answer(user_message, response_text)
35
+ if not is_safe:
36
+ logger().warning(f"Safety guard blocked generated response: {safety_reason}")
37
+ # Return safe fallback response
38
+ return "I apologize, but I cannot provide a response to that query as it may contain unsafe content. Please consult with a qualified healthcare professional for medical advice."
39
+ else:
40
+ logger().info(f"Generated response passed safety validation: {safety_reason}")
41
+ except Exception as e:
42
+ logger().error(f"Safety guard error in medical response: {e}")
43
+ # Fail open for now - allow response through if guard fails
44
+ logger().warning("Safety guard failed, allowing generated response through")
45
+
46
  logger().info(f"Gemini response generated, length: {len(response_text)} chars")
47
  return response_text
48
 
src/services/service.py CHANGED
@@ -131,7 +131,7 @@ class EMRService:
131
 
132
  # Generate embeddings
133
  combined_text = " | ".join(text_parts)
134
- embeddings = await self.embedding_client.generate_embeddings(combined_text)
135
 
136
  return embeddings
137
 
@@ -181,7 +181,7 @@ class EMRService:
181
  """Search EMR entries using semantic similarity."""
182
  try:
183
  # Generate embeddings for the search query
184
- query_embeddings = await self.embedding_client.generate_embeddings(query)
185
 
186
  # Search using semantic similarity
187
  entries = search_emr_by_semantic_similarity(
 
131
 
132
  # Generate embeddings
133
  combined_text = " | ".join(text_parts)
134
+ embeddings = self.embedding_client.embed(combined_text)[0]
135
 
136
  return embeddings
137
 
 
181
  """Search EMR entries using semantic similarity."""
182
  try:
183
  # Generate embeddings for the search query
184
+ query_embeddings = self.embedding_client.embed(query)[0]
185
 
186
  # Search using semantic similarity
187
  entries = search_emr_by_semantic_similarity(
static/css/emr.css CHANGED
@@ -115,6 +115,365 @@
115
  letter-spacing: 0.05em;
116
  }
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  /* Controls */
119
  .emr-controls {
120
  background-color: var(--bg-primary);
@@ -531,4 +890,291 @@
531
  .emr-table tbody tr:hover {
532
  background-color: var(--dark-bg-secondary);
533
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  }
 
115
  letter-spacing: 0.05em;
116
  }
117
 
118
+ /* File Upload Section */
119
+ .emr-upload-section {
120
+ background-color: var(--bg-primary);
121
+ border-bottom: 1px solid var(--border-color);
122
+ padding: var(--spacing-lg);
123
+ max-width: 1200px;
124
+ margin: 0 auto;
125
+ }
126
+
127
+ .upload-container {
128
+ max-width: 600px;
129
+ margin: 0 auto;
130
+ }
131
+
132
+ .upload-header {
133
+ text-align: center;
134
+ margin-bottom: var(--spacing-lg);
135
+ }
136
+
137
+ .upload-header h3 {
138
+ margin: 0 0 var(--spacing-sm) 0;
139
+ color: var(--text-primary);
140
+ font-size: 1.25rem;
141
+ font-weight: 600;
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ gap: var(--spacing-sm);
146
+ }
147
+
148
+ .upload-header p {
149
+ margin: 0;
150
+ color: var(--text-secondary);
151
+ font-size: 0.875rem;
152
+ }
153
+
154
+ .upload-area {
155
+ border: 2px dashed var(--border-color);
156
+ border-radius: 12px;
157
+ padding: var(--spacing-2xl);
158
+ text-align: center;
159
+ background-color: var(--bg-secondary);
160
+ transition: all var(--transition-fast);
161
+ cursor: pointer;
162
+ }
163
+
164
+ .upload-area:hover {
165
+ border-color: var(--primary-color);
166
+ background-color: var(--bg-tertiary);
167
+ }
168
+
169
+ .upload-area.dragover {
170
+ border-color: var(--primary-color);
171
+ background-color: var(--primary-color);
172
+ color: white;
173
+ }
174
+
175
+ .upload-content i {
176
+ font-size: 3rem;
177
+ color: var(--primary-color);
178
+ margin-bottom: var(--spacing-md);
179
+ }
180
+
181
+ .upload-area.dragover .upload-content i {
182
+ color: white;
183
+ }
184
+
185
+ .upload-content h4 {
186
+ margin: 0 0 var(--spacing-sm) 0;
187
+ color: var(--text-primary);
188
+ font-size: 1.125rem;
189
+ font-weight: 600;
190
+ }
191
+
192
+ .upload-area.dragover .upload-content h4 {
193
+ color: white;
194
+ }
195
+
196
+ .upload-content p {
197
+ margin: 0 0 var(--spacing-lg) 0;
198
+ color: var(--text-secondary);
199
+ font-size: 0.875rem;
200
+ }
201
+
202
+ .upload-area.dragover .upload-content p {
203
+ color: white;
204
+ }
205
+
206
+ .upload-progress {
207
+ margin-top: var(--spacing-lg);
208
+ }
209
+
210
+ .progress-bar {
211
+ width: 100%;
212
+ height: 8px;
213
+ background-color: var(--bg-tertiary);
214
+ border-radius: 4px;
215
+ overflow: hidden;
216
+ margin-bottom: var(--spacing-sm);
217
+ }
218
+
219
+ .progress-fill {
220
+ height: 100%;
221
+ background-color: var(--primary-color);
222
+ transition: width var(--transition-normal);
223
+ width: 0%;
224
+ }
225
+
226
+ .progress-text {
227
+ font-size: 0.875rem;
228
+ color: var(--text-secondary);
229
+ text-align: center;
230
+ display: block;
231
+ }
232
+
233
+ /* Document Preview Modal */
234
+ .document-preview-modal {
235
+ max-width: 900px;
236
+ max-height: 85vh;
237
+ overflow-y: auto;
238
+ }
239
+
240
+ .document-preview-section {
241
+ margin-bottom: var(--spacing-lg);
242
+ background-color: var(--bg-secondary);
243
+ border-radius: 8px;
244
+ padding: var(--spacing-md);
245
+ border: 1px solid var(--border-color);
246
+ }
247
+
248
+ .document-preview-section h4 {
249
+ margin: 0 0 var(--spacing-md) 0;
250
+ color: var(--text-primary);
251
+ font-size: 1rem;
252
+ font-weight: 600;
253
+ border-bottom: 1px solid var(--border-color);
254
+ padding-bottom: var(--spacing-sm);
255
+ display: flex;
256
+ align-items: center;
257
+ gap: var(--spacing-sm);
258
+ }
259
+
260
+ .document-preview-section h4 i {
261
+ color: var(--primary-color);
262
+ }
263
+
264
+ .editable-field {
265
+ background-color: var(--bg-primary);
266
+ border: 1px solid var(--border-color);
267
+ border-radius: 6px;
268
+ padding: var(--spacing-sm);
269
+ margin-bottom: var(--spacing-sm);
270
+ min-height: 40px;
271
+ transition: border-color var(--transition-fast);
272
+ }
273
+
274
+ .editable-field:focus {
275
+ outline: none;
276
+ border-color: var(--primary-color);
277
+ box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
278
+ }
279
+
280
+ .editable-list {
281
+ list-style: none;
282
+ padding: 0;
283
+ margin: 0;
284
+ }
285
+
286
+ .editable-list li {
287
+ background-color: var(--bg-primary);
288
+ border: 1px solid var(--border-color);
289
+ border-radius: 6px;
290
+ padding: var(--spacing-sm);
291
+ margin-bottom: var(--spacing-sm);
292
+ display: flex;
293
+ align-items: center;
294
+ gap: var(--spacing-sm);
295
+ }
296
+
297
+ .editable-list li input {
298
+ flex: 1;
299
+ background: none;
300
+ border: none;
301
+ outline: none;
302
+ color: var(--text-primary);
303
+ font-size: 0.875rem;
304
+ }
305
+
306
+ .editable-list li button {
307
+ background: none;
308
+ border: none;
309
+ color: var(--accent-color);
310
+ cursor: pointer;
311
+ padding: var(--spacing-xs);
312
+ border-radius: 4px;
313
+ transition: background-color var(--transition-fast);
314
+ }
315
+
316
+ .editable-list li button:hover {
317
+ background-color: var(--bg-tertiary);
318
+ }
319
+
320
+ .add-item-btn {
321
+ background-color: var(--primary-color);
322
+ color: white;
323
+ border: none;
324
+ border-radius: 6px;
325
+ padding: var(--spacing-sm) var(--spacing-md);
326
+ cursor: pointer;
327
+ font-size: 0.875rem;
328
+ transition: background-color var(--transition-fast);
329
+ display: flex;
330
+ align-items: center;
331
+ gap: var(--spacing-xs);
332
+ }
333
+
334
+ .add-item-btn:hover {
335
+ background-color: var(--primary-hover);
336
+ }
337
+
338
+ .medication-preview-item {
339
+ background-color: var(--bg-primary);
340
+ border: 1px solid var(--border-color);
341
+ border-radius: 8px;
342
+ padding: var(--spacing-md);
343
+ margin-bottom: var(--spacing-sm);
344
+ }
345
+
346
+ .medication-preview-item h5 {
347
+ margin: 0 0 var(--spacing-sm) 0;
348
+ color: var(--text-primary);
349
+ font-size: 1rem;
350
+ font-weight: 600;
351
+ }
352
+
353
+ .medication-detail-row {
354
+ display: grid;
355
+ grid-template-columns: 1fr 1fr;
356
+ gap: var(--spacing-md);
357
+ margin-bottom: var(--spacing-sm);
358
+ }
359
+
360
+ .medication-detail-row:last-child {
361
+ margin-bottom: 0;
362
+ }
363
+
364
+ .medication-detail-row label {
365
+ font-size: 0.75rem;
366
+ color: var(--text-secondary);
367
+ text-transform: uppercase;
368
+ letter-spacing: 0.05em;
369
+ margin-bottom: var(--spacing-xs);
370
+ display: block;
371
+ }
372
+
373
+ .medication-detail-row input {
374
+ width: 100%;
375
+ background-color: var(--bg-secondary);
376
+ border: 1px solid var(--border-color);
377
+ border-radius: 4px;
378
+ padding: var(--spacing-sm);
379
+ color: var(--text-primary);
380
+ font-size: 0.875rem;
381
+ }
382
+
383
+ .medication-detail-row input:focus {
384
+ outline: none;
385
+ border-color: var(--primary-color);
386
+ }
387
+
388
+ .vital-signs-preview-grid {
389
+ display: grid;
390
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
391
+ gap: var(--spacing-md);
392
+ }
393
+
394
+ .vital-sign-preview-item {
395
+ background-color: var(--bg-primary);
396
+ border: 1px solid var(--border-color);
397
+ border-radius: 8px;
398
+ padding: var(--spacing-md);
399
+ text-align: center;
400
+ }
401
+
402
+ .vital-sign-preview-item label {
403
+ font-size: 0.75rem;
404
+ color: var(--text-secondary);
405
+ text-transform: uppercase;
406
+ letter-spacing: 0.05em;
407
+ margin-bottom: var(--spacing-xs);
408
+ display: block;
409
+ }
410
+
411
+ .vital-sign-preview-item input {
412
+ width: 100%;
413
+ background-color: var(--bg-secondary);
414
+ border: 1px solid var(--border-color);
415
+ border-radius: 4px;
416
+ padding: var(--spacing-sm);
417
+ color: var(--text-primary);
418
+ font-size: 0.875rem;
419
+ text-align: center;
420
+ }
421
+
422
+ .vital-sign-preview-item input:focus {
423
+ outline: none;
424
+ border-color: var(--primary-color);
425
+ }
426
+
427
+ .lab-result-preview-item {
428
+ background-color: var(--bg-primary);
429
+ border: 1px solid var(--border-color);
430
+ border-radius: 8px;
431
+ padding: var(--spacing-md);
432
+ margin-bottom: var(--spacing-sm);
433
+ }
434
+
435
+ .lab-result-preview-item h5 {
436
+ margin: 0 0 var(--spacing-sm) 0;
437
+ color: var(--text-primary);
438
+ font-size: 1rem;
439
+ font-weight: 600;
440
+ }
441
+
442
+ .lab-result-detail-row {
443
+ display: grid;
444
+ grid-template-columns: 1fr 1fr 1fr;
445
+ gap: var(--spacing-md);
446
+ margin-bottom: var(--spacing-sm);
447
+ }
448
+
449
+ .lab-result-detail-row:last-child {
450
+ margin-bottom: 0;
451
+ }
452
+
453
+ .lab-result-detail-row label {
454
+ font-size: 0.75rem;
455
+ color: var(--text-secondary);
456
+ text-transform: uppercase;
457
+ letter-spacing: 0.05em;
458
+ margin-bottom: var(--spacing-xs);
459
+ display: block;
460
+ }
461
+
462
+ .lab-result-detail-row input {
463
+ width: 100%;
464
+ background-color: var(--bg-secondary);
465
+ border: 1px solid var(--border-color);
466
+ border-radius: 4px;
467
+ padding: var(--spacing-sm);
468
+ color: var(--text-primary);
469
+ font-size: 0.875rem;
470
+ }
471
+
472
+ .lab-result-detail-row input:focus {
473
+ outline: none;
474
+ border-color: var(--primary-color);
475
+ }
476
+
477
  /* Controls */
478
  .emr-controls {
479
  background-color: var(--bg-primary);
 
890
  .emr-table tbody tr:hover {
891
  background-color: var(--dark-bg-secondary);
892
  }
893
+ }
894
+
895
+ /* Tabbed Interface */
896
+ .emr-tabs {
897
+ display: flex;
898
+ background-color: var(--bg-primary);
899
+ border-bottom: 1px solid var(--border-color);
900
+ margin-bottom: var(--spacing-lg);
901
+ border-radius: 8px 8px 0 0;
902
+ overflow-x: auto;
903
+ }
904
+
905
+ .tab-btn {
906
+ background: none;
907
+ border: none;
908
+ padding: var(--spacing-md) var(--spacing-lg);
909
+ cursor: pointer;
910
+ color: var(--text-secondary);
911
+ font-weight: 500;
912
+ transition: all var(--transition-fast);
913
+ border-bottom: 3px solid transparent;
914
+ white-space: nowrap;
915
+ min-width: 120px;
916
+ text-align: center;
917
+ }
918
+
919
+ .tab-btn:hover {
920
+ color: var(--primary-color);
921
+ background-color: var(--bg-secondary);
922
+ }
923
+
924
+ .tab-btn.active {
925
+ color: var(--primary-color);
926
+ border-bottom-color: var(--primary-color);
927
+ background-color: var(--bg-secondary);
928
+ }
929
+
930
+ .tab-content {
931
+ display: none;
932
+ animation: fadeIn 0.3s ease-in-out;
933
+ }
934
+
935
+ .tab-content.active {
936
+ display: block;
937
+ }
938
+
939
+ @keyframes fadeIn {
940
+ from { opacity: 0; transform: translateY(10px); }
941
+ to { opacity: 1; transform: translateY(0); }
942
+ }
943
+
944
+ /* EMR Sections */
945
+ .emr-section {
946
+ background-color: var(--bg-primary);
947
+ border-radius: 8px;
948
+ padding: var(--spacing-lg);
949
+ margin-bottom: var(--spacing-lg);
950
+ box-shadow: var(--shadow-sm);
951
+ }
952
+
953
+ .emr-section h3 {
954
+ margin: 0 0 var(--spacing-lg) 0;
955
+ color: var(--text-primary);
956
+ font-size: 1.25rem;
957
+ font-weight: 600;
958
+ border-bottom: 2px solid var(--primary-color);
959
+ padding-bottom: var(--spacing-sm);
960
+ }
961
+
962
+ /* Diagnosis Timeline */
963
+ .diagnosis-timeline {
964
+ display: flex;
965
+ flex-direction: column;
966
+ gap: var(--spacing-md);
967
+ }
968
+
969
+ .diagnosis-item {
970
+ display: flex;
971
+ align-items: center;
972
+ padding: var(--spacing-md);
973
+ background-color: var(--bg-secondary);
974
+ border-radius: 8px;
975
+ border-left: 4px solid var(--primary-color);
976
+ transition: all var(--transition-fast);
977
+ }
978
+
979
+ .diagnosis-item:hover {
980
+ background-color: var(--bg-tertiary);
981
+ transform: translateX(4px);
982
+ }
983
+
984
+ .diagnosis-date {
985
+ font-size: 0.875rem;
986
+ color: var(--text-muted);
987
+ min-width: 120px;
988
+ margin-right: var(--spacing-md);
989
+ }
990
+
991
+ .diagnosis-name {
992
+ font-weight: 600;
993
+ color: var(--text-primary);
994
+ flex: 1;
995
+ }
996
+
997
+ .diagnosis-confidence {
998
+ background-color: var(--primary-color);
999
+ color: white;
1000
+ padding: var(--spacing-xs) var(--spacing-sm);
1001
+ border-radius: 12px;
1002
+ font-size: 0.75rem;
1003
+ font-weight: 600;
1004
+ }
1005
+
1006
+ /* Medications Grid */
1007
+ .medications-grid {
1008
+ display: grid;
1009
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1010
+ gap: var(--spacing-md);
1011
+ }
1012
+
1013
+ .medication-card {
1014
+ background-color: var(--bg-secondary);
1015
+ border-radius: 8px;
1016
+ padding: var(--spacing-md);
1017
+ border: 1px solid var(--border-color);
1018
+ transition: all var(--transition-fast);
1019
+ }
1020
+
1021
+ .medication-card:hover {
1022
+ border-color: var(--primary-color);
1023
+ box-shadow: var(--shadow-md);
1024
+ }
1025
+
1026
+ .medication-name {
1027
+ font-weight: 600;
1028
+ color: var(--text-primary);
1029
+ margin-bottom: var(--spacing-sm);
1030
+ font-size: 1.1rem;
1031
+ }
1032
+
1033
+ .medication-details {
1034
+ display: flex;
1035
+ flex-direction: column;
1036
+ gap: var(--spacing-xs);
1037
+ color: var(--text-secondary);
1038
+ font-size: 0.875rem;
1039
+ }
1040
+
1041
+ .medication-detail {
1042
+ display: flex;
1043
+ justify-content: space-between;
1044
+ }
1045
+
1046
+ .medication-detail strong {
1047
+ color: var(--text-primary);
1048
+ }
1049
+
1050
+ /* Vital Signs Chart */
1051
+ .vitals-chart-container {
1052
+ background-color: var(--bg-secondary);
1053
+ border-radius: 8px;
1054
+ padding: var(--spacing-lg);
1055
+ margin-bottom: var(--spacing-lg);
1056
+ text-align: center;
1057
+ }
1058
+
1059
+ .vitals-chart-container canvas {
1060
+ max-width: 100%;
1061
+ height: auto;
1062
+ }
1063
+
1064
+ /* Vital Signs Table */
1065
+ .vitals-table-container {
1066
+ overflow-x: auto;
1067
+ }
1068
+
1069
+ .vitals-table {
1070
+ width: 100%;
1071
+ border-collapse: collapse;
1072
+ background-color: var(--bg-primary);
1073
+ border-radius: 8px;
1074
+ overflow: hidden;
1075
+ box-shadow: var(--shadow-sm);
1076
+ }
1077
+
1078
+ .vitals-table th,
1079
+ .vitals-table td {
1080
+ padding: var(--spacing-md);
1081
+ text-align: left;
1082
+ border-bottom: 1px solid var(--border-color);
1083
+ }
1084
+
1085
+ .vitals-table th {
1086
+ background-color: var(--bg-tertiary);
1087
+ font-weight: 600;
1088
+ color: var(--text-primary);
1089
+ }
1090
+
1091
+ .vitals-table td {
1092
+ color: var(--text-secondary);
1093
+ }
1094
+
1095
+ .vitals-table tr:hover {
1096
+ background-color: var(--bg-secondary);
1097
+ }
1098
+
1099
+ /* Lab Results */
1100
+ .lab-results-container {
1101
+ display: flex;
1102
+ flex-direction: column;
1103
+ gap: var(--spacing-md);
1104
+ }
1105
+
1106
+ .lab-result-item {
1107
+ background-color: var(--bg-secondary);
1108
+ border-radius: 8px;
1109
+ padding: var(--spacing-md);
1110
+ border-left: 4px solid var(--secondary-color);
1111
+ }
1112
+
1113
+ .lab-result-header {
1114
+ display: flex;
1115
+ justify-content: space-between;
1116
+ align-items: center;
1117
+ margin-bottom: var(--spacing-sm);
1118
+ }
1119
+
1120
+ .lab-test-name {
1121
+ font-weight: 600;
1122
+ color: var(--text-primary);
1123
+ }
1124
+
1125
+ .lab-test-value {
1126
+ font-size: 1.1rem;
1127
+ font-weight: 600;
1128
+ color: var(--secondary-color);
1129
+ }
1130
+
1131
+ .lab-test-details {
1132
+ display: flex;
1133
+ gap: var(--spacing-md);
1134
+ color: var(--text-secondary);
1135
+ font-size: 0.875rem;
1136
+ }
1137
+
1138
+ /* Procedures Timeline */
1139
+ .procedures-timeline {
1140
+ display: flex;
1141
+ flex-direction: column;
1142
+ gap: var(--spacing-md);
1143
+ }
1144
+
1145
+ .procedure-item {
1146
+ display: flex;
1147
+ align-items: center;
1148
+ padding: var(--spacing-md);
1149
+ background-color: var(--bg-secondary);
1150
+ border-radius: 8px;
1151
+ border-left: 4px solid var(--accent-color);
1152
+ transition: all var(--transition-fast);
1153
+ }
1154
+
1155
+ .procedure-item:hover {
1156
+ background-color: var(--bg-tertiary);
1157
+ transform: translateX(4px);
1158
+ }
1159
+
1160
+ .procedure-date {
1161
+ font-size: 0.875rem;
1162
+ color: var(--text-muted);
1163
+ min-width: 120px;
1164
+ margin-right: var(--spacing-md);
1165
+ }
1166
+
1167
+ .procedure-name {
1168
+ font-weight: 600;
1169
+ color: var(--text-primary);
1170
+ flex: 1;
1171
+ }
1172
+
1173
+ .procedure-status {
1174
+ background-color: var(--accent-color);
1175
+ color: white;
1176
+ padding: var(--spacing-xs) var(--spacing-sm);
1177
+ border-radius: 12px;
1178
+ font-size: 0.75rem;
1179
+ font-weight: 600;
1180
  }
static/css/styles.css CHANGED
@@ -456,9 +456,122 @@ body {
456
  border-color: var(--primary-color);
457
  }
458
 
459
- .message-text h3 {
460
- margin-bottom: var(--spacing-md);
461
- color: inherit;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  }
463
 
464
  .message-text p {
 
456
  border-color: var(--primary-color);
457
  }
458
 
459
+ /* Tagged Title Styling */
460
+ .tagged-title {
461
+ display: flex;
462
+ align-items: center;
463
+ margin: var(--spacing-lg) 0;
464
+ position: relative;
465
+ width: 100%;
466
+ }
467
+
468
+ .tagged-title-bar {
469
+ width: 4px;
470
+ height: 100%;
471
+ background-color: #20b2aa; /* Teal/green color */
472
+ border-radius: 2px;
473
+ margin-right: 0;
474
+ flex-shrink: 0;
475
+ min-height: 40px;
476
+ }
477
+
478
+ .tagged-title-content {
479
+ flex: 1;
480
+ background-color: #f5f5f5; /* Light gray background */
481
+ border-radius: 0 8px 8px 0;
482
+ padding: var(--spacing-md) var(--spacing-lg);
483
+ margin-left: 0;
484
+ display: flex;
485
+ align-items: center;
486
+ min-height: 40px;
487
+ }
488
+
489
+ .tagged-title-content h1,
490
+ .tagged-title-content h2,
491
+ .tagged-title-content h3,
492
+ .tagged-title-content h4,
493
+ .tagged-title-content h5,
494
+ .tagged-title-content h6 {
495
+ margin: 0;
496
+ color: #8b4513; /* Dark brown/maroon color */
497
+ font-weight: 700;
498
+ text-transform: uppercase;
499
+ font-size: 0.9rem;
500
+ letter-spacing: 0.5px;
501
+ }
502
+
503
+ /* Blue Bubble Styling */
504
+ .blue-bubble {
505
+ display: inline-block;
506
+ background-color: #4a90e2; /* Medium blue */
507
+ color: white;
508
+ padding: 4px 8px;
509
+ border-radius: 12px;
510
+ font-size: 0.85rem;
511
+ font-weight: 500;
512
+ margin: 0 2px;
513
+ white-space: nowrap;
514
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
515
+ line-height: 1.2;
516
+ vertical-align: baseline;
517
+ }
518
+
519
+ /* Dark theme adjustments */
520
+ [data-theme="dark"] .tagged-title-content {
521
+ background-color: #2a2a2a; /* Darker gray for dark theme */
522
+ }
523
+
524
+ [data-theme="dark"] .tagged-title-content h1,
525
+ [data-theme="dark"] .tagged-title-content h2,
526
+ [data-theme="dark"] .tagged-title-content h3,
527
+ [data-theme="dark"] .tagged-title-content h4,
528
+ [data-theme="dark"] .tagged-title-content h5,
529
+ [data-theme="dark"] .tagged-title-content h6 {
530
+ color: #d4a574; /* Lighter brown for dark theme */
531
+ }
532
+
533
+ [data-theme="dark"] .blue-bubble {
534
+ background-color: #5ba0f2; /* Slightly lighter blue for dark theme */
535
+ }
536
+
537
+ /* Ensure proper spacing and layout within message content */
538
+ .message-text .tagged-title {
539
+ margin: var(--spacing-md) 0;
540
+ }
541
+
542
+ .message-text .tagged-title:first-child {
543
+ margin-top: 0;
544
+ }
545
+
546
+ .message-text .tagged-title:last-child {
547
+ margin-bottom: 0;
548
+ }
549
+
550
+ /* Ensure blue bubbles work well within paragraphs */
551
+ .message-text p .blue-bubble {
552
+ margin: 0 4px;
553
+ }
554
+
555
+ /* Responsive adjustments for smaller screens */
556
+ @media (max-width: 768px) {
557
+ .tagged-title-content {
558
+ padding: var(--spacing-sm) var(--spacing-md);
559
+ min-height: 36px;
560
+ }
561
+
562
+ .tagged-title-content h1,
563
+ .tagged-title-content h2,
564
+ .tagged-title-content h3,
565
+ .tagged-title-content h4,
566
+ .tagged-title-content h5,
567
+ .tagged-title-content h6 {
568
+ font-size: 0.8rem;
569
+ }
570
+
571
+ .blue-bubble {
572
+ font-size: 0.8rem;
573
+ padding: 3px 6px;
574
+ }
575
  }
576
 
577
  .message-text p {
static/emr.html CHANGED
@@ -51,6 +51,34 @@
51
  </div>
52
  </div>
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  <!-- Search and Filters -->
55
  <div class="emr-controls">
56
  <div class="search-container">
@@ -76,27 +104,107 @@
76
  </div>
77
  </div>
78
 
79
- <!-- EMR Entries Table -->
80
  <div class="emr-content">
81
- <div class="emr-table-container">
82
- <table class="emr-table" id="emrTable">
83
- <thead>
84
- <tr>
85
- <th>Date & Time</th>
86
- <th>Type</th>
87
- <th>Diagnosis</th>
88
- <th>Medications</th>
89
- <th>Vital Signs</th>
90
- <th>Confidence</th>
91
- <th>Actions</th>
92
- </tr>
93
- </thead>
94
- <tbody id="emrTableBody">
95
- <!-- EMR entries will be populated here -->
96
- </tbody>
97
- </table>
 
 
 
 
 
 
 
 
 
 
 
 
98
  </div>
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  <!-- Loading State -->
101
  <div class="loading-state" id="loadingState" style="display: none;">
102
  <div class="loading-spinner">
@@ -159,6 +267,23 @@
159
  </div>
160
  </div>
161
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  </div>
163
 
164
  <script src="/static/js/emr.js"></script>
 
51
  </div>
52
  </div>
53
 
54
+ <!-- File Upload Section -->
55
+ <div class="emr-upload-section">
56
+ <div class="upload-container">
57
+ <div class="upload-header">
58
+ <h3><i class="fas fa-upload"></i> Upload Medical Document</h3>
59
+ <p>Upload PDF, image, or document files to extract medical information</p>
60
+ </div>
61
+ <div class="upload-area" id="uploadArea">
62
+ <div class="upload-content">
63
+ <i class="fas fa-cloud-upload-alt"></i>
64
+ <h4>Drop files here or click to browse</h4>
65
+ <p>Supported formats: PDF, DOC, DOCX, JPG, PNG, TIFF</p>
66
+ <input type="file" id="fileInput" accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.tiff" multiple style="display: none;">
67
+ <button class="btn btn-primary" id="uploadBtn">
68
+ <i class="fas fa-folder-open"></i>
69
+ Choose Files
70
+ </button>
71
+ </div>
72
+ </div>
73
+ <div class="upload-progress" id="uploadProgress" style="display: none;">
74
+ <div class="progress-bar">
75
+ <div class="progress-fill" id="progressFill"></div>
76
+ </div>
77
+ <span class="progress-text" id="progressText">Uploading...</span>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
  <!-- Search and Filters -->
83
  <div class="emr-controls">
84
  <div class="search-container">
 
104
  </div>
105
  </div>
106
 
107
+ <!-- EMR Content Tabs -->
108
  <div class="emr-content">
109
+ <div class="emr-tabs">
110
+ <button class="tab-btn active" data-tab="overview">Overview</button>
111
+ <button class="tab-btn" data-tab="diagnosis">Diagnoses</button>
112
+ <button class="tab-btn" data-tab="medications">Medications</button>
113
+ <button class="tab-btn" data-tab="vitals">Vital Signs</button>
114
+ <button class="tab-btn" data-tab="lab">Lab Results</button>
115
+ <button class="tab-btn" data-tab="procedures">Procedures</button>
116
+ </div>
117
+
118
+ <!-- Overview Tab -->
119
+ <div class="tab-content active" id="overview-tab">
120
+ <div class="emr-table-container">
121
+ <table class="emr-table" id="emrTable">
122
+ <thead>
123
+ <tr>
124
+ <th>Date & Time</th>
125
+ <th>Type</th>
126
+ <th>Diagnosis</th>
127
+ <th>Medications</th>
128
+ <th>Vital Signs</th>
129
+ <th>Confidence</th>
130
+ <th>Actions</th>
131
+ </tr>
132
+ </thead>
133
+ <tbody id="emrTableBody">
134
+ <!-- EMR entries will be populated here -->
135
+ </tbody>
136
+ </table>
137
+ </div>
138
  </div>
139
 
140
+ <!-- Diagnosis Tab -->
141
+ <div class="tab-content" id="diagnosis-tab">
142
+ <div class="emr-section">
143
+ <h3>Diagnosis History</h3>
144
+ <div class="diagnosis-timeline" id="diagnosisTimeline">
145
+ <!-- Diagnosis entries will be populated here -->
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Medications Tab -->
151
+ <div class="tab-content" id="medications-tab">
152
+ <div class="emr-section">
153
+ <h3>Medication History</h3>
154
+ <div class="medications-grid" id="medicationsGrid">
155
+ <!-- Medication entries will be populated here -->
156
+ </div>
157
+ </div>
158
+ </div>
159
+
160
+ <!-- Vital Signs Tab -->
161
+ <div class="tab-content" id="vitals-tab">
162
+ <div class="emr-section">
163
+ <h3>Vital Signs Trends</h3>
164
+ <div class="vitals-chart-container">
165
+ <canvas id="vitalsChart" width="800" height="400"></canvas>
166
+ </div>
167
+ <div class="vitals-table-container">
168
+ <table class="vitals-table" id="vitalsTable">
169
+ <thead>
170
+ <tr>
171
+ <th>Date</th>
172
+ <th>Blood Pressure</th>
173
+ <th>Heart Rate</th>
174
+ <th>Temperature</th>
175
+ <th>Respiratory Rate</th>
176
+ <th>Oxygen Saturation</th>
177
+ </tr>
178
+ </thead>
179
+ <tbody id="vitalsTableBody">
180
+ <!-- Vital signs entries will be populated here -->
181
+ </tbody>
182
+ </table>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <!-- Lab Results Tab -->
188
+ <div class="tab-content" id="lab-tab">
189
+ <div class="emr-section">
190
+ <h3>Laboratory Results</h3>
191
+ <div class="lab-results-container" id="labResultsContainer">
192
+ <!-- Lab results will be populated here -->
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Procedures Tab -->
198
+ <div class="tab-content" id="procedures-tab">
199
+ <div class="emr-section">
200
+ <h3>Medical Procedures</h3>
201
+ <div class="procedures-timeline" id="proceduresTimeline">
202
+ <!-- Procedure entries will be populated here -->
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+
208
  <!-- Loading State -->
209
  <div class="loading-state" id="loadingState" style="display: none;">
210
  <div class="loading-spinner">
 
267
  </div>
268
  </div>
269
  </div>
270
+
271
+ <!-- Document Analysis Preview Modal -->
272
+ <div class="modal" id="documentPreviewModal">
273
+ <div class="modal-content document-preview-modal">
274
+ <div class="modal-header">
275
+ <h3><i class="fas fa-file-medical"></i> Document Analysis Preview</h3>
276
+ <button class="modal-close" id="documentPreviewModalClose">&times;</button>
277
+ </div>
278
+ <div class="modal-body" id="documentPreviewContent">
279
+ <!-- Document analysis results will be populated here -->
280
+ </div>
281
+ <div class="modal-footer">
282
+ <button class="btn btn-secondary" id="documentPreviewCancel">Cancel</button>
283
+ <button class="btn btn-primary" id="saveDocumentAnalysis">Save to EMR</button>
284
+ </div>
285
+ </div>
286
+ </div>
287
  </div>
288
 
289
  <script src="/static/js/emr.js"></script>
static/index.html CHANGED
@@ -93,16 +93,25 @@
93
  </div>
94
  <div class="message-content">
95
  <div class="message-text">
96
- <h3>πŸ‘‹ Welcome to Medical AI Assistant</h3>
 
 
 
 
 
 
 
97
  <p>I'm here to help you with medical questions, diagnosis assistance, and healthcare information. I can:</p>
98
- <ul>
99
- <li>πŸ” Answer medical questions and provide information</li>
100
- <li>πŸ“‹ Help with symptom analysis and differential diagnosis</li>
101
- <li>πŸ’Š Provide medication and treatment information</li>
102
- <li>πŸ“š Explain medical procedures and conditions</li>
103
- <li>⚠️ Offer general health advice (not medical diagnosis)</li>
104
- </ul>
105
- <p><strong>Important:</strong> This is for informational purposes only. Always consult with qualified healthcare professionals for medical advice.</p>
 
 
106
  <p>How can I assist you today?</p>
107
  </div>
108
  <div class="message-time">Just now</div>
 
93
  </div>
94
  <div class="message-content">
95
  <div class="message-text">
96
+ πŸ‘‹ Welcome to Medical AI Assistant
97
+
98
+ <div class="tagged-title">
99
+ <div class="tagged-title-bar"></div>
100
+ <div class="tagged-title-content">
101
+ <h1>Who am I, and what can I do?</h1>
102
+ </div>
103
+ </div>
104
  <p>I'm here to help you with medical questions, diagnosis assistance, and healthcare information. I can:</p>
105
+ <div class="tagged-title">
106
+ <div class="tagged-title-bar"></div>
107
+ <div class="tagged-title-content">
108
+ <h1>Key Features</h1>
109
+ </div>
110
+ </div>
111
+ <p><span class="blue-bubble">Medical Information:</span> I can provide evidence-based medical information and explanations.</p>
112
+ <p><span class="blue-bubble">Symptom Analysis:</span> I can help analyze symptoms and suggest possible conditions.</p>
113
+ <p><span class="blue-bubble">Treatment Guidance:</span> I can explain treatments, medications, and procedures.</p>
114
+ <p><span class="blue-bubble">Important:</span> This is for informational purposes only. Always consult with qualified healthcare professionals for medical advice.</p>
115
  <p>How can I assist you today?</p>
116
  </div>
117
  <div class="message-time">Just now</div>
static/js/app.js CHANGED
@@ -401,13 +401,27 @@ class MedicalChatbotApp {
401
 
402
  getWelcomeMessage() {
403
  return `πŸ‘‹ Welcome to Medical AI Assistant
 
 
 
404
  I'm here to help you with medical questions, diagnosis assistance, and healthcare information. I can:
 
405
  πŸ” Answer medical questions and provide information
406
  πŸ“‹ Help with symptom analysis and differential diagnosis
407
  πŸ’Š Provide medication and treatment information
408
  πŸ“š Explain medical procedures and conditions
409
  ⚠️ Offer general health advice (not medical diagnosis)
 
 
 
 
 
 
 
 
 
410
  **Important:** This is for informational purposes only. Always consult with qualified healthcare professionals for medical advice.
 
411
  How can I assist you today?`;
412
  }
413
 
@@ -1280,6 +1294,12 @@ How can I assist you today?`;
1280
  }
1281
 
1282
  updatePatientDisplay(patientId, patientName = null) {
 
 
 
 
 
 
1283
  const status = document.getElementById('patientStatus');
1284
  const actions = document.getElementById('patientActions');
1285
  const emrLink = document.getElementById('emrLink');
@@ -1860,15 +1880,20 @@ How can I assist you today?`;
1860
 
1861
  formatMessageContent(content) {
1862
  return content
1863
- // Handle headers (1-6 # symbols)
1864
  .replace(/^#{1,6}\s+(.+)$/gm, (match, text, offset, string) => {
1865
  const level = match.match(/^#+/)[0].length;
1866
- return `<h${level}>${text}</h${level}>`;
 
 
 
 
 
1867
  })
1868
- // Handle bold text
1869
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
1870
- // Handle italic text
1871
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
1872
  // Handle line breaks
1873
  .replace(/\n/g, '<br>')
1874
  // Handle emojis with colors
 
401
 
402
  getWelcomeMessage() {
403
  return `πŸ‘‹ Welcome to Medical AI Assistant
404
+
405
+ # Who am I, and what can I do?
406
+
407
  I'm here to help you with medical questions, diagnosis assistance, and healthcare information. I can:
408
+
409
  πŸ” Answer medical questions and provide information
410
  πŸ“‹ Help with symptom analysis and differential diagnosis
411
  πŸ’Š Provide medication and treatment information
412
  πŸ“š Explain medical procedures and conditions
413
  ⚠️ Offer general health advice (not medical diagnosis)
414
+
415
+ # Key Features
416
+
417
+ **Medical Information:** I can provide evidence-based medical information and explanations.
418
+
419
+ **Symptom Analysis:** I can help analyze symptoms and suggest possible conditions.
420
+
421
+ **Treatment Guidance:** I can explain treatments, medications, and procedures.
422
+
423
  **Important:** This is for informational purposes only. Always consult with qualified healthcare professionals for medical advice.
424
+
425
  How can I assist you today?`;
426
  }
427
 
 
1294
  }
1295
 
1296
  updatePatientDisplay(patientId, patientName = null) {
1297
+ // Safety check: don't update display if patientId is undefined or null
1298
+ if (!patientId || patientId === 'undefined' || patientId === 'null') {
1299
+ console.warn('updatePatientDisplay called with invalid patientId:', patientId);
1300
+ return;
1301
+ }
1302
+
1303
  const status = document.getElementById('patientStatus');
1304
  const actions = document.getElementById('patientActions');
1305
  const emrLink = document.getElementById('emrLink');
 
1880
 
1881
  formatMessageContent(content) {
1882
  return content
1883
+ // Handle headers (1-6 # symbols) - create tagged titles
1884
  .replace(/^#{1,6}\s+(.+)$/gm, (match, text, offset, string) => {
1885
  const level = match.match(/^#+/)[0].length;
1886
+ return `<div class="tagged-title">
1887
+ <div class="tagged-title-bar"></div>
1888
+ <div class="tagged-title-content">
1889
+ <h${level}>${text}</h${level}>
1890
+ </div>
1891
+ </div>`;
1892
  })
1893
+ // Handle bold text - create blue bubbles (improved regex to handle edge cases)
1894
+ .replace(/\*\*([^*]+)\*\*/g, '<span class="blue-bubble">$1</span>')
1895
+ // Handle italic text (only if not already processed as bold)
1896
+ .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>')
1897
  // Handle line breaks
1898
  .replace(/\n/g, '<br>')
1899
  // Handle emojis with colors
static/js/emr.js CHANGED
@@ -49,8 +49,18 @@ class EMRPage {
49
  this.applyFilters();
50
  });
51
 
 
 
 
 
 
 
 
52
  // Modal handlers
53
  this.setupModalHandlers();
 
 
 
54
  }
55
 
56
  setupModalHandlers() {
@@ -95,21 +105,100 @@ class EMRPage {
95
  searchModal.classList.remove('show');
96
  });
97
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  }
99
 
100
  async loadPatientFromURL() {
101
  const urlParams = new URLSearchParams(window.location.search);
102
  const patientId = urlParams.get('patient_id');
103
 
104
- if (patientId) {
 
105
  this.currentPatientId = patientId;
106
  await this.loadPatientInfo();
107
  } else {
108
  // Try to get from localStorage
109
  const savedPatientId = localStorage.getItem('medicalChatbotPatientId');
110
- if (savedPatientId) {
111
  this.currentPatientId = savedPatientId;
112
  await this.loadPatientInfo();
 
 
 
113
  }
114
  }
115
  }
@@ -556,6 +645,650 @@ class EMRPage {
556
  emptyState.style.display = 'block';
557
  tableContainer.style.display = 'none';
558
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  }
560
 
561
  // Initialize the EMR page when DOM is loaded
 
49
  this.applyFilters();
50
  });
51
 
52
+ // Tab buttons
53
+ document.querySelectorAll('.tab-btn').forEach(btn => {
54
+ btn.addEventListener('click', (e) => {
55
+ this.switchTab(e.target.dataset.tab);
56
+ });
57
+ });
58
+
59
  // Modal handlers
60
  this.setupModalHandlers();
61
+
62
+ // File upload handlers
63
+ this.setupFileUploadHandlers();
64
  }
65
 
66
  setupModalHandlers() {
 
105
  searchModal.classList.remove('show');
106
  });
107
  }
108
+
109
+ // Document Preview Modal
110
+ const documentPreviewModal = document.getElementById('documentPreviewModal');
111
+ const documentPreviewModalClose = document.getElementById('documentPreviewModalClose');
112
+ const documentPreviewCancel = document.getElementById('documentPreviewCancel');
113
+ const saveDocumentAnalysis = document.getElementById('saveDocumentAnalysis');
114
+
115
+ if (documentPreviewModalClose) {
116
+ documentPreviewModalClose.addEventListener('click', () => {
117
+ documentPreviewModal.classList.remove('show');
118
+ });
119
+ }
120
+
121
+ if (documentPreviewCancel) {
122
+ documentPreviewCancel.addEventListener('click', () => {
123
+ documentPreviewModal.classList.remove('show');
124
+ });
125
+ }
126
+
127
+ if (saveDocumentAnalysis) {
128
+ saveDocumentAnalysis.addEventListener('click', () => {
129
+ this.saveDocumentAnalysis();
130
+ });
131
+ }
132
+ }
133
+
134
+ setupFileUploadHandlers() {
135
+ const uploadArea = document.getElementById('uploadArea');
136
+ const fileInput = document.getElementById('fileInput');
137
+ const uploadBtn = document.getElementById('uploadBtn');
138
+ const uploadProgress = document.getElementById('uploadProgress');
139
+
140
+ // Click to upload
141
+ if (uploadBtn) {
142
+ uploadBtn.addEventListener('click', () => {
143
+ fileInput.click();
144
+ });
145
+ }
146
+
147
+ if (uploadArea) {
148
+ uploadArea.addEventListener('click', () => {
149
+ fileInput.click();
150
+ });
151
+ }
152
+
153
+ // File input change
154
+ if (fileInput) {
155
+ fileInput.addEventListener('change', (e) => {
156
+ if (e.target.files.length > 0) {
157
+ this.handleFileUpload(e.target.files);
158
+ }
159
+ });
160
+ }
161
+
162
+ // Drag and drop
163
+ if (uploadArea) {
164
+ uploadArea.addEventListener('dragover', (e) => {
165
+ e.preventDefault();
166
+ uploadArea.classList.add('dragover');
167
+ });
168
+
169
+ uploadArea.addEventListener('dragleave', (e) => {
170
+ e.preventDefault();
171
+ uploadArea.classList.remove('dragover');
172
+ });
173
+
174
+ uploadArea.addEventListener('drop', (e) => {
175
+ e.preventDefault();
176
+ uploadArea.classList.remove('dragover');
177
+
178
+ if (e.dataTransfer.files.length > 0) {
179
+ this.handleFileUpload(e.dataTransfer.files);
180
+ }
181
+ });
182
+ }
183
  }
184
 
185
  async loadPatientFromURL() {
186
  const urlParams = new URLSearchParams(window.location.search);
187
  const patientId = urlParams.get('patient_id');
188
 
189
+ // Check if patientId is valid (not undefined, null, or empty)
190
+ if (patientId && patientId !== 'undefined' && patientId !== 'null' && patientId.trim() !== '') {
191
  this.currentPatientId = patientId;
192
  await this.loadPatientInfo();
193
  } else {
194
  // Try to get from localStorage
195
  const savedPatientId = localStorage.getItem('medicalChatbotPatientId');
196
+ if (savedPatientId && savedPatientId !== 'undefined' && savedPatientId !== 'null' && savedPatientId.trim() !== '') {
197
  this.currentPatientId = savedPatientId;
198
  await this.loadPatientInfo();
199
+ } else {
200
+ console.warn('No valid patient ID found in URL or localStorage');
201
+ this.showEmptyState();
202
  }
203
  }
204
  }
 
645
  emptyState.style.display = 'block';
646
  tableContainer.style.display = 'none';
647
  }
648
+
649
+ switchTab(tabName) {
650
+ // Update tab buttons
651
+ document.querySelectorAll('.tab-btn').forEach(btn => {
652
+ btn.classList.remove('active');
653
+ });
654
+ document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
655
+
656
+ // Update tab content
657
+ document.querySelectorAll('.tab-content').forEach(content => {
658
+ content.classList.remove('active');
659
+ });
660
+ document.getElementById(`${tabName}-tab`).classList.add('active');
661
+
662
+ // Load specific tab data
663
+ switch (tabName) {
664
+ case 'diagnosis':
665
+ this.renderDiagnosisTab();
666
+ break;
667
+ case 'medications':
668
+ this.renderMedicationsTab();
669
+ break;
670
+ case 'vitals':
671
+ this.renderVitalsTab();
672
+ break;
673
+ case 'lab':
674
+ this.renderLabTab();
675
+ break;
676
+ case 'procedures':
677
+ this.renderProceduresTab();
678
+ break;
679
+ }
680
+ }
681
+
682
+ renderDiagnosisTab() {
683
+ const timeline = document.getElementById('diagnosisTimeline');
684
+ const diagnoses = [];
685
+
686
+ this.emrEntries.forEach(entry => {
687
+ if (entry.extracted_data.diagnosis && entry.extracted_data.diagnosis.length > 0) {
688
+ entry.extracted_data.diagnosis.forEach(diagnosis => {
689
+ diagnoses.push({
690
+ name: diagnosis,
691
+ date: new Date(entry.created_at).toLocaleDateString(),
692
+ confidence: Math.round(entry.confidence_score * 100)
693
+ });
694
+ });
695
+ }
696
+ });
697
+
698
+ if (diagnoses.length === 0) {
699
+ timeline.innerHTML = '<p class="no-data">No diagnoses found in EMR entries.</p>';
700
+ return;
701
+ }
702
+
703
+ timeline.innerHTML = diagnoses.map(diagnosis => `
704
+ <div class="diagnosis-item">
705
+ <div class="diagnosis-date">${diagnosis.date}</div>
706
+ <div class="diagnosis-name">${diagnosis.name}</div>
707
+ <div class="diagnosis-confidence">${diagnosis.confidence}%</div>
708
+ </div>
709
+ `).join('');
710
+ }
711
+
712
+ renderMedicationsTab() {
713
+ const grid = document.getElementById('medicationsGrid');
714
+ const medications = [];
715
+
716
+ this.emrEntries.forEach(entry => {
717
+ if (entry.extracted_data.medications && entry.extracted_data.medications.length > 0) {
718
+ entry.extracted_data.medications.forEach(med => {
719
+ medications.push({
720
+ name: med.name,
721
+ dosage: med.dosage || 'Not specified',
722
+ frequency: med.frequency || 'Not specified',
723
+ duration: med.duration || 'Not specified',
724
+ date: new Date(entry.created_at).toLocaleDateString()
725
+ });
726
+ });
727
+ }
728
+ });
729
+
730
+ if (medications.length === 0) {
731
+ grid.innerHTML = '<p class="no-data">No medications found in EMR entries.</p>';
732
+ return;
733
+ }
734
+
735
+ grid.innerHTML = medications.map(med => `
736
+ <div class="medication-card">
737
+ <div class="medication-name">${med.name}</div>
738
+ <div class="medication-details">
739
+ <div class="medication-detail">
740
+ <strong>Dosage:</strong> <span>${med.dosage}</span>
741
+ </div>
742
+ <div class="medication-detail">
743
+ <strong>Frequency:</strong> <span>${med.frequency}</span>
744
+ </div>
745
+ <div class="medication-detail">
746
+ <strong>Duration:</strong> <span>${med.duration}</span>
747
+ </div>
748
+ <div class="medication-detail">
749
+ <strong>Date:</strong> <span>${med.date}</span>
750
+ </div>
751
+ </div>
752
+ </div>
753
+ `).join('');
754
+ }
755
+
756
+ renderVitalsTab() {
757
+ const tableBody = document.getElementById('vitalsTableBody');
758
+ const vitalsData = [];
759
+
760
+ this.emrEntries.forEach(entry => {
761
+ if (entry.extracted_data.vital_signs) {
762
+ const vitals = entry.extracted_data.vital_signs;
763
+ if (Object.values(vitals).some(v => v)) {
764
+ vitalsData.push({
765
+ date: new Date(entry.created_at).toLocaleDateString(),
766
+ blood_pressure: vitals.blood_pressure || '-',
767
+ heart_rate: vitals.heart_rate || '-',
768
+ temperature: vitals.temperature || '-',
769
+ respiratory_rate: vitals.respiratory_rate || '-',
770
+ oxygen_saturation: vitals.oxygen_saturation || '-'
771
+ });
772
+ }
773
+ }
774
+ });
775
+
776
+ if (vitalsData.length === 0) {
777
+ tableBody.innerHTML = '<tr><td colspan="6" class="no-data">No vital signs found in EMR entries.</td></tr>';
778
+ return;
779
+ }
780
+
781
+ tableBody.innerHTML = vitalsData.map(vitals => `
782
+ <tr>
783
+ <td>${vitals.date}</td>
784
+ <td>${vitals.blood_pressure}</td>
785
+ <td>${vitals.heart_rate}</td>
786
+ <td>${vitals.temperature}</td>
787
+ <td>${vitals.respiratory_rate}</td>
788
+ <td>${vitals.oxygen_saturation}</td>
789
+ </tr>
790
+ `).join('');
791
+ }
792
+
793
+ renderLabTab() {
794
+ const container = document.getElementById('labResultsContainer');
795
+ const labResults = [];
796
+
797
+ this.emrEntries.forEach(entry => {
798
+ if (entry.extracted_data.lab_results && entry.extracted_data.lab_results.length > 0) {
799
+ entry.extracted_data.lab_results.forEach(lab => {
800
+ labResults.push({
801
+ test_name: lab.test_name,
802
+ value: lab.value,
803
+ unit: lab.unit || '',
804
+ reference_range: lab.reference_range || 'Not specified',
805
+ date: new Date(entry.created_at).toLocaleDateString()
806
+ });
807
+ });
808
+ }
809
+ });
810
+
811
+ if (labResults.length === 0) {
812
+ container.innerHTML = '<p class="no-data">No lab results found in EMR entries.</p>';
813
+ return;
814
+ }
815
+
816
+ container.innerHTML = labResults.map(lab => `
817
+ <div class="lab-result-item">
818
+ <div class="lab-result-header">
819
+ <div class="lab-test-name">${lab.test_name}</div>
820
+ <div class="lab-test-value">${lab.value} ${lab.unit}</div>
821
+ </div>
822
+ <div class="lab-test-details">
823
+ <span><strong>Date:</strong> ${lab.date}</span>
824
+ <span><strong>Reference Range:</strong> ${lab.reference_range}</span>
825
+ </div>
826
+ </div>
827
+ `).join('');
828
+ }
829
+
830
+ renderProceduresTab() {
831
+ const timeline = document.getElementById('proceduresTimeline');
832
+ const procedures = [];
833
+
834
+ this.emrEntries.forEach(entry => {
835
+ if (entry.extracted_data.procedures && entry.extracted_data.procedures.length > 0) {
836
+ entry.extracted_data.procedures.forEach(procedure => {
837
+ procedures.push({
838
+ name: procedure,
839
+ date: new Date(entry.created_at).toLocaleDateString(),
840
+ confidence: Math.round(entry.confidence_score * 100)
841
+ });
842
+ });
843
+ }
844
+ });
845
+
846
+ if (procedures.length === 0) {
847
+ timeline.innerHTML = '<p class="no-data">No procedures found in EMR entries.</p>';
848
+ return;
849
+ }
850
+
851
+ timeline.innerHTML = procedures.map(procedure => `
852
+ <div class="procedure-item">
853
+ <div class="procedure-date">${procedure.date}</div>
854
+ <div class="procedure-name">${procedure.name}</div>
855
+ <div class="procedure-status">${procedure.confidence}%</div>
856
+ </div>
857
+ `).join('');
858
+ }
859
+
860
+ async handleFileUpload(files) {
861
+ if (!this.currentPatientId) {
862
+ alert('Please select a patient first');
863
+ return;
864
+ }
865
+
866
+ const file = files[0]; // Handle only the first file for now
867
+
868
+ // Validate file type
869
+ const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'image/jpeg', 'image/jpg', 'image/png', 'image/tiff'];
870
+ if (!allowedTypes.includes(file.type)) {
871
+ alert('Unsupported file type. Please upload PDF, DOC, DOCX, JPG, PNG, or TIFF files.');
872
+ return;
873
+ }
874
+
875
+ // Validate file size (10MB limit)
876
+ if (file.size > 10 * 1024 * 1024) {
877
+ alert('File size exceeds 10MB limit.');
878
+ return;
879
+ }
880
+
881
+ this.showUploadProgress(true);
882
+
883
+ try {
884
+ // Create FormData
885
+ const formData = new FormData();
886
+ formData.append('patient_id', this.currentPatientId);
887
+ formData.append('file', file);
888
+
889
+ // Upload and analyze document
890
+ const response = await fetch('/emr/preview-document', {
891
+ method: 'POST',
892
+ body: formData
893
+ });
894
+
895
+ if (!response.ok) {
896
+ const error = await response.json();
897
+ throw new Error(error.detail || 'Failed to analyze document');
898
+ }
899
+
900
+ const result = await response.json();
901
+ this.showDocumentPreview(result);
902
+
903
+ } catch (error) {
904
+ console.error('Error uploading file:', error);
905
+ alert(`Error analyzing document: ${error.message}`);
906
+ } finally {
907
+ this.showUploadProgress(false);
908
+ }
909
+ }
910
+
911
+ showUploadProgress(show) {
912
+ const uploadProgress = document.getElementById('uploadProgress');
913
+ const uploadArea = document.getElementById('uploadArea');
914
+
915
+ if (show) {
916
+ uploadProgress.style.display = 'block';
917
+ uploadArea.style.display = 'none';
918
+ } else {
919
+ uploadProgress.style.display = 'none';
920
+ uploadArea.style.display = 'block';
921
+ }
922
+ }
923
+
924
+ showDocumentPreview(analysisResult) {
925
+ const modal = document.getElementById('documentPreviewModal');
926
+ const content = document.getElementById('documentPreviewContent');
927
+
928
+ // Store the analysis result for saving
929
+ this.currentDocumentAnalysis = analysisResult;
930
+
931
+ content.innerHTML = this.renderDocumentPreview(analysisResult);
932
+ modal.classList.add('show');
933
+ }
934
+
935
+ renderDocumentPreview(data) {
936
+ const { filename, confidence_score, extracted_data } = data;
937
+
938
+ return `
939
+ <div class="document-preview-section">
940
+ <h4><i class="fas fa-file"></i> Document Information</h4>
941
+ <p><strong>Filename:</strong> ${filename}</p>
942
+ <p><strong>Confidence Score:</strong> ${Math.round(confidence_score * 100)}%</p>
943
+ </div>
944
+
945
+ ${extracted_data.overview ? `
946
+ <div class="document-preview-section">
947
+ <h4><i class="fas fa-eye"></i> Overview</h4>
948
+ <textarea class="editable-field" id="overviewField" rows="3">${extracted_data.overview}</textarea>
949
+ </div>
950
+ ` : ''}
951
+
952
+ <div class="document-preview-section">
953
+ <h4><i class="fas fa-stethoscope"></i> Diagnoses</h4>
954
+ <ul class="editable-list" id="diagnosisList">
955
+ ${(extracted_data.diagnosis || []).map(diagnosis => `
956
+ <li>
957
+ <input type="text" value="${diagnosis}" class="diagnosis-input">
958
+ <button type="button" onclick="emrPage.removeListItem(this)"><i class="fas fa-trash"></i></button>
959
+ </li>
960
+ `).join('')}
961
+ </ul>
962
+ <button type="button" class="add-item-btn" onclick="emrPage.addDiagnosis()">
963
+ <i class="fas fa-plus"></i> Add Diagnosis
964
+ </button>
965
+ </div>
966
+
967
+ <div class="document-preview-section">
968
+ <h4><i class="fas fa-exclamation-triangle"></i> Symptoms</h4>
969
+ <ul class="editable-list" id="symptomsList">
970
+ ${(extracted_data.symptoms || []).map(symptom => `
971
+ <li>
972
+ <input type="text" value="${symptom}" class="symptom-input">
973
+ <button type="button" onclick="emrPage.removeListItem(this)"><i class="fas fa-trash"></i></button>
974
+ </li>
975
+ `).join('')}
976
+ </ul>
977
+ <button type="button" class="add-item-btn" onclick="emrPage.addSymptom()">
978
+ <i class="fas fa-plus"></i> Add Symptom
979
+ </button>
980
+ </div>
981
+
982
+ <div class="document-preview-section">
983
+ <h4><i class="fas fa-pills"></i> Medications</h4>
984
+ <div id="medicationsList">
985
+ ${(extracted_data.medications || []).map((med, index) => `
986
+ <div class="medication-preview-item">
987
+ <h5>Medication ${index + 1}</h5>
988
+ <div class="medication-detail-row">
989
+ <div>
990
+ <label>Name</label>
991
+ <input type="text" value="${med.name || ''}" class="med-name-input">
992
+ </div>
993
+ <div>
994
+ <label>Dosage</label>
995
+ <input type="text" value="${med.dosage || ''}" class="med-dosage-input">
996
+ </div>
997
+ </div>
998
+ <div class="medication-detail-row">
999
+ <div>
1000
+ <label>Frequency</label>
1001
+ <input type="text" value="${med.frequency || ''}" class="med-frequency-input">
1002
+ </div>
1003
+ <div>
1004
+ <label>Duration</label>
1005
+ <input type="text" value="${med.duration || ''}" class="med-duration-input">
1006
+ </div>
1007
+ </div>
1008
+ <button type="button" onclick="emrPage.removeMedication(this)" style="margin-top: 10px; background: #dc3545; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
1009
+ <i class="fas fa-trash"></i> Remove
1010
+ </button>
1011
+ </div>
1012
+ `).join('')}
1013
+ </div>
1014
+ <button type="button" class="add-item-btn" onclick="emrPage.addMedication()">
1015
+ <i class="fas fa-plus"></i> Add Medication
1016
+ </button>
1017
+ </div>
1018
+
1019
+ ${extracted_data.vital_signs ? `
1020
+ <div class="document-preview-section">
1021
+ <h4><i class="fas fa-heartbeat"></i> Vital Signs</h4>
1022
+ <div class="vital-signs-preview-grid">
1023
+ <div class="vital-sign-preview-item">
1024
+ <label>Blood Pressure</label>
1025
+ <input type="text" value="${extracted_data.vital_signs.blood_pressure || ''}" id="bpInput">
1026
+ </div>
1027
+ <div class="vital-sign-preview-item">
1028
+ <label>Heart Rate</label>
1029
+ <input type="text" value="${extracted_data.vital_signs.heart_rate || ''}" id="hrInput">
1030
+ </div>
1031
+ <div class="vital-sign-preview-item">
1032
+ <label>Temperature</label>
1033
+ <input type="text" value="${extracted_data.vital_signs.temperature || ''}" id="tempInput">
1034
+ </div>
1035
+ <div class="vital-sign-preview-item">
1036
+ <label>Respiratory Rate</label>
1037
+ <input type="text" value="${extracted_data.vital_signs.respiratory_rate || ''}" id="rrInput">
1038
+ </div>
1039
+ <div class="vital-sign-preview-item">
1040
+ <label>Oxygen Saturation</label>
1041
+ <input type="text" value="${extracted_data.vital_signs.oxygen_saturation || ''}" id="o2Input">
1042
+ </div>
1043
+ </div>
1044
+ </div>
1045
+ ` : ''}
1046
+
1047
+ <div class="document-preview-section">
1048
+ <h4><i class="fas fa-flask"></i> Lab Results</h4>
1049
+ <div id="labResultsList">
1050
+ ${(extracted_data.lab_results || []).map((lab, index) => `
1051
+ <div class="lab-result-preview-item">
1052
+ <h5>Lab Test ${index + 1}</h5>
1053
+ <div class="lab-result-detail-row">
1054
+ <div>
1055
+ <label>Test Name</label>
1056
+ <input type="text" value="${lab.test_name || ''}" class="lab-name-input">
1057
+ </div>
1058
+ <div>
1059
+ <label>Value</label>
1060
+ <input type="text" value="${lab.value || ''}" class="lab-value-input">
1061
+ </div>
1062
+ <div>
1063
+ <label>Unit</label>
1064
+ <input type="text" value="${lab.unit || ''}" class="lab-unit-input">
1065
+ </div>
1066
+ </div>
1067
+ <div class="lab-result-detail-row">
1068
+ <div>
1069
+ <label>Reference Range</label>
1070
+ <input type="text" value="${lab.reference_range || ''}" class="lab-range-input">
1071
+ </div>
1072
+ </div>
1073
+ <button type="button" onclick="emrPage.removeLabResult(this)" style="margin-top: 10px; background: #dc3545; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
1074
+ <i class="fas fa-trash"></i> Remove
1075
+ </button>
1076
+ </div>
1077
+ `).join('')}
1078
+ </div>
1079
+ <button type="button" class="add-item-btn" onclick="emrPage.addLabResult()">
1080
+ <i class="fas fa-plus"></i> Add Lab Result
1081
+ </button>
1082
+ </div>
1083
+
1084
+ <div class="document-preview-section">
1085
+ <h4><i class="fas fa-procedures"></i> Procedures</h4>
1086
+ <ul class="editable-list" id="proceduresList">
1087
+ ${(extracted_data.procedures || []).map(procedure => `
1088
+ <li>
1089
+ <input type="text" value="${procedure}" class="procedure-input">
1090
+ <button type="button" onclick="emrPage.removeListItem(this)"><i class="fas fa-trash"></i></button>
1091
+ </li>
1092
+ `).join('')}
1093
+ </ul>
1094
+ <button type="button" class="add-item-btn" onclick="emrPage.addProcedure()">
1095
+ <i class="fas fa-plus"></i> Add Procedure
1096
+ </button>
1097
+ </div>
1098
+
1099
+ <div class="document-preview-section">
1100
+ <h4><i class="fas fa-sticky-note"></i> Notes</h4>
1101
+ <textarea class="editable-field" id="notesField" rows="4">${extracted_data.notes || ''}</textarea>
1102
+ </div>
1103
+ `;
1104
+ }
1105
+
1106
+ addDiagnosis() {
1107
+ const list = document.getElementById('diagnosisList');
1108
+ const li = document.createElement('li');
1109
+ li.innerHTML = `
1110
+ <input type="text" value="" class="diagnosis-input" placeholder="Enter diagnosis">
1111
+ <button type="button" onclick="emrPage.removeListItem(this)"><i class="fas fa-trash"></i></button>
1112
+ `;
1113
+ list.appendChild(li);
1114
+ }
1115
+
1116
+ addSymptom() {
1117
+ const list = document.getElementById('symptomsList');
1118
+ const li = document.createElement('li');
1119
+ li.innerHTML = `
1120
+ <input type="text" value="" class="symptom-input" placeholder="Enter symptom">
1121
+ <button type="button" onclick="emrPage.removeListItem(this)"><i class="fas fa-trash"></i></button>
1122
+ `;
1123
+ list.appendChild(li);
1124
+ }
1125
+
1126
+ addMedication() {
1127
+ const list = document.getElementById('medicationsList');
1128
+ const index = list.children.length;
1129
+ const div = document.createElement('div');
1130
+ div.className = 'medication-preview-item';
1131
+ div.innerHTML = `
1132
+ <h5>Medication ${index + 1}</h5>
1133
+ <div class="medication-detail-row">
1134
+ <div>
1135
+ <label>Name</label>
1136
+ <input type="text" value="" class="med-name-input" placeholder="Medication name">
1137
+ </div>
1138
+ <div>
1139
+ <label>Dosage</label>
1140
+ <input type="text" value="" class="med-dosage-input" placeholder="Dosage">
1141
+ </div>
1142
+ </div>
1143
+ <div class="medication-detail-row">
1144
+ <div>
1145
+ <label>Frequency</label>
1146
+ <input type="text" value="" class="med-frequency-input" placeholder="Frequency">
1147
+ </div>
1148
+ <div>
1149
+ <label>Duration</label>
1150
+ <input type="text" value="" class="med-duration-input" placeholder="Duration">
1151
+ </div>
1152
+ </div>
1153
+ <button type="button" onclick="emrPage.removeMedication(this)" style="margin-top: 10px; background: #dc3545; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
1154
+ <i class="fas fa-trash"></i> Remove
1155
+ </button>
1156
+ `;
1157
+ list.appendChild(div);
1158
+ }
1159
+
1160
+ addLabResult() {
1161
+ const list = document.getElementById('labResultsList');
1162
+ const index = list.children.length;
1163
+ const div = document.createElement('div');
1164
+ div.className = 'lab-result-preview-item';
1165
+ div.innerHTML = `
1166
+ <h5>Lab Test ${index + 1}</h5>
1167
+ <div class="lab-result-detail-row">
1168
+ <div>
1169
+ <label>Test Name</label>
1170
+ <input type="text" value="" class="lab-name-input" placeholder="Test name">
1171
+ </div>
1172
+ <div>
1173
+ <label>Value</label>
1174
+ <input type="text" value="" class="lab-value-input" placeholder="Test value">
1175
+ </div>
1176
+ <div>
1177
+ <label>Unit</label>
1178
+ <input type="text" value="" class="lab-unit-input" placeholder="Unit">
1179
+ </div>
1180
+ </div>
1181
+ <div class="lab-result-detail-row">
1182
+ <div>
1183
+ <label>Reference Range</label>
1184
+ <input type="text" value="" class="lab-range-input" placeholder="Normal range">
1185
+ </div>
1186
+ </div>
1187
+ <button type="button" onclick="emrPage.removeLabResult(this)" style="margin-top: 10px; background: #dc3545; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
1188
+ <i class="fas fa-trash"></i> Remove
1189
+ </button>
1190
+ `;
1191
+ list.appendChild(div);
1192
+ }
1193
+
1194
+ addProcedure() {
1195
+ const list = document.getElementById('proceduresList');
1196
+ const li = document.createElement('li');
1197
+ li.innerHTML = `
1198
+ <input type="text" value="" class="procedure-input" placeholder="Enter procedure">
1199
+ <button type="button" onclick="emrPage.removeListItem(this)"><i class="fas fa-trash"></i></button>
1200
+ `;
1201
+ list.appendChild(li);
1202
+ }
1203
+
1204
+ removeListItem(button) {
1205
+ button.parentElement.remove();
1206
+ }
1207
+
1208
+ removeMedication(button) {
1209
+ button.parentElement.remove();
1210
+ }
1211
+
1212
+ removeLabResult(button) {
1213
+ button.parentElement.remove();
1214
+ }
1215
+
1216
+ async saveDocumentAnalysis() {
1217
+ if (!this.currentDocumentAnalysis) {
1218
+ alert('No document analysis to save');
1219
+ return;
1220
+ }
1221
+
1222
+ try {
1223
+ // Collect all the edited data
1224
+ const extractedData = this.collectEditedData();
1225
+
1226
+ const formData = new FormData();
1227
+ formData.append('patient_id', this.currentPatientId);
1228
+ formData.append('filename', this.currentDocumentAnalysis.filename);
1229
+ formData.append('extracted_data', JSON.stringify(extractedData));
1230
+ formData.append('confidence_score', this.currentDocumentAnalysis.confidence_score);
1231
+
1232
+ const response = await fetch('/emr/save-document-analysis', {
1233
+ method: 'POST',
1234
+ body: formData
1235
+ });
1236
+
1237
+ if (!response.ok) {
1238
+ const error = await response.json();
1239
+ throw new Error(error.detail || 'Failed to save document analysis');
1240
+ }
1241
+
1242
+ const result = await response.json();
1243
+
1244
+ // Close modal and refresh EMR data
1245
+ document.getElementById('documentPreviewModal').classList.remove('show');
1246
+ this.loadEMRData();
1247
+ this.loadPatientStats();
1248
+
1249
+ alert('Document analysis saved successfully!');
1250
+
1251
+ } catch (error) {
1252
+ console.error('Error saving document analysis:', error);
1253
+ alert(`Error saving document analysis: ${error.message}`);
1254
+ }
1255
+ }
1256
+
1257
+ collectEditedData() {
1258
+ const data = {
1259
+ overview: document.getElementById('overviewField')?.value || '',
1260
+ diagnosis: Array.from(document.querySelectorAll('.diagnosis-input')).map(input => input.value).filter(val => val.trim()),
1261
+ symptoms: Array.from(document.querySelectorAll('.symptom-input')).map(input => input.value).filter(val => val.trim()),
1262
+ medications: Array.from(document.querySelectorAll('.medication-preview-item')).map(item => ({
1263
+ name: item.querySelector('.med-name-input')?.value || '',
1264
+ dosage: item.querySelector('.med-dosage-input')?.value || '',
1265
+ frequency: item.querySelector('.med-frequency-input')?.value || '',
1266
+ duration: item.querySelector('.med-duration-input')?.value || ''
1267
+ })).filter(med => med.name.trim()),
1268
+ vital_signs: {
1269
+ blood_pressure: document.getElementById('bpInput')?.value || null,
1270
+ heart_rate: document.getElementById('hrInput')?.value || null,
1271
+ temperature: document.getElementById('tempInput')?.value || null,
1272
+ respiratory_rate: document.getElementById('rrInput')?.value || null,
1273
+ oxygen_saturation: document.getElementById('o2Input')?.value || null
1274
+ },
1275
+ lab_results: Array.from(document.querySelectorAll('.lab-result-preview-item')).map(item => ({
1276
+ test_name: item.querySelector('.lab-name-input')?.value || '',
1277
+ value: item.querySelector('.lab-value-input')?.value || '',
1278
+ unit: item.querySelector('.lab-unit-input')?.value || '',
1279
+ reference_range: item.querySelector('.lab-range-input')?.value || ''
1280
+ })).filter(lab => lab.test_name.trim()),
1281
+ procedures: Array.from(document.querySelectorAll('.procedure-input')).map(input => input.value).filter(val => val.trim()),
1282
+ notes: document.getElementById('notesField')?.value || ''
1283
+ };
1284
+
1285
+ // Clean up empty vital signs
1286
+ if (Object.values(data.vital_signs).every(val => !val)) {
1287
+ data.vital_signs = null;
1288
+ }
1289
+
1290
+ return data;
1291
+ }
1292
  }
1293
 
1294
  // Initialize the EMR page when DOM is loaded
static/patient.html CHANGED
@@ -28,7 +28,7 @@
28
  <option value="Intersex">Intersex</option>
29
  </select>
30
  </label>
31
- <label>Ethnicity<input type="ethnicity" id="ethnicity"></label>
32
  </div>
33
  <!-- Row 3: Contact Information -->
34
  <div class="row contact-info">
 
28
  <option value="Intersex">Intersex</option>
29
  </select>
30
  </label>
31
+ <label>Ethnicity<input type="text" id="ethnicity"></label>
32
  </div>
33
  <!-- Row 3: Contact Information -->
34
  <div class="row contact-info">
tests/{test_memory_manager.py β†’ core/test_memory_manager.py} RENAMED
File without changes
tests/{test_state.py β†’ core/test_state.py} RENAMED
File without changes
tests/{test_account.py β†’ models/test_account.py} RENAMED
@@ -10,7 +10,7 @@ from src.data.connection import ActionFailed, Collections, get_collection
10
  from src.data.repositories import account as account_repo
11
  from src.models.account import Account
12
  from src.utils.logger import logger
13
- from tests.base_test import BaseMongoTest
14
 
15
 
16
  class TestAccountRepository(BaseMongoTest):
 
10
  from src.data.repositories import account as account_repo
11
  from src.models.account import Account
12
  from src.utils.logger import logger
13
+ from ..base_test import BaseMongoTest
14
 
15
 
16
  class TestAccountRepository(BaseMongoTest):
tests/{test_patient.py β†’ models/test_patient.py} RENAMED
@@ -8,7 +8,7 @@ from src.data.connection import ActionFailed, Collections, get_collection
8
  from src.data.repositories import patient as patient_repo
9
  from src.models.patient import Patient
10
  from src.utils.logger import logger
11
- from tests.base_test import BaseMongoTest
12
 
13
 
14
  class TestPatientRepository(BaseMongoTest):
 
8
  from src.data.repositories import patient as patient_repo
9
  from src.models.patient import Patient
10
  from src.utils.logger import logger
11
+ from ..base_test import BaseMongoTest
12
 
13
 
14
  class TestPatientRepository(BaseMongoTest):
tests/{test_session.py β†’ models/test_session.py} RENAMED
@@ -10,7 +10,7 @@ from src.data.connection import ActionFailed, Collections, get_collection
10
  from src.data.repositories import session as session_repo
11
  from src.models.session import Message, Session
12
  from src.utils.logger import logger
13
- from tests.base_test import BaseMongoTest
14
 
15
 
16
  class TestSessionRepository(BaseMongoTest):
 
10
  from src.data.repositories import session as session_repo
11
  from src.models.session import Message, Session
12
  from src.utils.logger import logger
13
+ from ..base_test import BaseMongoTest
14
 
15
 
16
  class TestSessionRepository(BaseMongoTest):
tests/services/test_guard.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tests/test_guard.py
2
+
3
+ import pytest
4
+ import os
5
+ from unittest.mock import Mock, patch, MagicMock
6
+ from src.services.guard import SafetyGuard
7
+
8
+
9
+ class TestSafetyGuard:
10
+ """Test suite for SafetyGuard functionality."""
11
+
12
+ @pytest.fixture
13
+ def mock_settings(self):
14
+ """Mock settings for testing."""
15
+ with patch('src.services.guard.settings') as mock_settings:
16
+ mock_settings.SAFETY_GUARD_TIMEOUT = 30
17
+ mock_settings.SAFETY_GUARD_ENABLED = True
18
+ mock_settings.SAFETY_GUARD_FAIL_OPEN = True
19
+ yield mock_settings
20
+
21
+ @pytest.fixture
22
+ def mock_rotator(self):
23
+ """Mock NVIDIA rotator for testing."""
24
+ mock_rotator = Mock()
25
+ mock_rotator.get_key.return_value = "test_api_key"
26
+ mock_rotator.rotate.return_value = "test_api_key_2"
27
+ return mock_rotator
28
+
29
+ @pytest.fixture
30
+ def safety_guard(self, mock_settings, mock_rotator):
31
+ """Create SafetyGuard instance for testing."""
32
+ return SafetyGuard(mock_rotator)
33
+
34
+ def test_init_with_valid_config(self, mock_settings, mock_rotator):
35
+ """Test SafetyGuard initialization with valid configuration."""
36
+ guard = SafetyGuard(mock_rotator)
37
+ assert guard.nvidia_rotator == mock_rotator
38
+ assert guard.timeout_s == 30
39
+ assert guard.enabled is True
40
+ assert guard.fail_open is True
41
+
42
+ def test_init_without_api_key(self, mock_settings):
43
+ """Test SafetyGuard initialization without API key."""
44
+ mock_rotator = Mock()
45
+ mock_rotator.get_key.return_value = None
46
+ with pytest.raises(ValueError, match="No NVIDIA API keys found"):
47
+ SafetyGuard(mock_rotator)
48
+
49
+ def test_chunk_text_empty(self, safety_guard):
50
+ """Test text chunking with empty text."""
51
+ result = safety_guard._chunk_text("")
52
+ assert result == [""]
53
+
54
+ def test_chunk_text_short(self, safety_guard):
55
+ """Test text chunking with short text."""
56
+ text = "Short text"
57
+ result = safety_guard._chunk_text(text)
58
+ assert result == [text]
59
+
60
+ def test_chunk_text_long(self, safety_guard):
61
+ """Test text chunking with long text."""
62
+ text = "a" * 5000 # Longer than default chunk size
63
+ result = safety_guard._chunk_text(text)
64
+ assert len(result) > 1
65
+ assert all(len(chunk) <= 2800 for chunk in result)
66
+
67
+ def test_parse_guard_reply_safe(self, safety_guard):
68
+ """Test parsing safe guard reply."""
69
+ is_safe, reason = safety_guard._parse_guard_reply("SAFE")
70
+ assert is_safe is True
71
+ assert reason == ""
72
+
73
+ def test_parse_guard_reply_unsafe(self, safety_guard):
74
+ """Test parsing unsafe guard reply."""
75
+ is_safe, reason = safety_guard._parse_guard_reply("UNSAFE: contains harmful content")
76
+ assert is_safe is False
77
+ assert reason == "contains harmful content"
78
+
79
+ def test_parse_guard_reply_empty(self, safety_guard):
80
+ """Test parsing empty guard reply."""
81
+ is_safe, reason = safety_guard._parse_guard_reply("")
82
+ assert is_safe is True
83
+ assert reason == "guard_unavailable"
84
+
85
+ def test_parse_guard_reply_unknown(self, safety_guard):
86
+ """Test parsing unknown guard reply."""
87
+ is_safe, reason = safety_guard._parse_guard_reply("UNKNOWN_RESPONSE")
88
+ assert is_safe is False
89
+ assert len(reason) <= 180
90
+
91
+ def test_is_medical_query_symptoms(self, safety_guard):
92
+ """Test medical query detection for symptoms."""
93
+ queries = [
94
+ "I have a headache",
95
+ "My chest hurts",
96
+ "I'm experiencing nausea",
97
+ "I have a fever"
98
+ ]
99
+ for query in queries:
100
+ assert safety_guard._is_medical_query(query) is True
101
+
102
+ def test_is_medical_query_conditions(self, safety_guard):
103
+ """Test medical query detection for conditions."""
104
+ queries = [
105
+ "What is diabetes?",
106
+ "How to treat hypertension",
107
+ "Symptoms of depression",
108
+ "Cancer treatment options"
109
+ ]
110
+ for query in queries:
111
+ assert safety_guard._is_medical_query(query) is True
112
+
113
+ def test_is_medical_query_treatments(self, safety_guard):
114
+ """Test medical query detection for treatments."""
115
+ queries = [
116
+ "What medication should I take?",
117
+ "Physical therapy exercises",
118
+ "Surgery options",
119
+ "Vaccination schedule"
120
+ ]
121
+ for query in queries:
122
+ assert safety_guard._is_medical_query(query) is True
123
+
124
+ def test_is_medical_query_non_medical(self, safety_guard):
125
+ """Test medical query detection for non-medical content."""
126
+ queries = [
127
+ "What's the weather like?",
128
+ "How to cook pasta",
129
+ "Best restaurants in town",
130
+ "Python programming tutorial"
131
+ ]
132
+ for query in queries:
133
+ assert safety_guard._is_medical_query(query) is False
134
+
135
+ def test_detect_harmful_content_safe_medical(self, safety_guard):
136
+ """Test harmful content detection for safe medical content."""
137
+ text = "I have a headache and need advice on pain management"
138
+ is_safe, reason = safety_guard._detect_harmful_content(text)
139
+ assert is_safe is True
140
+ assert reason == "medical_content"
141
+
142
+ def test_detect_harmful_content_dangerous_medical(self, safety_guard):
143
+ """Test harmful content detection for dangerous medical content."""
144
+ text = "You should overdose on your medication"
145
+ is_safe, reason = safety_guard._detect_harmful_content(text)
146
+ assert is_safe is False
147
+ assert "dangerous_medical_advice" in reason
148
+
149
+ def test_detect_harmful_content_suicide(self, safety_guard):
150
+ """Test harmful content detection for suicide-related content."""
151
+ text = "How to kill yourself"
152
+ is_safe, reason = safety_guard._detect_harmful_content(text)
153
+ assert is_safe is False
154
+ assert "suicide_self_harm" in reason
155
+
156
+ def test_detect_harmful_content_violence(self, safety_guard):
157
+ """Test harmful content detection for violent content."""
158
+ text = "How to harm others with weapons"
159
+ is_safe, reason = safety_guard._detect_harmful_content(text)
160
+ assert is_safe is False
161
+ assert "violence" in reason
162
+
163
+ def test_assess_risk_level_medical_low(self, safety_guard):
164
+ """Test risk assessment for low-risk medical content."""
165
+ text = "I have a mild headache"
166
+ risk_level, risk_score = safety_guard._assess_risk_level(text)
167
+ assert risk_level == "low"
168
+ assert 0.0 <= risk_score <= 1.0
169
+
170
+ def test_assess_risk_level_medical_high(self, safety_guard):
171
+ """Test risk assessment for high-risk medical content."""
172
+ text = "You should commit suicide and overdose on drugs"
173
+ risk_level, risk_score = safety_guard._assess_risk_level(text)
174
+ assert risk_level == "high"
175
+ assert risk_score > 0.6
176
+
177
+ def test_assess_risk_level_non_medical(self, safety_guard):
178
+ """Test risk assessment for non-medical content."""
179
+ text = "This is just a normal conversation"
180
+ risk_level, risk_score = safety_guard._assess_risk_level(text)
181
+ assert risk_level == "low"
182
+ assert 0.0 <= risk_score <= 1.0
183
+
184
+ @patch('src.services.guard.requests.post')
185
+ def test_call_guard_success(self, mock_post, safety_guard):
186
+ """Test successful guard API call."""
187
+ mock_response = Mock()
188
+ mock_response.status_code = 200
189
+ mock_response.json.return_value = {
190
+ "choices": [{"message": {"content": "SAFE"}}]
191
+ }
192
+ mock_post.return_value = mock_response
193
+
194
+ messages = [{"role": "user", "content": "test message"}]
195
+ result = safety_guard._call_guard(messages)
196
+ assert result == "SAFE"
197
+
198
+ @patch('src.services.guard.requests.post')
199
+ def test_call_guard_failure(self, mock_post, safety_guard):
200
+ """Test guard API call failure."""
201
+ mock_response = Mock()
202
+ mock_response.status_code = 400
203
+ mock_response.text = "Bad Request"
204
+ mock_post.return_value = mock_response
205
+
206
+ messages = [{"role": "user", "content": "test message"}]
207
+ result = safety_guard._call_guard(messages)
208
+ assert result == ""
209
+
210
+ def test_check_user_query_disabled(self, mock_settings):
211
+ """Test user query check when guard is disabled."""
212
+ mock_settings.SAFETY_GUARD_ENABLED = False
213
+ guard = SafetyGuard()
214
+
215
+ is_safe, reason = guard.check_user_query("any query")
216
+ assert is_safe is True
217
+ assert reason == "guard_disabled"
218
+
219
+ def test_check_user_query_medical(self, safety_guard):
220
+ """Test user query check for medical content."""
221
+ with patch.object(safety_guard, '_call_guard') as mock_call:
222
+ is_safe, reason = safety_guard.check_user_query("I have a headache")
223
+ assert is_safe is True
224
+ assert reason == "medical_query"
225
+ mock_call.assert_not_called()
226
+
227
+ def test_check_user_query_non_medical_safe(self, safety_guard):
228
+ """Test user query check for safe non-medical content."""
229
+ with patch.object(safety_guard, '_call_guard') as mock_call, \
230
+ patch.object(safety_guard, '_parse_guard_reply') as mock_parse:
231
+ mock_call.return_value = "SAFE"
232
+ mock_parse.return_value = (True, "")
233
+
234
+ is_safe, reason = safety_guard.check_user_query("What's the weather?")
235
+ assert is_safe is True
236
+ assert reason == ""
237
+
238
+ def test_check_user_query_non_medical_unsafe(self, safety_guard):
239
+ """Test user query check for unsafe non-medical content."""
240
+ with patch.object(safety_guard, '_call_guard') as mock_call, \
241
+ patch.object(safety_guard, '_parse_guard_reply') as mock_parse:
242
+ mock_call.return_value = "UNSAFE: harmful content"
243
+ mock_parse.return_value = (False, "harmful content")
244
+
245
+ is_safe, reason = safety_guard.check_user_query("How to harm others")
246
+ assert is_safe is False
247
+ assert reason == "harmful content"
248
+
249
+ def test_check_model_answer_disabled(self, mock_settings):
250
+ """Test model answer check when guard is disabled."""
251
+ mock_settings.SAFETY_GUARD_ENABLED = False
252
+ guard = SafetyGuard()
253
+
254
+ is_safe, reason = guard.check_model_answer("query", "answer")
255
+ assert is_safe is True
256
+ assert reason == "guard_disabled"
257
+
258
+ def test_check_model_answer_medical_safe(self, safety_guard):
259
+ """Test model answer check for safe medical content."""
260
+ is_safe, reason = safety_guard.check_model_answer(
261
+ "I have a headache",
262
+ "You should rest and drink water"
263
+ )
264
+ assert is_safe is True
265
+ assert reason == "medical_content"
266
+
267
+ def test_check_model_answer_medical_unsafe(self, safety_guard):
268
+ """Test model answer check for unsafe medical content."""
269
+ is_safe, reason = safety_guard.check_model_answer(
270
+ "I have a headache",
271
+ "You should overdose on medication"
272
+ )
273
+ assert is_safe is False
274
+ assert "dangerous medical advice" in reason
275
+
276
+ def test_check_model_answer_high_risk(self, safety_guard):
277
+ """Test model answer check for high-risk content."""
278
+ with patch.object(safety_guard, '_call_guard') as mock_call, \
279
+ patch.object(safety_guard, '_parse_guard_reply') as mock_parse:
280
+ mock_call.return_value = "SAFE"
281
+ mock_parse.return_value = (True, "")
282
+
283
+ is_safe, reason = safety_guard.check_model_answer(
284
+ "How to harm others",
285
+ "You should use weapons to attack people"
286
+ )
287
+ assert is_safe is True
288
+ assert reason == "high_risk_validated"
289
+
290
+ def test_enhance_messages_with_context_medical(self, safety_guard):
291
+ """Test message enhancement for medical content."""
292
+ messages = [{"role": "user", "content": "I have a headache"}]
293
+ enhanced = safety_guard._enhance_messages_with_context(messages)
294
+
295
+ assert len(enhanced) == 1
296
+ assert "MEDICAL CONTEXT" in enhanced[0]["content"]
297
+ assert "I have a headache" in enhanced[0]["content"]
298
+
299
+ def test_enhance_messages_with_context_non_medical(self, safety_guard):
300
+ """Test message enhancement for non-medical content."""
301
+ messages = [{"role": "user", "content": "What's the weather?"}]
302
+ enhanced = safety_guard._enhance_messages_with_context(messages)
303
+
304
+ assert enhanced == messages # Should remain unchanged
305
+
306
+ def test_global_safety_guard_instance(self):
307
+ """Test that global safety guard instance is None by default."""
308
+ from src.services.guard import safety_guard
309
+ assert safety_guard is None
310
+
311
+
312
+ if __name__ == "__main__":
313
+ pytest.main([__file__])
tests/{test_medical_memory.py β†’ services/test_medical_memory.py} RENAMED
@@ -8,7 +8,7 @@ from src.data.connection import ActionFailed, Collections
8
  from src.data.repositories import medical_memory as medical_memory_repo
9
  from src.models.medical import MedicalMemory, SemanticSearchResult
10
  from src.utils.logger import logger
11
- from tests.base_test import BaseMongoTest
12
 
13
 
14
  class TestMedicalMemoryRepository(BaseMongoTest):
 
8
  from src.data.repositories import medical_memory as medical_memory_repo
9
  from src.models.medical import MedicalMemory, SemanticSearchResult
10
  from src.utils.logger import logger
11
+ from ..base_test import BaseMongoTest
12
 
13
 
14
  class TestMedicalMemoryRepository(BaseMongoTest):
tests/{test_medical_record.py β†’ services/test_medical_record.py} RENAMED
@@ -9,7 +9,7 @@ from src.data.connection import ActionFailed, Collections
9
  from src.data.repositories import medical_record as medical_record_repo
10
  from src.models.medical import MedicalRecord
11
  from src.utils.logger import logger
12
- from tests.base_test import BaseMongoTest
13
 
14
 
15
  class TestMedicalRecordRepository(BaseMongoTest):
 
9
  from src.data.repositories import medical_record as medical_record_repo
10
  from src.models.medical import MedicalRecord
11
  from src.utils.logger import logger
12
+ from ..base_test import BaseMongoTest
13
 
14
 
15
  class TestMedicalRecordRepository(BaseMongoTest):
tests/{test_utils.py β†’ utils/test_utils.py} RENAMED
@@ -7,7 +7,7 @@ from pymongo.errors import ConnectionFailure
7
  from src.data import utils as db_utils
8
  from src.data.connection import ActionFailed, Collections, get_collection
9
  from src.utils.logger import logger
10
- from tests.base_test import BaseMongoTest
11
 
12
 
13
  class TestDatabaseUtils(BaseMongoTest):
 
7
  from src.data import utils as db_utils
8
  from src.data.connection import ActionFailed, Collections, get_collection
9
  from src.utils.logger import logger
10
+ from ..base_test import BaseMongoTest
11
 
12
 
13
  class TestDatabaseUtils(BaseMongoTest):