File size: 6,790 Bytes
e870cb3 005b3e2 e870cb3 a36c5e5 8748b68 e870cb3 8748b68 e870cb3 a36c5e5 e870cb3 8748b68 e870cb3 8748b68 e870cb3 4448a55 8748b68 4448a55 8748b68 4448a55 a36c5e5 4448a55 e870cb3 8f43e80 e870cb3 8f43e80 e870cb3 6b525e7 8f43e80 005b3e2 8f43e80 005b3e2 6b525e7 8f43e80 005b3e2 e870cb3 8f43e80 e870cb3 005b3e2 e870cb3 8f43e80 005b3e2 6b525e7 e870cb3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | """
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:
# Use PersistentClient so data survives restarts
_client = chromadb.PersistentClient(path="./chroma_db")
_collection = _client.get_or_create_collection("patient_memories")
return _collection
# ---------------------------------------------------------------------------
# Write
# ---------------------------------------------------------------------------
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()
# Use provided session_id or generate a new one
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
# ---------------------------------------------------------------------------
# Read
# ---------------------------------------------------------------------------
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", "[]"))
})
# Sort by timestamp (ISO format strings sort correctly)
summaries.sort(key=lambda x: x["timestamp"], reverse=True)
return summaries
# ---------------------------------------------------------------------------
# System prompt builder
# ---------------------------------------------------------------------------
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)"),
)
|