""" Structured output parser for Lumi. Qwen3 outputs in its native thinking-first format: Internal reasoning — NEVER sent to TTS or shown to user. [avatar_tag] Short opening line. Full warm companion response continues here. TTS latency trick: stream until appears, then fire TTS immediately on the opening line that follows. parse_structured_output() returns a dict with: - avatar_tag : one of the 5 states (defaults to "smile" if missing) - opening_line : fires TTS after is seen (latency-hiding trick) - full_response : complete response text (think block stripped) """ import re VALID_TAGS = {"smile", "nod", "concerned", "gentle", "laugh"} _TAG_RE = re.compile(r"\[(\w+)\]") # anywhere in text _TAG_START = re.compile(r"^\[(\w+)\]") # at start of text _THINK_RE = re.compile(r".*?", re.DOTALL) _ACTION_RE = re.compile(r"\[\[ACTION:\s*([^\]]+)\]\]") def parse_structured_output(text: str) -> dict: text = text.strip() # Strip think block first — works for both tag-first and think-first formats clean = _THINK_RE.sub("", text).strip() # --- action: extract [[ACTION: TYPE | ...]] tags --- action = None action_match = _ACTION_RE.search(clean) if action_match: raw_content = action_match.group(1).strip() parts = [p.strip() for p in raw_content.split("|")] if parts: action = {"type": parts[0], "payload": parts[1:]} # Clean the body for the user (remove the action tag) clean_body = _ACTION_RE.sub("", clean).strip() else: clean_body = clean # --- avatar tag: prefer at start of clean text, fall back to anywhere ---- tag_match = _TAG_START.match(clean_body) or _TAG_RE.search(clean_body) if tag_match and tag_match.group(1).lower() in VALID_TAGS: avatar_tag = tag_match.group(1).lower() else: avatar_tag = "smile" # --- opening line: first line of the clean text after removing the tag --- tag_bracket = f"[{avatar_tag}]" body = clean_body.replace(tag_bracket, "").replace(tag_bracket.upper(), "").strip() lines = [l.strip() for l in body.split("\n") if l.strip()] opening_line = lines[0] if lines else body[:80] full_response = body return { "avatar_tag": avatar_tag, "opening_line": opening_line, # fire TTS after token "full_response": full_response, # append to TTS queue "action": action } def extract_facts_from_response(text: str) -> list[str]: """ Heuristically extract memorable facts from a model response. Used to build the patient's persistent memory in ChromaDB. """ facts = [] lines = text.split(".") for line in lines: line = line.strip() # keep lines that mention personal details, preferences, family keywords = ("granddaughter", "grandson", "daughter", "son", "wife", "husband", "likes", "loves", "favourite", "favorite", "anniversary", "birthday", "garden", "music", "hymn", "lonely", "feels", "misses", "remembers") if any(k in line.lower() for k in keywords) and len(line) > 15: facts.append(line) return facts[:5] # cap at 5 per turn