Spaces:
Sleeping
Sleeping
teest
Browse files- analysis.py +50 -3
- api/__init__.py +148 -150
- api/routes/txagent.py +11 -1
- models/entities.py +57 -8
- routes/patients.py +93 -14
- utils.py +10 -1
analysis.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from typing import Optional, Tuple, List
|
| 2 |
from enum import Enum
|
| 3 |
from config import agent, patients_collection, analysis_collection, alerts_collection, logger
|
|
|
|
| 4 |
from models import RiskLevel
|
| 5 |
from utils import (
|
| 6 |
structure_medical_response,
|
|
@@ -26,6 +27,10 @@ class NotificationStatus(str, Enum):
|
|
| 26 |
|
| 27 |
async def create_alert(patient_id: str, risk_data: dict):
|
| 28 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
alert_doc = {
|
| 30 |
"patient_id": patient_id,
|
| 31 |
"type": "suicide_risk",
|
|
@@ -38,7 +43,7 @@ async def create_alert(patient_id: str, risk_data: dict):
|
|
| 38 |
"type": "risk_alert",
|
| 39 |
"status": "unread",
|
| 40 |
"title": f"Suicide Risk: {risk_data['level'].capitalize()}",
|
| 41 |
-
"message": f"Patient {patient_id} shows {risk_data['level']} risk factors",
|
| 42 |
"icon": "⚠️",
|
| 43 |
"action_url": f"/patient/{patient_id}/risk-assessment",
|
| 44 |
"priority": "high" if risk_data["level"] in ["high", "severe"] else "medium"
|
|
@@ -47,10 +52,43 @@ async def create_alert(patient_id: str, risk_data: dict):
|
|
| 47 |
|
| 48 |
await alerts_collection.insert_one(alert_doc)
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
# Simplified WebSocket notification - remove Hugging Face specific code
|
| 51 |
await broadcast_notification(alert_doc["notification"])
|
| 52 |
|
| 53 |
-
logger.warning(f"⚠️ Created suicide risk alert for patient {patient_id}")
|
| 54 |
return alert_doc
|
| 55 |
except Exception as e:
|
| 56 |
logger.error(f"Failed to create alert: {str(e)}")
|
|
@@ -164,7 +202,16 @@ async def analyze_patient(patient: dict):
|
|
| 164 |
return
|
| 165 |
|
| 166 |
# Generate analysis
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
message = (
|
| 169 |
"You are a clinical decision support AI.\n\n"
|
| 170 |
"Given the patient document below:\n"
|
|
|
|
| 1 |
from typing import Optional, Tuple, List
|
| 2 |
from enum import Enum
|
| 3 |
from config import agent, patients_collection, analysis_collection, alerts_collection, logger
|
| 4 |
+
from db.mongo import users_collection, notifications_collection
|
| 5 |
from models import RiskLevel
|
| 6 |
from utils import (
|
| 7 |
structure_medical_response,
|
|
|
|
| 27 |
|
| 28 |
async def create_alert(patient_id: str, risk_data: dict):
|
| 29 |
try:
|
| 30 |
+
# Get patient information for better notification message
|
| 31 |
+
patient = await patients_collection.find_one({"fhir_id": patient_id})
|
| 32 |
+
patient_name = patient.get("full_name", "Unknown Patient") if patient else "Unknown Patient"
|
| 33 |
+
|
| 34 |
alert_doc = {
|
| 35 |
"patient_id": patient_id,
|
| 36 |
"type": "suicide_risk",
|
|
|
|
| 43 |
"type": "risk_alert",
|
| 44 |
"status": "unread",
|
| 45 |
"title": f"Suicide Risk: {risk_data['level'].capitalize()}",
|
| 46 |
+
"message": f"Patient {patient_name} ({patient_id}) shows {risk_data['level']} risk factors",
|
| 47 |
"icon": "⚠️",
|
| 48 |
"action_url": f"/patient/{patient_id}/risk-assessment",
|
| 49 |
"priority": "high" if risk_data["level"] in ["high", "severe"] else "medium"
|
|
|
|
| 52 |
|
| 53 |
await alerts_collection.insert_one(alert_doc)
|
| 54 |
|
| 55 |
+
# Create notifications for all doctors and admins when risk is high or moderate
|
| 56 |
+
if risk_data["level"] in ["high", "moderate", "severe"]:
|
| 57 |
+
# Get all users with doctor or admin roles
|
| 58 |
+
doctors_and_admins = await users_collection.find({
|
| 59 |
+
"roles": {"$in": ["doctor", "admin"]}
|
| 60 |
+
}).to_list(length=None)
|
| 61 |
+
|
| 62 |
+
logger.info(f"📧 Creating notifications for {len(doctors_and_admins)} doctors/admins")
|
| 63 |
+
|
| 64 |
+
# Create individual notifications for each doctor/admin
|
| 65 |
+
notifications = []
|
| 66 |
+
for user in doctors_and_admins:
|
| 67 |
+
notification = {
|
| 68 |
+
"user_id": user["email"],
|
| 69 |
+
"patient_id": patient_id,
|
| 70 |
+
"message": f"🚨 {risk_data['level'].upper()} suicide risk detected for patient {patient_name}",
|
| 71 |
+
"timestamp": datetime.utcnow(),
|
| 72 |
+
"severity": "high" if risk_data["level"] in ["high", "severe"] else "medium",
|
| 73 |
+
"read": False,
|
| 74 |
+
"type": "suicide_risk_alert",
|
| 75 |
+
"risk_level": risk_data["level"],
|
| 76 |
+
"risk_score": risk_data["score"],
|
| 77 |
+
"patient_name": patient_name
|
| 78 |
+
}
|
| 79 |
+
notifications.append(notification)
|
| 80 |
+
|
| 81 |
+
if notifications:
|
| 82 |
+
# Insert all notifications at once
|
| 83 |
+
result = await notifications_collection.insert_many(notifications)
|
| 84 |
+
logger.info(f"✅ Created {len(result.inserted_ids)} notifications for risk alert")
|
| 85 |
+
else:
|
| 86 |
+
logger.warning("⚠️ No doctors or admins found to notify")
|
| 87 |
+
|
| 88 |
# Simplified WebSocket notification - remove Hugging Face specific code
|
| 89 |
await broadcast_notification(alert_doc["notification"])
|
| 90 |
|
| 91 |
+
logger.warning(f"⚠️ Created suicide risk alert for patient {patient_id} ({patient_name}) - Level: {risk_data['level']}")
|
| 92 |
return alert_doc
|
| 93 |
except Exception as e:
|
| 94 |
logger.error(f"Failed to create alert: {str(e)}")
|
|
|
|
| 202 |
return
|
| 203 |
|
| 204 |
# Generate analysis
|
| 205 |
+
# Custom JSON encoder to handle datetime objects
|
| 206 |
+
class DateTimeEncoder(json.JSONEncoder):
|
| 207 |
+
def default(self, obj):
|
| 208 |
+
if isinstance(obj, datetime):
|
| 209 |
+
return obj.isoformat()
|
| 210 |
+
elif hasattr(obj, '__dict__'):
|
| 211 |
+
return obj.__dict__
|
| 212 |
+
return super().default(obj)
|
| 213 |
+
|
| 214 |
+
doc = json.dumps(serialized, indent=2, cls=DateTimeEncoder)
|
| 215 |
message = (
|
| 216 |
"You are a clinical decision support AI.\n\n"
|
| 217 |
"Given the patient document below:\n"
|
api/__init__.py
CHANGED
|
@@ -17,7 +17,7 @@ def get_router():
|
|
| 17 |
from .routes.pdf import pdf
|
| 18 |
from .routes.appointments import router as appointments
|
| 19 |
from .routes.messaging import router as messaging
|
| 20 |
-
from .routes.txagent import router as txagent, get_txagent, ChatRequest
|
| 21 |
from fastapi import Depends, HTTPException, UploadFile, File
|
| 22 |
from core.security import get_current_user
|
| 23 |
import asyncio
|
|
@@ -31,161 +31,159 @@ def get_router():
|
|
| 31 |
router.include_router(pdf, prefix="/patients", tags=["pdf"]) # Keep prefix for PDF routes
|
| 32 |
router.include_router(appointments, prefix="/appointments", tags=["appointments"])
|
| 33 |
router.include_router(messaging, tags=["messaging"])
|
| 34 |
-
router.include_router(txagent, tags=["txagent"])
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
@router.post("/chat-stream")
|
| 38 |
-
async def chat_stream(
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
):
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
|
| 85 |
-
#
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
raise HTTPException(status_code=500, detail="Error generating voice output")
|
| 131 |
|
| 132 |
-
#
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
)
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
raise HTTPException(status_code=500, detail="Error analyzing report")
|
| 189 |
|
| 190 |
return router
|
| 191 |
|
|
|
|
| 17 |
from .routes.pdf import pdf
|
| 18 |
from .routes.appointments import router as appointments
|
| 19 |
from .routes.messaging import router as messaging
|
| 20 |
+
# from .routes.txagent import router as txagent, get_txagent, ChatRequest # Commented out - using separate TxAgent API
|
| 21 |
from fastapi import Depends, HTTPException, UploadFile, File
|
| 22 |
from core.security import get_current_user
|
| 23 |
import asyncio
|
|
|
|
| 31 |
router.include_router(pdf, prefix="/patients", tags=["pdf"]) # Keep prefix for PDF routes
|
| 32 |
router.include_router(appointments, prefix="/appointments", tags=["appointments"])
|
| 33 |
router.include_router(messaging, tags=["messaging"])
|
| 34 |
+
# router.include_router(txagent, tags=["txagent"]) # Commented out - using separate TxAgent API
|
| 35 |
|
| 36 |
+
# TxAgent endpoints commented out - using separate TxAgent API
|
| 37 |
+
# @router.post("/chat-stream")
|
| 38 |
+
# async def chat_stream(
|
| 39 |
+
# request: ChatRequest,
|
| 40 |
+
# current_user: dict = Depends(get_current_user)
|
| 41 |
+
# ):
|
| 42 |
+
# """Streaming chat endpoint for TxAgent"""
|
| 43 |
+
# import logging
|
| 44 |
+
# logger = logging.getLogger(__name__)
|
| 45 |
+
#
|
| 46 |
+
# try:
|
| 47 |
+
# if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
|
| 48 |
+
# raise HTTPException(status_code=403, detail="Only doctors and admins can use TxAgent")
|
| 49 |
|
| 50 |
+
# # Get TxAgent instance
|
| 51 |
+
# txagent = get_txagent()
|
| 52 |
+
#
|
| 53 |
+
# if txagent is None:
|
| 54 |
+
# # Fallback response if TxAgent is not available
|
| 55 |
+
# response_text = f"TxAgent is currently unavailable. Please try again later. Your message was: {request.message}"
|
| 56 |
+
# else:
|
| 57 |
+
# # Get real response from TxAgent
|
| 58 |
+
# try:
|
| 59 |
+
# response_text = txagent.chat(
|
| 60 |
+
# message=request.message,
|
| 61 |
+
# temperature=request.temperature,
|
| 62 |
+
# max_new_tokens=request.max_new_tokens
|
| 63 |
+
# )
|
| 64 |
+
# except Exception as e:
|
| 65 |
+
# logger.error(f"TxAgent chat failed: {e}")
|
| 66 |
+
# response_text = f"Sorry, I encountered an error processing your request: {request.message}. Please try again."
|
| 67 |
|
| 68 |
+
# # Return streaming text without data: prefix
|
| 69 |
+
# async def generate():
|
| 70 |
+
# # Send the complete response as plain text
|
| 71 |
+
# yield response_text
|
| 72 |
|
| 73 |
+
# return StreamingResponse(
|
| 74 |
+
# generate(),
|
| 75 |
+
# media_type="text/plain",
|
| 76 |
+
# headers={
|
| 77 |
+
# "Cache-Control": "no-cache",
|
| 78 |
+
# "Connection": "keep-alive"
|
| 79 |
+
# }
|
| 80 |
+
# )
|
| 81 |
+
# except Exception as e:
|
| 82 |
+
# logger.error(f"Error in chat stream: {e}")
|
| 83 |
+
# raise HTTPException(status_code=500, detail="Failed to process chat stream")
|
| 84 |
|
| 85 |
+
# @router.post("/voice/synthesize")
|
| 86 |
+
# async def voice_synthesize_root(
|
| 87 |
+
# request: dict,
|
| 88 |
+
# current_user: dict = Depends(get_current_user)
|
| 89 |
+
# ):
|
| 90 |
+
# """Voice synthesis endpoint at root level for frontend compatibility"""
|
| 91 |
+
# try:
|
| 92 |
+
# if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
|
| 93 |
+
# raise HTTPException(status_code=403, detail="Only doctors and admins can use voice features")
|
| 94 |
+
#
|
| 95 |
+
# logger.info(f"Voice synthesis initiated by {current_user['email']}")
|
| 96 |
+
#
|
| 97 |
+
# # Extract parameters from request
|
| 98 |
+
# text = request.get('text', '')
|
| 99 |
+
# language = request.get('language', 'en-US')
|
| 100 |
+
# return_format = request.get('return_format', 'mp3')
|
| 101 |
+
#
|
| 102 |
+
# if not text:
|
| 103 |
+
# raise HTTPException(status_code=400, detail="Text is required")
|
| 104 |
+
#
|
| 105 |
+
# # Import voice function
|
| 106 |
+
# try:
|
| 107 |
+
# from voice import text_to_speech
|
| 108 |
+
# except ImportError:
|
| 109 |
+
# # Fallback if voice module is not available
|
| 110 |
+
# raise HTTPException(status_code=500, detail="Voice synthesis not available")
|
| 111 |
+
#
|
| 112 |
+
# # Convert language code for gTTS (e.g., 'en-US' -> 'en')
|
| 113 |
+
# language_code = language.split('-')[0] if '-' in language else language
|
| 114 |
+
#
|
| 115 |
+
# # Generate speech
|
| 116 |
+
# audio_data = text_to_speech(text, language=language_code)
|
| 117 |
+
#
|
| 118 |
+
# # Return audio data
|
| 119 |
+
# return StreamingResponse(
|
| 120 |
+
# io.BytesIO(audio_data),
|
| 121 |
+
# media_type=f"audio/{return_format}",
|
| 122 |
+
# headers={"Content-Disposition": f"attachment; filename=speech.{return_format}"}
|
| 123 |
+
# )
|
| 124 |
+
#
|
| 125 |
+
# except HTTPException:
|
| 126 |
+
# raise
|
| 127 |
+
# except Exception as e:
|
| 128 |
+
# logger.error(f"Error in voice synthesis: {e}")
|
| 129 |
+
# raise HTTPException(status_code=500, detail="Error generating voice output")
|
|
|
|
| 130 |
|
| 131 |
+
# @router.post("/analyze-report")
|
| 132 |
+
# async def analyze_report_root(
|
| 133 |
+
# file: UploadFile = File(...),
|
| 134 |
+
# current_user: dict = Depends(get_current_user)
|
| 135 |
+
# ):
|
| 136 |
+
# """Analyze uploaded report (PDF, DOCX, etc.) at root level"""
|
| 137 |
+
# try:
|
| 138 |
+
# if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
|
| 139 |
+
# raise HTTPException(status_code=403, detail="Only doctors and admins can analyze reports")
|
| 140 |
+
#
|
| 141 |
+
# logger.info(f"Report analysis initiated by {current_user['email']}")
|
| 142 |
+
#
|
| 143 |
+
# # Read file content
|
| 144 |
+
# file_content = await file.read()
|
| 145 |
+
# file_extension = file.filename.split('.')[-1].lower()
|
| 146 |
+
#
|
| 147 |
+
# # Extract text based on file type
|
| 148 |
+
# if file_extension == 'pdf':
|
| 149 |
+
# try:
|
| 150 |
+
# from voice import extract_text_from_pdf
|
| 151 |
+
# text_content = extract_text_from_pdf(file_content)
|
| 152 |
+
# except Exception as e:
|
| 153 |
+
# logger.error(f"PDF text extraction failed: {e}")
|
| 154 |
+
# text_content = "Failed to extract text from PDF"
|
| 155 |
+
# elif file_extension in ['docx', 'doc']:
|
| 156 |
+
# try:
|
| 157 |
+
# from docx import Document
|
| 158 |
+
# if Document:
|
| 159 |
+
# doc = Document(io.BytesIO(file_content))
|
| 160 |
+
# text_content = '\n'.join([paragraph.text for paragraph in doc.paragraphs])
|
| 161 |
+
# else:
|
| 162 |
+
# text_content = "Document processing not available"
|
| 163 |
+
# except Exception as e:
|
| 164 |
+
# logger.error(f"DOCX text extraction failed: {e}")
|
| 165 |
+
# text_content = "Failed to extract text from document"
|
| 166 |
+
# else:
|
| 167 |
+
# text_content = "Unsupported file format"
|
| 168 |
+
#
|
| 169 |
+
# # Analyze the content (for now, return a simple analysis)
|
| 170 |
+
# analysis_result = {
|
| 171 |
+
# "file_name": file.filename,
|
| 172 |
+
# "file_type": file_extension,
|
| 173 |
+
# "extracted_text": text_content[:500] + "..." if len(text_content) > 500 else text_content,
|
| 174 |
+
# "analysis": {
|
| 175 |
+
# "summary": f"Analyzed {file.filename} containing {len(text_content)} characters",
|
| 176 |
+
# "key_findings": ["Sample finding 1", "Sample finding 2"],
|
| 177 |
+
# "recommendations": ["Sample recommendation 1", "Sample recommendation 2"]
|
| 178 |
+
# },
|
| 179 |
+
# "timestamp": datetime.utcnow().isoformat()
|
| 180 |
+
# }
|
| 181 |
+
#
|
| 182 |
+
# return analysis_result
|
| 183 |
+
#
|
| 184 |
+
# except Exception as e:
|
| 185 |
+
# logger.error(f"Error analyzing report: {e}")
|
| 186 |
+
# raise HTTPException(status_code=500, detail="Error analyzing report")
|
|
|
|
| 187 |
|
| 188 |
return router
|
| 189 |
|
api/routes/txagent.py
CHANGED
|
@@ -236,7 +236,17 @@ async def get_patient_analysis_results(
|
|
| 236 |
"factors": risk_factors
|
| 237 |
},
|
| 238 |
"summary": analysis.get("summary", ""),
|
| 239 |
-
"recommendations": analysis.get("recommendations", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
}
|
| 241 |
enriched_results.append(formatted_analysis)
|
| 242 |
|
|
|
|
| 236 |
"factors": risk_factors
|
| 237 |
},
|
| 238 |
"summary": analysis.get("summary", ""),
|
| 239 |
+
"recommendations": analysis.get("recommendations", []),
|
| 240 |
+
# Add patient demographic information for modal display
|
| 241 |
+
"date_of_birth": patient.get("date_of_birth"),
|
| 242 |
+
"gender": patient.get("gender"),
|
| 243 |
+
"city": patient.get("city"),
|
| 244 |
+
"state": patient.get("state"),
|
| 245 |
+
"address": patient.get("address"),
|
| 246 |
+
"postal_code": patient.get("postal_code"),
|
| 247 |
+
"country": patient.get("country"),
|
| 248 |
+
"marital_status": patient.get("marital_status"),
|
| 249 |
+
"language": patient.get("language")
|
| 250 |
}
|
| 251 |
enriched_results.append(formatted_analysis)
|
| 252 |
|
models/entities.py
CHANGED
|
@@ -3,36 +3,78 @@ from typing import List, Optional
|
|
| 3 |
from datetime import datetime
|
| 4 |
|
| 5 |
class Condition(BaseModel):
|
| 6 |
-
id: str
|
| 7 |
code: str
|
| 8 |
status: Optional[str] = ""
|
| 9 |
onset_date: Optional[str] = None
|
| 10 |
recorded_date: Optional[str] = None
|
| 11 |
verification_status: Optional[str] = ""
|
|
|
|
| 12 |
|
| 13 |
class Medication(BaseModel):
|
| 14 |
-
id: str
|
| 15 |
name: str
|
| 16 |
status: str
|
| 17 |
prescribed_date: Optional[str] = None
|
| 18 |
requester: Optional[str] = ""
|
| 19 |
dosage: Optional[str] = ""
|
|
|
|
|
|
|
| 20 |
|
| 21 |
class Encounter(BaseModel):
|
| 22 |
-
id: str
|
| 23 |
type: str
|
| 24 |
status: str
|
| 25 |
-
period: dict
|
| 26 |
service_provider: Optional[str] = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
class Note(BaseModel):
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
context: Optional[str] = ""
|
| 33 |
author: Optional[str] = "System"
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
class PatientCreate(BaseModel):
|
|
|
|
| 36 |
full_name: str
|
| 37 |
gender: str
|
| 38 |
date_of_birth: str
|
|
@@ -43,7 +85,14 @@ class PatientCreate(BaseModel):
|
|
| 43 |
country: Optional[str] = "US"
|
| 44 |
marital_status: Optional[str] = "Never Married"
|
| 45 |
language: Optional[str] = "en"
|
|
|
|
|
|
|
|
|
|
| 46 |
conditions: Optional[List[Condition]] = []
|
| 47 |
medications: Optional[List[Medication]] = []
|
| 48 |
encounters: Optional[List[Encounter]] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
notes: Optional[List[Note]] = []
|
|
|
|
| 3 |
from datetime import datetime
|
| 4 |
|
| 5 |
class Condition(BaseModel):
|
| 6 |
+
id: Optional[str] = None
|
| 7 |
code: str
|
| 8 |
status: Optional[str] = ""
|
| 9 |
onset_date: Optional[str] = None
|
| 10 |
recorded_date: Optional[str] = None
|
| 11 |
verification_status: Optional[str] = ""
|
| 12 |
+
category: Optional[str] = ""
|
| 13 |
|
| 14 |
class Medication(BaseModel):
|
| 15 |
+
id: Optional[str] = None
|
| 16 |
name: str
|
| 17 |
status: str
|
| 18 |
prescribed_date: Optional[str] = None
|
| 19 |
requester: Optional[str] = ""
|
| 20 |
dosage: Optional[str] = ""
|
| 21 |
+
intent: Optional[str] = ""
|
| 22 |
+
priority: Optional[str] = ""
|
| 23 |
|
| 24 |
class Encounter(BaseModel):
|
| 25 |
+
id: Optional[str] = None
|
| 26 |
type: str
|
| 27 |
status: str
|
| 28 |
+
period: Optional[dict] = None
|
| 29 |
service_provider: Optional[str] = ""
|
| 30 |
+
class_type: Optional[str] = ""
|
| 31 |
+
reason: Optional[str] = ""
|
| 32 |
+
|
| 33 |
+
class Observation(BaseModel):
|
| 34 |
+
id: Optional[str] = None
|
| 35 |
+
code: str
|
| 36 |
+
value: Optional[str] = ""
|
| 37 |
+
unit: Optional[str] = ""
|
| 38 |
+
status: Optional[str] = ""
|
| 39 |
+
effective_date: Optional[str] = None
|
| 40 |
+
category: Optional[str] = ""
|
| 41 |
+
|
| 42 |
+
class Procedure(BaseModel):
|
| 43 |
+
id: Optional[str] = None
|
| 44 |
+
code: str
|
| 45 |
+
status: str
|
| 46 |
+
performed_date: Optional[str] = None
|
| 47 |
+
performer: Optional[str] = ""
|
| 48 |
+
reason: Optional[str] = ""
|
| 49 |
+
|
| 50 |
+
class Immunization(BaseModel):
|
| 51 |
+
id: Optional[str] = None
|
| 52 |
+
vaccine: str
|
| 53 |
+
status: str
|
| 54 |
+
date: Optional[str] = None
|
| 55 |
+
lot_number: Optional[str] = ""
|
| 56 |
+
expiration_date: Optional[str] = ""
|
| 57 |
+
manufacturer: Optional[str] = ""
|
| 58 |
+
|
| 59 |
+
class Allergy(BaseModel):
|
| 60 |
+
id: Optional[str] = None
|
| 61 |
+
substance: str
|
| 62 |
+
status: Optional[str] = ""
|
| 63 |
+
severity: Optional[str] = ""
|
| 64 |
+
onset_date: Optional[str] = None
|
| 65 |
+
reaction: Optional[str] = ""
|
| 66 |
|
| 67 |
class Note(BaseModel):
|
| 68 |
+
id: Optional[str] = None
|
| 69 |
+
title: Optional[str] = ""
|
| 70 |
+
date: Optional[str] = None
|
|
|
|
| 71 |
author: Optional[str] = "System"
|
| 72 |
+
content: str
|
| 73 |
+
type: Optional[str] = ""
|
| 74 |
+
context: Optional[str] = ""
|
| 75 |
|
| 76 |
class PatientCreate(BaseModel):
|
| 77 |
+
fhir_id: Optional[str] = None
|
| 78 |
full_name: str
|
| 79 |
gender: str
|
| 80 |
date_of_birth: str
|
|
|
|
| 85 |
country: Optional[str] = "US"
|
| 86 |
marital_status: Optional[str] = "Never Married"
|
| 87 |
language: Optional[str] = "en"
|
| 88 |
+
source: Optional[str] = "appointment_booking"
|
| 89 |
+
import_date: Optional[str] = None
|
| 90 |
+
last_updated: Optional[str] = None
|
| 91 |
conditions: Optional[List[Condition]] = []
|
| 92 |
medications: Optional[List[Medication]] = []
|
| 93 |
encounters: Optional[List[Encounter]] = []
|
| 94 |
+
observations: Optional[List[Observation]] = []
|
| 95 |
+
procedures: Optional[List[Procedure]] = []
|
| 96 |
+
immunizations: Optional[List[Immunization]] = []
|
| 97 |
+
allergies: Optional[List[Allergy]] = []
|
| 98 |
notes: Optional[List[Note]] = []
|
routes/patients.py
CHANGED
|
@@ -52,7 +52,7 @@ class ConditionUpdate(BaseModel):
|
|
| 52 |
onset_date: Optional[str] = None
|
| 53 |
recorded_date: Optional[str] = None
|
| 54 |
verification_status: Optional[str] = None
|
| 55 |
-
|
| 56 |
|
| 57 |
class MedicationUpdate(BaseModel):
|
| 58 |
id: Optional[str] = None
|
|
@@ -61,6 +61,8 @@ class MedicationUpdate(BaseModel):
|
|
| 61 |
prescribed_date: Optional[str] = None
|
| 62 |
requester: Optional[str] = None
|
| 63 |
dosage: Optional[str] = None
|
|
|
|
|
|
|
| 64 |
|
| 65 |
class EncounterUpdate(BaseModel):
|
| 66 |
id: Optional[str] = None
|
|
@@ -68,6 +70,42 @@ class EncounterUpdate(BaseModel):
|
|
| 68 |
status: Optional[str] = None
|
| 69 |
period: Optional[Dict[str, str]] = None
|
| 70 |
service_provider: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
class NoteUpdate(BaseModel):
|
| 73 |
id: Optional[str] = None
|
|
@@ -75,6 +113,8 @@ class NoteUpdate(BaseModel):
|
|
| 75 |
date: Optional[str] = None
|
| 76 |
author: Optional[str] = None
|
| 77 |
content: Optional[str] = None
|
|
|
|
|
|
|
| 78 |
|
| 79 |
class PatientUpdate(BaseModel):
|
| 80 |
full_name: Optional[str] = None
|
|
@@ -90,6 +130,10 @@ class PatientUpdate(BaseModel):
|
|
| 90 |
conditions: Optional[List[ConditionUpdate]] = None
|
| 91 |
medications: Optional[List[MedicationUpdate]] = None
|
| 92 |
encounters: Optional[List[EncounterUpdate]] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
notes: Optional[List[NoteUpdate]] = None
|
| 94 |
|
| 95 |
@router.get("/debug/count")
|
|
@@ -132,18 +176,25 @@ async def create_patient(
|
|
| 132 |
patient_doc = patient_data.dict()
|
| 133 |
now = datetime.utcnow().isoformat()
|
| 134 |
|
| 135 |
-
# Add system-generated fields
|
| 136 |
-
patient_doc.
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
patient_doc[field] = []
|
| 148 |
|
| 149 |
# Insert the patient document
|
|
@@ -888,11 +939,15 @@ async def update_patient(
|
|
| 888 |
if value is not None:
|
| 889 |
update_ops["$set"][key] = value
|
| 890 |
|
| 891 |
-
# Handle array updates (
|
| 892 |
array_fields = {
|
| 893 |
"conditions": update_data.conditions,
|
| 894 |
"medications": update_data.medications,
|
| 895 |
"encounters": update_data.encounters,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
"notes": update_data.notes
|
| 897 |
}
|
| 898 |
|
|
@@ -927,6 +982,26 @@ async def update_patient(
|
|
| 927 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 928 |
detail=f"Encounter type is required for {field}"
|
| 929 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 930 |
if field == "notes" and not item_dict.get("content"):
|
| 931 |
raise HTTPException(
|
| 932 |
status_code=status.HTTP_400_BAD_REQUEST,
|
|
@@ -973,6 +1048,10 @@ async def update_patient(
|
|
| 973 |
"conditions": updated_patient.get("conditions", []),
|
| 974 |
"medications": updated_patient.get("medications", []),
|
| 975 |
"encounters": updated_patient.get("encounters", []),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 976 |
"notes": updated_patient.get("notes", []),
|
| 977 |
"source": updated_patient.get("source"),
|
| 978 |
"import_date": updated_patient.get("import_date"),
|
|
|
|
| 52 |
onset_date: Optional[str] = None
|
| 53 |
recorded_date: Optional[str] = None
|
| 54 |
verification_status: Optional[str] = None
|
| 55 |
+
category: Optional[str] = None
|
| 56 |
|
| 57 |
class MedicationUpdate(BaseModel):
|
| 58 |
id: Optional[str] = None
|
|
|
|
| 61 |
prescribed_date: Optional[str] = None
|
| 62 |
requester: Optional[str] = None
|
| 63 |
dosage: Optional[str] = None
|
| 64 |
+
intent: Optional[str] = None
|
| 65 |
+
priority: Optional[str] = None
|
| 66 |
|
| 67 |
class EncounterUpdate(BaseModel):
|
| 68 |
id: Optional[str] = None
|
|
|
|
| 70 |
status: Optional[str] = None
|
| 71 |
period: Optional[Dict[str, str]] = None
|
| 72 |
service_provider: Optional[str] = None
|
| 73 |
+
class_type: Optional[str] = None
|
| 74 |
+
reason: Optional[str] = None
|
| 75 |
+
|
| 76 |
+
class ObservationUpdate(BaseModel):
|
| 77 |
+
id: Optional[str] = None
|
| 78 |
+
code: Optional[str] = None
|
| 79 |
+
value: Optional[str] = None
|
| 80 |
+
unit: Optional[str] = None
|
| 81 |
+
status: Optional[str] = None
|
| 82 |
+
effective_date: Optional[str] = None
|
| 83 |
+
category: Optional[str] = None
|
| 84 |
+
|
| 85 |
+
class ProcedureUpdate(BaseModel):
|
| 86 |
+
id: Optional[str] = None
|
| 87 |
+
code: Optional[str] = None
|
| 88 |
+
status: Optional[str] = None
|
| 89 |
+
performed_date: Optional[str] = None
|
| 90 |
+
performer: Optional[str] = None
|
| 91 |
+
reason: Optional[str] = None
|
| 92 |
+
|
| 93 |
+
class ImmunizationUpdate(BaseModel):
|
| 94 |
+
id: Optional[str] = None
|
| 95 |
+
vaccine: Optional[str] = None
|
| 96 |
+
status: Optional[str] = None
|
| 97 |
+
date: Optional[str] = None
|
| 98 |
+
lot_number: Optional[str] = None
|
| 99 |
+
expiration_date: Optional[str] = None
|
| 100 |
+
manufacturer: Optional[str] = None
|
| 101 |
+
|
| 102 |
+
class AllergyUpdate(BaseModel):
|
| 103 |
+
id: Optional[str] = None
|
| 104 |
+
substance: Optional[str] = None
|
| 105 |
+
status: Optional[str] = None
|
| 106 |
+
severity: Optional[str] = None
|
| 107 |
+
onset_date: Optional[str] = None
|
| 108 |
+
reaction: Optional[str] = None
|
| 109 |
|
| 110 |
class NoteUpdate(BaseModel):
|
| 111 |
id: Optional[str] = None
|
|
|
|
| 113 |
date: Optional[str] = None
|
| 114 |
author: Optional[str] = None
|
| 115 |
content: Optional[str] = None
|
| 116 |
+
type: Optional[str] = None
|
| 117 |
+
context: Optional[str] = None
|
| 118 |
|
| 119 |
class PatientUpdate(BaseModel):
|
| 120 |
full_name: Optional[str] = None
|
|
|
|
| 130 |
conditions: Optional[List[ConditionUpdate]] = None
|
| 131 |
medications: Optional[List[MedicationUpdate]] = None
|
| 132 |
encounters: Optional[List[EncounterUpdate]] = None
|
| 133 |
+
observations: Optional[List[ObservationUpdate]] = None
|
| 134 |
+
procedures: Optional[List[ProcedureUpdate]] = None
|
| 135 |
+
immunizations: Optional[List[ImmunizationUpdate]] = None
|
| 136 |
+
allergies: Optional[List[AllergyUpdate]] = None
|
| 137 |
notes: Optional[List[NoteUpdate]] = None
|
| 138 |
|
| 139 |
@router.get("/debug/count")
|
|
|
|
| 176 |
patient_doc = patient_data.dict()
|
| 177 |
now = datetime.utcnow().isoformat()
|
| 178 |
|
| 179 |
+
# Add system-generated fields if not provided
|
| 180 |
+
if not patient_doc.get('fhir_id'):
|
| 181 |
+
patient_doc['fhir_id'] = str(uuid.uuid4())
|
| 182 |
+
if not patient_doc.get('import_date'):
|
| 183 |
+
patient_doc['import_date'] = now
|
| 184 |
+
if not patient_doc.get('last_updated'):
|
| 185 |
+
patient_doc['last_updated'] = now
|
| 186 |
+
if not patient_doc.get('source'):
|
| 187 |
+
patient_doc['source'] = 'appointment_booking'
|
| 188 |
+
|
| 189 |
+
patient_doc['created_by'] = current_user.get('email')
|
| 190 |
+
|
| 191 |
+
# Ensure all array fields exist even if empty
|
| 192 |
+
array_fields = [
|
| 193 |
+
'conditions', 'medications', 'encounters', 'observations',
|
| 194 |
+
'procedures', 'immunizations', 'allergies', 'notes'
|
| 195 |
+
]
|
| 196 |
+
for field in array_fields:
|
| 197 |
+
if field not in patient_doc or patient_doc[field] is None:
|
| 198 |
patient_doc[field] = []
|
| 199 |
|
| 200 |
# Insert the patient document
|
|
|
|
| 939 |
if value is not None:
|
| 940 |
update_ops["$set"][key] = value
|
| 941 |
|
| 942 |
+
# Handle array updates (all array fields)
|
| 943 |
array_fields = {
|
| 944 |
"conditions": update_data.conditions,
|
| 945 |
"medications": update_data.medications,
|
| 946 |
"encounters": update_data.encounters,
|
| 947 |
+
"observations": update_data.observations,
|
| 948 |
+
"procedures": update_data.procedures,
|
| 949 |
+
"immunizations": update_data.immunizations,
|
| 950 |
+
"allergies": update_data.allergies,
|
| 951 |
"notes": update_data.notes
|
| 952 |
}
|
| 953 |
|
|
|
|
| 982 |
status_code=status.HTTP_400_BAD_REQUEST,
|
| 983 |
detail=f"Encounter type is required for {field}"
|
| 984 |
)
|
| 985 |
+
if field == "observations" and not item_dict.get("code"):
|
| 986 |
+
raise HTTPException(
|
| 987 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 988 |
+
detail=f"Observation code is required for {field}"
|
| 989 |
+
)
|
| 990 |
+
if field == "procedures" and not item_dict.get("code"):
|
| 991 |
+
raise HTTPException(
|
| 992 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 993 |
+
detail=f"Procedure code is required for {field}"
|
| 994 |
+
)
|
| 995 |
+
if field == "immunizations" and not item_dict.get("vaccine"):
|
| 996 |
+
raise HTTPException(
|
| 997 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 998 |
+
detail=f"Immunization vaccine is required for {field}"
|
| 999 |
+
)
|
| 1000 |
+
if field == "allergies" and not item_dict.get("substance"):
|
| 1001 |
+
raise HTTPException(
|
| 1002 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 1003 |
+
detail=f"Allergy substance is required for {field}"
|
| 1004 |
+
)
|
| 1005 |
if field == "notes" and not item_dict.get("content"):
|
| 1006 |
raise HTTPException(
|
| 1007 |
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
| 1048 |
"conditions": updated_patient.get("conditions", []),
|
| 1049 |
"medications": updated_patient.get("medications", []),
|
| 1050 |
"encounters": updated_patient.get("encounters", []),
|
| 1051 |
+
"observations": updated_patient.get("observations", []),
|
| 1052 |
+
"procedures": updated_patient.get("procedures", []),
|
| 1053 |
+
"immunizations": updated_patient.get("immunizations", []),
|
| 1054 |
+
"allergies": updated_patient.get("allergies", []),
|
| 1055 |
"notes": updated_patient.get("notes", []),
|
| 1056 |
"source": updated_patient.get("source"),
|
| 1057 |
"import_date": updated_patient.get("import_date"),
|
utils.py
CHANGED
|
@@ -63,7 +63,16 @@ def serialize_patient(patient: dict) -> dict:
|
|
| 63 |
|
| 64 |
def compute_patient_data_hash(data: dict) -> str:
|
| 65 |
"""Compute hash of patient data for change detection"""
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
return hashlib.sha256(serialized.encode()).hexdigest()
|
| 68 |
|
| 69 |
def compute_file_content_hash(file_content: bytes) -> str:
|
|
|
|
| 63 |
|
| 64 |
def compute_patient_data_hash(data: dict) -> str:
|
| 65 |
"""Compute hash of patient data for change detection"""
|
| 66 |
+
# Custom JSON encoder to handle datetime objects
|
| 67 |
+
class DateTimeEncoder(json.JSONEncoder):
|
| 68 |
+
def default(self, obj):
|
| 69 |
+
if isinstance(obj, datetime):
|
| 70 |
+
return obj.isoformat()
|
| 71 |
+
elif isinstance(obj, ObjectId):
|
| 72 |
+
return str(obj)
|
| 73 |
+
return super().default(obj)
|
| 74 |
+
|
| 75 |
+
serialized = json.dumps(data, sort_keys=True, cls=DateTimeEncoder)
|
| 76 |
return hashlib.sha256(serialized.encode()).hexdigest()
|
| 77 |
|
| 78 |
def compute_file_content_hash(file_content: bytes) -> str:
|