Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- alz_companion/agent.py +403 -0
- alz_companion/prompts.py +266 -0
alz_companion/agent.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import base64
|
| 5 |
+
import time
|
| 6 |
+
import tempfile
|
| 7 |
+
import re # <-- ADD THIS LINE
|
| 8 |
+
|
| 9 |
+
from typing import List, Dict, Any, Optional
|
| 10 |
+
|
| 11 |
+
# OpenAI for LLM (optional)
|
| 12 |
+
try:
|
| 13 |
+
from openai import OpenAI
|
| 14 |
+
except Exception: # pragma: no cover
|
| 15 |
+
OpenAI = None # type: ignore
|
| 16 |
+
|
| 17 |
+
# LangChain & RAG
|
| 18 |
+
from langchain.schema import Document
|
| 19 |
+
from langchain_community.vectorstores import FAISS
|
| 20 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 21 |
+
|
| 22 |
+
# TTS
|
| 23 |
+
try:
|
| 24 |
+
from gtts import gTTS
|
| 25 |
+
except Exception: # pragma: no cover
|
| 26 |
+
gTTS = None # type: ignore
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
from .prompts import (
|
| 30 |
+
SYSTEM_TEMPLATE, ANSWER_TEMPLATE_CALM, ANSWER_TEMPLATE_ADQ,
|
| 31 |
+
SAFETY_GUARDRAILS, RISK_FOOTER, render_emotion_guidelines, CLASSIFICATION_PROMPT,
|
| 32 |
+
# Add the new templates to the import list
|
| 33 |
+
ROUTER_PROMPT,
|
| 34 |
+
ANSWER_TEMPLATE_FACTUAL,
|
| 35 |
+
ANSWER_TEMPLATE_GENERAL_KNOWLEDGE,
|
| 36 |
+
ANSWER_TEMPLATE_GENERAL,
|
| 37 |
+
QUERY_EXPANSION_PROMPT
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# -----------------------------
|
| 41 |
+
# Multimodal Processing Functions
|
| 42 |
+
# -----------------------------
|
| 43 |
+
|
| 44 |
+
def _openai_client() -> Optional[OpenAI]:
|
| 45 |
+
api_key = os.getenv("OPENAI_API_KEY", "").strip()
|
| 46 |
+
return OpenAI(api_key=api_key) if api_key and OpenAI else None
|
| 47 |
+
|
| 48 |
+
# In agent.py
|
| 49 |
+
|
| 50 |
+
def describe_image(image_path: str) -> str:
|
| 51 |
+
"""Uses a vision model to describe an image for context."""
|
| 52 |
+
client = _openai_client()
|
| 53 |
+
if not client:
|
| 54 |
+
return "(Image description failed: OpenAI API key not configured.)"
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
# --- FIX START ---
|
| 58 |
+
# Determine the MIME type based on the file extension
|
| 59 |
+
extension = os.path.splitext(image_path)[1].lower()
|
| 60 |
+
if extension == ".png":
|
| 61 |
+
mime_type = "image/png"
|
| 62 |
+
elif extension in [".jpg", ".jpeg"]:
|
| 63 |
+
mime_type = "image/jpeg"
|
| 64 |
+
elif extension == ".gif":
|
| 65 |
+
mime_type = "image/gif"
|
| 66 |
+
elif extension == ".webp":
|
| 67 |
+
mime_type = "image/webp"
|
| 68 |
+
else:
|
| 69 |
+
# Default to JPEG, but this handles the most common cases
|
| 70 |
+
mime_type = "image/jpeg"
|
| 71 |
+
# --- FIX END ---
|
| 72 |
+
|
| 73 |
+
with open(image_path, "rb") as image_file:
|
| 74 |
+
base64_image = base64.b64encode(image_file.read()).decode('utf-8')
|
| 75 |
+
|
| 76 |
+
response = client.chat.completions.create(
|
| 77 |
+
model="gpt-4o",
|
| 78 |
+
messages=[
|
| 79 |
+
{
|
| 80 |
+
"role": "user",
|
| 81 |
+
"content": [
|
| 82 |
+
{"type": "text", "text": "Describe this image in a concise, factual way for a memory journal. Focus on people, places, and key objects. For example: 'A photo of John and Mary smiling on a bench at the park.'"},
|
| 83 |
+
{
|
| 84 |
+
"type": "image_url",
|
| 85 |
+
# Use the dynamically determined MIME type
|
| 86 |
+
"image_url": {"url": f"data:{mime_type};base64,{base64_image}"}
|
| 87 |
+
}
|
| 88 |
+
],
|
| 89 |
+
}
|
| 90 |
+
],
|
| 91 |
+
max_tokens=100,
|
| 92 |
+
)
|
| 93 |
+
return response.choices[0].message.content or "No description available."
|
| 94 |
+
except Exception as e:
|
| 95 |
+
return f"[Image description error: {e}]"
|
| 96 |
+
|
| 97 |
+
# -----------------------------
|
| 98 |
+
# NLU Classification Function
|
| 99 |
+
# -----------------------------
|
| 100 |
+
# Since the LLM's response will now contain both a <thinking> block and a JSON block,
|
| 101 |
+
# we need to update the detect_tags_from_query function to correctly parse it.
|
| 102 |
+
|
| 103 |
+
# In agent.py
|
| 104 |
+
|
| 105 |
+
def detect_tags_from_query(query: str, behavior_options: list, emotion_options: list, topic_options: list, context_options: list) -> Dict[str, Any]:
|
| 106 |
+
"""Uses a Chain-of-Thought prompt to classify the user's query."""
|
| 107 |
+
behavior_str = ", ".join(f'"{opt}"' for opt in behavior_options if opt != "None")
|
| 108 |
+
emotion_str = ", ".join(f'"{opt}"' for opt in emotion_options if opt != "None")
|
| 109 |
+
topic_str = ", ".join(f'"{opt}"' for opt in topic_options if opt != "None")
|
| 110 |
+
context_str = ", ".join(f'"{opt}"' for opt in context_options if opt != "None")
|
| 111 |
+
|
| 112 |
+
prompt = CLASSIFICATION_PROMPT.format(
|
| 113 |
+
behavior_options=behavior_str,
|
| 114 |
+
emotion_options=emotion_str,
|
| 115 |
+
topic_options=topic_str,
|
| 116 |
+
context_options=context_str,
|
| 117 |
+
query=query
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
messages = [{"role": "system", "content": "You are a helpful NLU classification assistant. Follow the instructions precisely."}, {"role": "user", "content": prompt}]
|
| 121 |
+
response_str = call_llm(messages, temperature=0.1)
|
| 122 |
+
|
| 123 |
+
print(f"\n--- NLU Full Response ---\n{response_str}\n-----------------------\n")
|
| 124 |
+
|
| 125 |
+
result_dict = {
|
| 126 |
+
"detected_behaviors": [], "detected_emotion": "None",
|
| 127 |
+
"detected_topic": "None", "detected_contexts": []
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
try:
|
| 131 |
+
# --- ROBUST PARSING LOGIC ---
|
| 132 |
+
# Find the first '{' and the last '}' to isolate the JSON object
|
| 133 |
+
start_brace = response_str.find('{')
|
| 134 |
+
end_brace = response_str.rfind('}')
|
| 135 |
+
|
| 136 |
+
if start_brace != -1 and end_brace != -1 and end_brace > start_brace:
|
| 137 |
+
json_str = response_str[start_brace : end_brace + 1]
|
| 138 |
+
result = json.loads(json_str)
|
| 139 |
+
|
| 140 |
+
# Safely process the results from the LLM
|
| 141 |
+
result_dict["detected_behaviors"] = [b for b in result.get("detected_behaviors", []) if b in behavior_options]
|
| 142 |
+
result_dict["detected_emotion"] = result.get("detected_emotion") if result.get("detected_emotion") in emotion_options else "None"
|
| 143 |
+
result_dict["detected_topic"] = result.get("detected_topic") if result.get("detected_topic") in topic_options else "None"
|
| 144 |
+
result_dict["detected_contexts"] = [c for c in result.get("detected_contexts", []) if c in context_options]
|
| 145 |
+
# --- END OF ROBUST LOGIC ---
|
| 146 |
+
|
| 147 |
+
return result_dict
|
| 148 |
+
except (json.JSONDecodeError, AttributeError) as e:
|
| 149 |
+
print(f"ERROR parsing CoT JSON: {e}")
|
| 150 |
+
return result_dict
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# -----------------------------
|
| 154 |
+
# Embeddings & VectorStore
|
| 155 |
+
# -----------------------------
|
| 156 |
+
|
| 157 |
+
def _default_embeddings():
|
| 158 |
+
"""Lightweight, widely available model."""
|
| 159 |
+
model_name = os.getenv("EMBEDDINGS_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
|
| 160 |
+
return HuggingFaceEmbeddings(model_name=model_name)
|
| 161 |
+
|
| 162 |
+
def build_or_load_vectorstore(docs: List[Document], index_path: str, is_personal: bool = False) -> FAISS:
|
| 163 |
+
os.makedirs(os.path.dirname(index_path), exist_ok=True)
|
| 164 |
+
if os.path.isdir(index_path) and os.path.exists(os.path.join(index_path, "index.faiss")):
|
| 165 |
+
try:
|
| 166 |
+
return FAISS.load_local(index_path, _default_embeddings(), allow_dangerous_deserialization=True)
|
| 167 |
+
except Exception:
|
| 168 |
+
pass
|
| 169 |
+
|
| 170 |
+
if is_personal and not docs:
|
| 171 |
+
docs = [Document(page_content="(This is the start of the personal memory journal.)", metadata={"source": "placeholder"})]
|
| 172 |
+
|
| 173 |
+
vs = FAISS.from_documents(docs, _default_embeddings())
|
| 174 |
+
vs.save_local(index_path)
|
| 175 |
+
return vs
|
| 176 |
+
|
| 177 |
+
def texts_from_jsonl(path: str) -> List[Document]:
|
| 178 |
+
out: List[Document] = []
|
| 179 |
+
try:
|
| 180 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 181 |
+
for i, line in enumerate(f):
|
| 182 |
+
line = line.strip()
|
| 183 |
+
if not line: continue
|
| 184 |
+
obj = json.loads(line)
|
| 185 |
+
txt = obj.get("text") or ""
|
| 186 |
+
if not isinstance(txt, str) or not txt.strip(): continue
|
| 187 |
+
md = {"source": os.path.basename(path), "chunk": i}
|
| 188 |
+
for k in ("behaviors", "emotion"):
|
| 189 |
+
if k in obj: md[k] = obj[k]
|
| 190 |
+
out.append(Document(page_content=txt, metadata=md))
|
| 191 |
+
except Exception:
|
| 192 |
+
return []
|
| 193 |
+
return out
|
| 194 |
+
|
| 195 |
+
def bootstrap_vectorstore(sample_paths: List[str] | None = None, index_path: str = "data/faiss_index") -> FAISS:
|
| 196 |
+
docs: List[Document] = []
|
| 197 |
+
for p in (sample_paths or []):
|
| 198 |
+
try:
|
| 199 |
+
if p.lower().endswith(".jsonl"):
|
| 200 |
+
docs.extend(texts_from_jsonl(p))
|
| 201 |
+
else:
|
| 202 |
+
with open(p, "r", encoding="utf-8", errors="ignore") as fh:
|
| 203 |
+
docs.append(Document(page_content=fh.read(), metadata={"source": os.path.basename(p)}))
|
| 204 |
+
except Exception:
|
| 205 |
+
continue
|
| 206 |
+
if not docs:
|
| 207 |
+
docs = [Document(page_content="(empty index)", metadata={"source": "placeholder"})]
|
| 208 |
+
return build_or_load_vectorstore(docs, index_path=index_path)
|
| 209 |
+
|
| 210 |
+
# -----------------------------
|
| 211 |
+
# LLM Call
|
| 212 |
+
# -----------------------------
|
| 213 |
+
# updated the detect_tags_from_query function to call call_llm with a new stop argument,
|
| 214 |
+
# but I failed to update the call_llm function itself to accept that argument.
|
| 215 |
+
# Now fix call_llm function:
|
| 216 |
+
def call_llm(messages: List[Dict[str, str]], temperature: float = 0.6, stop: Optional[List[str]] = None) -> str:
|
| 217 |
+
"""Call OpenAI Chat Completions if available; else return a fallback."""
|
| 218 |
+
client = _openai_client()
|
| 219 |
+
model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
| 220 |
+
if not client:
|
| 221 |
+
return "(Offline Mode: OpenAI API key not configured.)"
|
| 222 |
+
try:
|
| 223 |
+
# Prepare arguments for the API call to handle the optional 'stop' parameter
|
| 224 |
+
api_args = {
|
| 225 |
+
"model": model,
|
| 226 |
+
"messages": messages,
|
| 227 |
+
"temperature": float(temperature if temperature is not None else 0.6)
|
| 228 |
+
}
|
| 229 |
+
if stop:
|
| 230 |
+
api_args["stop"] = stop
|
| 231 |
+
|
| 232 |
+
resp = client.chat.completions.create(**api_args)
|
| 233 |
+
return (resp.choices[0].message.content or "").strip()
|
| 234 |
+
except Exception as e:
|
| 235 |
+
return f"[LLM API Error: {e}]"
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
# -----------------------------
|
| 239 |
+
# Prompting & RAG Chain
|
| 240 |
+
# -----------------------------
|
| 241 |
+
|
| 242 |
+
def _format_sources(docs: List[Document]) -> List[str]:
|
| 243 |
+
return list(set(d.metadata.get("source", "unknown") for d in docs))
|
| 244 |
+
|
| 245 |
+
# In agent.py, replace the existing make_rag_chain function with this new one to handle general & specific conversations .
|
| 246 |
+
# The logic for the "factual_question" path needs to be updated to perform the expansion query
|
| 247 |
+
|
| 248 |
+
def make_rag_chain(
|
| 249 |
+
vs_general: FAISS,
|
| 250 |
+
vs_personal: FAISS,
|
| 251 |
+
*,
|
| 252 |
+
role: str = "patient",
|
| 253 |
+
temperature: float = 0.6,
|
| 254 |
+
language: str = "English",
|
| 255 |
+
patient_name: str = "the patient",
|
| 256 |
+
caregiver_name: str = "the caregiver",
|
| 257 |
+
tone: str = "warm",
|
| 258 |
+
):
|
| 259 |
+
"""Returns a callable that performs the complete, intelligent RAG process."""
|
| 260 |
+
|
| 261 |
+
def _format_docs(docs: List[Document], default_msg: str) -> str:
|
| 262 |
+
if not docs: return default_msg
|
| 263 |
+
unique_docs = {doc.page_content: doc for doc in docs}.values()
|
| 264 |
+
return "\n".join([f"- {d.page_content.strip()}" for d in unique_docs])
|
| 265 |
+
|
| 266 |
+
def _answer_fn(query: str, chat_history: List[Dict[str, str]], scenario_tag: Optional[str] = None, emotion_tag: Optional[str] = None) -> Dict[str, Any]:
|
| 267 |
+
|
| 268 |
+
router_messages = [{"role": "user", "content": ROUTER_PROMPT.format(query=query)}]
|
| 269 |
+
query_type = call_llm(router_messages, temperature=0.0).strip().lower()
|
| 270 |
+
print(f"Query classified as: {query_type}")
|
| 271 |
+
|
| 272 |
+
system_message = SYSTEM_TEMPLATE.format(tone=tone, language=language, patient_name=patient_name or "the patient", caregiver_name=caregiver_name or "the caregiver", guardrails=SAFETY_GUARDRAILS)
|
| 273 |
+
messages = [{"role": "system", "content": system_message}]
|
| 274 |
+
messages.extend(chat_history)
|
| 275 |
+
|
| 276 |
+
# --- NEW 'general_knowledge_question' PATH ---
|
| 277 |
+
if "general_knowledge_question" in query_type:
|
| 278 |
+
user_prompt = ANSWER_TEMPLATE_GENERAL_KNOWLEDGE.format(question=query, language=language)
|
| 279 |
+
messages.append({"role": "user", "content": user_prompt})
|
| 280 |
+
answer = call_llm(messages, temperature=temperature)
|
| 281 |
+
return {"answer": answer, "sources": ["General Knowledge"]}
|
| 282 |
+
# --- END NEW PATH ---
|
| 283 |
+
|
| 284 |
+
elif "factual_question" in query_type:
|
| 285 |
+
# ... (This entire section for query expansion and factual search remains the same)
|
| 286 |
+
print(f"Performing query expansion for: '{query}'")
|
| 287 |
+
expansion_prompt = QUERY_EXPANSION_PROMPT.format(question=query)
|
| 288 |
+
expansion_response = call_llm([{"role": "user", "content": expansion_prompt}], temperature=0.1)
|
| 289 |
+
|
| 290 |
+
try:
|
| 291 |
+
clean_response = expansion_response.strip().replace("```json", "").replace("```", "")
|
| 292 |
+
expanded_queries = json.loads(clean_response)
|
| 293 |
+
search_queries = [query] + expanded_queries
|
| 294 |
+
except json.JSONDecodeError:
|
| 295 |
+
search_queries = [query]
|
| 296 |
+
|
| 297 |
+
print(f"Searching with queries: {search_queries}")
|
| 298 |
+
retriever_personal = vs_personal.as_retriever(search_kwargs={"k": 2})
|
| 299 |
+
retriever_general = vs_general.as_retriever(search_kwargs={"k": 2})
|
| 300 |
+
|
| 301 |
+
all_docs = []
|
| 302 |
+
for q in search_queries:
|
| 303 |
+
all_docs.extend(retriever_personal.invoke(q))
|
| 304 |
+
all_docs.extend(retriever_general.invoke(q))
|
| 305 |
+
|
| 306 |
+
context = _format_docs(all_docs, "(No relevant information found in the memory journal.)")
|
| 307 |
+
|
| 308 |
+
user_prompt = ANSWER_TEMPLATE_FACTUAL.format(context=context, question=query, language=language)
|
| 309 |
+
messages.append({"role": "user", "content": user_prompt})
|
| 310 |
+
answer = call_llm(messages, temperature=temperature)
|
| 311 |
+
return {"answer": answer, "sources": _format_sources(all_docs)}
|
| 312 |
+
|
| 313 |
+
elif "general_conversation" in query_type:
|
| 314 |
+
user_prompt = ANSWER_TEMPLATE_GENERAL.format(question=query, language=language)
|
| 315 |
+
messages.append({"role": "user", "content": user_prompt})
|
| 316 |
+
answer = call_llm(messages, temperature=temperature)
|
| 317 |
+
return {"answer": answer, "sources": []}
|
| 318 |
+
|
| 319 |
+
else: # Default to the original caregiving logic
|
| 320 |
+
# ... (This entire section for caregiving scenarios remains the same)
|
| 321 |
+
search_filter = {}
|
| 322 |
+
if scenario_tag and scenario_tag != "None":
|
| 323 |
+
search_filter["behaviors"] = scenario_tag.lower()
|
| 324 |
+
if emotion_tag and emotion_tag != "None":
|
| 325 |
+
search_filter["emotion"] = emotion_tag.lower()
|
| 326 |
+
|
| 327 |
+
if search_filter:
|
| 328 |
+
personal_docs = vs_personal.similarity_search(query, k=3, filter=search_filter)
|
| 329 |
+
general_docs = vs_general.similarity_search(query, k=3, filter=search_filter)
|
| 330 |
+
else:
|
| 331 |
+
retriever_personal = vs_personal.as_retriever(search_kwargs={"k": 3})
|
| 332 |
+
retriever_general = vs_general.as_retriever(search_kwargs={"k": 3})
|
| 333 |
+
personal_docs = retriever_personal.invoke(query)
|
| 334 |
+
general_docs = retriever_general.invoke(query)
|
| 335 |
+
|
| 336 |
+
personal_context = _format_docs(personal_docs, "(No relevant personal memories found.)")
|
| 337 |
+
general_context = _format_docs(general_docs, "(No general guidance found.)")
|
| 338 |
+
|
| 339 |
+
first_emotion = None
|
| 340 |
+
all_docs_care = personal_docs + general_docs
|
| 341 |
+
for doc in all_docs_care:
|
| 342 |
+
if "emotion" in doc.metadata and doc.metadata["emotion"]:
|
| 343 |
+
emotion_data = doc.metadata["emotion"]
|
| 344 |
+
if isinstance(emotion_data, list): first_emotion = emotion_data[0]
|
| 345 |
+
else: first_emotion = emotion_data
|
| 346 |
+
if first_emotion: break
|
| 347 |
+
|
| 348 |
+
emotions_context = render_emotion_guidelines(first_emotion or emotion_tag)
|
| 349 |
+
is_tagged_scenario = (scenario_tag and scenario_tag != "None") or (emotion_tag and emotion_tag != "None") or (first_emotion is not None)
|
| 350 |
+
template = ANSWER_TEMPLATE_ADQ if is_tagged_scenario else ANSWER_TEMPLATE_CALM
|
| 351 |
+
|
| 352 |
+
if template == ANSWER_TEMPLATE_ADQ:
|
| 353 |
+
user_prompt = template.format(general_context=general_context, personal_context=personal_context, question=query, scenario_tag=scenario_tag, emotions_context=emotions_context, role=role, language=language)
|
| 354 |
+
else:
|
| 355 |
+
combined_context = f"General Guidance:\n{general_context}\n\nPersonal Memories:\n{personal_context}"
|
| 356 |
+
user_prompt = template.format(context=combined_context, question=query, language=language)
|
| 357 |
+
|
| 358 |
+
messages.append({"role": "user", "content": user_prompt})
|
| 359 |
+
answer = call_llm(messages, temperature=temperature)
|
| 360 |
+
|
| 361 |
+
high_risk_scenarios = ["exit_seeking", "wandering", "elopement"]
|
| 362 |
+
if scenario_tag and scenario_tag.lower() in high_risk_scenarios:
|
| 363 |
+
answer += f"\n\n---\n{RISK_FOOTER}"
|
| 364 |
+
|
| 365 |
+
return {"answer": answer, "sources": _format_sources(all_docs_care)}
|
| 366 |
+
|
| 367 |
+
return _answer_fn
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def answer_query(chain, question: str, **kwargs) -> Dict[str, Any]:
|
| 371 |
+
if not callable(chain): return {"answer": "[Error: RAG chain is not callable]", "sources": []}
|
| 372 |
+
chat_history, scenario_tag, emotion_tag = kwargs.get("chat_history", []), kwargs.get("scenario_tag"), kwargs.get("emotion_tag")
|
| 373 |
+
try:
|
| 374 |
+
return chain(question, chat_history=chat_history, scenario_tag=scenario_tag, emotion_tag=emotion_tag)
|
| 375 |
+
except Exception as e:
|
| 376 |
+
print(f"ERROR in answer_query: {e}")
|
| 377 |
+
return {"answer": f"[Error executing chain: {e}]", "sources": []}
|
| 378 |
+
|
| 379 |
+
# -----------------------------
|
| 380 |
+
# TTS & Transcription
|
| 381 |
+
# -----------------------------
|
| 382 |
+
def synthesize_tts(text: str, lang: str = "en"):
|
| 383 |
+
if not text or gTTS is None: return None
|
| 384 |
+
try:
|
| 385 |
+
fd, path = tempfile.mkstemp(suffix=".mp3")
|
| 386 |
+
os.close(fd)
|
| 387 |
+
tts = gTTS(text=text, lang=(lang or "en"))
|
| 388 |
+
tts.save(path)
|
| 389 |
+
return path
|
| 390 |
+
except Exception:
|
| 391 |
+
return None
|
| 392 |
+
|
| 393 |
+
def transcribe_audio(filepath: str, lang: str = "en"):
|
| 394 |
+
client = _openai_client()
|
| 395 |
+
if not client:
|
| 396 |
+
return "[Transcription failed: API key not configured]"
|
| 397 |
+
api_args = {"model": "whisper-1"}
|
| 398 |
+
if lang and lang != "auto":
|
| 399 |
+
api_args["language"] = lang
|
| 400 |
+
with open(filepath, "rb") as audio_file:
|
| 401 |
+
transcription = client.audio.transcriptions.create(file=audio_file, **api_args)
|
| 402 |
+
return transcription.text
|
| 403 |
+
|
alz_companion/prompts.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Prompts for the Alzheimer’s AI Companion.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
# ------------------------ Behaviour‑level tags ------------------------
|
| 6 |
+
BEHAVIOUR_TAGS = {
|
| 7 |
+
# Tags from "The Father"
|
| 8 |
+
"repetitive_questioning": ["validation", "gentle_redirection", "offer_distraction"],
|
| 9 |
+
"confusion": ["reassurance", "time_place_orientation", "photo_anchors"],
|
| 10 |
+
"wandering": ["walk_along_support", "simple_landmarks", "visual_cues", "safe_wandering_space"],
|
| 11 |
+
"agitation": ["de-escalating_tone", "validate_feelings", "reduce_stimulation", "simple_choices"],
|
| 12 |
+
"false_accusations": ["reassure_no_blame", "avoid_arguing", "redirect_activity"],
|
| 13 |
+
"address_memory_loss": ["encourage_ID_bracelet_or_GPS", "place_contact_info_in_wallet", "inform_trusted_neighbors", "avoid_quizzing_on_address"],
|
| 14 |
+
"hallucinations_delusions": ["avoid_arguing_or_correcting", "validate_the_underlying_emotion", "offer_reassurance_of_safety", "gently_redirect_to_real_activity", "check_for_physical_triggers"],
|
| 15 |
+
|
| 16 |
+
# Tags from "Still Alice" (and others for future use)
|
| 17 |
+
"exit_seeking": ["validation", "calm_presence", "safe_wandering_space", "environmental_cues"],
|
| 18 |
+
"aphasia": ["patience", "simple_language", "nonverbal_cues", "validation"],
|
| 19 |
+
"withdrawal": ["gentle_invitation", "calm_presence", "offer_familiar_comforts", "no_pressure"],
|
| 20 |
+
"affection": ["reciprocate_warmth", "positive_reinforcement", "simple_shared_activity"],
|
| 21 |
+
"sleep_disturbance": ["establish_calm_bedtime_routine", "limit_daytime_naps", "check_for_discomfort_or_pain"],
|
| 22 |
+
"anxiety": ["calm_reassurance", "simple_breathing_exercise", "reduce_environmental_stimuli"],
|
| 23 |
+
"depression_sadness": ["validate_feelings_of_sadness", "encourage_simple_pleasant_activity", "ensure_social_connection"],
|
| 24 |
+
"orientation_check": ["gentle_orientation_cues", "use_familiar_landmarks", "avoid_quizzing"],
|
| 25 |
+
|
| 26 |
+
# Tags from "Away from Her"
|
| 27 |
+
"misidentification": ["gently_correct_with_context", "use_photos_as_anchors", "respond_to_underlying_emotion", "avoid_insistent_correction"],
|
| 28 |
+
|
| 29 |
+
# Other useful tags
|
| 30 |
+
"sundowning_restlessness": ["predictable_routine", "soft_lighting", "low_stimulation", "familiar_music"],
|
| 31 |
+
"object_misplacement": ["nonconfrontational_search", "fixed_storage_spots"],
|
| 32 |
+
|
| 33 |
+
# --- New Tags from Test Fixtures ---
|
| 34 |
+
"validation": [],
|
| 35 |
+
"gentle_reorientation": [],
|
| 36 |
+
"de-escalation": [],
|
| 37 |
+
"distraction": [],
|
| 38 |
+
"spaced_cueing": [],
|
| 39 |
+
"reassurance": [],
|
| 40 |
+
"psychoeducation": [],
|
| 41 |
+
"goal_breakdown": [],
|
| 42 |
+
"routine_structuring": [],
|
| 43 |
+
"reminiscence_prompting": [],
|
| 44 |
+
"reframing": [],
|
| 45 |
+
"distress_tolerance": [],
|
| 46 |
+
"caregiver_communication_template": [],
|
| 47 |
+
"personalised_music_activation": [],
|
| 48 |
+
"memory_probe": [],
|
| 49 |
+
"safety_brief": [],
|
| 50 |
+
"follow_up_prompt": []
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# ------------------------ Emotion styles & helpers ------------------------
|
| 54 |
+
EMOTION_STYLES = {
|
| 55 |
+
"confusion": {"tone": "calm, orienting, concrete", "playbook": ["Offer a simple time/place orientation cue (who/where/when).", "Reference one familiar anchor (photo/object/person).", "Use short sentences and one step at a time."]},
|
| 56 |
+
"fear": {"tone": "reassuring, safety-forward, gentle", "playbook": ["Acknowledge fear without contradiction.", "Provide a clear safety cue (e.g., 'You’re safe here with me').", "Reduce novelty and stimulation; suggest one safe action."]},
|
| 57 |
+
"anger": {"tone": "de-escalating, validating, low-arousal", "playbook": ["Validate the feeling; avoid arguing/correcting.", "Keep voice low and sentences short.", "Offer a simple choice to restore control (e.g., 'tea or water?')."]},
|
| 58 |
+
"sadness": {"tone": "warm, empathetic, gentle reminiscence", "playbook": ["Acknowledge loss/longing.", "Invite one comforting memory or familiar song.", "Keep pace slow; avoid tasking."]},
|
| 59 |
+
"warmth": {"tone": "affirming, appreciative", "playbook": ["Reflect gratitude and positive connection.", "Reinforce what’s going well.", "Keep it light; don’t overload with new info."]},
|
| 60 |
+
"joy": {"tone": "supportive, celebratory (but not overstimulating)", "playbook": ["Share the joy briefly; match energy gently.", "Offer a simple, pleasant follow-up activity.", "Avoid adding complex tasks."]},
|
| 61 |
+
"calm": {"tone": "matter-of-fact, concise, steady", "playbook": ["Keep instructions simple.", "Maintain steady pace.", "No extra soothing needed."]},
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
def render_emotion_guidelines(emotion: str | None) -> str:
|
| 65 |
+
e = (emotion or "").strip().lower()
|
| 66 |
+
if e not in EMOTION_STYLES:
|
| 67 |
+
return "Emotion: (auto)\nDesired tone: calm, clear.\nWhen replying, reassure if distress is apparent; prioritise validation and simple choices."
|
| 68 |
+
style = EMOTION_STYLES[e]
|
| 69 |
+
bullet = "\n".join([f"- {x}" for x in style["playbook"]])
|
| 70 |
+
return f"Emotion: {e}\nDesired tone: {style['tone']}\nWhen replying, follow:\n{bullet}"
|
| 71 |
+
|
| 72 |
+
# ------------------------ NLU Classification ------------------------
|
| 73 |
+
# Sometimes, especially with complex instructions, the AI can fail to generate the JSON correctly,
|
| 74 |
+
# which causes the code that reads the response to fail and return "None"
|
| 75 |
+
# SOLUTION: Improve with a hybrid approach with "Few-Shot" Prompting and step-by-step Chain of Thought
|
| 76 |
+
|
| 77 |
+
CLASSIFICATION_PROMPT = """You are an expert NLU engine. Your task is to analyze the user's query to deeply understand their underlying intent and classify it correctly.
|
| 78 |
+
|
| 79 |
+
--- INSTRUCTIONS ---
|
| 80 |
+
First, in a <thinking> block, you must reason step-by-step about the user's query by following these points:
|
| 81 |
+
- **Literal Meaning:** What is the user literally asking or stating?
|
| 82 |
+
- **Underlying Situation:** What is the deeper emotional state or situation being described? (e.g., caregiver burnout, patient confusion, a request for practical help).
|
| 83 |
+
- **User's Primary Goal:** Is the user's main goal **Practical Planning** (seeking a plan, strategy, or how-to advice) or **Emotional Support** (seeking comfort, validation, or reassurance)? This is the most important step.
|
| 84 |
+
- **Tag Selection:** Based on the primary goal, explain which tags from the provided lists are the most appropriate and why. If the goal is practical, prioritize practical tags. If the goal is emotional, prioritize emotional support tags.
|
| 85 |
+
|
| 86 |
+
# - **User's Goal:** What is their true goal? (e.g., Are they seeking factual information, emotional reassurance, or a practical action plan?).
|
| 87 |
+
# - **Tag Selection:** Based on the goal and situation, explain which tags from the provided lists are the most appropriate and why.
|
| 88 |
+
|
| 89 |
+
Second, after your reasoning, provide a single, valid JSON object with the final classification.
|
| 90 |
+
|
| 91 |
+
--- PROVIDED TAGS ---
|
| 92 |
+
Behaviors: {behavior_options}
|
| 93 |
+
Emotions: {emotion_options}
|
| 94 |
+
Topics: {topic_options}
|
| 95 |
+
Contexts: {context_options}
|
| 96 |
+
|
| 97 |
+
--- EXAMPLES ---
|
| 98 |
+
|
| 99 |
+
User Query: "She looked right through me today, like I wasn't even there."
|
| 100 |
+
<thinking>
|
| 101 |
+
1. **Literal Meaning:** The user's loved one did not seem to recognize them.
|
| 102 |
+
2. **Underlying Situation:** The user is expressing emotional pain and a feeling of invisibility due to the disease's progression.
|
| 103 |
+
3. **User's Goal:** They are seeking comfort and a strategy to cope with this painful experience.
|
| 104 |
+
4. **Tag Selection:** The goal is emotional support, so `behaviors` should be `validation` and `reminiscence_prompting`. The `emotion` is `sadness`. The `topic` is `treatment_option:reassurance`.
|
| 105 |
+
</thinking>
|
| 106 |
+
{{
|
| 107 |
+
"detected_behaviors": ["validation", "reminiscence_prompting"],
|
| 108 |
+
"detected_emotion": "sadness",
|
| 109 |
+
"detected_topic": "treatment_option:reassurance",
|
| 110 |
+
"detected_contexts": ["relationship_spouse", "setting_care_home"]
|
| 111 |
+
}}
|
| 112 |
+
|
| 113 |
+
User Query: "He’s withdrawn today."
|
| 114 |
+
<thinking>
|
| 115 |
+
1. **Literal Meaning:** The patient is not engaging.
|
| 116 |
+
2. **Underlying Situation:** The user is observing the patient's apathy and wants to help.
|
| 117 |
+
3. **User's Goal:** They are implicitly asking for a practical action plan to engage the patient.
|
| 118 |
+
4. **Tag Selection:** The problem is `withdrawal`, but the goal implies a solution. A known strategy is music, so the `behavior` should include the proactive `personalised_music_activation`. The `topic` is `treatment_option:music_therapy`. The user's tone is `calm`.
|
| 119 |
+
</thinking>
|
| 120 |
+
{{
|
| 121 |
+
"detected_behaviors": ["withdrawal", "personalised_music_activation"],
|
| 122 |
+
"detected_emotion": "calm",
|
| 123 |
+
"detected_topic": "treatment_option:music_therapy",
|
| 124 |
+
"detected_contexts": ["setting_care_home"]
|
| 125 |
+
}}
|
| 126 |
+
---
|
| 127 |
+
|
| 128 |
+
User Query: "{query}"
|
| 129 |
+
|
| 130 |
+
<thinking>
|
| 131 |
+
"""
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# ------------------------ Guardrails ------------------------
|
| 135 |
+
SAFETY_GUARDRAILS = """Never provide medical diagnoses or dosing. If a situation implies imminent risk (e.g., wandering/elopement, severe agitation, choking, falls), signpost immediate support from onsite staff or emergency services. Use respectful, person‑centred language. Keep guidance concrete and stepwise."""
|
| 136 |
+
|
| 137 |
+
# ------------------------ System & Answer Templates ------------------------
|
| 138 |
+
SYSTEM_TEMPLATE = """You are an Alzheimer’s caregiving companion. Address the patient as {patient_name} and the caregiver as {caregiver_name}. Ground every suggestion in retrieved evidence when possible. If unsure, say so plainly.
|
| 139 |
+
{guardrails}
|
| 140 |
+
--- IMPORTANT RULE ---
|
| 141 |
+
You MUST write your entire response in {language} ONLY. This is a strict instruction. Do not use any other language, even if the user or the retrieved context uses a different language. Your final output must be in {language}."""
|
| 142 |
+
|
| 143 |
+
ANSWER_TEMPLATE_CALM = """Context:
|
| 144 |
+
{context}
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
Question from user: {question}
|
| 148 |
+
|
| 149 |
+
---
|
| 150 |
+
Instructions:
|
| 151 |
+
Based on the context, write a gentle and supportive response in a single, natural-sounding paragraph.
|
| 152 |
+
Your response should:
|
| 153 |
+
1. Start by briefly and calmly acknowledging the user's situation or feeling.
|
| 154 |
+
2. Weave 2-3 practical, compassionate suggestions from the context into your paragraph. Do not use a numbered or bulleted list.
|
| 155 |
+
3. Conclude with a short, reassuring phrase.
|
| 156 |
+
4. You MUST use the retrieved context to directly address the user's specific **Question**.
|
| 157 |
+
Your response in {language}:"""
|
| 158 |
+
|
| 159 |
+
# For scenarios tagged with a specific behavior (e.g., agitation, confusion)
|
| 160 |
+
ANSWER_TEMPLATE_ADQ = """--- General Guidance from Knowledge Base ---
|
| 161 |
+
{general_context}
|
| 162 |
+
|
| 163 |
+
--- Relevant Personal Memories ---
|
| 164 |
+
{personal_context}
|
| 165 |
+
|
| 166 |
+
---
|
| 167 |
+
Care scenario: {scenario_tag}
|
| 168 |
+
Response Guidelines:
|
| 169 |
+
{emotions_context}
|
| 170 |
+
Question from user: {question}
|
| 171 |
+
|
| 172 |
+
---
|
| 173 |
+
Instructions:
|
| 174 |
+
Based on ALL the information above, write a **concise, warm, and validating** response for the {role} in a single, natural-sounding paragraph. **Keep the total response to 2-4 sentences.**
|
| 175 |
+
If possible, weave details from the 'Relevant Personal Memories' into your suggestions to make the response feel more personal and familiar.
|
| 176 |
+
Pay close attention to the Response Guidelines to tailor your tone.
|
| 177 |
+
Your response should follow this pattern:
|
| 178 |
+
1. Start by validating the user's feeling or concern with a unique, empathetic opening. DO NOT USE THE SAME OPENING PHRASE REPEATEDLY. Choose from different styles of openers, such as:
|
| 179 |
+
- Acknowledging the difficulty: "That sounds like a very challenging situation..."
|
| 180 |
+
- Expressing understanding: "I can see why that would be worrying..."
|
| 181 |
+
- Stating a shared goal: "Let's walk through how we can handle that..."
|
| 182 |
+
- Directly validating the feeling: "It's completely understandable to feel frustrated when..."
|
| 183 |
+
2. Gently offer **1-2 of the most important practical steps**, combining general guidance with personal memories where appropriate. Do not use a list.
|
| 184 |
+
3. If the scenario involves risk (like exit_seeking), subtly include a safety cue.
|
| 185 |
+
4. End with a compassionate, de-escalation phrase.
|
| 186 |
+
Your response in {language}:"""
|
| 187 |
+
|
| 188 |
+
RISK_FOOTER = """If safety is a concern right now, please seek immediate assistance from onsite staff or local emergency services."""
|
| 189 |
+
|
| 190 |
+
# ------------------------ Router & Specialized Templates ------------------------
|
| 191 |
+
|
| 192 |
+
# --- NEW: Template for expanding user queries for better retrieval ---
|
| 193 |
+
QUERY_EXPANSION_PROMPT = """You are a helpful AI assistant. Your task is to rephrase a user's question into 3 different, semantically similar questions to improve document retrieval.
|
| 194 |
+
Provide the rephrased questions as a JSON list of strings.
|
| 195 |
+
|
| 196 |
+
User Question: "{question}"
|
| 197 |
+
|
| 198 |
+
JSON List:
|
| 199 |
+
"""
|
| 200 |
+
|
| 201 |
+
# Template for routing/classifying the user's intent
|
| 202 |
+
ROUTER_PROMPT = """You are an expert NLU router. Your task is to classify the user's query into one of four categories:
|
| 203 |
+
1. `caregiving_scenario`: The user is describing a situation, asking for advice, or expressing a concern related to Alzheimer's or caregiving.
|
| 204 |
+
2. `factual_question`: The user is asking a direct question about a personal memory, person, or event that would be stored in the memory journal.
|
| 205 |
+
3. `general_knowledge_question`: The user is asking a general knowledge question about the world, facts, or topics not related to personal memories or caregiving (e.g., 'What is the capital of France?', 'Who directed the movie Inception?').
|
| 206 |
+
4. `general_conversation`: The user is making a general conversational remark, like a greeting, a thank you, or a simple statement that does not require a knowledge base lookup.
|
| 207 |
+
|
| 208 |
+
User Query: "{query}"
|
| 209 |
+
|
| 210 |
+
Respond with ONLY a single category name from the list above.
|
| 211 |
+
Category: """
|
| 212 |
+
|
| 213 |
+
# Template for answering direct factual questions
|
| 214 |
+
ANSWER_TEMPLATE_FACTUAL = """Context:
|
| 215 |
+
{context}
|
| 216 |
+
|
| 217 |
+
---
|
| 218 |
+
Question from user: {question}
|
| 219 |
+
|
| 220 |
+
---
|
| 221 |
+
Instructions:
|
| 222 |
+
Based on the provided context, directly and concisely answer the user's question.
|
| 223 |
+
- If the context contains the answer, state it clearly and naturally. Keep your response to a maximum of 3 sentences.
|
| 224 |
+
- If the context does not contain the answer, respond in a warm and friendly tone that you couldn't find a memory of that topic and gently ask if the user would like to talk more about it or add it as a new memory.
|
| 225 |
+
- Do not offer advice or suggestions unless they are part of the retrieved context.
|
| 226 |
+
- ABSOLUTELY DO NOT invent, create, or hallucinate any stories, characters, or details. Your knowledge is limited to the provided context ONLY.
|
| 227 |
+
|
| 228 |
+
Your response MUST be in {language}:"""
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
# --- NEW: Template for answering general knowledge questions ---
|
| 232 |
+
# Template for answering general knowledge questions
|
| 233 |
+
ANSWER_TEMPLATE_GENERAL_KNOWLEDGE = """You are a factual answering engine.
|
| 234 |
+
Your task is to directly answer the user's general knowledge question based on your training data.
|
| 235 |
+
|
| 236 |
+
Instructions:
|
| 237 |
+
- Be factual and concise. Go straight to the answer.
|
| 238 |
+
- If the answer requires a list of examples, provide a maximum of 3 items. Do not use numbering.
|
| 239 |
+
- Do NOT include apologies or disclaimers about your knowledge cutoff date.
|
| 240 |
+
# - Do NOT recommend external websites or other services.
|
| 241 |
+
# - Do NOT ask conversational follow-up questions.
|
| 242 |
+
|
| 243 |
+
User's Question: "{question}"
|
| 244 |
+
|
| 245 |
+
Your factual response in {language}:"""
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
# Template for general, non-RAG conversation
|
| 249 |
+
ANSWER_TEMPLATE_GENERAL = """You are a warm and friendly AI companion. The user has just said: "{question}".
|
| 250 |
+
Respond in a brief, natural, and conversational way. Do not try to provide caregiving advice unless the user asks for it.
|
| 251 |
+
Your response MUST be in {language}:"""
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
# ------------------------ Convenience exports ------------------------
|
| 255 |
+
__all__ = [
|
| 256 |
+
"SYSTEM_TEMPLATE", "ANSWER_TEMPLATE_CALM", "ANSWER_TEMPLATE_ADQ",
|
| 257 |
+
"SAFETY_GUARDRAILS", "RISK_FOOTER", "BEHAVIOUR_TAGS", "EMOTION_STYLES",
|
| 258 |
+
"render_emotion_guidelines", "CLASSIFICATION_PROMPT",
|
| 259 |
+
|
| 260 |
+
# --- New additions ---
|
| 261 |
+
"QUERY_EXPANSION_PROMPT"
|
| 262 |
+
"ROUTER_PROMPT",
|
| 263 |
+
"ANSWER_TEMPLATE_FACTUAL",
|
| 264 |
+
"ANSWER_TEMPLATE_GENERAL_KNOWLEDGE",
|
| 265 |
+
"ANSWER_TEMPLATE_GENERAL"
|
| 266 |
+
]
|