Delete services
Browse files- services/__pycache__/auth_service.cpython-311.pyc +0 -0
- services/__pycache__/conversation_orchestrator.cpython-311.pyc +0 -0
- services/__pycache__/conversation_orchestrator.cpython-313.pyc +0 -0
- services/__pycache__/email_service.cpython-311.pyc +0 -0
- services/__pycache__/patient_service.cpython-311.pyc +0 -0
- services/__pycache__/patient_service.cpython-313.pyc +0 -0
- services/__pycache__/risk_assessment_service.cpython-311.pyc +0 -0
- services/__pycache__/risk_assessment_service.cpython-313.pyc +0 -0
- services/auth_service.py +0 -43
- services/conversation_orchestrator.py +0 -181
- services/email_service.py +0 -65
- services/patient_service.py +0 -121
- services/risk_assessment_service.py +0 -17
services/__pycache__/auth_service.cpython-311.pyc
DELETED
|
Binary file (3.12 kB)
|
|
|
services/__pycache__/conversation_orchestrator.cpython-311.pyc
DELETED
|
Binary file (10.3 kB)
|
|
|
services/__pycache__/conversation_orchestrator.cpython-313.pyc
DELETED
|
Binary file (7.81 kB)
|
|
|
services/__pycache__/email_service.cpython-311.pyc
DELETED
|
Binary file (3.13 kB)
|
|
|
services/__pycache__/patient_service.cpython-311.pyc
DELETED
|
Binary file (8.84 kB)
|
|
|
services/__pycache__/patient_service.cpython-313.pyc
DELETED
|
Binary file (8.25 kB)
|
|
|
services/__pycache__/risk_assessment_service.cpython-311.pyc
DELETED
|
Binary file (1.87 kB)
|
|
|
services/__pycache__/risk_assessment_service.cpython-313.pyc
DELETED
|
Binary file (1.68 kB)
|
|
|
services/auth_service.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Authentication Service
|
| 3 |
-
Handles JWT token generation, validation, and password hashing.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
from datetime import datetime, timedelta, timezone
|
| 8 |
-
from jose import JWTError, jwt
|
| 9 |
-
import bcrypt
|
| 10 |
-
from dotenv import load_dotenv
|
| 11 |
-
|
| 12 |
-
load_dotenv()
|
| 13 |
-
|
| 14 |
-
SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
|
| 15 |
-
if not SECRET_KEY:
|
| 16 |
-
raise ValueError("JWT_SECRET_KEY is not set in environment")
|
| 17 |
-
|
| 18 |
-
ALGORITHM = os.environ.get("JWT_ALGORITHM")
|
| 19 |
-
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.environ.get("JWT_ACCESS_TOKEN_EXPIRE_MINUTES"))
|
| 20 |
-
|
| 21 |
-
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 22 |
-
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
|
| 23 |
-
|
| 24 |
-
def get_password_hash(password: str) -> str:
|
| 25 |
-
salt = bcrypt.gensalt()
|
| 26 |
-
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
|
| 27 |
-
|
| 28 |
-
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
|
| 29 |
-
to_encode = data.copy()
|
| 30 |
-
if expires_delta:
|
| 31 |
-
expire = datetime.now(timezone.utc) + expires_delta
|
| 32 |
-
else:
|
| 33 |
-
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 34 |
-
to_encode.update({"exp": expire})
|
| 35 |
-
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 36 |
-
return encoded_jwt
|
| 37 |
-
|
| 38 |
-
def decode_access_token(token: str) -> dict | None:
|
| 39 |
-
try:
|
| 40 |
-
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 41 |
-
return payload
|
| 42 |
-
except JWTError:
|
| 43 |
-
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/conversation_orchestrator.py
DELETED
|
@@ -1,181 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Conversation Orchestrator Service
|
| 3 |
-
Coordinates the conversation logic, manages LLM calls, and interfaces with the risk assessment service.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import time
|
| 7 |
-
from typing import Optional
|
| 8 |
-
from core.ports import LLMProvider, VectorStore, SessionStore, ProfileStore, LLM1Output
|
| 9 |
-
from core.schemas import ChatResult
|
| 10 |
-
from services.risk_assessment_service import RiskAssessmentService
|
| 11 |
-
from core.logger import get_logger, serinity_logger
|
| 12 |
-
|
| 13 |
-
class ConversationOrchestrator:
|
| 14 |
-
"""
|
| 15 |
-
Central coordinator for the application's core logic.
|
| 16 |
-
Implements a 'Sync 3-Pipeline' architecture.
|
| 17 |
-
"""
|
| 18 |
-
def __init__(
|
| 19 |
-
self,
|
| 20 |
-
llm_provider: LLMProvider,
|
| 21 |
-
vector_store: VectorStore,
|
| 22 |
-
session_store: SessionStore,
|
| 23 |
-
profile_store: ProfileStore,
|
| 24 |
-
risk_service: RiskAssessmentService
|
| 25 |
-
):
|
| 26 |
-
self.llm_provider = llm_provider
|
| 27 |
-
self.vector_store = vector_store
|
| 28 |
-
self.session_store = session_store
|
| 29 |
-
self.profile_store = profile_store
|
| 30 |
-
self.risk_service = risk_service
|
| 31 |
-
|
| 32 |
-
def _format_list(self, items: list) -> str:
|
| 33 |
-
if not items:
|
| 34 |
-
return "None identified yet"
|
| 35 |
-
return "\n • " + "\n • ".join(items)
|
| 36 |
-
|
| 37 |
-
def handle_message(self, session_id: str, message: str, emotion: Optional[str], default_patient_id: Optional[str]) -> ChatResult:
|
| 38 |
-
start_time = time.time()
|
| 39 |
-
|
| 40 |
-
patient_id = self.session_store.get_patient_id(session_id) or default_patient_id
|
| 41 |
-
patient_info = self.profile_store.get_patient(patient_id) if patient_id else None
|
| 42 |
-
user_name = patient_info.get("name", "Guest Patient") if patient_info else "Guest Patient"
|
| 43 |
-
|
| 44 |
-
session_number = self.session_store.get_session_count(patient_id) if patient_id else 1
|
| 45 |
-
log = get_logger(session_id, user_name, session_number)
|
| 46 |
-
|
| 47 |
-
emotion = (emotion or "").lower().strip()
|
| 48 |
-
user_message = message
|
| 49 |
-
if emotion and emotion not in ("neutral", "unknown", ""):
|
| 50 |
-
user_message = f"[vocal tone: {emotion}] {user_message}"
|
| 51 |
-
|
| 52 |
-
log.user_message(message, emotion=emotion or None)
|
| 53 |
-
|
| 54 |
-
self.session_store.append_message(session_id, "user", user_message)
|
| 55 |
-
|
| 56 |
-
history = self.session_store.get_working_context(session_id, llm_engine=self.llm_provider)
|
| 57 |
-
|
| 58 |
-
try:
|
| 59 |
-
llm1_response = self.llm_provider.psychiatrist_response(history, patient_info)
|
| 60 |
-
log.llm1_decision(llm1_response)
|
| 61 |
-
|
| 62 |
-
base_risk = self.risk_service.assess(message, llm1_response, None)
|
| 63 |
-
|
| 64 |
-
intent = llm1_response.intent
|
| 65 |
-
if base_risk and intent != "ANALYZE":
|
| 66 |
-
intent = "ANALYZE"
|
| 67 |
-
|
| 68 |
-
final_message = llm1_response.assistant_message
|
| 69 |
-
|
| 70 |
-
# Sync fast-path for user queries
|
| 71 |
-
if intent == "QUERY" and llm1_response.search_query:
|
| 72 |
-
retrieved_context = self.vector_store.retrieve(llm1_response.search_query, k=5)
|
| 73 |
-
log.analyze_triggered(llm1_response.search_query, "query")
|
| 74 |
-
log.retrieved_context(retrieved_context)
|
| 75 |
-
final_message = self.llm_provider.psychiatrist_query_response(history, retrieved_context)
|
| 76 |
-
|
| 77 |
-
# Synchronous execution of LLM2 pipeline
|
| 78 |
-
if intent == "ANALYZE" and patient_id:
|
| 79 |
-
llm2_message = self._run_sync_analysis(session_id, patient_id, llm1_response.clinical_summary, llm1_response.assistant_message)
|
| 80 |
-
if llm2_message:
|
| 81 |
-
final_message = llm2_message
|
| 82 |
-
|
| 83 |
-
self.session_store.append_message(session_id, "assistant", final_message)
|
| 84 |
-
latency_ms = int((time.time() - start_time) * 1000)
|
| 85 |
-
log.assistant_reply(final_message, risk_injected=base_risk, latency_ms=latency_ms)
|
| 86 |
-
|
| 87 |
-
chat_result = ChatResult(
|
| 88 |
-
assistant_message=final_message,
|
| 89 |
-
intent=intent,
|
| 90 |
-
risk_flagged=base_risk,
|
| 91 |
-
job_id=None,
|
| 92 |
-
clinical_summary=llm1_response.clinical_summary
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
return chat_result
|
| 96 |
-
except Exception as e:
|
| 97 |
-
log.error("handle_message_failed", e)
|
| 98 |
-
serinity_logger.error(f"Error in handle_message: {str(e)}")
|
| 99 |
-
fallback_message = "I'm having a little trouble connecting my thoughts right now. Could you repeat that?"
|
| 100 |
-
self.session_store.append_message(session_id, "assistant", fallback_message)
|
| 101 |
-
return ChatResult(
|
| 102 |
-
assistant_message=fallback_message,
|
| 103 |
-
intent="UNKNOWN",
|
| 104 |
-
risk_flagged=False,
|
| 105 |
-
job_id=None,
|
| 106 |
-
clinical_summary=None
|
| 107 |
-
)
|
| 108 |
-
|
| 109 |
-
def _run_sync_analysis(self, session_id: str, patient_id: str, clinical_summary: Optional[str] = None, llm1_draft: Optional[str] = None) -> Optional[str]:
|
| 110 |
-
"""
|
| 111 |
-
Executes the heavy LLM2 pattern analysis synchronously and returns the LLM2 generated response.
|
| 112 |
-
"""
|
| 113 |
-
session_number = self.session_store.get_session_count(patient_id) if patient_id else 1
|
| 114 |
-
log = get_logger(session_id, "Sync Analysis Task", session_number)
|
| 115 |
-
try:
|
| 116 |
-
history = self.session_store.get_working_context(session_id, llm_engine=self.llm_provider)
|
| 117 |
-
if clinical_summary and clinical_summary.strip():
|
| 118 |
-
retrieval_query = clinical_summary
|
| 119 |
-
query_source = "clinical_summary"
|
| 120 |
-
else:
|
| 121 |
-
retrieval_query = "\n".join(
|
| 122 |
-
msg["content"]
|
| 123 |
-
for msg in history[-10:]
|
| 124 |
-
if msg.get("role") == "user"
|
| 125 |
-
)
|
| 126 |
-
query_source = "user_messages_fallback"
|
| 127 |
-
|
| 128 |
-
log.analyze_triggered(retrieval_query, query_source)
|
| 129 |
-
retrieved_context = self.vector_store.retrieve(retrieval_query)
|
| 130 |
-
log.retrieved_context(retrieved_context)
|
| 131 |
-
|
| 132 |
-
existing_profile_recap = self.profile_store.build_profile_recap(patient_id) or "No previous clinical profile exists for this patient."
|
| 133 |
-
|
| 134 |
-
patient_info = self.profile_store.get_patient(patient_id) if patient_id else None
|
| 135 |
-
demographics = ""
|
| 136 |
-
if patient_info:
|
| 137 |
-
demographics = (
|
| 138 |
-
f"PATIENT DEMOGRAPHICS:\n"
|
| 139 |
-
f"- Name: {patient_info.get('name', 'Unknown')}\n"
|
| 140 |
-
f"- Age: {patient_info.get('age', 'Unknown')}\n"
|
| 141 |
-
f"- Gender: {patient_info.get('gender', 'Unknown')}\n"
|
| 142 |
-
f"- Nationality: {patient_info.get('nationality', 'Unknown')}\n"
|
| 143 |
-
f"- Primary Concern: {patient_info.get('primary_concern', 'Unknown')}\n\n"
|
| 144 |
-
)
|
| 145 |
-
|
| 146 |
-
context_prompt = (
|
| 147 |
-
f"{demographics}"
|
| 148 |
-
f"Existing Clinical Profile:\n{existing_profile_recap}\n\n"
|
| 149 |
-
)
|
| 150 |
-
if clinical_summary and clinical_summary.strip():
|
| 151 |
-
context_prompt += f"Current Session Clinical Summary:\n{clinical_summary}\n\n"
|
| 152 |
-
|
| 153 |
-
context_prompt += (
|
| 154 |
-
f"Retrieved Clinical Reference (Sims' Symptoms in the Mind):\n{retrieved_context}\n\n"
|
| 155 |
-
)
|
| 156 |
-
|
| 157 |
-
if llm1_draft:
|
| 158 |
-
context_prompt += (
|
| 159 |
-
f"Intern's Draft Response:\n{llm1_draft}\n\n"
|
| 160 |
-
)
|
| 161 |
-
|
| 162 |
-
context_prompt += (
|
| 163 |
-
"Please perform pattern analysis across all eight domains. Evaluate how the user is behaving now (based on the current session messages and summary) compared to their existing profile, and output the updated patterns. "
|
| 164 |
-
"You must also review the Intern's Draft Response, refine and amplify it using your deeper clinical insights, and provide the final polished conversational response."
|
| 165 |
-
)
|
| 166 |
-
|
| 167 |
-
llm2_input = history[-10:] + [{
|
| 168 |
-
"role": "user",
|
| 169 |
-
"content": context_prompt
|
| 170 |
-
}]
|
| 171 |
-
llm2_response = self.llm_provider.internal_reasoning(llm2_input)
|
| 172 |
-
log.llm2_output(llm2_response)
|
| 173 |
-
|
| 174 |
-
self.profile_store.update_long_term_memory(patient_id, llm2_response)
|
| 175 |
-
serinity_logger.info(f"Synchronous analysis completed for patient {patient_id}.")
|
| 176 |
-
return llm2_response.assistant_message
|
| 177 |
-
|
| 178 |
-
except Exception as e:
|
| 179 |
-
log.error("sync_analysis_failed", e)
|
| 180 |
-
serinity_logger.error(f"Synchronous analysis failed: {e}")
|
| 181 |
-
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/email_service.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import requests
|
| 3 |
-
from typing import Optional
|
| 4 |
-
from core.logger import serinity_logger as logger
|
| 5 |
-
|
| 6 |
-
def send_otp_email(to_email: str, subject: str, otp_code: str, body_text: Optional[str] = None):
|
| 7 |
-
"""
|
| 8 |
-
Sends an OTP email either via Brevo (if CLOUD_MODE=true) or to the terminal (if CLOUD_MODE=false).
|
| 9 |
-
"""
|
| 10 |
-
cloud_mode = os.getenv("CLOUD_MODE", "false").lower() == "true"
|
| 11 |
-
|
| 12 |
-
# Format the body message
|
| 13 |
-
if not body_text:
|
| 14 |
-
body_text = f"Your verification code is: {otp_code}"
|
| 15 |
-
else:
|
| 16 |
-
body_text = body_text.replace("{otp_code}", otp_code)
|
| 17 |
-
|
| 18 |
-
if not cloud_mode:
|
| 19 |
-
# Local Mode: Print to terminal
|
| 20 |
-
print(f"\n" + "="*40)
|
| 21 |
-
print(f"[LOCAL EMAIL SIMULATION]")
|
| 22 |
-
print(f"To: {to_email}")
|
| 23 |
-
print(f"Subject: {subject}")
|
| 24 |
-
print(f"Body:\n{body_text}")
|
| 25 |
-
print(f"="*40 + "\n")
|
| 26 |
-
logger.info(f"Local email simulated for {to_email} (Subject: {subject})")
|
| 27 |
-
return True
|
| 28 |
-
|
| 29 |
-
# Cloud Mode: Send via Brevo
|
| 30 |
-
brevo_api_key = os.getenv("BREVO_API_KEY")
|
| 31 |
-
if not brevo_api_key:
|
| 32 |
-
logger.error("BREVO_API_KEY is not set in environment variables.")
|
| 33 |
-
return False
|
| 34 |
-
|
| 35 |
-
url = "https://api.brevo.com/v3/smtp/email"
|
| 36 |
-
headers = {
|
| 37 |
-
"accept": "application/json",
|
| 38 |
-
"api-key": brevo_api_key,
|
| 39 |
-
"content-type": "application/json"
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
payload = {
|
| 43 |
-
"sender": {
|
| 44 |
-
"name": "Serinity AI",
|
| 45 |
-
"email": os.getenv("BREVO_SENDER_EMAIL")
|
| 46 |
-
},
|
| 47 |
-
"to": [
|
| 48 |
-
{
|
| 49 |
-
"email": to_email
|
| 50 |
-
}
|
| 51 |
-
],
|
| 52 |
-
"subject": subject,
|
| 53 |
-
"htmlContent": f"<html><body><p>{body_text}</p></body></html>"
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
try:
|
| 57 |
-
response = requests.post(url, json=payload, headers=headers, timeout=10)
|
| 58 |
-
response.raise_for_status()
|
| 59 |
-
logger.info(f"Email sent successfully via Brevo to {to_email}")
|
| 60 |
-
return True
|
| 61 |
-
except requests.exceptions.RequestException as e:
|
| 62 |
-
logger.error(f"Failed to send email via Brevo: {str(e)}")
|
| 63 |
-
if hasattr(e, 'response') and e.response is not None:
|
| 64 |
-
logger.error(f"Brevo API Response: {e.response.text}")
|
| 65 |
-
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/patient_service.py
DELETED
|
@@ -1,121 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Patient Service
|
| 3 |
-
Manages high-level lifecycle of patient sessions including creation, summarization, and abandoned session sweeping.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from typing import Optional, Tuple
|
| 7 |
-
from core.ports import ProfileStore, SessionStore, LLMProvider
|
| 8 |
-
from core.logger import get_logger, close_logger, serinity_logger
|
| 9 |
-
|
| 10 |
-
class PatientService:
|
| 11 |
-
"""
|
| 12 |
-
Service responsible for managing the high-level lifecycle of patient sessions.
|
| 13 |
-
Handles session initialization, profile recap generation, and session termination/summarization.
|
| 14 |
-
"""
|
| 15 |
-
def __init__(self, profile_store: ProfileStore, session_store: SessionStore, llm_provider: LLMProvider):
|
| 16 |
-
self.profile_store = profile_store
|
| 17 |
-
self.session_store = session_store
|
| 18 |
-
self.llm_provider = llm_provider
|
| 19 |
-
|
| 20 |
-
def create_new_session(self, default_patient_id: Optional[str] = None) -> Tuple[str, str, str]:
|
| 21 |
-
session_id = self.session_store.create_session(patient_id=default_patient_id)
|
| 22 |
-
resolved_patient_id = self.session_store.get_patient_id(session_id)
|
| 23 |
-
|
| 24 |
-
recap = self.profile_store.build_profile_recap(resolved_patient_id) if resolved_patient_id else None
|
| 25 |
-
opening_context = self.llm_provider.generate_opening_context(recap)
|
| 26 |
-
llm1_response = self.llm_provider.psychiatrist_response(opening_context)
|
| 27 |
-
|
| 28 |
-
self.session_store.append_message(session_id, "assistant", llm1_response.assistant_message)
|
| 29 |
-
|
| 30 |
-
patient_info = self.profile_store.get_patient(resolved_patient_id) if resolved_patient_id else None
|
| 31 |
-
user_name = patient_info.get("name", "Guest Patient") if patient_info else "Guest Patient"
|
| 32 |
-
session_number = self.session_store.get_session_count(resolved_patient_id) if resolved_patient_id else 1
|
| 33 |
-
log = get_logger(session_id, user_name, session_number)
|
| 34 |
-
log.session_start(resolved_patient_id)
|
| 35 |
-
log.assistant_reply(llm1_response.assistant_message, risk_injected=False)
|
| 36 |
-
|
| 37 |
-
return session_id, llm1_response.assistant_message, resolved_patient_id
|
| 38 |
-
|
| 39 |
-
def end_session(self, session_id: str, default_patient_id: Optional[str] = None) -> None:
|
| 40 |
-
"""
|
| 41 |
-
Synchronously ends the session.
|
| 42 |
-
Immediate teardown logic goes here.
|
| 43 |
-
"""
|
| 44 |
-
self.session_store.end_session(session_id)
|
| 45 |
-
|
| 46 |
-
def get_active_session(self, patient_id: str) -> Optional[str]:
|
| 47 |
-
"""Check if the patient currently has an active session."""
|
| 48 |
-
return self.session_store.get_active_session(patient_id)
|
| 49 |
-
|
| 50 |
-
def get_session_messages(self, session_id: str) -> list[dict]:
|
| 51 |
-
"""Retrieve all raw messages for a given session."""
|
| 52 |
-
return self.session_store.get_all_messages(session_id)
|
| 53 |
-
def reset_patient_data(self, patient_id: str) -> None:
|
| 54 |
-
"""
|
| 55 |
-
Resets the patient's data, including sessions, messages, and profile.
|
| 56 |
-
"""
|
| 57 |
-
self.profile_store.reset_patient_data(patient_id)
|
| 58 |
-
|
| 59 |
-
def delete_patient(self, patient_id: str) -> None:
|
| 60 |
-
"""
|
| 61 |
-
Deletes a patient and all their associated data completely.
|
| 62 |
-
"""
|
| 63 |
-
self.profile_store.delete_patient(patient_id)
|
| 64 |
-
|
| 65 |
-
def generate_session_summary(self, session_id: str, default_patient_id: Optional[str] = None) -> None:
|
| 66 |
-
"""
|
| 67 |
-
Asynchronously generates and saves a clinical summary of the session and updates the profile via LLM3.
|
| 68 |
-
"""
|
| 69 |
-
patient_id = self.session_store.get_patient_id(session_id) or default_patient_id
|
| 70 |
-
|
| 71 |
-
history = self.session_store.get_working_context(session_id)
|
| 72 |
-
|
| 73 |
-
# Check if the user actually sent any messages
|
| 74 |
-
has_user_messages = any(msg.get("role") == "user" for msg in history)
|
| 75 |
-
|
| 76 |
-
summary = "No conversation occurred."
|
| 77 |
-
llm3_output = None
|
| 78 |
-
if has_user_messages and patient_id:
|
| 79 |
-
try:
|
| 80 |
-
patient_info = self.profile_store.get_patient(patient_id)
|
| 81 |
-
old_profile = self.profile_store.get_patient_profile(patient_id)
|
| 82 |
-
llm3_output = self.llm_provider.generate_end_of_session_profile(old_profile, history, patient_info)
|
| 83 |
-
|
| 84 |
-
if not llm3_output.update_profile:
|
| 85 |
-
summary = "Not enough conversation to generate a meaningful summary."
|
| 86 |
-
else:
|
| 87 |
-
summary = llm3_output.session_summary
|
| 88 |
-
self.profile_store.update_patient_profile(patient_id, llm3_output)
|
| 89 |
-
self.profile_store.update_long_term_memory(patient_id, llm3_output)
|
| 90 |
-
|
| 91 |
-
if llm3_output.updated_primary_concern:
|
| 92 |
-
self.profile_store.update_primary_concern(patient_id, llm3_output.updated_primary_concern)
|
| 93 |
-
|
| 94 |
-
except Exception as e:
|
| 95 |
-
summary = f"Summary generation failed: {e}"
|
| 96 |
-
|
| 97 |
-
self.session_store.save_session_summary(session_id, summary)
|
| 98 |
-
|
| 99 |
-
patient_info = self.profile_store.get_patient(patient_id) if patient_id else None
|
| 100 |
-
user_name = patient_info.get("name", "Guest Patient") if patient_info else "Guest Patient"
|
| 101 |
-
|
| 102 |
-
session_number = self.session_store.get_session_count(patient_id) if patient_id else 1
|
| 103 |
-
log = get_logger(session_id, user_name, session_number)
|
| 104 |
-
if llm3_output:
|
| 105 |
-
log.llm3_output(llm3_output)
|
| 106 |
-
else:
|
| 107 |
-
log._log_event("session_summary", {"summary": summary})
|
| 108 |
-
close_logger(session_id)
|
| 109 |
-
|
| 110 |
-
def sweep_abandoned_sessions(self, timeout_minutes: int = 30) -> None:
|
| 111 |
-
"""
|
| 112 |
-
Finds sessions inactive for `timeout_minutes` and generates summaries for them, then closes them.
|
| 113 |
-
"""
|
| 114 |
-
abandoned_ids = self.session_store.get_abandoned_sessions(timeout_minutes)
|
| 115 |
-
for session_id in abandoned_ids:
|
| 116 |
-
try:
|
| 117 |
-
serinity_logger.info(f"Sweeping abandoned session {session_id}...")
|
| 118 |
-
self.generate_session_summary(session_id)
|
| 119 |
-
self.end_session(session_id)
|
| 120 |
-
except Exception as e:
|
| 121 |
-
serinity_logger.error(f"Failed to sweep session {session_id}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/risk_assessment_service.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
from typing import List, Optional
|
| 2 |
-
from core.ports import RiskSignal, LLM1Output, LLM2Output
|
| 3 |
-
|
| 4 |
-
class RiskAssessmentService:
|
| 5 |
-
"""
|
| 6 |
-
Evaluates patient messages and LLM responses against a suite of RiskSignals.
|
| 7 |
-
Ensures patient safety by identifying crisis or self-harm intents in real-time.
|
| 8 |
-
"""
|
| 9 |
-
def __init__(self, signals: List[RiskSignal]):
|
| 10 |
-
self._signals = signals
|
| 11 |
-
|
| 12 |
-
def assess(self, message: str, llm1_output: LLM1Output, llm2_output: Optional[LLM2Output] = None) -> bool:
|
| 13 |
-
"""Evaluate all risk signals and return True if any indicate a risk."""
|
| 14 |
-
return any(
|
| 15 |
-
signal.check(message, llm1_output, llm2_output)
|
| 16 |
-
for signal in self._signals
|
| 17 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|