Ali2206 commited on
Commit
6ed180c
·
1 Parent(s): a745a50
Files changed (6) hide show
  1. analysis.py +50 -3
  2. api/__init__.py +148 -150
  3. api/routes/txagent.py +11 -1
  4. models/entities.py +57 -8
  5. routes/patients.py +93 -14
  6. 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
- doc = json.dumps(serialized, indent=2)
 
 
 
 
 
 
 
 
 
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
- # Add the chat-stream endpoint at the root level
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
- # Add voice synthesis endpoint at the root level for frontend compatibility
86
- @router.post("/voice/synthesize")
87
- async def voice_synthesize_root(
88
- request: dict,
89
- current_user: dict = Depends(get_current_user)
90
- ):
91
- """Voice synthesis endpoint at root level for frontend compatibility"""
92
- try:
93
- if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
94
- raise HTTPException(status_code=403, detail="Only doctors and admins can use voice features")
95
-
96
- logger.info(f"Voice synthesis initiated by {current_user['email']}")
97
-
98
- # Extract parameters from request
99
- text = request.get('text', '')
100
- language = request.get('language', 'en-US')
101
- return_format = request.get('return_format', 'mp3')
102
-
103
- if not text:
104
- raise HTTPException(status_code=400, detail="Text is required")
105
-
106
- # Import voice function
107
- try:
108
- from voice import text_to_speech
109
- except ImportError:
110
- # Fallback if voice module is not available
111
- raise HTTPException(status_code=500, detail="Voice synthesis not available")
112
-
113
- # Convert language code for gTTS (e.g., 'en-US' -> 'en')
114
- language_code = language.split('-')[0] if '-' in language else language
115
-
116
- # Generate speech
117
- audio_data = text_to_speech(text, language=language_code)
118
-
119
- # Return audio data
120
- return StreamingResponse(
121
- io.BytesIO(audio_data),
122
- media_type=f"audio/{return_format}",
123
- headers={"Content-Disposition": f"attachment; filename=speech.{return_format}"}
124
- )
125
-
126
- except HTTPException:
127
- raise
128
- except Exception as e:
129
- logger.error(f"Error in voice synthesis: {e}")
130
- raise HTTPException(status_code=500, detail="Error generating voice output")
131
 
132
- # Add analyze-report endpoint at the root level for frontend compatibility
133
- @router.post("/analyze-report")
134
- async def analyze_report_root(
135
- file: UploadFile = File(...),
136
- current_user: dict = Depends(get_current_user)
137
- ):
138
- """Analyze uploaded report (PDF, DOCX, etc.) at root level"""
139
- try:
140
- if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
141
- raise HTTPException(status_code=403, detail="Only doctors and admins can analyze reports")
142
-
143
- logger.info(f"Report analysis initiated by {current_user['email']}")
144
-
145
- # Read file content
146
- file_content = await file.read()
147
- file_extension = file.filename.split('.')[-1].lower()
148
-
149
- # Extract text based on file type
150
- if file_extension == 'pdf':
151
- try:
152
- from voice import extract_text_from_pdf
153
- text_content = extract_text_from_pdf(file_content)
154
- except Exception as e:
155
- logger.error(f"PDF text extraction failed: {e}")
156
- text_content = "Failed to extract text from PDF"
157
- elif file_extension in ['docx', 'doc']:
158
- try:
159
- from docx import Document
160
- if Document:
161
- doc = Document(io.BytesIO(file_content))
162
- text_content = '\n'.join([paragraph.text for paragraph in doc.paragraphs])
163
- else:
164
- text_content = "Document processing not available"
165
- except Exception as e:
166
- logger.error(f"DOCX text extraction failed: {e}")
167
- text_content = "Failed to extract text from document"
168
- else:
169
- text_content = "Unsupported file format"
170
-
171
- # Analyze the content (for now, return a simple analysis)
172
- analysis_result = {
173
- "file_name": file.filename,
174
- "file_type": file_extension,
175
- "extracted_text": text_content[:500] + "..." if len(text_content) > 500 else text_content,
176
- "analysis": {
177
- "summary": f"Analyzed {file.filename} containing {len(text_content)} characters",
178
- "key_findings": ["Sample finding 1", "Sample finding 2"],
179
- "recommendations": ["Sample recommendation 1", "Sample recommendation 2"]
180
- },
181
- "timestamp": datetime.utcnow().isoformat()
182
- }
183
-
184
- return analysis_result
185
-
186
- except Exception as e:
187
- logger.error(f"Error analyzing report: {e}")
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
- date: str
30
- type: str
31
- text: str
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
- notes: Optional[str] = None
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.update({
137
- "fhir_id": str(uuid.uuid4()),
138
- "import_date": now,
139
- "last_updated": now,
140
- "source": "manual",
141
- "created_by": current_user.get('email')
142
- })
143
-
144
- # Ensure arrays exist even if empty
145
- for field in ['conditions', 'medications', 'encounters', 'notes']:
146
- if field not in patient_doc:
 
 
 
 
 
 
 
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 (conditions, medications, encounters, notes)
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
- serialized = json.dumps(data, sort_keys=True)
 
 
 
 
 
 
 
 
 
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: