Spaces:
Sleeping
Sleeping
device token
Browse files- api/routes/txagent.py +298 -12
- 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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')
|