Ali2206 commited on
Commit
4459bdc
·
1 Parent(s): bd7030e

device token

Browse files
Files changed (2) hide show
  1. api/routes/txagent.py +298 -12
  2. utils.py +34 -47
api/routes/txagent.py CHANGED
@@ -43,7 +43,7 @@ except ImportError as e:
43
  TXAGENT_AVAILABLE = False
44
 
45
  try:
46
- from utils import clean_text_response
47
  except ImportError:
48
  # Fallback: define the function locally if import fails
49
  def clean_text_response(text: str) -> str:
@@ -51,6 +51,21 @@ except ImportError:
51
  text = re.sub(r'\n\s*\n', '\n\n', text)
52
  text = re.sub(r'[ ]+', ' ', text)
53
  return text.replace("**", "").replace("__", "").strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  try:
56
  from analysis import analyze_patient_report
@@ -84,17 +99,7 @@ txagent_instance = None
84
 
85
  def _normalize_risk_level(risk_level):
86
  """Normalize risk level names to match expected format"""
87
- risk_level_mapping = {
88
- 'low': 'low',
89
- 'medium': 'moderate',
90
- 'moderate': 'moderate',
91
- 'high': 'high',
92
- 'severe': 'severe',
93
- 'critical': 'severe',
94
- 'none': 'none',
95
- 'unknown': 'none'
96
- }
97
- return risk_level_mapping.get(risk_level.lower(), 'none')
98
 
99
  def get_txagent():
100
  """Get or create TxAgent instance"""
@@ -777,4 +782,285 @@ async def synthesize_voice(
777
  logger.error(f"Error in voice synthesis: {e}")
778
  raise HTTPException(status_code=500, detail="Error generating voice output")
779
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
780
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  TXAGENT_AVAILABLE = False
44
 
45
  try:
46
+ from utils import clean_text_response, format_risk_level, create_notification
47
  except ImportError:
48
  # Fallback: define the function locally if import fails
49
  def clean_text_response(text: str) -> str:
 
51
  text = re.sub(r'\n\s*\n', '\n\n', text)
52
  text = re.sub(r'[ ]+', ' ', text)
53
  return text.replace("**", "").replace("__", "").strip()
54
+
55
+ def format_risk_level(risk_level: str) -> str:
56
+ risk_level_mapping = {
57
+ 'low': 'low', 'medium': 'moderate', 'moderate': 'moderate',
58
+ 'high': 'high', 'severe': 'severe', 'critical': 'severe',
59
+ 'none': 'none', 'unknown': 'none'
60
+ }
61
+ return risk_level_mapping.get(risk_level.lower(), 'none')
62
+
63
+ def create_notification(user_id: str, title: str, message: str, notification_type: str = "info", patient_id: str = None) -> dict:
64
+ return {
65
+ "user_id": user_id, "title": title, "message": message,
66
+ "type": notification_type, "read": False,
67
+ "timestamp": datetime.utcnow(), "patient_id": patient_id
68
+ }
69
 
70
  try:
71
  from analysis import analyze_patient_report
 
99
 
100
  def _normalize_risk_level(risk_level):
101
  """Normalize risk level names to match expected format"""
102
+ return format_risk_level(risk_level)
 
 
 
 
 
 
 
 
 
 
103
 
104
  def get_txagent():
105
  """Get or create TxAgent instance"""
 
782
  logger.error(f"Error in voice synthesis: {e}")
783
  raise HTTPException(status_code=500, detail="Error generating voice output")
784
 
785
+ # Notifications endpoints
786
+ @router.get("/notifications")
787
+ async def get_notifications(current_user: dict = Depends(get_current_user)):
788
+ """Get notifications for the current user"""
789
+ try:
790
+ if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
791
+ raise HTTPException(status_code=403, detail="Only doctors and admins can access notifications")
792
+
793
+ logger.info(f"Fetching notifications for {current_user['email']}")
794
+
795
+ # Import database collections
796
+ from db.mongo import db
797
+ notifications_collection = db.notifications
798
+
799
+ # Get notifications for the current user
800
+ notifications = await notifications_collection.find({
801
+ "user_id": current_user.get('_id')
802
+ }).sort("timestamp", -1).limit(50).to_list(length=50)
803
+
804
+ return [
805
+ {
806
+ "id": str(notification["_id"]),
807
+ "title": notification.get("title", ""),
808
+ "message": notification.get("message", ""),
809
+ "type": notification.get("type", "info"),
810
+ "read": notification.get("read", False),
811
+ "timestamp": notification.get("timestamp"),
812
+ "patient_id": notification.get("patient_id")
813
+ }
814
+ for notification in notifications
815
+ ]
816
+
817
+ except Exception as e:
818
+ logger.error(f"Error getting notifications: {e}")
819
+ raise HTTPException(status_code=500, detail="Failed to get notifications")
820
+
821
+ @router.post("/notifications/{notification_id}/read")
822
+ async def mark_notification_read(
823
+ notification_id: str,
824
+ current_user: dict = Depends(get_current_user)
825
+ ):
826
+ """Mark a notification as read"""
827
+ try:
828
+ if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
829
+ raise HTTPException(status_code=403, detail="Only doctors and admins can mark notifications as read")
830
+
831
+ logger.info(f"Marking notification {notification_id} as read by {current_user['email']}")
832
+
833
+ # Import database collections
834
+ from db.mongo import db
835
+ notifications_collection = db.notifications
836
+
837
+ # Update the notification
838
+ result = await notifications_collection.update_one(
839
+ {
840
+ "_id": ObjectId(notification_id),
841
+ "user_id": current_user.get('_id')
842
+ },
843
+ {"$set": {"read": True, "read_at": datetime.utcnow()}}
844
+ )
845
+
846
+ if result.matched_count == 0:
847
+ raise HTTPException(status_code=404, detail="Notification not found")
848
+
849
+ return {"message": "Notification marked as read"}
850
+
851
+ except HTTPException:
852
+ raise
853
+ except Exception as e:
854
+ logger.error(f"Error marking notification as read: {e}")
855
+ raise HTTPException(status_code=500, detail="Failed to mark notification as read")
856
+
857
+ @router.post("/notifications/read-all")
858
+ async def mark_all_notifications_read(current_user: dict = Depends(get_current_user)):
859
+ """Mark all notifications as read for the current user"""
860
+ try:
861
+ if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
862
+ raise HTTPException(status_code=403, detail="Only doctors and admins can mark notifications as read")
863
+
864
+ logger.info(f"Marking all notifications as read for {current_user['email']}")
865
+
866
+ # Import database collections
867
+ from db.mongo import db
868
+ notifications_collection = db.notifications
869
+
870
+ # Update all unread notifications for the user
871
+ result = await notifications_collection.update_many(
872
+ {
873
+ "user_id": current_user.get('_id'),
874
+ "read": False
875
+ },
876
+ {"$set": {"read": True, "read_at": datetime.utcnow()}}
877
+ )
878
+
879
+ return {
880
+ "message": f"Marked {result.modified_count} notifications as read",
881
+ "modified_count": result.modified_count
882
+ }
883
+
884
+ except Exception as e:
885
+ logger.error(f"Error marking all notifications as read: {e}")
886
+ raise HTTPException(status_code=500, detail="Failed to mark notifications as read")
887
+
888
+ # Voice chat endpoint
889
+ @router.post("/voice/chat")
890
+ async def voice_chat(
891
+ audio: UploadFile = File(...),
892
+ current_user: dict = Depends(get_current_user)
893
+ ):
894
+ """Voice chat with TxAgent"""
895
+ try:
896
+ if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
897
+ raise HTTPException(status_code=403, detail="Only doctors and admins can use voice features")
898
+
899
+ logger.info(f"Voice chat initiated by {current_user['email']}")
900
+
901
+ # Read audio file
902
+ audio_data = await audio.read()
903
+
904
+ # Transcribe audio to text
905
+ try:
906
+ transcription = recognize_speech(audio_data)
907
+ if isinstance(transcription, dict):
908
+ transcription_text = transcription.get("transcription", "")
909
+ else:
910
+ transcription_text = str(transcription)
911
+ except Exception as e:
912
+ logger.error(f"Speech recognition failed: {e}")
913
+ transcription_text = "Sorry, I couldn't understand the audio."
914
+
915
+ # Generate response (for now, a simple response)
916
+ response_text = f"I heard you say: '{transcription_text}'. How can I help you with patient care today?"
917
+
918
+ # Store voice chat in the database
919
+ try:
920
+ from db.mongo import db
921
+ chats_collection = db.chats
922
+
923
+ chat_entry = {
924
+ "message": transcription_text,
925
+ "response": response_text,
926
+ "user_id": current_user.get('_id'),
927
+ "user_email": current_user.get('email'),
928
+ "timestamp": datetime.utcnow(),
929
+ "chat_type": "voice_chat"
930
+ }
931
+
932
+ await chats_collection.insert_one(chat_entry)
933
+ logger.info(f"Voice chat stored in database for user {current_user['email']}")
934
+
935
+ except Exception as db_error:
936
+ logger.error(f"Failed to store voice chat in database: {str(db_error)}")
937
+
938
+ # Convert response to speech
939
+ try:
940
+ audio_response = text_to_speech(response_text, language="en")
941
+ except Exception as e:
942
+ logger.error(f"Text-to-speech failed: {e}")
943
+ audio_response = b"Sorry, I couldn't generate audio response."
944
+
945
+ return StreamingResponse(
946
+ io.BytesIO(audio_response),
947
+ media_type="audio/mpeg",
948
+ headers={"Content-Disposition": "attachment; filename=voice_response.mp3"}
949
+ )
950
+
951
+ except Exception as e:
952
+ logger.error(f"Error in voice chat: {e}")
953
+ raise HTTPException(status_code=500, detail="Error processing voice chat")
954
+
955
+ # Report analysis endpoint
956
+ @router.post("/analyze-report")
957
+ async def analyze_report(
958
+ file: UploadFile = File(...),
959
+ current_user: dict = Depends(get_current_user)
960
+ ):
961
+ """Analyze uploaded report (PDF, DOCX, etc.)"""
962
+ try:
963
+ if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
964
+ raise HTTPException(status_code=403, detail="Only doctors and admins can analyze reports")
965
+
966
+ logger.info(f"Report analysis initiated by {current_user['email']}")
967
+
968
+ # Read file content
969
+ file_content = await file.read()
970
+ file_extension = file.filename.split('.')[-1].lower()
971
+
972
+ # Extract text based on file type
973
+ if file_extension == 'pdf':
974
+ try:
975
+ text_content = extract_text_from_pdf(file_content)
976
+ except Exception as e:
977
+ logger.error(f"PDF text extraction failed: {e}")
978
+ text_content = "Failed to extract text from PDF"
979
+ elif file_extension in ['docx', 'doc']:
980
+ try:
981
+ if Document:
982
+ doc = Document(io.BytesIO(file_content))
983
+ text_content = '\n'.join([paragraph.text for paragraph in doc.paragraphs])
984
+ else:
985
+ text_content = "Document processing not available"
986
+ except Exception as e:
987
+ logger.error(f"DOCX text extraction failed: {e}")
988
+ text_content = "Failed to extract text from document"
989
+ else:
990
+ text_content = "Unsupported file format"
991
+
992
+ # Analyze the content (for now, return a simple analysis)
993
+ analysis_result = {
994
+ "file_name": file.filename,
995
+ "file_type": file_extension,
996
+ "extracted_text": text_content[:500] + "..." if len(text_content) > 500 else text_content,
997
+ "analysis": {
998
+ "summary": f"Analyzed {file.filename} containing {len(text_content)} characters",
999
+ "key_findings": ["Sample finding 1", "Sample finding 2"],
1000
+ "recommendations": ["Sample recommendation 1", "Sample recommendation 2"]
1001
+ },
1002
+ "timestamp": datetime.utcnow().isoformat()
1003
+ }
1004
+
1005
+ return analysis_result
1006
+
1007
+ except Exception as e:
1008
+ logger.error(f"Error analyzing report: {e}")
1009
+ raise HTTPException(status_code=500, detail="Error analyzing report")
1010
 
1011
+ # Patient deletion endpoint
1012
+ @router.delete("/patients/{patient_id}")
1013
+ async def delete_patient(
1014
+ patient_id: str,
1015
+ current_user: dict = Depends(get_current_user)
1016
+ ):
1017
+ """Delete a patient and all associated data"""
1018
+ try:
1019
+ if not any(role in current_user.get('roles', []) for role in ['admin']):
1020
+ raise HTTPException(status_code=403, detail="Only administrators can delete patients")
1021
+
1022
+ logger.info(f"Patient deletion initiated by {current_user['email']} for patient {patient_id}")
1023
+
1024
+ # Import database collections
1025
+ from db.mongo import db
1026
+ patients_collection = db.patients
1027
+ analysis_collection = db.patient_analysis_results
1028
+ chats_collection = db.chats
1029
+ notifications_collection = db.notifications
1030
+
1031
+ # Find the patient first
1032
+ patient = await patients_collection.find_one({"fhir_id": patient_id})
1033
+ if not patient:
1034
+ raise HTTPException(status_code=404, detail="Patient not found")
1035
+
1036
+ # Delete all associated data
1037
+ try:
1038
+ # Delete patient
1039
+ await patients_collection.delete_one({"fhir_id": patient_id})
1040
+
1041
+ # Delete analysis results
1042
+ await analysis_collection.delete_many({"patient_id": patient_id})
1043
+
1044
+ # Delete chats related to this patient
1045
+ await chats_collection.delete_many({"patient_id": patient_id})
1046
+
1047
+ # Delete notifications related to this patient
1048
+ await notifications_collection.delete_many({"patient_id": patient_id})
1049
+
1050
+ logger.info(f"Successfully deleted patient {patient_id} and all associated data")
1051
+
1052
+ return {
1053
+ "message": f"Patient {patient.get('full_name', patient_id)} and all associated data deleted successfully",
1054
+ "patient_id": patient_id,
1055
+ "deleted_at": datetime.utcnow().isoformat()
1056
+ }
1057
+
1058
+ except Exception as e:
1059
+ logger.error(f"Error during patient deletion: {e}")
1060
+ raise HTTPException(status_code=500, detail="Error deleting patient data")
1061
+
1062
+ except HTTPException:
1063
+ raise
1064
+ except Exception as e:
1065
+ logger.error(f"Error deleting patient {patient_id}: {e}")
1066
+ raise HTTPException(status_code=500, detail="Failed to delete patient")
utils.py CHANGED
@@ -6,60 +6,17 @@ from datetime import datetime
6
  from typing import Dict, List, Tuple
7
  from bson import ObjectId
8
  import logging
9
- from config import logger
10
- # Add to your utils.py
11
- from fastapi import WebSocket
12
- import asyncio
13
-
14
- class NotificationManager:
15
- def __init__(self):
16
- self.active_connections = {}
17
- self.notification_queue = asyncio.Queue()
18
-
19
- async def connect(self, websocket: WebSocket, user_id: str):
20
- await websocket.accept()
21
- self.active_connections[user_id] = websocket
22
-
23
- def disconnect(self, user_id: str):
24
- if user_id in self.active_connections:
25
- del self.active_connections[user_id]
26
-
27
- async def broadcast_notification(self, notification: dict):
28
- """Broadcast to all connected clients"""
29
- for connection in self.active_connections.values():
30
- try:
31
- await connection.send_json({
32
- "type": "notification",
33
- "data": notification
34
- })
35
- except Exception as e:
36
- logger.error(f"Error sending notification: {e}")
37
-
38
- notification_manager = NotificationManager()
39
-
40
- async def broadcast_notification(notification: dict):
41
- """Broadcast notification to relevant users"""
42
- # Determine recipients based on notification type/priority
43
- recipients = []
44
- if notification["priority"] == "high":
45
- recipients = ["psychiatrist", "emergency_team", "primary_care"]
46
- else:
47
- recipients = ["primary_care", "case_manager"]
48
-
49
- # Add to each recipient's notification queue
50
- await notification_manager.notification_queue.put({
51
- "recipients": recipients,
52
- "notification": notification
53
- })
54
-
55
 
 
56
 
57
  def clean_text_response(text: str) -> str:
 
58
  text = re.sub(r'\n\s*\n', '\n\n', text)
59
  text = re.sub(r'[ ]+', ' ', text)
60
  return text.replace("**", "").replace("__", "").strip()
61
 
62
  def extract_section(text: str, heading: str) -> str:
 
63
  try:
64
  pattern = rf"{re.escape(heading)}:\s*\n(.*?)(?=\n[A-Z][^\n]*:|\Z)"
65
  match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
@@ -69,6 +26,7 @@ def extract_section(text: str, heading: str) -> str:
69
  return ""
70
 
71
  def structure_medical_response(text: str) -> Dict:
 
72
  def extract_improved(text: str, heading: str) -> str:
73
  patterns = [
74
  rf"{re.escape(heading)}:\s*\n(.*?)(?=\n\s*\n|\Z)",
@@ -97,14 +55,43 @@ def structure_medical_response(text: str) -> Dict:
97
  }
98
 
99
  def serialize_patient(patient: dict) -> dict:
 
100
  patient_copy = patient.copy()
101
  if "_id" in patient_copy:
102
  patient_copy["_id"] = str(patient_copy["_id"])
103
  return patient_copy
104
 
105
  def compute_patient_data_hash(data: dict) -> str:
 
106
  serialized = json.dumps(data, sort_keys=True)
107
  return hashlib.sha256(serialized.encode()).hexdigest()
108
 
109
  def compute_file_content_hash(file_content: bytes) -> str:
110
- return hashlib.sha256(file_content).hexdigest()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  from typing import Dict, List, Tuple
7
  from bson import ObjectId
8
  import logging
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ logger = logging.getLogger(__name__)
11
 
12
  def clean_text_response(text: str) -> str:
13
+ """Clean and format text response"""
14
  text = re.sub(r'\n\s*\n', '\n\n', text)
15
  text = re.sub(r'[ ]+', ' ', text)
16
  return text.replace("**", "").replace("__", "").strip()
17
 
18
  def extract_section(text: str, heading: str) -> str:
19
+ """Extract a section from text based on heading"""
20
  try:
21
  pattern = rf"{re.escape(heading)}:\s*\n(.*?)(?=\n[A-Z][^\n]*:|\Z)"
22
  match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
 
26
  return ""
27
 
28
  def structure_medical_response(text: str) -> Dict:
29
+ """Structure medical response into sections"""
30
  def extract_improved(text: str, heading: str) -> str:
31
  patterns = [
32
  rf"{re.escape(heading)}:\s*\n(.*?)(?=\n\s*\n|\Z)",
 
55
  }
56
 
57
  def serialize_patient(patient: dict) -> dict:
58
+ """Serialize patient data for JSON response"""
59
  patient_copy = patient.copy()
60
  if "_id" in patient_copy:
61
  patient_copy["_id"] = str(patient_copy["_id"])
62
  return patient_copy
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:
70
+ """Compute hash of file content"""
71
+ return hashlib.sha256(file_content).hexdigest()
72
+
73
+ def create_notification(user_id: str, title: str, message: str, notification_type: str = "info", patient_id: str = None) -> dict:
74
+ """Create a notification object"""
75
+ return {
76
+ "user_id": user_id,
77
+ "title": title,
78
+ "message": message,
79
+ "type": notification_type,
80
+ "read": False,
81
+ "timestamp": datetime.utcnow(),
82
+ "patient_id": patient_id
83
+ }
84
+
85
+ def format_risk_level(risk_level: str) -> str:
86
+ """Normalize risk level names"""
87
+ risk_level_mapping = {
88
+ 'low': 'low',
89
+ 'medium': 'moderate',
90
+ 'moderate': 'moderate',
91
+ 'high': 'high',
92
+ 'severe': 'severe',
93
+ 'critical': 'severe',
94
+ 'none': 'none',
95
+ 'unknown': 'none'
96
+ }
97
+ return risk_level_mapping.get(risk_level.lower(), 'none')