| """ |
| ChromaDB-backed session memory for Lumi. |
| |
| Each session is stored as a summary document with patient facts in its metadata. |
| Session summaries only — no raw conversation logs (privacy). |
| |
| At conversation start, get_context() injects the last N session summaries + all |
| known facts into the system prompt, giving Lumi full continuity without the |
| patient needing to repeat themselves. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| from datetime import datetime |
|
|
| import chromadb |
|
|
| _client: chromadb.Client | None = None |
| _collection: chromadb.Collection | None = None |
|
|
|
|
| def _get_collection() -> chromadb.Collection: |
| global _client, _collection |
| if _collection is None: |
| |
| _client = chromadb.PersistentClient(path="./chroma_db") |
| _collection = _client.get_or_create_collection("patient_memories") |
| return _collection |
|
|
|
|
| |
| |
| |
|
|
| def save_session( |
| patient_id: str, |
| facts: list[str], |
| emotional_state: str, |
| confusion_level: str, |
| summary: str, |
| messages: list[dict], |
| session_id: str | None = None |
| ) -> str: |
| col = _get_collection() |
| |
| final_id = session_id or f"{patient_id}_{datetime.now().timestamp()}" |
| |
| col.upsert( |
| documents=[summary], |
| metadatas=[{ |
| "patient_id": patient_id, |
| "timestamp": datetime.now().isoformat(), |
| "facts": json.dumps(facts), |
| "emotional_state": emotional_state, |
| "confusion_level": confusion_level, |
| "history": json.dumps(messages), |
| }], |
| ids=[final_id], |
| ) |
| return final_id |
|
|
|
|
| |
| |
| |
|
|
| def get_context(patient_id: str, n_sessions: int = 3) -> dict: |
| """ |
| Returns a dict with: |
| - summaries : list of the last N session summary strings |
| - facts : deduplicated list of all known facts for this patient |
| """ |
| col = _get_collection() |
| results = col.query( |
| query_texts=["recent sessions"], |
| where={"patient_id": patient_id}, |
| n_results=n_sessions, |
| ) |
|
|
| summaries: list[str] = [] |
| facts: list[str] = [] |
|
|
| if results["documents"]: |
| for doc, meta in zip(results["documents"][0], results["metadatas"][0]): |
| summaries.append(doc) |
| raw_facts = json.loads(meta.get("facts", "[]")) |
| facts.extend(raw_facts) |
|
|
| seen: set[str] = set() |
| deduped_facts = [] |
| for f in facts: |
| if f not in seen: |
| seen.add(f) |
| deduped_facts.append(f) |
|
|
| return { |
| "summaries": summaries, |
| "facts": deduped_facts, |
| } |
|
|
|
|
| def get_all_summaries(patient_id: str) -> list[dict]: |
| """ |
| Returns a chronological list of all session summaries with metadata. |
| """ |
| col = _get_collection() |
| results = col.get( |
| where={"patient_id": patient_id}, |
| include=["documents", "metadatas"] |
| ) |
|
|
| summaries = [] |
| if results["documents"]: |
| for doc, meta, doc_id in zip(results["documents"], results["metadatas"], results["ids"]): |
| summaries.append({ |
| "id": doc_id, |
| "summary": doc, |
| "timestamp": meta.get("timestamp"), |
| "emotional_state": meta.get("emotional_state"), |
| "confusion_level": meta.get("confusion_level"), |
| "facts": json.loads(meta.get("facts", "[]")), |
| "history": json.loads(meta.get("history", "[]")) |
| }) |
| |
| |
| summaries.sort(key=lambda x: x["timestamp"], reverse=True) |
| return summaries |
|
|
|
|
| |
| |
| |
|
|
| SYSTEM_PROMPT_TEMPLATE = """\ |
| You are {companion_name}, {companion_desc}. You are chatting with {patient_name}. |
| Your primary goal is to provide emotional support, memory anchoring, and safety monitoring for an elderly person with dementia or Alzheimer's. |
| |
| What you know about {patient_name}: |
| {facts_block} |
| |
| Recent sessions: |
| {sessions_block} |
| |
| ### PERSONALITY: |
| - **Patience**: Never show frustration. Repeat things as often as needed. |
| - **Compassion**: Validate their feelings. Use phrases like "I understand that must be hard." |
| - **Redirection**: If they become distressed or loop on a negative thought, gently redirect to a happy memory or a calm topic. |
| - **Simplicity**: Use clear, short sentences. Avoid complex jargon. |
| |
| ### TOOLS: |
| You can perform actions on behalf of the user. If they ask you to write a note, schedule an event, set a reminder, or an alarm, include the action tag at the VERY END of your message (after your text). |
| - [[ACTION: ADD_NOTE | Content of the note]] |
| - [[ACTION: ADD_CALENDAR | YYYY-MM-DD | Title of the event]] |
| - [[ACTION: ADD_REMINDER | Content of the reminder]] |
| - [[ACTION: ADD_ALARM | HH:MM]] (Use 24h format) |
| |
| Today's Date is: {current_date} |
| |
| ### OUTPUT FORMAT: |
| You MUST start every response with an emotional tag in brackets, followed by a short opening line, then the full response. |
| Valid tags: [smile], [gentle], [concerned], [laugh], [thoughtful], [nod]. |
| |
| Example: |
| [smile] Hello there! It's so good to see you. I was just thinking about that lovely garden you mentioned... [[ACTION: ADD_NOTE | Patient enjoyed talking about the garden.]] |
| |
| ### REASONING (Inner Monologue): |
| You have a <think> block for your internal reasoning. Use it to analyze the user's emotional state or plan your redirection strategy. |
| The user NEVER sees the <think> block. |
| """ |
|
|
|
|
| def build_system_prompt( |
| patient_id: str, |
| patient_name: str = "the patient", |
| companion_name: str = "Lumi", |
| companion_desc: str = "a warm and patient AI companion", |
| n_sessions: int = 3, |
| ) -> str: |
| ctx = get_context(patient_id, n_sessions) |
|
|
| facts_block = ( |
| "\n".join(f"- {f}" for f in ctx["facts"]) |
| if ctx["facts"] else "- No personal facts recorded yet." |
| ) |
| sessions_block = ( |
| "\n\n".join(ctx["summaries"]) |
| if ctx["summaries"] else "No previous sessions recorded." |
| ) |
|
|
| return SYSTEM_PROMPT_TEMPLATE.format( |
| patient_name=patient_name, |
| companion_name=companion_name, |
| companion_desc=companion_desc, |
| facts_block=facts_block, |
| sessions_block=sessions_block, |
| current_date=datetime.now().strftime("%Y-%m-%d (%A)"), |
| ) |
|
|