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)"),
    )