""" Patient Service Manages high-level lifecycle of patient sessions including creation, summarization, and abandoned session sweeping. """ from typing import Optional, Tuple from core.ports import ProfileStore, SessionStore, LLMProvider from core.logger import get_logger, close_logger, serinity_logger class PatientService: """ Service responsible for managing the high-level lifecycle of patient sessions. Handles session initialization, profile recap generation, and session termination/summarization. """ def __init__(self, profile_store: ProfileStore, session_store: SessionStore, llm_provider: LLMProvider): self.profile_store = profile_store self.session_store = session_store self.llm_provider = llm_provider def create_new_session(self, default_patient_id: Optional[str] = None) -> Tuple[str, str, str]: session_id = self.session_store.create_session(patient_id=default_patient_id) resolved_patient_id = self.session_store.get_patient_id(session_id) recap = self.profile_store.build_profile_recap(resolved_patient_id) if resolved_patient_id else None opening_context = self.llm_provider.generate_opening_context(recap) llm1_response = self.llm_provider.psychiatrist_response(opening_context) self.session_store.append_message(session_id, "assistant", llm1_response.assistant_message) patient_info = self.profile_store.get_patient(resolved_patient_id) if resolved_patient_id else None user_name = patient_info.get("name", "Guest Patient") if patient_info else "Guest Patient" session_number = self.session_store.get_session_count(resolved_patient_id) if resolved_patient_id else 1 log = get_logger(session_id, user_name, session_number) log.session_start(resolved_patient_id) log.assistant_reply(llm1_response.assistant_message, risk_injected=False) return session_id, llm1_response.assistant_message, resolved_patient_id def end_session(self, session_id: str, default_patient_id: Optional[str] = None) -> None: """ Synchronously ends the session. Immediate teardown logic goes here. """ self.session_store.end_session(session_id) def get_active_session(self, patient_id: str) -> Optional[str]: """Check if the patient currently has an active session.""" return self.session_store.get_active_session(patient_id) def get_session_messages(self, session_id: str) -> list[dict]: """Retrieve all raw messages for a given session.""" return self.session_store.get_all_messages(session_id) def reset_patient_data(self, patient_id: str) -> None: """ Resets the patient's data, including sessions, messages, and profile. """ self.profile_store.reset_patient_data(patient_id) def delete_patient(self, patient_id: str) -> None: """ Deletes a patient and all their associated data completely. """ self.profile_store.delete_patient(patient_id) def generate_session_summary(self, session_id: str, default_patient_id: Optional[str] = None) -> None: """ Asynchronously generates and saves a clinical summary of the session and updates the profile via LLM3. """ patient_id = self.session_store.get_patient_id(session_id) or default_patient_id history = self.session_store.get_working_context(session_id) # Check if the user actually sent any messages has_user_messages = any(msg.get("role") == "user" for msg in history) summary = "No conversation occurred." llm3_output = None if has_user_messages and patient_id: try: patient_info = self.profile_store.get_patient(patient_id) old_profile = self.profile_store.get_patient_profile(patient_id) llm3_output = self.llm_provider.generate_end_of_session_profile(old_profile, history, patient_info) if not llm3_output.update_profile: summary = "Not enough conversation to generate a meaningful summary." else: summary = llm3_output.session_summary self.profile_store.update_patient_profile(patient_id, llm3_output) self.profile_store.update_long_term_memory(patient_id, llm3_output) if llm3_output.updated_primary_concern: self.profile_store.update_primary_concern(patient_id, llm3_output.updated_primary_concern) except Exception as e: summary = f"Summary generation failed: {e}" self.session_store.save_session_summary(session_id, summary) patient_info = self.profile_store.get_patient(patient_id) if patient_id else None user_name = patient_info.get("name", "Guest Patient") if patient_info else "Guest Patient" session_number = self.session_store.get_session_count(patient_id) if patient_id else 1 log = get_logger(session_id, user_name, session_number) if llm3_output: log.llm3_output(llm3_output) else: log._log_event("session_summary", {"summary": summary}) close_logger(session_id) def sweep_abandoned_sessions(self, timeout_minutes: int = 30) -> None: """ Finds sessions inactive for `timeout_minutes` and generates summaries for them, then closes them. """ abandoned_ids = self.session_store.get_abandoned_sessions(timeout_minutes) for session_id in abandoned_ids: try: serinity_logger.info(f"Sweeping abandoned session {session_id}...") self.generate_session_summary(session_id) self.end_session(session_id) except Exception as e: serinity_logger.error(f"Failed to sweep session {session_id}: {e}")