KeenWoo commited on
Commit
797ac54
·
verified ·
1 Parent(s): 323448c

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +1033 -0
app.py ADDED
@@ -0,0 +1,1033 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import shutil
4
+ import gradio as gr
5
+ import tempfile
6
+ from datetime import datetime
7
+ from typing import List, Dict, Any, Optional
8
+ from pytube import YouTube
9
+ from pathlib import Path
10
+ import re
11
+ import pandas as pd
12
+
13
+ # --- Agent Imports ---
14
+ try:
15
+ from alz_companion.agent import (
16
+ bootstrap_vectorstore, make_rag_chain, answer_query, synthesize_tts,
17
+ transcribe_audio, detect_tags_from_query, describe_image, build_or_load_vectorstore,
18
+ _default_embeddings, route_query_type, call_llm
19
+ )
20
+ from alz_companion.prompts import (
21
+ BEHAVIOUR_TAGS, EMOTION_STYLES, FAITHFULNESS_JUDGE_PROMPT
22
+ )
23
+ from langchain.schema import Document
24
+ from langchain_community.vectorstores import FAISS
25
+ AGENT_OK = True
26
+ except Exception as e:
27
+ AGENT_OK = False
28
+ class Document:
29
+ def __init__(self, page_content, metadata): self.page_content, self.metadata = page_content, metadata
30
+ class FAISS:
31
+ def __init__(self):
32
+ self.docstore = type('obj', (object,), {'_dict': {}})()
33
+ def add_documents(self, docs):
34
+ start_idx = len(self.docstore._dict)
35
+ for i, d in enumerate(docs, start_idx):
36
+ self.docstore._dict[i] = d
37
+ def save_local(self, path): pass
38
+ @classmethod
39
+ def from_documents(cls, docs, embeddings=None):
40
+ inst = cls()
41
+ inst.add_documents(docs)
42
+ return inst
43
+ def build_or_load_vectorstore(docs, index_path, is_personal=False): return FAISS.from_documents(docs or [], embeddings=None)
44
+ def bootstrap_vectorstore(sample_paths=None, index_path="data/"): return object()
45
+ def make_rag_chain(vs_general, vs_personal, **kwargs): return lambda q, **k: {"answer": f"(Demo) You asked: {q}", "sources": []}
46
+ def answer_query(chain, q, **kwargs): return chain(q, **kwargs)
47
+ def synthesize_tts(text: str, lang: str = "en"): return None
48
+ def transcribe_audio(filepath: str, lang: str = "en"): return "This is a transcribed message."
49
+ def detect_tags_from_query(*args, **kwargs): return {"detected_behavior": "None", "detected_emotion": "None"}
50
+ def describe_image(image_path: str): return "This is a description of an image."
51
+ def _default_embeddings(): return None
52
+ def route_query_type(query: str): return "general_conversation"
53
+ def call_llm(messages, **kwargs): return "Cannot call LLM in fallback mode."
54
+ BEHAVIOUR_TAGS, EMOTION_STYLES, FAITHFULNESS_JUDGE_PROMPT = {"None": []}, {"None": {}}, ""
55
+ print(f"WARNING: Could not import from alz_companion ({e}). Running in UI-only demo mode.")
56
+
57
+
58
+ # --- NEW: Import for Evaluation Logic ---
59
+ try:
60
+ from evaluate import load_test_fixtures, run_comprehensive_evaluation
61
+ except ImportError:
62
+ # Fallback if evaluate.py is not found
63
+ def load_test_fixtures(): print("WARNING: evaluate.py not found.")
64
+ def run_comprehensive_evaluation(*args, **kwargs): return "Evaluation module not found.", []
65
+
66
+
67
+ # --- Centralized Configuration ---
68
+ CONFIG = {
69
+ "themes": ["All", "The Father", "Still Alice", "Away from Her", "Alive Inside", "General Caregiving"],
70
+ "roles": ["patient", "caregiver"],
71
+ "disease_stages": ["Default: Mild Stage", "Moderate Stage", "Advanced Stage"],
72
+ "behavior_tags": ["None"] + list(BEHAVIOUR_TAGS.keys()),
73
+ "emotion_tags": ["None"] + list(EMOTION_STYLES.keys()),
74
+ "topic_tags": ["None", "caregiving_advice", "medical_fact", "personal_story", "research_update", "treatment_option:home_safety", "treatment_option:long_term_care", "treatment_option:music_therapy", "treatment_option:reassurance", "treatment_option:routine_structuring", "treatment_option:validation_therapy"],
75
+ "context_tags": ["None", "disease_stage_mild", "disease_stage_moderate", "disease_stage_advanced", "disease_stage_unspecified", "interaction_mode_one_to_one", "interaction_mode_small_group", "interaction_mode_group_activity", "relationship_family", "relationship_spouse", "relationship_staff_or_caregiver", "relationship_unspecified", "setting_home_or_community", "setting_care_home", "setting_clinic_or_hospital"],
76
+ "languages": {"English": "en", "Chinese": "zh", "Cantonese": "zh-yue", "Korean": "ko", "Japanese": "ja", "Malay": "ms", "French": "fr", "Spanish": "es", "Hindi": "hi", "Arabic": "ar"},
77
+ "tones": ["warm", "empathetic", "caring", "reassuring", "calm", "optimistic", "motivating", "neutral", "formal", "humorous"],
78
+ # --- ADD THIS NEW KEY AND LIST ---
79
+ "music_moods": [
80
+ "Confusion or Disorientation",
81
+ "Reminiscence and Connection",
82
+ "Sundowning or Restlessness",
83
+ "Sadness or Longing",
84
+ "Anxiety or Fear",
85
+ "Agitation or Anger",
86
+ "Joy or Affection"
87
+ ]
88
+ # --- END OF ADDITION ---
89
+ }
90
+
91
+ # --- File Management & Vector Store Logic ---
92
+ def _storage_root() -> Path:
93
+ for p in [Path(os.getenv("SPACE_STORAGE", "")), Path("/data"), Path.home() / ".cache" / "alz_companion"]:
94
+ if not p: continue
95
+ try:
96
+ p.mkdir(parents=True, exist_ok=True)
97
+ (p / ".write_test").write_text("ok")
98
+ (p / ".write_test").unlink(missing_ok=True)
99
+ return p
100
+ except Exception: continue
101
+ tmp = Path(tempfile.gettempdir()) / "alz_companion"
102
+ tmp.mkdir(parents=True, exist_ok=True)
103
+ return tmp
104
+ STORAGE_ROOT = _storage_root()
105
+ INDEX_BASE = STORAGE_ROOT / "index"
106
+ # --- NEW: Define path for the auto-loading folder ---
107
+ PERSISTENT_MEMORY_PATH = Path(__file__).parent / "Personal Memory Bank"
108
+ # --- END NEW ---
109
+ PERSONAL_DATA_BASE = STORAGE_ROOT / "personal"
110
+ UPLOADS_BASE = INDEX_BASE / "uploads"
111
+ PERSONAL_INDEX_PATH = str(PERSONAL_DATA_BASE / "personal_faiss_index")
112
+ NLU_EXAMPLES_INDEX_PATH = str(INDEX_BASE / "nlu_examples_faiss_index")
113
+ THEME_PATHS = {t: str(INDEX_BASE / f"faiss_index_{t.replace(' ', '').lower()}") for t in CONFIG["themes"]}
114
+ os.makedirs(UPLOADS_BASE, exist_ok=True)
115
+ os.makedirs(PERSONAL_DATA_BASE, exist_ok=True)
116
+ # --- NEW: Create the folders on startup if it does not exist ---
117
+ os.makedirs(PERSISTENT_MEMORY_PATH, exist_ok=True)
118
+ # --- END NEW ---
119
+
120
+
121
+ for p in THEME_PATHS.values(): os.makedirs(p, exist_ok=True)
122
+ vectorstores = {}
123
+ personal_vectorstore = None
124
+ nlu_vectorstore = None
125
+
126
+ try:
127
+ personal_vectorstore = build_or_load_vectorstore([], PERSONAL_INDEX_PATH, is_personal=True)
128
+ except Exception:
129
+ personal_vectorstore = None
130
+ def bootstrap_nlu_vectorstore(example_file: str, index_path: str) -> FAISS:
131
+ if not os.path.exists(example_file):
132
+ print(f"WARNING: NLU example file not found at {example_file}. NLU will be less accurate.")
133
+ return build_or_load_vectorstore([], index_path)
134
+ docs = []
135
+ with open(example_file, "r", encoding="utf-8") as f:
136
+ for line in f:
137
+ try:
138
+ data = json.loads(line)
139
+ doc = Document(page_content=data["query"], metadata=data)
140
+ docs.append(doc)
141
+ except (json.JSONDecodeError, KeyError): continue
142
+ print(f"Found and loaded {len(docs)} NLU training examples.")
143
+ if os.path.exists(index_path): shutil.rmtree(index_path)
144
+ return build_or_load_vectorstore(docs, index_path)
145
+
146
+
147
+ # In app.py, near the other path definitions
148
+ PERSONAL_MUSIC_BASE = PERSONAL_DATA_BASE / "music"
149
+ os.makedirs(PERSONAL_MUSIC_BASE, exist_ok=True)
150
+
151
+ # In app.py, replace your existing versions of these three functions with the code below.
152
+ # --- Function 1: Auto-loads non-music memories from the 'Personal Memory Bank' folder ---
153
+ def load_personal_files_from_folder():
154
+ """
155
+ Scans the 'Personal Memory Bank' folder and loads new multi-modal files
156
+ (text, audio, video, images) into the personal vectorstore.
157
+ """
158
+ global personal_vectorstore
159
+ print("Scanning 'Personal Memory Bank' folder for new files...")
160
+ if not os.path.exists(PERSISTENT_MEMORY_PATH):
161
+ return
162
+
163
+ # Define supported file extensions
164
+ TEXT_EXTENSIONS = (".txt",)
165
+ AUDIO_EXTENSIONS = (".mp3", ".wav", ".m4a", ".flac")
166
+ VIDEO_EXTENSIONS = (".mp4", ".mov", ".avi", ".mkv")
167
+ IMAGE_EXTENSIONS = (".jpg", ".jpeg", ".png", ".gif", ".bmp")
168
+
169
+ # Get a list of sources already in the vectorstore to avoid re-processing files
170
+ existing_sources = set()
171
+ if personal_vectorstore and hasattr(personal_vectorstore.docstore, '_dict'):
172
+ for doc in personal_vectorstore.docstore._dict.values():
173
+ existing_sources.add(doc.metadata.get("source"))
174
+
175
+ docs_to_add = []
176
+ for filename in os.listdir(PERSISTENT_MEMORY_PATH):
177
+ if filename in existing_sources:
178
+ continue
179
+
180
+ filepath = PERSISTENT_MEMORY_PATH / filename
181
+ content_to_process = ""
182
+
183
+ file_lower = filename.lower()
184
+
185
+ if file_lower.endswith(TEXT_EXTENSIONS):
186
+ print(f" - Found new text file to load: {filename}")
187
+ with open(filepath, "r", encoding="utf-8") as f:
188
+ content_to_process = f.read()
189
+
190
+ elif file_lower.endswith(AUDIO_EXTENSIONS) or file_lower.endswith(VIDEO_EXTENSIONS):
191
+ media_type = "Audio" if file_lower.endswith(AUDIO_EXTENSIONS) else "Video"
192
+ print(f" - Found new {media_type} file to transcribe: {filename}")
193
+ try:
194
+ transcribed_text = transcribe_audio(str(filepath))
195
+ title = os.path.splitext(filename)[0].replace('_', ' ').replace('-', ' ')
196
+ content_to_process = f"Title: {title}\n\nContent: {transcribed_text}"
197
+ except Exception as e:
198
+ print(f" - ERROR: Failed to transcribe {filename}. Reason: {e}")
199
+ continue
200
+
201
+ elif file_lower.endswith(IMAGE_EXTENSIONS):
202
+ print(f" - Found new Image file to describe: {filename}")
203
+ try:
204
+ description = describe_image(str(filepath))
205
+ title = os.path.splitext(filename)[0].replace('_', ' ').replace('-', ' ')
206
+ content_to_process = f"Title: {title}\n\nContent: {description}"
207
+ except Exception as e:
208
+ print(f" - ERROR: Failed to describe {filename}. Reason: {e}")
209
+ continue
210
+
211
+ if content_to_process:
212
+ docs_to_add.extend(parse_and_tag_entries(content_to_process, source=filename, settings={}))
213
+
214
+ if docs_to_add:
215
+ if personal_vectorstore is None:
216
+ personal_vectorstore = build_or_load_vectorstore(docs_to_add, PERSONAL_INDEX_PATH, is_personal=True)
217
+ else:
218
+ personal_vectorstore.add_documents(docs_to_add)
219
+
220
+ personal_vectorstore.save_local(PERSONAL_INDEX_PATH)
221
+ print(f"Successfully added {len(docs_to_add)} new document(s) from the folder.")
222
+
223
+
224
+ # --- Function 2: Auto-syncs music from the 'Music Library' folder (Hybrid Approach) ---
225
+ def sync_music_library_from_folder():
226
+ """Scans 'Music Library' folder, syncs manifest for playback, and adds lyrics to vectorstore."""
227
+ global personal_vectorstore
228
+ music_library_path = PERSISTENT_MEMORY_PATH / "Music Library"
229
+ os.makedirs(music_library_path, exist_ok=True)
230
+
231
+ manifest_path = PERSONAL_MUSIC_BASE / "music_manifest.json"
232
+ manifest = {}
233
+ if manifest_path.exists():
234
+ with open(manifest_path, "r") as f: manifest = json.load(f)
235
+
236
+ existing_sources = set()
237
+ if personal_vectorstore and hasattr(personal_vectorstore.docstore, '_dict'):
238
+ for doc in personal_vectorstore.docstore._dict.values():
239
+ existing_sources.add(doc.metadata.get("source"))
240
+
241
+ print("Scanning 'Music Library' folder for new songs...")
242
+ filename_pattern = re.compile(r'^(.*?) - (.*?) - (.*?)\.(mp3|wav|m4a|ogg|flac)$', re.IGNORECASE)
243
+
244
+ synced_count = 0
245
+ docs_to_add = []
246
+ for filename in os.listdir(music_library_path):
247
+ song_id = filename.replace(" ", "_").lower()
248
+ if song_id in manifest and filename in existing_sources:
249
+ continue
250
+
251
+ match = filename_pattern.match(filename)
252
+ if match:
253
+ print(f" - Found new song to sync: {filename}")
254
+ title, artist, tag = match.groups()[:3]
255
+
256
+ source_path = music_library_path / filename
257
+ dest_path = PERSONAL_MUSIC_BASE / filename
258
+ if not os.path.exists(dest_path):
259
+ shutil.copy2(str(source_path), str(dest_path))
260
+
261
+ # Add to manifest for playback system
262
+ song_metadata = {"title": title.strip(), "artist": artist.strip(), "moods": [tag.strip().lower()], "filepath": str(dest_path)}
263
+ manifest[song_id] = song_metadata
264
+
265
+ # --- NEW HYBRID LOGIC: Transcribe and prep for vectorstore ---
266
+ # Transcribe and prep for semantic memory system (vectorstore)
267
+ if filename not in existing_sources:
268
+ try:
269
+ print(f" - Transcribing '{title}' for memory bank...")
270
+ lyrics = transcribe_audio(str(dest_path))
271
+ content_for_rag = (
272
+ f"Title: Song - {song_metadata['title']}\n"
273
+ f"Artist: {song_metadata['artist']}\n"
274
+ f"Moods: {', '.join(song_metadata['moods'])}\n\n"
275
+ f"Lyrics:\n{lyrics}"
276
+ )
277
+ docs_to_add.extend(parse_and_tag_entries(content_for_rag, source=filename, settings={}))
278
+ except Exception as e:
279
+ print(f" - WARNING: Failed to transcribe {filename} for memory bank. Error: {e}")
280
+ # --- END OF NEW HYBRID LOGIC ---
281
+ synced_count += 1
282
+
283
+ if synced_count > 0:
284
+ with open(manifest_path, "w") as f: json.dump(manifest, f, indent=2)
285
+ print(f"Successfully synced {synced_count} new song(s) to the music manifest.")
286
+
287
+ if docs_to_add:
288
+ if personal_vectorstore is None:
289
+ personal_vectorstore = build_or_load_vectorstore(docs_to_add, PERSONAL_INDEX_PATH, is_personal=True)
290
+ else:
291
+ personal_vectorstore.add_documents(docs_to_add)
292
+ personal_vectorstore.save_local(PERSONAL_INDEX_PATH)
293
+ print(f"Successfully added lyrics for {len(docs_to_add)} song(s) to the personal vectorstore.")
294
+
295
+
296
+ def canonical_theme(tk: str) -> str: return tk if tk in CONFIG["themes"] else "All"
297
+ def theme_upload_dir(theme: str) -> str:
298
+ p = UPLOADS_BASE / f"theme_{canonical_theme(theme).replace(' ', '').lower()}"
299
+ p.mkdir(exist_ok=True)
300
+ return str(p)
301
+ def load_manifest(theme: str) -> Dict[str, Any]:
302
+ p = os.path.join(theme_upload_dir(theme), "manifest.json")
303
+ if os.path.exists(p):
304
+ try:
305
+ with open(p, "r", encoding="utf-8") as f: return json.load(f)
306
+ except Exception: pass
307
+ return {"files": {}}
308
+ def save_manifest(theme: str, man: Dict[str, Any]):
309
+ with open(os.path.join(theme_upload_dir(theme), "manifest.json"), "w", encoding="utf-8") as f: json.dump(man, f, indent=2)
310
+ def list_theme_files(theme: str) -> List[tuple[str, bool]]:
311
+ man = load_manifest(theme)
312
+ base = theme_upload_dir(theme)
313
+ found = [(n, bool(e)) for n, e in man.get("files", {}).items() if os.path.exists(os.path.join(base, n))]
314
+ existing = {n for n, e in found}
315
+ for name in sorted(os.listdir(base)):
316
+ if name not in existing and os.path.isfile(os.path.join(base, name)): found.append((name, False))
317
+ man["files"] = dict(found)
318
+ save_manifest(theme, man)
319
+ return found
320
+ def copy_into_theme(theme: str, src_path: str) -> str:
321
+ fname = os.path.basename(src_path)
322
+ dest = os.path.join(theme_upload_dir(theme), fname)
323
+ shutil.copy2(src_path, dest)
324
+ return dest
325
+ def seed_files_into_theme(theme: str):
326
+ SEED_FILES = [("sample_data/caregiving_tips.txt", True), ("sample_data/the_father_segments_enriched_harmonized_plus.jsonl", True), ("sample_data/still_alice_enriched_harmonized_plus.jsonl", True), ("sample_data/away_from_her_enriched_harmonized_plus.jsonl", True), ("sample_data/alive_inside_enriched_harmonized.jsonl", True)]
327
+ man, changed = load_manifest(theme), False
328
+ for path, enable in SEED_FILES:
329
+ if not os.path.exists(path): continue
330
+ fname = os.path.basename(path)
331
+ if not os.path.exists(os.path.join(theme_upload_dir(theme), fname)):
332
+ copy_into_theme(theme, path)
333
+ man["files"][fname] = bool(enable)
334
+ changed = True
335
+ if changed: save_manifest(theme, man)
336
+ def ensure_index(theme='All'):
337
+ theme = canonical_theme(theme)
338
+ if theme in vectorstores: return vectorstores[theme]
339
+ upload_dir = theme_upload_dir(theme)
340
+ enabled_files = [os.path.join(upload_dir, n) for n, enabled in list_theme_files(theme) if enabled]
341
+ index_path = THEME_PATHS.get(theme)
342
+ vectorstores[theme] = bootstrap_vectorstore(sample_paths=enabled_files, index_path=index_path)
343
+ return vectorstores[theme]
344
+
345
+ # --- Gradio Callbacks ---
346
+ # In app.py, modify the collect_settings function
347
+
348
+ def collect_settings(*args):
349
+ keys = ["role", "patient_name", "caregiver_name", "tone", "language", "tts_lang", "temperature",
350
+ # --- ADD "disease_stage" to this list ---
351
+ "disease_stage",
352
+ "behaviour_tag", "emotion_tag", "topic_tag", "active_theme", "tts_on", "debug_mode"]
353
+ return dict(zip(keys, args))
354
+
355
+
356
+ # In app.py, replace the entire parse_and_tag_entries function.
357
+ def parse_and_tag_entries(text_content: str, source: str, settings: dict = None) -> List[Document]:
358
+ docs_to_add = []
359
+ # This logic correctly handles both simple text and complex journal entries
360
+ entries = re.split(r'\n(?:---|--|-|-\*-|-\.-)\n', text_content)
361
+ if len(entries) == 1 and "title:" not in entries[0].lower() and "content:" not in entries[0].lower():
362
+ entries = [text_content] # Treat simple text as a single entry
363
+
364
+ for entry in entries:
365
+ if not entry.strip(): continue
366
+
367
+ lines = entry.strip().split('\n')
368
+ title_line = lines[0].split(':', 1)
369
+ title = title_line[1].strip() if len(title_line) > 1 and "title:" in lines[0].lower() else "Untitled Text Entry"
370
+ content_part = "\n".join(lines[1:])
371
+ content = content_part.split(':', 1)[1].strip() if "content:" in content_part.lower() else content_part.strip() or entry.strip()
372
+
373
+ if not content: continue
374
+
375
+ full_content = f"Title: {title}\n\nContent: {content}"
376
+
377
+ detected_tags = detect_tags_from_query(
378
+ content, nlu_vectorstore=nlu_vectorstore,
379
+ behavior_options=CONFIG["behavior_tags"], emotion_options=CONFIG["emotion_tags"],
380
+ topic_options=CONFIG["topic_tags"], context_options=CONFIG["context_tags"],
381
+ settings=settings
382
+ )
383
+
384
+ metadata = {"source": source, "title": title}
385
+
386
+ # --- START: CORRECTED METADATA ASSIGNMENT ---
387
+ if detected_tags.get("detected_behaviors"):
388
+ metadata["behaviors"] = [b.lower() for b in detected_tags["detected_behaviors"]]
389
+ detected_emotion = detected_tags.get("detected_emotion")
390
+ if detected_emotion and detected_emotion != "None":
391
+ metadata["emotion"] = detected_emotion.lower()
392
+
393
+ # Correctly handle the plural "detected_topics" key and list value
394
+ detected_topics = detected_tags.get("detected_topics")
395
+ if detected_topics:
396
+ metadata["topic_tags"] = [t.lower() for t in detected_topics]
397
+
398
+ if detected_tags.get("detected_contexts"):
399
+ metadata["context_tags"] = [c.lower() for c in detected_tags["detected_contexts"]]
400
+ # --- END: CORRECTED METADATA ASSIGNMENT ---
401
+
402
+ docs_to_add.append(Document(page_content=full_content, metadata=metadata))
403
+
404
+ return docs_to_add
405
+
406
+
407
+ def handle_add_knowledge(title, text_input, file_input, image_input, yt_url, settings):
408
+ global personal_vectorstore
409
+ docs_to_add = []
410
+ source, content = "Unknown", ""
411
+ if text_input and text_input.strip():
412
+ source, content = "Text Input", f"Title: {title or 'Untitled'}\n\nContent: {text_input}"
413
+ elif file_input:
414
+ source = os.path.basename(file_input.name)
415
+ if file_input.name.lower().endswith('.txt'):
416
+ with open(file_input.name, 'r', encoding='utf-8') as f: content = f.read()
417
+ else:
418
+ transcribed = transcribe_audio(file_input.name)
419
+ content = f"Title: {title or 'Audio/Video Note'}\n\nContent: {transcribed}"
420
+ elif image_input:
421
+ source, description = "Image Input", describe_image(image_input)
422
+ content = f"Title: {title or 'Image Note'}\n\nContent: {description}"
423
+ elif yt_url and ("youtube.com" in yt_url or "youtu.be" in yt_url):
424
+ try:
425
+ yt = YouTube(yt_url)
426
+ with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_audio_file:
427
+ yt.streams.get_audio_only().download(filename=temp_audio_file.name)
428
+ transcribed = transcribe_audio(temp_audio_file.name)
429
+ os.remove(temp_audio_file.name)
430
+ source, content = f"YouTube: {yt.title}", f"Title: {title or yt.title}\n\nContent: {transcribed}"
431
+ except Exception as e:
432
+ return f"Error processing YouTube link: {e}"
433
+ else:
434
+ return "Please provide content to add."
435
+ if content:
436
+ docs_to_add = parse_and_tag_entries(content, source, settings=settings)
437
+ if not docs_to_add: return "No processable content found to add."
438
+ if personal_vectorstore is None:
439
+ personal_vectorstore = build_or_load_vectorstore(docs_to_add, PERSONAL_INDEX_PATH, is_personal=True)
440
+ else:
441
+ personal_vectorstore.add_documents(docs_to_add)
442
+ personal_vectorstore.save_local(PERSONAL_INDEX_PATH)
443
+ return f"Successfully added {len(docs_to_add)} new memory/memories."
444
+
445
+
446
+ # In app.py, add this new handler function
447
+
448
+ def handle_add_music(file, title, artist, mood):
449
+ if not all([file, title, artist, mood]):
450
+ return "Please fill out all fields."
451
+
452
+ # Save the audio file
453
+ filename = os.path.basename(file.name)
454
+ dest_path = PERSONAL_MUSIC_BASE / filename
455
+ shutil.copy2(file.name, str(dest_path))
456
+
457
+ # Save the metadata to a manifest file
458
+ manifest_path = PERSONAL_MUSIC_BASE / "music_manifest.json"
459
+ manifest = {}
460
+ if manifest_path.exists():
461
+ with open(manifest_path, "r") as f:
462
+ manifest = json.load(f)
463
+
464
+ song_id = filename.replace(" ", "_").lower()
465
+ manifest[song_id] = {
466
+ "title": title.strip(),
467
+ "artist": artist.strip(),
468
+ # "moods": [m.strip().lower() for m in mood.split(",")],
469
+ "moods": [m.lower() for m in mood], # Correctly handles the list from the dropdown
470
+ "filepath": str(dest_path) # Store the full path for backend access
471
+ }
472
+
473
+ with open(manifest_path, "w") as f:
474
+ json.dump(manifest, f, indent=2)
475
+
476
+ return f"Successfully added '{title}' to the music library."
477
+
478
+ # In app.py, add these two new functions (e.g., after the handle_add_music function)
479
+
480
+ def list_music_library():
481
+ """Loads the music manifest and formats it for the Gradio UI."""
482
+ manifest_path = PERSONAL_MUSIC_BASE / "music_manifest.json"
483
+ if not manifest_path.exists():
484
+ return gr.update(value=[["Library is empty", "", ""]]), gr.update(choices=[], value=None)
485
+
486
+ with open(manifest_path, "r") as f:
487
+ manifest = json.load(f)
488
+
489
+ if not manifest:
490
+ return gr.update(value=[["Library is empty", "", ""]]), gr.update(choices=[], value=None)
491
+
492
+ display_data = [[data['title'], data['artist'], ", ".join(data['moods'])] for data in manifest.values()]
493
+
494
+ # Use the song's unique ID (the key in the manifest) for the delete dropdown
495
+ delete_choices = list(manifest.keys())
496
+
497
+ return gr.update(value=display_data), gr.update(choices=delete_choices, value=None)
498
+
499
+ def delete_music_from_library(song_id_to_delete):
500
+ """Deletes a song from the manifest, the audio file, and the vectorstore."""
501
+ global personal_vectorstore
502
+ if not song_id_to_delete:
503
+ return "No music selected to delete."
504
+
505
+ # 1. Remove from manifest and delete audio file
506
+ manifest_path = PERSONAL_MUSIC_BASE / "music_manifest.json"
507
+ if not manifest_path.exists(): return "Error: Music manifest not found."
508
+
509
+ with open(manifest_path, "r") as f: manifest = json.load(f)
510
+
511
+ song_to_delete = manifest.pop(song_id_to_delete, None)
512
+ if not song_to_delete: return f"Error: Could not find song ID {song_id_to_delete} in manifest."
513
+
514
+ with open(manifest_path, "w") as f: json.dump(manifest, f, indent=2)
515
+
516
+ try:
517
+ os.remove(song_to_delete['filepath'])
518
+ except OSError as e:
519
+ print(f"Error deleting audio file {song_to_delete['filepath']}: {e}")
520
+
521
+ # 2. Remove lyrics from the personal vectorstore
522
+ if personal_vectorstore and hasattr(personal_vectorstore.docstore, '_dict'):
523
+ filename_to_delete = os.path.basename(song_to_delete['filepath'])
524
+ all_docs = list(personal_vectorstore.docstore._dict.values())
525
+
526
+ # Find the document whose source matches the audio filename
527
+ docs_to_keep = [d for d in all_docs if d.metadata.get("source") != filename_to_delete]
528
+
529
+ if len(all_docs) > len(docs_to_keep):
530
+ if not docs_to_keep: # If it was the last doc
531
+ if os.path.isdir(PERSONAL_INDEX_PATH): shutil.rmtree(PERSONAL_INDEX_PATH)
532
+ personal_vectorstore = build_or_load_vectorstore([], PERSONAL_INDEX_PATH, is_personal=True)
533
+ else:
534
+ new_vs = FAISS.from_documents(docs_to_keep, _default_embeddings())
535
+ new_vs.save_local(PERSONAL_INDEX_PATH)
536
+ personal_vectorstore = new_vs
537
+ return f"Successfully deleted '{song_to_delete['title']}' from the library and memory bank."
538
+
539
+ return f"Successfully deleted '{song_to_delete['title']}' from the music library."
540
+
541
+
542
+ def chat_fn(user_text, audio_file, settings, chat_history):
543
+
544
+ # --- ADD THIS DEBUG BLOCK AT THE TOP ---
545
+ print("\n" + "="*50)
546
+ print(f"[DEBUG app.py] chat_fn received settings: {settings}")
547
+ print("="*50 + "\n")
548
+ # --- END OF ADDITION ---
549
+
550
+ global personal_vectorstore
551
+ question = (user_text or "").strip()
552
+ if audio_file and not question:
553
+ try:
554
+ question = transcribe_audio(audio_file, lang=CONFIG["languages"].get(settings.get("tts_lang", "English"), "en"))
555
+ except Exception as e:
556
+ err_msg = f"Audio Error: {e}" if settings.get("debug_mode") else "Sorry, I couldn't understand the audio."
557
+ chat_history.append({"role": "assistant", "content": err_msg})
558
+ return "", None, chat_history
559
+
560
+ if not question:
561
+ return "", None, chat_history
562
+
563
+ # --- START FIX 1: Correctly process the incoming chat_history (list of dicts) ---
564
+ # The incoming chat_history is already in the desired format for the API,
565
+ # we just need to filter out our special system messages (like sources).
566
+ api_chat_history = [
567
+ msg for msg in chat_history
568
+ if msg.get("content") and not msg["content"].strip().startswith("*(")
569
+ ]
570
+
571
+ # Append the new user question to the history that will be displayed in the UI
572
+ chat_history.append({"role": "user", "content": question})
573
+ # --- END FIX 1 ---
574
+
575
+ # NEW
576
+ query_type = route_query_type(question, severity=settings.get("disease_stage", "Default: Mild Stage"))
577
+ # query_type = route_query_type(question)
578
+ # --- ADD THIS DEBUG PRINT ---
579
+ print(f"[DEBUG] Router classified query as: {query_type}")
580
+ # --- END OF ADDITION ---
581
+
582
+
583
+ final_tags = { "scenario_tag": None, "emotion_tag": None, "topic_tag": None, "context_tags": [] }
584
+ manual_behavior = settings.get("behaviour_tag", "None")
585
+ manual_emotion = settings.get("emotion_tag", "None")
586
+ manual_topic = settings.get("topic_tag", "None")
587
+
588
+ auto_detected_context = ""
589
+ if not all(m == "None" for m in [manual_behavior, manual_emotion, manual_topic]):
590
+ # --- ADD THIS DEBUG PRINT ---
591
+ print(f"[DEBUG app.py] Manual override DETECTED. Behavior='{manual_behavior}', Emotion='{manual_emotion}', Topic='{manual_topic}'")
592
+ # --- END OF ADDITION ---
593
+
594
+ final_tags["scenario_tag"] = manual_behavior if manual_behavior != "None" else None
595
+ final_tags["emotion_tag"] = manual_emotion if manual_emotion != "None" else None
596
+ final_tags["topic_tag"] = manual_topic if manual_topic != "None" else None
597
+
598
+ # NEW: Expand detecting emotions and behaviors for caregiving to music playing
599
+ # whenever a request to play music, the system will first analyze their query to detect an underlying emotion or behavior
600
+ elif "caregiving_scenario" in query_type or "play_music_request" in query_type:
601
+
602
+ # --- NEW DEBUG BLOCK: Print inputs before calling NLU ---
603
+ print("\n--- [DEBUG app.py] Preparing to call NLU ---")
604
+ print(f" - Query to Analyze: '{question}'")
605
+ print(f" - NLU Vectorstore Loaded: {nlu_vectorstore is not None}")
606
+ print(f" - Current Settings Passed: {settings}")
607
+ print("------------------------------------------")
608
+ # --- END OF NEW DEBUG BLOCK ---
609
+
610
+ detected_tags = detect_tags_from_query(
611
+ question, nlu_vectorstore=nlu_vectorstore, behavior_options=CONFIG["behavior_tags"],
612
+ emotion_options=CONFIG["emotion_tags"], topic_options=CONFIG["topic_tags"],
613
+ context_options=CONFIG["context_tags"], settings=settings)
614
+
615
+ # --- ADD THIS DEBUG PRINT ---
616
+ print(f"[DEBUG app.py] Raw NLU output: {detected_tags}")
617
+ # --- END OF ADDITION ---
618
+
619
+ behaviors = detected_tags.get("detected_behaviors")
620
+ final_tags["scenario_tag"] = behaviors[0] if behaviors else None
621
+ final_tags["emotion_tag"] = detected_tags.get("detected_emotion")
622
+ final_tags["topic_tag"] = detected_tags.get("detected_topic")
623
+ final_tags["context_tags"] = detected_tags.get("detected_contexts", [])
624
+
625
+ # --- ADD THIS DEBUG PRINT ---
626
+ print(f"[DEBUG] NLU detected tags: {final_tags}")
627
+ # --- END OF ADDITION ---
628
+
629
+ detected_parts = [f"{k.split('_')[1]}=`{v}`" for k, v in final_tags.items() if v and v != "None" and v != []]
630
+ if detected_parts:
631
+ auto_detected_context = f"*(Auto-detected context: {', '.join(detected_parts)})*"
632
+
633
+ vs_general = ensure_index(settings.get("active_theme", "All"))
634
+ if personal_vectorstore is None:
635
+ personal_vectorstore = build_or_load_vectorstore([], PERSONAL_INDEX_PATH, is_personal=True)
636
+
637
+ # OLD rag_settings = {k: settings.get(k) for k in ["role", "temperature", "language", "patient_name", "caregiver_name", "tone"]}
638
+ # NEW add "disease_stage"
639
+ # rag_settings = {k: settings.get(k) for k in ["role", "temperature", "language", "patient_name", "caregiver_name", "tone", "disease_stage"]}
640
+
641
+ # First, construct the path to the manifest file.
642
+ manifest_path_str = str(PERSONAL_MUSIC_BASE / "music_manifest.json")
643
+
644
+ # Then, gather all the settings from the UI into the dictionary.
645
+ rag_settings = {k: settings.get(k) for k in ["role", "temperature", "language", "patient_name", "caregiver_name", "tone", "disease_stage"]}
646
+
647
+ # Finally, add the special manifest path to that same dictionary.
648
+ rag_settings["music_manifest_path"] = manifest_path_str
649
+
650
+ chain = make_rag_chain(vs_general, personal_vectorstore, **rag_settings)
651
+
652
+ response = answer_query(chain, question, query_type=query_type, chat_history=api_chat_history, **final_tags)
653
+
654
+ # --- MUSIC PLAYBACK LOGIC START ---
655
+
656
+ # 1. Extract the text answer and the potential music file path from the agent's response.
657
+ answer = response.get("answer", "[No answer found]")
658
+ audio_playback_url = response.get("audio_playback_url")
659
+
660
+ # 2. Append the text part of the response to the chat history so the user sees it.
661
+ chat_history.append({"role": "assistant", "content": answer})
662
+
663
+ if auto_detected_context:
664
+ chat_history.append({"role": "assistant", "content": auto_detected_context})
665
+ if response.get("sources"):
666
+ chat_history.append({"role": "assistant", "content": f"*(Sources used: {', '.join(response['sources'])})*"})
667
+
668
+ # 3. Decide what to play in the audio component: music takes priority over TTS.
669
+ audio_out_update = None
670
+ if audio_playback_url:
671
+ # If a music URL was returned, update the audio component to play that music file.
672
+ song_title = os.path.basename(audio_playback_url)
673
+ audio_out_update = gr.update(value=audio_playback_url, visible=True, label=f"Now Playing: {song_title}", autoplay=True)
674
+ elif settings.get("tts_on") and answer:
675
+ # Otherwise, if no music is playing and TTS is on, fall back to reading the text answer aloud.
676
+ tts_file = synthesize_tts(answer, lang=CONFIG["languages"].get(settings.get("tts_lang"), "en"))
677
+ audio_out_update = gr.update(value=tts_file, visible=bool(tts_file), label="Response Audio", autoplay=True)
678
+
679
+ # 4. Return all the updates for the Gradio UI.
680
+ return "", audio_out_update, chat_history
681
+
682
+ # --- MUSIC PLAYBACK LOGIC END ---
683
+
684
+
685
+ # The save_chat_to_memory function incorrectly assumes the history is
686
+ # a list of tuples, like [(True, "..."), (False, "...")]
687
+ # However, The chat_fn function correctly builds the chat_history as
688
+ # a list of dictionaries, like this:
689
+ # [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
690
+ # To correctly parse the list of dictionaries.
691
+ def save_chat_to_memory(chat_history):
692
+ if not chat_history:
693
+ return "Nothing to save."
694
+
695
+ # --- START: MODIFIED LOGIC ---
696
+ # Correctly processes the list of dictionaries from the chatbot
697
+ formatted_chat = [
698
+ f"{msg.get('role', 'assistant').capitalize()}: {msg.get('content', '').strip()}"
699
+ for msg in chat_history
700
+ if isinstance(msg, dict) and msg.get('content') and not msg.get('content', '').strip().startswith("*(")
701
+ ]
702
+ # --- END: MODIFIED LOGIC ---
703
+
704
+ if not formatted_chat:
705
+ return "No conversation to save."
706
+
707
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
708
+ title = f"Conversation from {timestamp}"
709
+ full_content = f"Title: {title}\n\nContent:\n" + "\n".join(formatted_chat)
710
+ doc = Document(page_content=full_content, metadata={"source": "Saved Chat", "title": title})
711
+
712
+ global personal_vectorstore
713
+ if personal_vectorstore is None:
714
+ personal_vectorstore = build_or_load_vectorstore([doc], PERSONAL_INDEX_PATH, is_personal=True)
715
+ else:
716
+ personal_vectorstore.add_documents([doc])
717
+
718
+ personal_vectorstore.save_local(PERSONAL_INDEX_PATH)
719
+ return f"Conversation from {timestamp} saved."
720
+
721
+
722
+ def list_personal_memories():
723
+ global personal_vectorstore
724
+ if personal_vectorstore is None or not hasattr(personal_vectorstore.docstore, '_dict') or not personal_vectorstore.docstore._dict:
725
+ return gr.update(value=[["No memories", "", ""]]), gr.update(choices=[], value=None)
726
+ docs = list(personal_vectorstore.docstore._dict.values())
727
+ return gr.update(value=[[d.metadata.get('title', '...'), d.metadata.get('source', '...'), d.page_content] for d in docs]), gr.update(choices=[d.page_content for d in docs])
728
+ def delete_personal_memory(memory_to_delete):
729
+ global personal_vectorstore
730
+ if personal_vectorstore is None or not memory_to_delete: return "No memory selected."
731
+ all_docs = list(personal_vectorstore.docstore._dict.values())
732
+ docs_to_keep = [d for d in all_docs if d.page_content != memory_to_delete]
733
+ if len(all_docs) == len(docs_to_keep): return "Error: Could not find memory."
734
+ if not docs_to_keep:
735
+ if os.path.isdir(PERSONAL_INDEX_PATH): shutil.rmtree(PERSONAL_INDEX_PATH)
736
+ personal_vectorstore = build_or_load_vectorstore([], PERSONAL_INDEX_PATH, is_personal=True)
737
+ else:
738
+ new_vs = FAISS.from_documents(docs_to_keep, _default_embeddings())
739
+ new_vs.save_local(PERSONAL_INDEX_PATH)
740
+ personal_vectorstore = new_vs
741
+ return "Successfully deleted memory."
742
+
743
+ # --- EVALUATION FUNCTIONS: move them into evaluate.py
744
+ # def evaluate_nlu_tags(expected: Dict[str, Any], actual: Dict[str, Any], tag_key: str, expected_key_override: str = None) -> Dict[str, float]:
745
+ # def _parse_judge_json(raw_str: str) -> dict | None:
746
+ # def run_comprehensive_evaluation():
747
+
748
+ def upload_knowledge(files, theme):
749
+ for f in files: copy_into_theme(theme, f.name)
750
+ if theme in vectorstores: del vectorstores[theme]
751
+ return f"Uploaded {len(files)} file(s)."
752
+ def save_file_selection(theme, enabled):
753
+ man = load_manifest(theme)
754
+ for fname in man['files']: man['files'][fname] = fname in enabled
755
+ save_manifest(theme, man)
756
+ if theme in vectorstores: del vectorstores[theme]
757
+ return f"Settings saved for theme '{theme}'."
758
+ def refresh_file_list_ui(theme):
759
+ files = list_theme_files(theme)
760
+ return gr.update(choices=[f for f, _ in files], value=[f for f, en in files if en]), f"Found {len(files)} file(s)."
761
+ def auto_setup_on_load(theme):
762
+ if not os.listdir(theme_upload_dir(theme)): seed_files_into_theme(theme)
763
+ # Changed the 11th argument from "All" to "None"
764
+ settings = collect_settings("patient", "", "", "warm", "English", "English", 0.7,
765
+ "None", "None", "None", "None", True, False)
766
+ files_ui, status = refresh_file_list_ui(theme)
767
+ return settings, files_ui, status
768
+
769
+ def test_save_file():
770
+ try:
771
+ path = PERSONAL_DATA_BASE / "persistence_test.txt"
772
+ path.write_text(f"File saved at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
773
+ return f"✅ Success! Wrote test file to: {path}"
774
+ except Exception as e: return f"❌ Error! Failed to write file: {e}"
775
+ def check_test_file():
776
+ path = PERSONAL_DATA_BASE / "persistence_test.txt"
777
+ if path.exists(): return f"✅ Success! Found test file. Contents: '{path.read_text()}'"
778
+ return f"❌ Failure. Test file not found at: {path}"
779
+
780
+ # --- UI Definition ---
781
+ CSS = """
782
+ .gradio-container { font-size: 14px; }
783
+ #chatbot { min-height: 400px; }
784
+ #audio_in audio, #audio_out audio { max-height: 40px; }
785
+ #audio_in .waveform, #audio_out .waveform { display: none !important; }
786
+ #audio_in, #audio_out { min-height: 0px !important; }
787
+ """
788
+
789
+ # OLD: add allowed_paths so the UI can access the music files
790
+ # with gr.Blocks(theme=gr.themes.Soft(), css=CSS, allowed_paths=[str(PERSONAL_MUSIC_BASE)]) as demo:
791
+ with gr.Blocks(theme=gr.themes.Soft(), css=CSS) as demo:
792
+ settings_state = gr.State({})
793
+ with gr.Tab("Chat"):
794
+ with gr.Row():
795
+ user_text = gr.Textbox(show_label=False, placeholder="Type your message here...", scale=7)
796
+ submit_btn = gr.Button("Send", variant="primary", scale=1)
797
+ with gr.Row():
798
+ audio_in = gr.Audio(sources=["microphone"], type="filepath", label="Voice Input", elem_id="audio_in")
799
+ audio_out = gr.Audio(label="Response Audio", autoplay=True, visible=True, elem_id="audio_out")
800
+
801
+ chatbot = gr.Chatbot(elem_id="chatbot", label="Conversation", type="messages")
802
+ chat_status = gr.Markdown()
803
+ with gr.Row():
804
+ clear_btn = gr.Button("Clear")
805
+ save_btn = gr.Button("Save to Memory")
806
+
807
+ with gr.Tab("Personalize"):
808
+ gr.Markdown("### **Upload Personal Memory**")
809
+ with gr.Accordion("Add Multimodal Data to Personal Memory Bank", open=True):
810
+ personal_title = gr.Textbox(label="Title")
811
+ personal_text = gr.Textbox(lines=5, label="Text Content")
812
+ with gr.Row():
813
+ personal_file = gr.File(label="Upload Audio/Video/Text File")
814
+ personal_image = gr.Image(type="filepath", label="Upload Image")
815
+ personal_yt_url = gr.Textbox(label="Or, provide a YouTube URL")
816
+ personal_add_btn = gr.Button("Add Knowledge", variant="primary")
817
+ personal_status = gr.Markdown()
818
+
819
+ # In app.py, within the "Personalize" Tab
820
+ gr.Markdown("### **Upload Personal Music Library**")
821
+ with gr.Accordion("Add Music to Personal Memory Bank", open=False):
822
+ music_file = gr.File(label="Upload Audio File (.mp3, .wav)", file_types=["audio"])
823
+ music_title = gr.Textbox(label="Song Title (e.g., My Way)")
824
+ music_artist = gr.Textbox(label="Artist (e.g., Frank Sinatra)")
825
+ # music_mood = gr.Textbox(label="Mood Tags (comma-separated, e.g., calm, happy, nostalgic)")
826
+ # NEW: Add a dropdown menu music tag selection based on emotion and behavior tags
827
+ music_mood = gr.Dropdown(
828
+ CONFIG["music_moods"],
829
+ label="Select Moods/Contexts for this Song",
830
+ multiselect=True
831
+ )
832
+ music_add_btn = gr.Button("Add Music", variant="primary")
833
+ music_status = gr.Markdown()
834
+
835
+ gr.Markdown("### **Manage Personal Memory Bank**")
836
+ with gr.Accordion("View/Hide Details", open=False):
837
+ personal_memory_display = gr.DataFrame(headers=["Title", "Source", "Content"], label="Saved Memories", row_count=(5, "dynamic"))
838
+ personal_refresh_btn = gr.Button("Refresh Memories")
839
+ personal_delete_selector = gr.Dropdown(label="Select memory to delete", scale=3, interactive=True)
840
+ personal_delete_btn = gr.Button("Delete Selected", variant="stop", scale=1)
841
+ personal_delete_status = gr.Markdown()
842
+
843
+ # --- NEW UI FOR MUSIC MANAGEMENT ---
844
+ gr.Markdown("### **Manage Music Library**")
845
+ with gr.Accordion("View/Hide Music Details", open=False):
846
+ music_library_display = gr.DataFrame(
847
+ headers=["Title", "Artist", "Moods"],
848
+ label="Music Library",
849
+ row_count=(5, "dynamic")
850
+ )
851
+ music_refresh_btn = gr.Button("Refresh Music List")
852
+ music_delete_selector = gr.Dropdown(
853
+ label="Select music to delete",
854
+ scale=3,
855
+ interactive=True
856
+ )
857
+ music_delete_btn = gr.Button("Delete Selected Music", variant="stop", scale=1)
858
+ music_delete_status = gr.Markdown()
859
+ # --- END OF NEW UI ---
860
+
861
+ with gr.Tab("Settings"):
862
+ with gr.Group():
863
+ gr.Markdown("## Conversation & Persona Settings")
864
+ with gr.Row():
865
+ role = gr.Radio(CONFIG["roles"], value="patient", label="Your Role")
866
+ patient_name = gr.Textbox(label="Patient's Name")
867
+ caregiver_name = gr.Textbox(label="Caregiver's Name")
868
+ with gr.Row():
869
+ temperature = gr.Slider(0.0, 1.2, value=0.7, step=0.1, label="Creativity")
870
+ tone = gr.Dropdown(CONFIG["tones"], value="warm", label="Response Tone")
871
+ with gr.Row():
872
+ # --- ADD THIS NEW DROPDOWN ---
873
+ # disease_stage = gr.Dropdown(CONFIG["disease_stages"], value="Normal / Unspecified", label="Assumed Disease Stage")
874
+ disease_stage = gr.Dropdown(CONFIG["disease_stages"], value="Default: Mild Stage", label="Assumed Disease Stage")
875
+ # --- END OF ADDITION ---
876
+ behaviour_tag = gr.Dropdown(CONFIG["behavior_tags"], value="None", label="Behaviour Filter (Manual)")
877
+ emotion_tag = gr.Dropdown(CONFIG["emotion_tags"], value="None", label="Emotion Filter (Manual)")
878
+ topic_tag = gr.Dropdown(CONFIG["topic_tags"], value="None", label="Topic Tag Filter (Manual)")
879
+ with gr.Accordion("Language, Voice & Debugging", open=False):
880
+ language = gr.Dropdown(list(CONFIG["languages"].keys()), value="English", label="Response Language")
881
+ tts_lang = gr.Dropdown(list(CONFIG["languages"].keys()), value="English", label="Voice Language")
882
+ tts_on = gr.Checkbox(True, label="Enable Voice Response")
883
+ debug_mode = gr.Checkbox(False, label="Show Debug Info")
884
+ gr.Markdown("--- \n ## General Knowledge Base Management")
885
+ with gr.Row():
886
+ with gr.Column(scale=1):
887
+ files_in = gr.File(file_count="multiple", file_types=[".jsonl", ".txt"], label="Upload Knowledge Files")
888
+ upload_btn = gr.Button("Upload to Theme")
889
+ seed_btn = gr.Button("Import Sample Data")
890
+ mgmt_status = gr.Markdown()
891
+ with gr.Column(scale=2):
892
+ active_theme = gr.Radio(CONFIG["themes"], value="All", label="Active Knowledge Theme")
893
+ files_box = gr.CheckboxGroup(choices=[], label="Enable Files for Selected Theme")
894
+ with gr.Row():
895
+ save_files_btn = gr.Button("Save Selection", variant="primary")
896
+ refresh_btn = gr.Button("Refresh List")
897
+ with gr.Accordion("Persistence Test", open=False):
898
+ test_save_btn = gr.Button("1. Run Persistence Test (Save File)")
899
+ check_save_btn = gr.Button("3. Check for Test File")
900
+ test_status = gr.Markdown()
901
+
902
+ # --- UPDATED TESTING TAB ---
903
+ with gr.Tab("Testing"):
904
+ gr.Markdown("## Comprehensive Performance Evaluation")
905
+ gr.Markdown("Click the button below to run a full evaluation on all test fixtures. This will test NLU (Routing & Tagging) and generate RAG responses for manual review.")
906
+
907
+ run_comprehensive_btn = gr.Button("Run Comprehensive Evaluation", variant="primary")
908
+
909
+ batch_summary_md = gr.Markdown("### Evaluation Summary: Not yet run.")
910
+
911
+ comprehensive_results_df = gr.DataFrame(
912
+ label="Detailed Evaluation Results",
913
+ elem_id="comprehensive_results_df",
914
+ headers=[
915
+ "Test ID","Title","Route Correct?","Expected Route","Actual Route",
916
+ "Behavior F1","Emotion F1","Topic F1","Context F1",
917
+ "Generated Answer","Sources","Source Count","Latency (ms)", "Faithfulness"
918
+ ],
919
+ interactive=False
920
+ )
921
+
922
+
923
+ # --- Event Wiring ---
924
+ all_settings = [
925
+ # Chat Tab Settings
926
+ role, patient_name, caregiver_name, tone, language, tts_lang, temperature,
927
+ # Disease Stage & Manual Filters
928
+ disease_stage, behaviour_tag, emotion_tag, topic_tag,
929
+ # Knowledge Base & Debug
930
+ active_theme, tts_on, debug_mode
931
+ ]
932
+ settings_state = gr.State({})
933
+
934
+ # In app.py, replace the event wiring loop right after the all_settings list
935
+
936
+ for component in all_settings:
937
+ component.change(fn=collect_settings, inputs=all_settings, outputs=settings_state)
938
+
939
+ submit_btn.click(fn=chat_fn, inputs=[user_text, audio_in, settings_state, chatbot], outputs=[user_text, audio_out, chatbot])
940
+
941
+ # for c in all_settings: c.change(fn=collect_settings, inputs=all_settings, outputs=settings_state)
942
+ # submit_btn.click(fn=chat_fn, inputs=[user_text, audio_in, settings_state, chatbot], outputs=[user_text, audio_out, chatbot])
943
+
944
+ save_btn.click(fn=save_chat_to_memory, inputs=[chatbot], outputs=[chat_status])
945
+ clear_btn.click(lambda: (None, None, [], None, "", ""), outputs=[user_text, audio_out, chatbot, audio_in, user_text, chat_status])
946
+
947
+ personal_add_btn.click(fn=handle_add_knowledge, inputs=[personal_title, personal_text, personal_file, personal_image, personal_yt_url, settings_state], outputs=[personal_status]).then(lambda: (None, None, None, None, None), outputs=[personal_title, personal_text, personal_file, personal_image, personal_yt_url])
948
+ # Wire the button to the function in the UI event wiring section
949
+ music_add_btn.click(
950
+ fn=handle_add_music,
951
+ inputs=[music_file, music_title, music_artist, music_mood],
952
+ outputs=[music_status]
953
+ )
954
+ # --- NEW EVENT WIRING FOR MUSIC MANAGEMENT ---
955
+ music_refresh_btn.click(
956
+ fn=list_music_library,
957
+ inputs=None,
958
+ outputs=[music_library_display, music_delete_selector]
959
+ )
960
+ music_delete_btn.click(
961
+ fn=delete_music_from_library,
962
+ inputs=[music_delete_selector],
963
+ outputs=[music_delete_status]
964
+ ).then(
965
+ fn=list_music_library,
966
+ inputs=None,
967
+ outputs=[music_library_display, music_delete_selector]
968
+ )
969
+ # --- END OF NEW WIRING ---
970
+
971
+ personal_refresh_btn.click(fn=list_personal_memories, inputs=None, outputs=[personal_memory_display, personal_delete_selector])
972
+ personal_delete_btn.click(fn=delete_personal_memory, inputs=[personal_delete_selector], outputs=[personal_delete_status]).then(fn=list_personal_memories, inputs=None, outputs=[personal_memory_display, personal_delete_selector])
973
+
974
+ upload_btn.click(upload_knowledge, inputs=[files_in, active_theme], outputs=[mgmt_status]).then(refresh_file_list_ui, inputs=[active_theme], outputs=[files_box, mgmt_status])
975
+ save_files_btn.click(save_file_selection, inputs=[active_theme, files_box], outputs=[mgmt_status])
976
+ seed_btn.click(seed_files_into_theme, inputs=[active_theme]).then(refresh_file_list_ui, inputs=[active_theme], outputs=[files_box, mgmt_status])
977
+ refresh_btn.click(refresh_file_list_ui, inputs=[active_theme], outputs=[files_box, mgmt_status])
978
+ active_theme.change(refresh_file_list_ui, inputs=[active_theme], outputs=[files_box, mgmt_status])
979
+
980
+ # Then update the .click() event handler
981
+ run_comprehensive_btn.click(
982
+ fn=lambda: run_comprehensive_evaluation(
983
+ vs_general=ensure_index("All"),
984
+ vs_personal=personal_vectorstore,
985
+ nlu_vectorstore=nlu_vectorstore,
986
+ config=CONFIG
987
+ ),
988
+ # The output list now has three components
989
+ outputs=[batch_summary_md, comprehensive_results_df, comprehensive_results_df]
990
+ )
991
+
992
+ demo.load(auto_setup_on_load, inputs=[active_theme], outputs=[settings_state, files_box, mgmt_status])
993
+ demo.load(load_test_fixtures)
994
+ test_save_btn.click(fn=test_save_file, inputs=None, outputs=[test_status])
995
+ check_save_btn.click(fn=check_test_file, inputs=None, outputs=[test_status])
996
+
997
+ # --- Startup Logic ---
998
+ # --- Function 3: The Startup Orchestrator ---
999
+ def pre_load_indexes():
1000
+ """Loads all data sources and runs the auto-loading functions at startup."""
1001
+ global personal_vectorstore, nlu_vectorstore
1002
+ print("Pre-loading all indexes at startup...")
1003
+ print(" - Loading NLU examples index...")
1004
+ nlu_vectorstore = bootstrap_nlu_vectorstore("nlu_training_examples.jsonl", NLU_EXAMPLES_INDEX_PATH)
1005
+ print(f" ...NLU index loaded.")
1006
+ for theme in CONFIG["themes"]:
1007
+ print(f" - Loading general index for theme: '{theme}'")
1008
+ try:
1009
+ ensure_index(theme)
1010
+ print(f" ...'{theme}' theme loaded.")
1011
+ except Exception as e:
1012
+ print(f" ...Error loading theme '{theme}': {e}")
1013
+
1014
+ print(" - Loading personal knowledge index...")
1015
+ try:
1016
+ personal_vectorstore = build_or_load_vectorstore([], PERSONAL_INDEX_PATH, is_personal=True)
1017
+ print(" ...Personal knowledge loaded.")
1018
+ except Exception as e:
1019
+ print(f" ...Error loading personal knowledge: {e}")
1020
+
1021
+ # NEW: auto-loading and syncing functions with a small pre-loaded Personal Memory Bank
1022
+ load_personal_files_from_folder()
1023
+ sync_music_library_from_folder()
1024
+
1025
+ print("All indexes and personal files loaded. Application is ready.")
1026
+
1027
+
1028
+
1029
+ if __name__ == "__main__":
1030
+ seed_files_into_theme('All')
1031
+ pre_load_indexes()
1032
+ demo.queue().launch(debug=True, allowed_paths=[str(PERSONAL_MUSIC_BASE)])
1033
+ # demo.queue().launch(debug=True)