Spaces:
Runtime error
Runtime error
Commit
·
f05b79e
1
Parent(s):
c6c9f51
Solve updatePatientDisplay func with undefined handler
Browse files- src/data/repositories/account.py +0 -220
- src/data/repositories/emr.py +0 -285
- src/data/repositories/medical_memory.py +0 -148
- src/data/repositories/medical_record.py +0 -93
- src/data/repositories/patient.py +0 -176
- src/data/repositories/session.py +0 -289
- static/js/app.js +6 -0
- static/js/emr.js +6 -2
src/data/repositories/account.py
DELETED
|
@@ -1,220 +0,0 @@
|
|
| 1 |
-
# src/data/repositories/account.py
|
| 2 |
-
"""
|
| 3 |
-
User account management operations for MongoDB.
|
| 4 |
-
Each account represents a doctor.
|
| 5 |
-
|
| 6 |
-
## Fields
|
| 7 |
-
_id: index
|
| 8 |
-
name: The name attached to the account
|
| 9 |
-
role: What type of account this is
|
| 10 |
-
specialty: Any extra information about the account
|
| 11 |
-
created_at: The timestamp when the account was created
|
| 12 |
-
updated_at: The timestamp when the account data was last modified
|
| 13 |
-
last_seen: The last time this account was retrieved
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
import re
|
| 17 |
-
from datetime import datetime, timezone
|
| 18 |
-
from typing import Any
|
| 19 |
-
|
| 20 |
-
from bson import ObjectId
|
| 21 |
-
from bson.errors import InvalidId
|
| 22 |
-
from pandas import DataFrame
|
| 23 |
-
from pymongo import ASCENDING
|
| 24 |
-
from pymongo.errors import ConnectionFailure, PyMongoError, WriteError
|
| 25 |
-
|
| 26 |
-
from src.data.connection import (ActionFailed, Collections, get_collection,
|
| 27 |
-
setup_collection)
|
| 28 |
-
from src.models.account import Account
|
| 29 |
-
from src.utils.logger import logger
|
| 30 |
-
|
| 31 |
-
VALID_ROLES = [
|
| 32 |
-
"Doctor",
|
| 33 |
-
"Healthcare Prof",
|
| 34 |
-
"Nurse",
|
| 35 |
-
"Caregiver",
|
| 36 |
-
"Physician",
|
| 37 |
-
"Medical Student",
|
| 38 |
-
"Other"
|
| 39 |
-
]
|
| 40 |
-
|
| 41 |
-
def init(
|
| 42 |
-
*,
|
| 43 |
-
collection_name: str = Collections.ACCOUNT,
|
| 44 |
-
validator_path: str = "schemas/account_validator.json",
|
| 45 |
-
drop: bool = False
|
| 46 |
-
):
|
| 47 |
-
"""Initializes the collection, applying schema validation."""
|
| 48 |
-
try:
|
| 49 |
-
if drop:
|
| 50 |
-
get_collection(collection_name).drop()
|
| 51 |
-
setup_collection(collection_name, validator_path)
|
| 52 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 53 |
-
logger().error(f"Failed to initialize collection '{collection_name}': {e}")
|
| 54 |
-
raise ActionFailed(f"Database operation failed during initialization: {e}") from e
|
| 55 |
-
|
| 56 |
-
def get_account_frame(
|
| 57 |
-
*,
|
| 58 |
-
collection_name: str = Collections.ACCOUNT
|
| 59 |
-
) -> DataFrame:
|
| 60 |
-
"""Get accounts as a pandas DataFrame, raising ActionFailed on error."""
|
| 61 |
-
try:
|
| 62 |
-
collection = get_collection(collection_name)
|
| 63 |
-
return DataFrame(collection.find())
|
| 64 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 65 |
-
logger().error(f"Failed to retrieve account frame: {e}")
|
| 66 |
-
raise ActionFailed(f"Could not retrieve accounts for DataFrame: {e}") from e
|
| 67 |
-
|
| 68 |
-
def create_account(
|
| 69 |
-
name: str,
|
| 70 |
-
role: str,
|
| 71 |
-
specialty: str | None = None,
|
| 72 |
-
*,
|
| 73 |
-
collection_name: str = Collections.ACCOUNT
|
| 74 |
-
) -> str:
|
| 75 |
-
"""Creates a new user account, raising ActionFailed on error."""
|
| 76 |
-
now = datetime.now(timezone.utc)
|
| 77 |
-
user_data: dict[str, Any] = {
|
| 78 |
-
"name": name,
|
| 79 |
-
"role" : role,
|
| 80 |
-
"created_at": now,
|
| 81 |
-
"updated_at": now,
|
| 82 |
-
"last_seen": now
|
| 83 |
-
}
|
| 84 |
-
if specialty:
|
| 85 |
-
user_data["specialty"] = specialty
|
| 86 |
-
|
| 87 |
-
try:
|
| 88 |
-
collection = get_collection(collection_name)
|
| 89 |
-
result = collection.insert_one(user_data)
|
| 90 |
-
logger().info(f"Created new account: {result.inserted_id}")
|
| 91 |
-
return str(result.inserted_id)
|
| 92 |
-
except WriteError as e:
|
| 93 |
-
logger().error(f"Failed to create account due to data conflict: {e}")
|
| 94 |
-
raise ActionFailed(f"Account could not be created. Data is conflicting or invalid.") from e
|
| 95 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 96 |
-
logger().error(f"Database error while creating account: {e}")
|
| 97 |
-
raise ActionFailed(f"A database error occurred while creating the account.") from e
|
| 98 |
-
|
| 99 |
-
def update_account(
|
| 100 |
-
user_id: str,
|
| 101 |
-
updates: dict[str, Any],
|
| 102 |
-
*,
|
| 103 |
-
collection_name: str = Collections.ACCOUNT
|
| 104 |
-
) -> bool:
|
| 105 |
-
"""Updates an existing user account, raising ActionFailed on error."""
|
| 106 |
-
try:
|
| 107 |
-
obj_user_id = ObjectId(user_id)
|
| 108 |
-
collection = get_collection(collection_name)
|
| 109 |
-
|
| 110 |
-
if "created_at" in updates:
|
| 111 |
-
logger().warning("Attempting to modify the 'created_at' attribute of an account. This is not allowed.")
|
| 112 |
-
updates.pop("created_at")
|
| 113 |
-
updates["updated_at"] = datetime.now(timezone.utc)
|
| 114 |
-
|
| 115 |
-
result = collection.update_one(
|
| 116 |
-
{"_id": obj_user_id},
|
| 117 |
-
{"$set": updates}
|
| 118 |
-
)
|
| 119 |
-
return result.modified_count > 0
|
| 120 |
-
except InvalidId as e:
|
| 121 |
-
logger().error(f"Invalid user_id format for update: '{user_id}'")
|
| 122 |
-
raise ActionFailed(f"The provided user ID '{user_id}' is not a valid format.") from e
|
| 123 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 124 |
-
logger().error(f"Database error while updating account '{user_id}': {e}")
|
| 125 |
-
raise ActionFailed(f"A database error occurred while updating the account.") from e
|
| 126 |
-
|
| 127 |
-
def get_account(
|
| 128 |
-
user_id: str,
|
| 129 |
-
*,
|
| 130 |
-
collection_name: str = Collections.ACCOUNT
|
| 131 |
-
) -> Account | None:
|
| 132 |
-
"""Retrieves an account by ID. Returns a Pydantic Account object or None."""
|
| 133 |
-
try:
|
| 134 |
-
obj_user_id = ObjectId(user_id)
|
| 135 |
-
collection = get_collection(collection_name)
|
| 136 |
-
now = datetime.now(timezone.utc)
|
| 137 |
-
|
| 138 |
-
account_dict = collection.find_one_and_update(
|
| 139 |
-
{"_id": obj_user_id},
|
| 140 |
-
{"$set": {"last_seen": now}},
|
| 141 |
-
return_document=True
|
| 142 |
-
)
|
| 143 |
-
|
| 144 |
-
if account_dict:
|
| 145 |
-
return Account.model_validate(account_dict)
|
| 146 |
-
return None
|
| 147 |
-
except InvalidId as e:
|
| 148 |
-
logger().error(f"Invalid user_id format for get: '{user_id}'")
|
| 149 |
-
raise ActionFailed(f"The provided user ID '{user_id}' is not a valid format.") from e
|
| 150 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 151 |
-
logger().error(f"Database error while getting account '{user_id}': {e}")
|
| 152 |
-
raise ActionFailed(f"A database error occurred while retrieving the account.") from e
|
| 153 |
-
|
| 154 |
-
def get_account_by_name(
|
| 155 |
-
name: str,
|
| 156 |
-
*,
|
| 157 |
-
collection_name: str = Collections.ACCOUNT
|
| 158 |
-
) -> Account | None:
|
| 159 |
-
"""
|
| 160 |
-
Gets an account by name. Returns a Pydantic Account object or None.
|
| 161 |
-
@deprecated Use search_accounts instead for more flexibility.
|
| 162 |
-
"""
|
| 163 |
-
logger().info(f"Trying to retrieve account: {name}")
|
| 164 |
-
try:
|
| 165 |
-
collection = get_collection(collection_name)
|
| 166 |
-
now = datetime.now(timezone.utc)
|
| 167 |
-
account_dict = collection.find_one_and_update(
|
| 168 |
-
{"name": name},
|
| 169 |
-
{"$set": {"last_seen": now}},
|
| 170 |
-
return_document=True
|
| 171 |
-
)
|
| 172 |
-
if account_dict:
|
| 173 |
-
return Account.model_validate(account_dict)
|
| 174 |
-
return None
|
| 175 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 176 |
-
logger().error(f"Database error while getting account by name '{name}': {e}")
|
| 177 |
-
raise ActionFailed(f"A database error occurred while retrieving the account by name.") from e
|
| 178 |
-
|
| 179 |
-
def search_accounts(
|
| 180 |
-
query: str,
|
| 181 |
-
limit: int = 10,
|
| 182 |
-
*,
|
| 183 |
-
collection_name: str = Collections.ACCOUNT
|
| 184 |
-
) -> list[Account]:
|
| 185 |
-
"""Searches accounts by name, returning a list of Pydantic Account objects."""
|
| 186 |
-
if not query:
|
| 187 |
-
return []
|
| 188 |
-
|
| 189 |
-
logger().info(f"Searching accounts with query: '{query}', limit: {limit}")
|
| 190 |
-
pattern = re.compile(re.escape(query), re.IGNORECASE)
|
| 191 |
-
|
| 192 |
-
try:
|
| 193 |
-
collection = get_collection(collection_name)
|
| 194 |
-
cursor = collection.find({
|
| 195 |
-
"name": {"$regex": pattern}
|
| 196 |
-
}).sort("name", ASCENDING).limit(limit)
|
| 197 |
-
|
| 198 |
-
accounts = [Account.model_validate(doc) for doc in cursor]
|
| 199 |
-
logger().info(f"Found {len(accounts)} accounts matching query")
|
| 200 |
-
return accounts
|
| 201 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 202 |
-
logger().error(f"Database error during account search for query '{query}': {e}")
|
| 203 |
-
raise ActionFailed(f"A database error occurred during the account search.") from e
|
| 204 |
-
|
| 205 |
-
def get_all_accounts(
|
| 206 |
-
limit: int = 50,
|
| 207 |
-
*,
|
| 208 |
-
collection_name: str = Collections.ACCOUNT
|
| 209 |
-
) -> list[Account]:
|
| 210 |
-
"""Gets all accounts, returning a list of Pydantic Account objects."""
|
| 211 |
-
try:
|
| 212 |
-
collection = get_collection(collection_name)
|
| 213 |
-
cursor = collection.find().sort("name", ASCENDING).limit(limit)
|
| 214 |
-
|
| 215 |
-
accounts = [Account.model_validate(doc) for doc in cursor]
|
| 216 |
-
logger().info(f"Retrieved {len(accounts)} accounts")
|
| 217 |
-
return accounts
|
| 218 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 219 |
-
logger().error(f"Database error while getting all accounts: {e}")
|
| 220 |
-
raise ActionFailed(f"A database error occurred while retrieving all accounts.") from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/data/repositories/emr.py
DELETED
|
@@ -1,285 +0,0 @@
|
|
| 1 |
-
# src/data/repositories/emr.py
|
| 2 |
-
|
| 3 |
-
import json
|
| 4 |
-
from datetime import datetime, timezone
|
| 5 |
-
from typing import Any, Dict, List, Optional
|
| 6 |
-
|
| 7 |
-
from bson import ObjectId
|
| 8 |
-
from pymongo import ASCENDING, DESCENDING
|
| 9 |
-
from pymongo.errors import ConnectionFailure, OperationFailure, PyMongoError
|
| 10 |
-
|
| 11 |
-
from src.data.connection import ActionFailed, get_collection, setup_collection
|
| 12 |
-
from src.models.emr import EMRCreateRequest, EMRResponse, ExtractedData
|
| 13 |
-
from src.utils.logger import logger
|
| 14 |
-
|
| 15 |
-
EMR_COLLECTION = "emr"
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def init():
|
| 19 |
-
"""Create the EMR collection with validation schema."""
|
| 20 |
-
try:
|
| 21 |
-
setup_collection(EMR_COLLECTION, "schemas/emr_validator.json")
|
| 22 |
-
# Create indexes for better performance
|
| 23 |
-
collection = get_collection(EMR_COLLECTION)
|
| 24 |
-
collection.create_index("patient_id")
|
| 25 |
-
collection.create_index("doctor_id")
|
| 26 |
-
collection.create_index("session_id")
|
| 27 |
-
collection.create_index("message_id")
|
| 28 |
-
collection.create_index("created_at")
|
| 29 |
-
collection.create_index([("patient_id", ASCENDING), ("created_at", DESCENDING)])
|
| 30 |
-
collection.create_index([("patient_id", ASCENDING), ("doctor_id", ASCENDING)])
|
| 31 |
-
collection.create_index([("session_id", ASCENDING), ("created_at", DESCENDING)])
|
| 32 |
-
collection.create_index("confidence_score")
|
| 33 |
-
logger().info("EMR collection created successfully with indexes")
|
| 34 |
-
except Exception as e:
|
| 35 |
-
logger().error(f"Error creating EMR collection: {e}")
|
| 36 |
-
raise
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def create_emr_entry(emr_data: EMRCreateRequest, embeddings: List[float]) -> str:
|
| 40 |
-
"""Create a new EMR entry in the database."""
|
| 41 |
-
try:
|
| 42 |
-
collection = get_collection(EMR_COLLECTION)
|
| 43 |
-
|
| 44 |
-
# Check if EMR entry already exists for this message
|
| 45 |
-
existing = collection.find_one({"message_id": emr_data.message_id})
|
| 46 |
-
if existing:
|
| 47 |
-
logger().warning(f"EMR entry already exists for message {emr_data.message_id}")
|
| 48 |
-
return str(existing["_id"])
|
| 49 |
-
|
| 50 |
-
now = datetime.now(timezone.utc)
|
| 51 |
-
|
| 52 |
-
doc = {
|
| 53 |
-
"patient_id": emr_data.patient_id,
|
| 54 |
-
"doctor_id": emr_data.doctor_id,
|
| 55 |
-
"message_id": emr_data.message_id,
|
| 56 |
-
"session_id": emr_data.session_id,
|
| 57 |
-
"original_message": emr_data.original_message,
|
| 58 |
-
"extracted_data": emr_data.extracted_data.model_dump(),
|
| 59 |
-
"embeddings": embeddings,
|
| 60 |
-
"confidence_score": emr_data.confidence_score,
|
| 61 |
-
"created_at": now,
|
| 62 |
-
"updated_at": now
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
result = collection.insert_one(doc)
|
| 66 |
-
logger().info(f"Created EMR entry for patient {emr_data.patient_id}, message {emr_data.message_id}")
|
| 67 |
-
return str(result.inserted_id)
|
| 68 |
-
|
| 69 |
-
except Exception as e:
|
| 70 |
-
logger().error(f"Error creating EMR entry: {e}")
|
| 71 |
-
raise
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def get_emr_by_id(emr_id: str) -> Optional[Dict[str, Any]]:
|
| 75 |
-
"""Get an EMR entry by its ID."""
|
| 76 |
-
try:
|
| 77 |
-
collection = get_collection(EMR_COLLECTION)
|
| 78 |
-
result = collection.find_one({"_id": ObjectId(emr_id)})
|
| 79 |
-
if result:
|
| 80 |
-
result["_id"] = str(result["_id"])
|
| 81 |
-
return result
|
| 82 |
-
except Exception as e:
|
| 83 |
-
logger().error(f"Error getting EMR by ID {emr_id}: {e}")
|
| 84 |
-
return None
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
def get_patient_emr_entries(
|
| 88 |
-
patient_id: str,
|
| 89 |
-
limit: int = 20,
|
| 90 |
-
offset: int = 0
|
| 91 |
-
) -> List[Dict[str, Any]]:
|
| 92 |
-
"""Get EMR entries for a specific patient, ordered by creation date."""
|
| 93 |
-
try:
|
| 94 |
-
collection = get_collection(EMR_COLLECTION)
|
| 95 |
-
cursor = collection.find(
|
| 96 |
-
{"patient_id": patient_id}
|
| 97 |
-
).sort("created_at", DESCENDING).skip(offset).limit(limit)
|
| 98 |
-
|
| 99 |
-
results = []
|
| 100 |
-
for doc in cursor:
|
| 101 |
-
doc["_id"] = str(doc["_id"])
|
| 102 |
-
results.append(doc)
|
| 103 |
-
|
| 104 |
-
logger().info(f"Retrieved {len(results)} EMR entries for patient {patient_id}")
|
| 105 |
-
return results
|
| 106 |
-
|
| 107 |
-
except Exception as e:
|
| 108 |
-
logger().error(f"Error getting patient EMR entries: {e}")
|
| 109 |
-
return []
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def search_emr_by_semantic_similarity(
|
| 113 |
-
patient_id: str,
|
| 114 |
-
query_embeddings: List[float],
|
| 115 |
-
limit: int = 10,
|
| 116 |
-
threshold: float = 0.7
|
| 117 |
-
) -> List[Dict[str, Any]]:
|
| 118 |
-
"""Search EMR entries using semantic similarity with embeddings."""
|
| 119 |
-
try:
|
| 120 |
-
collection = get_collection(EMR_COLLECTION)
|
| 121 |
-
|
| 122 |
-
# Use MongoDB's vector search capabilities if available
|
| 123 |
-
# For now, we'll implement a simple cosine similarity search
|
| 124 |
-
pipeline = [
|
| 125 |
-
{"$match": {"patient_id": patient_id}},
|
| 126 |
-
{
|
| 127 |
-
"$addFields": {
|
| 128 |
-
"similarity": {
|
| 129 |
-
"$let": {
|
| 130 |
-
"vars": {
|
| 131 |
-
"dotProduct": {
|
| 132 |
-
"$reduce": {
|
| 133 |
-
"input": {"$range": [0, {"$size": "$embeddings"}]},
|
| 134 |
-
"initialValue": 0,
|
| 135 |
-
"in": {
|
| 136 |
-
"$add": [
|
| 137 |
-
"$$value",
|
| 138 |
-
{
|
| 139 |
-
"$multiply": [
|
| 140 |
-
{"$arrayElemAt": ["$embeddings", "$$this"]},
|
| 141 |
-
{"$arrayElemAt": [query_embeddings, "$$this"]}
|
| 142 |
-
]
|
| 143 |
-
}
|
| 144 |
-
]
|
| 145 |
-
}
|
| 146 |
-
}
|
| 147 |
-
},
|
| 148 |
-
"magnitudeA": {
|
| 149 |
-
"$sqrt": {
|
| 150 |
-
"$reduce": {
|
| 151 |
-
"input": "$embeddings",
|
| 152 |
-
"initialValue": 0,
|
| 153 |
-
"in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}
|
| 154 |
-
}
|
| 155 |
-
}
|
| 156 |
-
},
|
| 157 |
-
"magnitudeB": {
|
| 158 |
-
"$sqrt": {
|
| 159 |
-
"$reduce": {
|
| 160 |
-
"input": query_embeddings,
|
| 161 |
-
"initialValue": 0,
|
| 162 |
-
"in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}
|
| 163 |
-
}
|
| 164 |
-
}
|
| 165 |
-
}
|
| 166 |
-
},
|
| 167 |
-
"in": {
|
| 168 |
-
"$divide": [
|
| 169 |
-
"$$dotProduct",
|
| 170 |
-
{"$multiply": ["$$magnitudeA", "$$magnitudeB"]}
|
| 171 |
-
]
|
| 172 |
-
}
|
| 173 |
-
}
|
| 174 |
-
}
|
| 175 |
-
}
|
| 176 |
-
},
|
| 177 |
-
{"$match": {"similarity": {"$gte": threshold}}},
|
| 178 |
-
{"$sort": {"similarity": DESCENDING}},
|
| 179 |
-
{"$limit": limit}
|
| 180 |
-
]
|
| 181 |
-
|
| 182 |
-
results = list(collection.aggregate(pipeline))
|
| 183 |
-
for doc in results:
|
| 184 |
-
doc["_id"] = str(doc["_id"])
|
| 185 |
-
|
| 186 |
-
logger().info(f"Found {len(results)} similar EMR entries for patient {patient_id}")
|
| 187 |
-
return results
|
| 188 |
-
|
| 189 |
-
except Exception as e:
|
| 190 |
-
logger().error(f"Error searching EMR by similarity: {e}")
|
| 191 |
-
return []
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
def update_emr_entry(emr_id: str, updates: Dict[str, Any]) -> bool:
|
| 195 |
-
"""Update an EMR entry."""
|
| 196 |
-
try:
|
| 197 |
-
collection = get_collection(EMR_COLLECTION)
|
| 198 |
-
updates["updated_at"] = datetime.now(timezone.utc)
|
| 199 |
-
|
| 200 |
-
result = collection.update_one(
|
| 201 |
-
{"_id": ObjectId(emr_id)},
|
| 202 |
-
{"$set": updates}
|
| 203 |
-
)
|
| 204 |
-
|
| 205 |
-
success = result.modified_count > 0
|
| 206 |
-
if success:
|
| 207 |
-
logger().info(f"Updated EMR entry {emr_id}")
|
| 208 |
-
else:
|
| 209 |
-
logger().warning(f"No EMR entry found with ID {emr_id}")
|
| 210 |
-
|
| 211 |
-
return success
|
| 212 |
-
|
| 213 |
-
except Exception as e:
|
| 214 |
-
logger().error(f"Error updating EMR entry {emr_id}: {e}")
|
| 215 |
-
return False
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
def delete_emr_entry(emr_id: str) -> bool:
|
| 219 |
-
"""Delete an EMR entry."""
|
| 220 |
-
try:
|
| 221 |
-
collection = get_collection(EMR_COLLECTION)
|
| 222 |
-
result = collection.delete_one({"_id": ObjectId(emr_id)})
|
| 223 |
-
|
| 224 |
-
success = result.deleted_count > 0
|
| 225 |
-
if success:
|
| 226 |
-
logger().info(f"Deleted EMR entry {emr_id}")
|
| 227 |
-
else:
|
| 228 |
-
logger().warning(f"No EMR entry found with ID {emr_id}")
|
| 229 |
-
|
| 230 |
-
return success
|
| 231 |
-
|
| 232 |
-
except Exception as e:
|
| 233 |
-
logger().error(f"Error deleting EMR entry {emr_id}: {e}")
|
| 234 |
-
return False
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
def check_emr_exists(message_id: str) -> bool:
|
| 238 |
-
"""Check if an EMR entry already exists for a message."""
|
| 239 |
-
try:
|
| 240 |
-
collection = get_collection(EMR_COLLECTION)
|
| 241 |
-
existing = collection.find_one({"message_id": message_id})
|
| 242 |
-
return existing is not None
|
| 243 |
-
except Exception as e:
|
| 244 |
-
logger().error(f"Error checking EMR existence: {e}")
|
| 245 |
-
return False
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
def get_emr_statistics(patient_id: str) -> Dict[str, Any]:
|
| 249 |
-
"""Get EMR statistics for a patient."""
|
| 250 |
-
try:
|
| 251 |
-
collection = get_collection(EMR_COLLECTION)
|
| 252 |
-
|
| 253 |
-
pipeline = [
|
| 254 |
-
{"$match": {"patient_id": patient_id}},
|
| 255 |
-
{
|
| 256 |
-
"$group": {
|
| 257 |
-
"_id": None,
|
| 258 |
-
"total_entries": {"$sum": 1},
|
| 259 |
-
"avg_confidence": {"$avg": "$confidence_score"},
|
| 260 |
-
"latest_entry": {"$max": "$created_at"},
|
| 261 |
-
"diagnosis_count": {
|
| 262 |
-
"$sum": {"$size": "$extracted_data.diagnosis"}
|
| 263 |
-
},
|
| 264 |
-
"medication_count": {
|
| 265 |
-
"$sum": {"$size": "$extracted_data.medications"}
|
| 266 |
-
}
|
| 267 |
-
}
|
| 268 |
-
}
|
| 269 |
-
]
|
| 270 |
-
|
| 271 |
-
result = list(collection.aggregate(pipeline))
|
| 272 |
-
if result:
|
| 273 |
-
return result[0]
|
| 274 |
-
else:
|
| 275 |
-
return {
|
| 276 |
-
"total_entries": 0,
|
| 277 |
-
"avg_confidence": 0,
|
| 278 |
-
"latest_entry": None,
|
| 279 |
-
"diagnosis_count": 0,
|
| 280 |
-
"medication_count": 0
|
| 281 |
-
}
|
| 282 |
-
|
| 283 |
-
except Exception as e:
|
| 284 |
-
logger().error(f"Error getting EMR statistics: {e}")
|
| 285 |
-
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/data/repositories/medical_memory.py
DELETED
|
@@ -1,148 +0,0 @@
|
|
| 1 |
-
# src/data/repositories/medical_memory.py
|
| 2 |
-
"""
|
| 3 |
-
Medical memory management operations for MongoDB.
|
| 4 |
-
Medical memories are unstructured summaries, often with vector embeddings for semantic search.
|
| 5 |
-
|
| 6 |
-
## Fields
|
| 7 |
-
_id: index
|
| 8 |
-
patient_id: The patient this memory relates to
|
| 9 |
-
doctor_id: The doctor involved in the context of this memory
|
| 10 |
-
session_id: The chat session this memory was derived from (optional)
|
| 11 |
-
summary: The unstructured text summary of the medical context
|
| 12 |
-
embedding: The vector embedding of the summary for semantic search (optional)
|
| 13 |
-
created_at: The timestamp when the memory was created
|
| 14 |
-
"""
|
| 15 |
-
from datetime import datetime, timezone
|
| 16 |
-
|
| 17 |
-
import numpy as np
|
| 18 |
-
from bson import ObjectId
|
| 19 |
-
from bson.errors import InvalidId
|
| 20 |
-
from pymongo import DESCENDING
|
| 21 |
-
from pymongo.errors import ConnectionFailure, PyMongoError, WriteError
|
| 22 |
-
|
| 23 |
-
from src.data.connection import (ActionFailed, Collections, get_collection,
|
| 24 |
-
setup_collection)
|
| 25 |
-
from src.models.medical import MedicalMemory, SemanticSearchResult
|
| 26 |
-
from src.utils.logger import logger
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def init(
|
| 30 |
-
*,
|
| 31 |
-
collection_name: str = Collections.MEDICAL_MEMORY,
|
| 32 |
-
validator_path: str = "schemas/medical_memory_validator.json",
|
| 33 |
-
drop: bool = False
|
| 34 |
-
):
|
| 35 |
-
"""Initializes the medical_memory collection, applying schema validation."""
|
| 36 |
-
try:
|
| 37 |
-
if drop:
|
| 38 |
-
get_collection(collection_name).drop()
|
| 39 |
-
setup_collection(collection_name, validator_path)
|
| 40 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 41 |
-
logger().error(f"Failed to initialize collection '{collection_name}': {e}")
|
| 42 |
-
raise ActionFailed(f"Database operation failed during initialization: {e}") from e
|
| 43 |
-
|
| 44 |
-
def create_memory(
|
| 45 |
-
patient_id: str,
|
| 46 |
-
doctor_id: str,
|
| 47 |
-
summary: str,
|
| 48 |
-
session_id: str | None = None,
|
| 49 |
-
embedding: list[float] | None = None,
|
| 50 |
-
*,
|
| 51 |
-
collection_name: str = Collections.MEDICAL_MEMORY
|
| 52 |
-
) -> str:
|
| 53 |
-
"""Saves a new medical memory summary, raising ActionFailed on error."""
|
| 54 |
-
try:
|
| 55 |
-
collection = get_collection(collection_name)
|
| 56 |
-
doc = {
|
| 57 |
-
"patient_id": ObjectId(patient_id),
|
| 58 |
-
"doctor_id": ObjectId(doctor_id),
|
| 59 |
-
"summary": summary,
|
| 60 |
-
"created_at": datetime.now(timezone.utc)
|
| 61 |
-
}
|
| 62 |
-
if session_id:
|
| 63 |
-
doc["session_id"] = ObjectId(session_id)
|
| 64 |
-
if embedding:
|
| 65 |
-
doc["embedding"] = embedding
|
| 66 |
-
|
| 67 |
-
result = collection.insert_one(doc)
|
| 68 |
-
return str(result.inserted_id)
|
| 69 |
-
except InvalidId as e:
|
| 70 |
-
logger().error(f"Invalid ObjectId format provided for medical memory: {e}")
|
| 71 |
-
raise ActionFailed("Patient, Doctor, or Session ID is not a valid format.") from e
|
| 72 |
-
except (WriteError, ConnectionFailure, PyMongoError) as e:
|
| 73 |
-
logger().error(f"Failed to create medical memory: {e}")
|
| 74 |
-
raise ActionFailed("A database error occurred while creating the medical memory.") from e
|
| 75 |
-
|
| 76 |
-
def get_recent_memories(
|
| 77 |
-
patient_id: str,
|
| 78 |
-
limit: int = 20,
|
| 79 |
-
*,
|
| 80 |
-
collection_name: str = Collections.MEDICAL_MEMORY
|
| 81 |
-
) -> list[MedicalMemory]:
|
| 82 |
-
"""Retrieves the most recent memory summaries for a patient."""
|
| 83 |
-
try:
|
| 84 |
-
obj_patient_id = ObjectId(patient_id)
|
| 85 |
-
collection = get_collection(collection_name)
|
| 86 |
-
cursor = collection.find(
|
| 87 |
-
{"patient_id": obj_patient_id}
|
| 88 |
-
).sort("created_at", DESCENDING).limit(limit)
|
| 89 |
-
|
| 90 |
-
return [MedicalMemory.model_validate(doc) for doc in cursor]
|
| 91 |
-
except InvalidId as e:
|
| 92 |
-
logger().error(f"Invalid patient_id format for get_recent_memories: '{patient_id}'")
|
| 93 |
-
raise ActionFailed("The provided patient ID is not a valid format.") from e
|
| 94 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 95 |
-
logger().error(f"Database error retrieving recent memories for patient '{patient_id}': {e}")
|
| 96 |
-
raise ActionFailed("A database error occurred while retrieving recent memories.") from e
|
| 97 |
-
|
| 98 |
-
def search_memories_semantic(
|
| 99 |
-
patient_id: str,
|
| 100 |
-
query_embedding: list[float],
|
| 101 |
-
limit: int = 5,
|
| 102 |
-
*,
|
| 103 |
-
collection_name: str = Collections.MEDICAL_MEMORY
|
| 104 |
-
) -> list[SemanticSearchResult]:
|
| 105 |
-
"""Searches memory summaries using semantic similarity with embeddings."""
|
| 106 |
-
try:
|
| 107 |
-
obj_patient_id = ObjectId(patient_id)
|
| 108 |
-
collection = get_collection(collection_name)
|
| 109 |
-
|
| 110 |
-
# In a real-world scenario, this would be an Atlas Vector Search query.
|
| 111 |
-
# This implementation fetches all docs and calculates similarity in the client.
|
| 112 |
-
docs = list(collection.find({
|
| 113 |
-
"patient_id": obj_patient_id,
|
| 114 |
-
"embedding": {"$exists": True}
|
| 115 |
-
}))
|
| 116 |
-
|
| 117 |
-
if not docs:
|
| 118 |
-
return []
|
| 119 |
-
|
| 120 |
-
query_vec = np.array(query_embedding, dtype="float32")
|
| 121 |
-
results = []
|
| 122 |
-
for doc in docs:
|
| 123 |
-
doc_vec = np.array(doc["embedding"], dtype="float32")
|
| 124 |
-
|
| 125 |
-
# Calculate cosine similarity
|
| 126 |
-
dot_product = np.dot(query_vec, doc_vec)
|
| 127 |
-
norm_query = np.linalg.norm(query_vec)
|
| 128 |
-
norm_doc = np.linalg.norm(doc_vec)
|
| 129 |
-
|
| 130 |
-
if norm_query > 0 and norm_doc > 0:
|
| 131 |
-
similarity = float(dot_product / (norm_query * norm_doc))
|
| 132 |
-
result_data = {
|
| 133 |
-
"summary": doc["summary"],
|
| 134 |
-
"similarity_score": similarity,
|
| 135 |
-
"created_at": doc["created_at"],
|
| 136 |
-
"session_id": doc.get("session_id")
|
| 137 |
-
}
|
| 138 |
-
results.append(SemanticSearchResult.model_validate(result_data))
|
| 139 |
-
|
| 140 |
-
# Sort by similarity (highest first) and return top results
|
| 141 |
-
results.sort(key=lambda x: x.similarity_score, reverse=True)
|
| 142 |
-
return results[:limit]
|
| 143 |
-
except InvalidId as e:
|
| 144 |
-
logger().error(f"Invalid patient_id format for semantic search: '{patient_id}'")
|
| 145 |
-
raise ActionFailed("The provided patient ID is not a valid format.") from e
|
| 146 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 147 |
-
logger().error(f"Database error during semantic search for patient '{patient_id}': {e}")
|
| 148 |
-
raise ActionFailed("A database error occurred during the semantic search.") from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/data/repositories/medical_record.py
DELETED
|
@@ -1,93 +0,0 @@
|
|
| 1 |
-
# src/data/repositories/medical_record.py
|
| 2 |
-
"""
|
| 3 |
-
Medical record management operations for MongoDB.
|
| 4 |
-
Medical records are structured, factual pieces of information about a patient.
|
| 5 |
-
|
| 6 |
-
## Fields
|
| 7 |
-
_id: index
|
| 8 |
-
patient_id: The patient this record belongs to
|
| 9 |
-
doctor_id: The doctor who created or is associated with this record
|
| 10 |
-
record_type: The category of the record (e.g., 'Consultation', 'LabResult')
|
| 11 |
-
details: An object containing the specific, structured data for the record
|
| 12 |
-
created_at: The timestamp when the record was created
|
| 13 |
-
updated_at: The timestamp when the record was last modified
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
from datetime import datetime, timezone
|
| 17 |
-
from typing import Any
|
| 18 |
-
|
| 19 |
-
from bson import ObjectId
|
| 20 |
-
from bson.errors import InvalidId
|
| 21 |
-
from pymongo import ASCENDING
|
| 22 |
-
from pymongo.errors import ConnectionFailure, PyMongoError, WriteError
|
| 23 |
-
|
| 24 |
-
from src.data.connection import (ActionFailed, Collections, get_collection,
|
| 25 |
-
setup_collection)
|
| 26 |
-
from src.models.medical import MedicalRecord
|
| 27 |
-
from src.utils.logger import logger
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def init(
|
| 31 |
-
*,
|
| 32 |
-
collection_name: str = Collections.MEDICAL_RECORDS,
|
| 33 |
-
validator_path: str = "schemas/medical_record_validator.json",
|
| 34 |
-
drop: bool = False
|
| 35 |
-
):
|
| 36 |
-
"""Initializes the medical_records collection, applying schema validation."""
|
| 37 |
-
try:
|
| 38 |
-
if drop:
|
| 39 |
-
get_collection(collection_name).drop()
|
| 40 |
-
setup_collection(collection_name, validator_path)
|
| 41 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 42 |
-
logger().error(f"Failed to initialize collection '{collection_name}': {e}")
|
| 43 |
-
raise ActionFailed(f"Database operation failed during initialization: {e}") from e
|
| 44 |
-
|
| 45 |
-
def create_medical_record(
|
| 46 |
-
patient_id: str,
|
| 47 |
-
doctor_id: str,
|
| 48 |
-
record_type: str,
|
| 49 |
-
details: dict[str, Any],
|
| 50 |
-
*,
|
| 51 |
-
collection_name: str = Collections.MEDICAL_RECORDS
|
| 52 |
-
) -> str:
|
| 53 |
-
"""Creates a new medical record, raising ActionFailed on error."""
|
| 54 |
-
now = datetime.now(timezone.utc)
|
| 55 |
-
try:
|
| 56 |
-
collection = get_collection(collection_name)
|
| 57 |
-
record_data = {
|
| 58 |
-
"patient_id": ObjectId(patient_id),
|
| 59 |
-
"doctor_id": ObjectId(doctor_id),
|
| 60 |
-
"record_type": record_type,
|
| 61 |
-
"details": details,
|
| 62 |
-
"created_at": now,
|
| 63 |
-
"updated_at": now
|
| 64 |
-
}
|
| 65 |
-
result = collection.insert_one(record_data)
|
| 66 |
-
return str(result.inserted_id)
|
| 67 |
-
except InvalidId as e:
|
| 68 |
-
logger().error(f"Invalid ObjectId format provided for medical record: {e}")
|
| 69 |
-
raise ActionFailed("Patient ID or Doctor ID is not a valid format.") from e
|
| 70 |
-
except (WriteError, ConnectionFailure, PyMongoError) as e:
|
| 71 |
-
logger().error(f"Failed to create medical record: {e}")
|
| 72 |
-
raise ActionFailed("A database error occurred while creating the medical record.") from e
|
| 73 |
-
|
| 74 |
-
def get_records_for_patient(
|
| 75 |
-
patient_id: str,
|
| 76 |
-
*,
|
| 77 |
-
collection_name: str = Collections.MEDICAL_RECORDS
|
| 78 |
-
) -> list[MedicalRecord]:
|
| 79 |
-
"""Retrieves all medical records for a patient, sorted by creation date."""
|
| 80 |
-
try:
|
| 81 |
-
obj_patient_id = ObjectId(patient_id)
|
| 82 |
-
collection = get_collection(collection_name)
|
| 83 |
-
cursor = collection.find(
|
| 84 |
-
{"patient_id": obj_patient_id}
|
| 85 |
-
).sort("created_at", ASCENDING)
|
| 86 |
-
|
| 87 |
-
return [MedicalRecord.model_validate(doc) for doc in cursor]
|
| 88 |
-
except InvalidId as e:
|
| 89 |
-
logger().error(f"Invalid patient_id format for get_records_for_patient: '{patient_id}'")
|
| 90 |
-
raise ActionFailed("The provided patient ID is not a valid format.") from e
|
| 91 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 92 |
-
logger().error(f"Database error retrieving records for patient '{patient_id}': {e}")
|
| 93 |
-
raise ActionFailed("A database error occurred while retrieving medical records.") from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/data/repositories/patient.py
DELETED
|
@@ -1,176 +0,0 @@
|
|
| 1 |
-
# src/data/repositories/patient.py
|
| 2 |
-
"""
|
| 3 |
-
Patient management operations for MongoDB.
|
| 4 |
-
A patient is a person who has been assigned to a doctor for treatment.
|
| 5 |
-
|
| 6 |
-
## Fields
|
| 7 |
-
_id: index
|
| 8 |
-
name: The name of the patient
|
| 9 |
-
age: How old the patient is
|
| 10 |
-
sex: Male or female
|
| 11 |
-
ethnicity: Geneological information
|
| 12 |
-
address: Where they live
|
| 13 |
-
phone: What their phone number is
|
| 14 |
-
email: What their email address it
|
| 15 |
-
medications: Any medications they are currently taking
|
| 16 |
-
past_assessment_summary: Summarisation of past assessments
|
| 17 |
-
assigned_doctor_id: The id of the account assigned to this patient
|
| 18 |
-
created_at: The timestamp when the patient was created
|
| 19 |
-
updated_at: The timestamp when the patient data was last modified
|
| 20 |
-
"""
|
| 21 |
-
|
| 22 |
-
import re
|
| 23 |
-
from datetime import datetime, timezone
|
| 24 |
-
from typing import Any
|
| 25 |
-
|
| 26 |
-
from bson import ObjectId
|
| 27 |
-
from bson.errors import InvalidId
|
| 28 |
-
from pymongo import ASCENDING
|
| 29 |
-
from pymongo.errors import ConnectionFailure, PyMongoError, WriteError
|
| 30 |
-
|
| 31 |
-
from src.data.connection import (ActionFailed, Collections, get_collection,
|
| 32 |
-
setup_collection)
|
| 33 |
-
from src.models.patient import Patient
|
| 34 |
-
from src.utils.logger import logger
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def init(
|
| 38 |
-
*,
|
| 39 |
-
collection_name: str = Collections.PATIENT,
|
| 40 |
-
validator_path: str = "schemas/patient_validator.json",
|
| 41 |
-
drop: bool = False
|
| 42 |
-
):
|
| 43 |
-
"""Initializes the collection, applying schema and indexes."""
|
| 44 |
-
try:
|
| 45 |
-
if drop:
|
| 46 |
-
get_collection(collection_name).drop()
|
| 47 |
-
setup_collection(collection_name, validator_path)
|
| 48 |
-
get_collection(collection_name).create_index("assigned_doctor_id")
|
| 49 |
-
logger("Init").info(f"Created index on assigned_doctor_id in '{collection_name}'")
|
| 50 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 51 |
-
logger().error(f"Failed to initialize collection '{collection_name}': {e}")
|
| 52 |
-
raise ActionFailed(f"Database operation failed during initialization: {e}") from e
|
| 53 |
-
|
| 54 |
-
def create_patient(
|
| 55 |
-
name: str,
|
| 56 |
-
age: int,
|
| 57 |
-
sex: str,
|
| 58 |
-
ethnicity: str | None = None,
|
| 59 |
-
address: str | None = None,
|
| 60 |
-
phone: str | None = None,
|
| 61 |
-
email: str | None = None,
|
| 62 |
-
medications: list[str] | None = None,
|
| 63 |
-
past_assessment_summary: str | None = None,
|
| 64 |
-
assigned_doctor_id: str | None = None,
|
| 65 |
-
*,
|
| 66 |
-
collection_name: str = Collections.PATIENT
|
| 67 |
-
) -> str:
|
| 68 |
-
"""Creates a new patient record, raising ActionFailed on error."""
|
| 69 |
-
now = datetime.now(timezone.utc)
|
| 70 |
-
patient_data = {
|
| 71 |
-
"name": name,
|
| 72 |
-
"age": age,
|
| 73 |
-
"sex": sex,
|
| 74 |
-
"created_at": now,
|
| 75 |
-
"updated_at": now
|
| 76 |
-
}
|
| 77 |
-
# Add optional fields to the dictionary
|
| 78 |
-
if ethnicity: patient_data["ethnicity"] = ethnicity
|
| 79 |
-
if address: patient_data["address"] = address
|
| 80 |
-
if phone: patient_data["phone"] = phone
|
| 81 |
-
if email: patient_data["email"] = email
|
| 82 |
-
if medications: patient_data["medications"] = medications
|
| 83 |
-
if past_assessment_summary: patient_data["past_assessment_summary"] = past_assessment_summary
|
| 84 |
-
|
| 85 |
-
try:
|
| 86 |
-
collection = get_collection(collection_name)
|
| 87 |
-
if assigned_doctor_id:
|
| 88 |
-
patient_data["assigned_doctor_id"] = ObjectId(assigned_doctor_id)
|
| 89 |
-
|
| 90 |
-
result = collection.insert_one(patient_data)
|
| 91 |
-
return str(result.inserted_id)
|
| 92 |
-
except InvalidId as e:
|
| 93 |
-
logger().error(f"Invalid assigned_doctor_id format: '{assigned_doctor_id}'")
|
| 94 |
-
raise ActionFailed(f"The assigned doctor ID is not a valid format.") from e
|
| 95 |
-
except WriteError as e:
|
| 96 |
-
logger().error(f"Failed to create patient due to validation or write error: {e}")
|
| 97 |
-
raise ActionFailed(f"Patient could not be created due to invalid data.") from e
|
| 98 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 99 |
-
logger().error(f"Database error while creating patient: {e}")
|
| 100 |
-
raise ActionFailed(f"A database error occurred while creating the patient.") from e
|
| 101 |
-
|
| 102 |
-
def get_patient_by_id(
|
| 103 |
-
patient_id: str,
|
| 104 |
-
*,
|
| 105 |
-
collection_name: str = Collections.PATIENT
|
| 106 |
-
) -> Patient | None:
|
| 107 |
-
"""Gets a patient by ID. Returns a Pydantic Patient object or None."""
|
| 108 |
-
logger().info(f"Searching for patient with id '{patient_id}'")
|
| 109 |
-
try:
|
| 110 |
-
obj_patient_id = ObjectId(patient_id)
|
| 111 |
-
collection = get_collection(collection_name)
|
| 112 |
-
patient_dict = collection.find_one({"_id": obj_patient_id})
|
| 113 |
-
|
| 114 |
-
if patient_dict:
|
| 115 |
-
return Patient.model_validate(patient_dict)
|
| 116 |
-
return None
|
| 117 |
-
except InvalidId as e:
|
| 118 |
-
logger().error(f"Invalid patient_id format for get: '{patient_id}'")
|
| 119 |
-
raise ActionFailed(f"The provided patient ID '{patient_id}' is not a valid format.") from e
|
| 120 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 121 |
-
logger().error(f"Database error in get_patient_by_id for ID '{patient_id}': {e}")
|
| 122 |
-
raise ActionFailed(f"A database error occurred while retrieving the patient.") from e
|
| 123 |
-
|
| 124 |
-
# TODO Make this more rigidly typed, maybe merge with create_patient?
|
| 125 |
-
def update_patient_profile(
|
| 126 |
-
patient_id: str,
|
| 127 |
-
updates: dict[str, Any],
|
| 128 |
-
*,
|
| 129 |
-
collection_name: str = Collections.PATIENT
|
| 130 |
-
) -> int:
|
| 131 |
-
"""Updates a patient's profile, raising ActionFailed on error."""
|
| 132 |
-
try:
|
| 133 |
-
obj_patient_id = ObjectId(patient_id)
|
| 134 |
-
collection = get_collection(collection_name)
|
| 135 |
-
updates["updated_at"] = datetime.now(timezone.utc)
|
| 136 |
-
|
| 137 |
-
result = collection.update_one(
|
| 138 |
-
{"_id": obj_patient_id},
|
| 139 |
-
{"$set": updates}
|
| 140 |
-
)
|
| 141 |
-
return result.modified_count
|
| 142 |
-
except InvalidId as e:
|
| 143 |
-
logger().error(f"Invalid patient_id format for update: '{patient_id}'")
|
| 144 |
-
raise ActionFailed(f"The provided patient ID '{patient_id}' is not a valid format.") from e
|
| 145 |
-
except WriteError as e:
|
| 146 |
-
logger().error(f"Failed to update patient '{patient_id}' due to validation or write error: {e}")
|
| 147 |
-
raise ActionFailed(f"Patient profile could not be updated due to invalid data.") from e
|
| 148 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 149 |
-
logger().error(f"Database error in update_patient_profile for ID '{patient_id}': {e}")
|
| 150 |
-
raise ActionFailed(f"A database error occurred while updating the patient profile.") from e
|
| 151 |
-
|
| 152 |
-
def search_patients(
|
| 153 |
-
query: str,
|
| 154 |
-
limit: int = 10,
|
| 155 |
-
*,
|
| 156 |
-
collection_name: str = Collections.PATIENT
|
| 157 |
-
) -> list[Patient]:
|
| 158 |
-
"""Searches patients by name, returning a list of Pydantic Patient objects."""
|
| 159 |
-
if not query:
|
| 160 |
-
return []
|
| 161 |
-
|
| 162 |
-
logger().info(f"Searching patients with query: '{query}', limit: {limit}")
|
| 163 |
-
pattern = re.compile(re.escape(query), re.IGNORECASE)
|
| 164 |
-
|
| 165 |
-
try:
|
| 166 |
-
collection = get_collection(collection_name)
|
| 167 |
-
cursor = collection.find({
|
| 168 |
-
"name": {"$regex": pattern}
|
| 169 |
-
}).sort("name", ASCENDING).limit(limit)
|
| 170 |
-
|
| 171 |
-
patients = [Patient.model_validate(doc) for doc in cursor]
|
| 172 |
-
logger().info(f"Found {len(patients)} patients matching query")
|
| 173 |
-
return patients
|
| 174 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 175 |
-
logger().error(f"Database error during patient search for query '{query}': {e}")
|
| 176 |
-
raise ActionFailed(f"A database error occurred during the patient search.") from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/data/repositories/session.py
DELETED
|
@@ -1,289 +0,0 @@
|
|
| 1 |
-
# src/data/repositories/session.py
|
| 2 |
-
"""
|
| 3 |
-
Chat session management operations for MongoDB.
|
| 4 |
-
A session is owned by an account and is related to a patient.
|
| 5 |
-
A session contains many messages.
|
| 6 |
-
|
| 7 |
-
## Fields
|
| 8 |
-
_id: index
|
| 9 |
-
account_id: The user account who owns this session
|
| 10 |
-
patient_id: The patient being discussed in this session
|
| 11 |
-
title: The title of this session
|
| 12 |
-
created_at: When this session was created
|
| 13 |
-
updated_at: When this session was updated, new message or title
|
| 14 |
-
messages: An array of messages sent in this session
|
| 15 |
-
messages._id: index, the order the messages were sent
|
| 16 |
-
messages.sent_by_user: Whether or not this was sent by the user or the ai
|
| 17 |
-
messages.content: The actual contents of the message
|
| 18 |
-
messages.timestamp: When the message was sent
|
| 19 |
-
"""
|
| 20 |
-
|
| 21 |
-
from datetime import datetime, timedelta, timezone
|
| 22 |
-
from typing import Any
|
| 23 |
-
|
| 24 |
-
from bson import ObjectId
|
| 25 |
-
from bson.errors import InvalidId
|
| 26 |
-
from pymongo import DESCENDING
|
| 27 |
-
from pymongo.errors import ConnectionFailure, PyMongoError, WriteError
|
| 28 |
-
|
| 29 |
-
from src.data.connection import (ActionFailed, Collections, get_collection,
|
| 30 |
-
setup_collection)
|
| 31 |
-
from src.models.session import Message, Session
|
| 32 |
-
from src.utils.logger import logger
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def init(
|
| 36 |
-
*,
|
| 37 |
-
collection_name: str = Collections.SESSION,
|
| 38 |
-
validator_path: str = "schemas/session_validator.json",
|
| 39 |
-
drop: bool = False
|
| 40 |
-
):
|
| 41 |
-
"""Initializes the collection, applying schema and indexes."""
|
| 42 |
-
try:
|
| 43 |
-
if drop:
|
| 44 |
-
get_collection(collection_name).drop()
|
| 45 |
-
setup_collection(collection_name, validator_path)
|
| 46 |
-
get_collection(collection_name).create_index("messages._id")
|
| 47 |
-
logger("Init").info(f"Created index on messages._id in '{collection_name}'")
|
| 48 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 49 |
-
logger().error(f"Failed to initialize collection '{collection_name}': {e}")
|
| 50 |
-
raise ActionFailed(f"Database operation failed during initialization: {e}") from e
|
| 51 |
-
|
| 52 |
-
def create_session(
|
| 53 |
-
account_id: str,
|
| 54 |
-
patient_id: str,
|
| 55 |
-
title: str,
|
| 56 |
-
*,
|
| 57 |
-
collection_name: str = Collections.SESSION
|
| 58 |
-
) -> Session:
|
| 59 |
-
"""Creates a new chat session, returning a Pydantic Session object."""
|
| 60 |
-
now = datetime.now(timezone.utc)
|
| 61 |
-
try:
|
| 62 |
-
collection = get_collection(collection_name)
|
| 63 |
-
session_data: dict[str, Any] = {
|
| 64 |
-
"account_id": ObjectId(account_id),
|
| 65 |
-
"patient_id": ObjectId(patient_id),
|
| 66 |
-
"title": title,
|
| 67 |
-
"created_at": now,
|
| 68 |
-
"updated_at": now,
|
| 69 |
-
"messages": []
|
| 70 |
-
}
|
| 71 |
-
result = collection.insert_one(session_data)
|
| 72 |
-
|
| 73 |
-
# Retrieve the newly created document to ensure all data is consistent
|
| 74 |
-
created_doc = collection.find_one({"_id": result.inserted_id})
|
| 75 |
-
if not created_doc:
|
| 76 |
-
raise ActionFailed("Failed to retrieve session immediately after creation.")
|
| 77 |
-
|
| 78 |
-
return Session.model_validate(created_doc)
|
| 79 |
-
except InvalidId as e:
|
| 80 |
-
logger().error(f"Invalid ObjectId format provided for session creation: {e}")
|
| 81 |
-
raise ActionFailed("Account ID or Patient ID is not a valid format.") from e
|
| 82 |
-
except (WriteError, ConnectionFailure, PyMongoError) as e:
|
| 83 |
-
logger().error(f"Failed to create chat session: {e}")
|
| 84 |
-
raise ActionFailed("A database error occurred while creating the session.") from e
|
| 85 |
-
|
| 86 |
-
def get_user_sessions(
|
| 87 |
-
account_id: str,
|
| 88 |
-
limit: int = 20,
|
| 89 |
-
*,
|
| 90 |
-
collection_name: str = Collections.SESSION
|
| 91 |
-
) -> list[Session]:
|
| 92 |
-
"""Retrieves sessions for a user, returning a list of Pydantic Session objects."""
|
| 93 |
-
try:
|
| 94 |
-
obj_account_id = ObjectId(account_id)
|
| 95 |
-
collection = get_collection(collection_name)
|
| 96 |
-
cursor = collection.find(
|
| 97 |
-
{"account_id": obj_account_id}
|
| 98 |
-
).sort("updated_at", DESCENDING).limit(limit)
|
| 99 |
-
|
| 100 |
-
return [Session.model_validate(doc) for doc in cursor]
|
| 101 |
-
except InvalidId as e:
|
| 102 |
-
logger().error(f"Invalid account_id format for get_user_sessions: '{account_id}'")
|
| 103 |
-
raise ActionFailed("The provided account ID is not a valid format.") from e
|
| 104 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 105 |
-
logger().error(f"Database error listing sessions for account '{account_id}': {e}")
|
| 106 |
-
raise ActionFailed("A database error occurred while retrieving user sessions.") from e
|
| 107 |
-
|
| 108 |
-
def list_patient_sessions(
|
| 109 |
-
patient_id: str,
|
| 110 |
-
limit: int = 20,
|
| 111 |
-
*,
|
| 112 |
-
collection_name: str = Collections.SESSION
|
| 113 |
-
) -> list[Session]:
|
| 114 |
-
"""Retrieves sessions for a patient, returning a list of Pydantic Session objects."""
|
| 115 |
-
try:
|
| 116 |
-
obj_patient_id = ObjectId(patient_id)
|
| 117 |
-
collection = get_collection(collection_name)
|
| 118 |
-
cursor = collection.find(
|
| 119 |
-
{"patient_id": obj_patient_id}
|
| 120 |
-
).sort("updated_at", DESCENDING).limit(limit)
|
| 121 |
-
|
| 122 |
-
return [Session.model_validate(doc) for doc in cursor]
|
| 123 |
-
except InvalidId as e:
|
| 124 |
-
logger().error(f"Invalid patient_id format for list_patient_sessions: '{patient_id}'")
|
| 125 |
-
raise ActionFailed("The provided patient ID is not a valid format.") from e
|
| 126 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 127 |
-
logger().error(f"Database error listing sessions for patient '{patient_id}': {e}")
|
| 128 |
-
raise ActionFailed("A database error occurred while retrieving patient sessions.") from e
|
| 129 |
-
|
| 130 |
-
def get_session(
|
| 131 |
-
session_id: str,
|
| 132 |
-
*,
|
| 133 |
-
collection_name: str = Collections.SESSION
|
| 134 |
-
) -> Session | None:
|
| 135 |
-
"""Retrieves a session. Returns a Pydantic Session object or None."""
|
| 136 |
-
try:
|
| 137 |
-
obj_session_id = ObjectId(session_id)
|
| 138 |
-
collection = get_collection(collection_name)
|
| 139 |
-
session_dict = collection.find_one({"_id": obj_session_id})
|
| 140 |
-
|
| 141 |
-
if session_dict:
|
| 142 |
-
return Session.model_validate(session_dict)
|
| 143 |
-
return None
|
| 144 |
-
except InvalidId as e:
|
| 145 |
-
logger().error(f"Invalid session_id format for get_session: '{session_id}'")
|
| 146 |
-
raise ActionFailed("The provided session ID is not a valid format.") from e
|
| 147 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 148 |
-
logger().error(f"Database error retrieving session '{session_id}': {e}")
|
| 149 |
-
raise ActionFailed("A database error occurred while retrieving the session.") from e
|
| 150 |
-
|
| 151 |
-
def get_session_messages(
|
| 152 |
-
session_id: str,
|
| 153 |
-
limit: int | None = None,
|
| 154 |
-
*,
|
| 155 |
-
collection_name: str = Collections.SESSION
|
| 156 |
-
) -> list[Message]:
|
| 157 |
-
"""Gets messages from a session, returning a list of Pydantic Message objects."""
|
| 158 |
-
try:
|
| 159 |
-
obj_session_id = ObjectId(session_id)
|
| 160 |
-
collection = get_collection(collection_name)
|
| 161 |
-
pipeline = [
|
| 162 |
-
{"$match": {"_id": obj_session_id}},
|
| 163 |
-
{"$unwind": "$messages"},
|
| 164 |
-
{"$sort": {"messages.timestamp": -1}},
|
| 165 |
-
{"$replaceRoot": {"newRoot": "$messages"}}
|
| 166 |
-
]
|
| 167 |
-
if limit:
|
| 168 |
-
pipeline.append({"$limit": limit})
|
| 169 |
-
|
| 170 |
-
message_dicts = list(collection.aggregate(pipeline))
|
| 171 |
-
return [Message.model_validate(doc) for doc in message_dicts]
|
| 172 |
-
except InvalidId as e:
|
| 173 |
-
logger().error(f"Invalid session_id format for get_session_messages: '{session_id}'")
|
| 174 |
-
raise ActionFailed("The provided session ID is not a valid format.") from e
|
| 175 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 176 |
-
logger().error(f"Database error retrieving messages for session '{session_id}': {e}")
|
| 177 |
-
raise ActionFailed("A database error occurred while retrieving messages.") from e
|
| 178 |
-
|
| 179 |
-
def update_session_title(
|
| 180 |
-
session_id: str,
|
| 181 |
-
title: str,
|
| 182 |
-
*,
|
| 183 |
-
collection_name: str = Collections.SESSION
|
| 184 |
-
) -> bool:
|
| 185 |
-
"""Updates a session's title, raising ActionFailed on error."""
|
| 186 |
-
try:
|
| 187 |
-
obj_session_id = ObjectId(session_id)
|
| 188 |
-
collection = get_collection(collection_name)
|
| 189 |
-
result = collection.update_one(
|
| 190 |
-
{"_id": obj_session_id},
|
| 191 |
-
{
|
| 192 |
-
"$set": {
|
| 193 |
-
"title": title,
|
| 194 |
-
"updated_at": datetime.now(timezone.utc)
|
| 195 |
-
}
|
| 196 |
-
}
|
| 197 |
-
)
|
| 198 |
-
return result.modified_count > 0
|
| 199 |
-
except InvalidId as e:
|
| 200 |
-
logger().error(f"Invalid session_id format for update_session_title: '{session_id}'")
|
| 201 |
-
raise ActionFailed("The provided session ID is not a valid format.") from e
|
| 202 |
-
except (WriteError, ConnectionFailure, PyMongoError) as e:
|
| 203 |
-
logger().error(f"Database error updating title for session '{session_id}': {e}")
|
| 204 |
-
raise ActionFailed("A database error occurred while updating the session title.") from e
|
| 205 |
-
|
| 206 |
-
def delete_session(
|
| 207 |
-
session_id: str,
|
| 208 |
-
*,
|
| 209 |
-
collection_name: str = Collections.SESSION
|
| 210 |
-
) -> bool:
|
| 211 |
-
"""Deletes a session, raising ActionFailed on error."""
|
| 212 |
-
try:
|
| 213 |
-
obj_session_id = ObjectId(session_id)
|
| 214 |
-
collection = get_collection(collection_name)
|
| 215 |
-
result = collection.delete_one({"_id": obj_session_id})
|
| 216 |
-
return result.deleted_count > 0
|
| 217 |
-
except InvalidId as e:
|
| 218 |
-
logger().error(f"Invalid session_id format for delete_session: '{session_id}'")
|
| 219 |
-
raise ActionFailed("The provided session ID is not a valid format.") from e
|
| 220 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 221 |
-
logger().error(f"Database error deleting session '{session_id}': {e}")
|
| 222 |
-
raise ActionFailed("A database error occurred while deleting the session.") from e
|
| 223 |
-
|
| 224 |
-
def prune_old_sessions(
|
| 225 |
-
days: int = 30,
|
| 226 |
-
*,
|
| 227 |
-
collection_name: str = Collections.SESSION
|
| 228 |
-
) -> int:
|
| 229 |
-
"""Deletes old sessions, raising ActionFailed on error."""
|
| 230 |
-
try:
|
| 231 |
-
collection = get_collection(collection_name)
|
| 232 |
-
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
| 233 |
-
result = collection.delete_many({"updated_at": {"$lt": cutoff}})
|
| 234 |
-
if result.deleted_count > 0:
|
| 235 |
-
logger().info(f"Deleted {result.deleted_count} old sessions (>{days} days)")
|
| 236 |
-
return result.deleted_count
|
| 237 |
-
except (ConnectionFailure, PyMongoError) as e:
|
| 238 |
-
logger().error(f"Database error during session prune: {e}")
|
| 239 |
-
raise ActionFailed("A database error occurred while pruning old sessions.") from e
|
| 240 |
-
|
| 241 |
-
def add_message(
|
| 242 |
-
session_id: str,
|
| 243 |
-
content: str,
|
| 244 |
-
sent_by_user: bool,
|
| 245 |
-
*,
|
| 246 |
-
collection_name: str = Collections.SESSION
|
| 247 |
-
):
|
| 248 |
-
"""Adds a message to a session, raising ActionFailed on error."""
|
| 249 |
-
try:
|
| 250 |
-
obj_session_id = ObjectId(session_id)
|
| 251 |
-
collection = get_collection(collection_name)
|
| 252 |
-
|
| 253 |
-
session = collection.find_one(
|
| 254 |
-
{"_id": obj_session_id},
|
| 255 |
-
{"messages": {"$slice": -1}}
|
| 256 |
-
)
|
| 257 |
-
if not session:
|
| 258 |
-
raise ActionFailed(f"Chat session not found: {session_id}")
|
| 259 |
-
|
| 260 |
-
messages = session.get("messages", [])
|
| 261 |
-
next_id = messages[0]["_id"] + 1 if messages else 0
|
| 262 |
-
now = datetime.now(timezone.utc)
|
| 263 |
-
message_data: dict[str, Any] = {
|
| 264 |
-
"_id": next_id,
|
| 265 |
-
"sent_by_user": sent_by_user,
|
| 266 |
-
"content": content,
|
| 267 |
-
"timestamp": now
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
result = collection.update_one(
|
| 271 |
-
{"_id": obj_session_id},
|
| 272 |
-
{
|
| 273 |
-
"$push": {"messages": message_data},
|
| 274 |
-
"$set": {"updated_at": now}
|
| 275 |
-
}
|
| 276 |
-
)
|
| 277 |
-
|
| 278 |
-
if result.modified_count == 0:
|
| 279 |
-
raise ActionFailed(f"Failed to add message to session, no documents modified: {session_id}")
|
| 280 |
-
|
| 281 |
-
except InvalidId as e:
|
| 282 |
-
logger().error(f"Invalid session_id format for add_message: '{session_id}'")
|
| 283 |
-
raise ActionFailed("The provided session ID is not a valid format.") from e
|
| 284 |
-
except ActionFailed as e:
|
| 285 |
-
logger().warning(str(e))
|
| 286 |
-
raise
|
| 287 |
-
except (WriteError, ConnectionFailure, PyMongoError) as e:
|
| 288 |
-
logger().error(f"Database error adding message to session '{session_id}': {e}")
|
| 289 |
-
raise ActionFailed("A database error occurred while adding the message.") from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/js/app.js
CHANGED
|
@@ -1290,6 +1290,12 @@ How can I assist you today?`;
|
|
| 1290 |
}
|
| 1291 |
|
| 1292 |
updatePatientDisplay(patientId, patientName = null) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1293 |
const status = document.getElementById('patientStatus');
|
| 1294 |
const actions = document.getElementById('patientActions');
|
| 1295 |
const emrLink = document.getElementById('emrLink');
|
|
|
|
| 1290 |
}
|
| 1291 |
|
| 1292 |
updatePatientDisplay(patientId, patientName = null) {
|
| 1293 |
+
// Safety check: don't update display if patientId is undefined or null
|
| 1294 |
+
if (!patientId || patientId === 'undefined' || patientId === 'null') {
|
| 1295 |
+
console.warn('updatePatientDisplay called with invalid patientId:', patientId);
|
| 1296 |
+
return;
|
| 1297 |
+
}
|
| 1298 |
+
|
| 1299 |
const status = document.getElementById('patientStatus');
|
| 1300 |
const actions = document.getElementById('patientActions');
|
| 1301 |
const emrLink = document.getElementById('emrLink');
|
static/js/emr.js
CHANGED
|
@@ -101,15 +101,19 @@ class EMRPage {
|
|
| 101 |
const urlParams = new URLSearchParams(window.location.search);
|
| 102 |
const patientId = urlParams.get('patient_id');
|
| 103 |
|
| 104 |
-
if (
|
|
|
|
| 105 |
this.currentPatientId = patientId;
|
| 106 |
await this.loadPatientInfo();
|
| 107 |
} else {
|
| 108 |
// Try to get from localStorage
|
| 109 |
const savedPatientId = localStorage.getItem('medicalChatbotPatientId');
|
| 110 |
-
if (savedPatientId) {
|
| 111 |
this.currentPatientId = savedPatientId;
|
| 112 |
await this.loadPatientInfo();
|
|
|
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
}
|
| 115 |
}
|
|
|
|
| 101 |
const urlParams = new URLSearchParams(window.location.search);
|
| 102 |
const patientId = urlParams.get('patient_id');
|
| 103 |
|
| 104 |
+
// Check if patientId is valid (not undefined, null, or empty)
|
| 105 |
+
if (patientId && patientId !== 'undefined' && patientId !== 'null' && patientId.trim() !== '') {
|
| 106 |
this.currentPatientId = patientId;
|
| 107 |
await this.loadPatientInfo();
|
| 108 |
} else {
|
| 109 |
// Try to get from localStorage
|
| 110 |
const savedPatientId = localStorage.getItem('medicalChatbotPatientId');
|
| 111 |
+
if (savedPatientId && savedPatientId !== 'undefined' && savedPatientId !== 'null' && savedPatientId.trim() !== '') {
|
| 112 |
this.currentPatientId = savedPatientId;
|
| 113 |
await this.loadPatientInfo();
|
| 114 |
+
} else {
|
| 115 |
+
console.warn('No valid patient ID found in URL or localStorage');
|
| 116 |
+
this.showEmptyState();
|
| 117 |
}
|
| 118 |
}
|
| 119 |
}
|