Upload pipeline/parser.py with huggingface_hub
Browse files- pipeline/parser.py +71 -0
pipeline/parser.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Structured output parser for Lumi.
|
| 3 |
+
|
| 4 |
+
Qwen3 outputs in its native thinking-first format:
|
| 5 |
+
<think>
|
| 6 |
+
Internal reasoning — NEVER sent to TTS or shown to user.
|
| 7 |
+
</think>
|
| 8 |
+
[avatar_tag] Short opening line.
|
| 9 |
+
Full warm companion response continues here.
|
| 10 |
+
|
| 11 |
+
TTS latency trick: stream until </think> appears, then fire TTS immediately
|
| 12 |
+
on the opening line that follows.
|
| 13 |
+
|
| 14 |
+
parse_structured_output() returns a dict with:
|
| 15 |
+
- avatar_tag : one of the 5 states (defaults to "smile" if missing)
|
| 16 |
+
- opening_line : fires TTS after </think> is seen (latency-hiding trick)
|
| 17 |
+
- full_response : complete response text (think block stripped)
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import re
|
| 21 |
+
|
| 22 |
+
VALID_TAGS = {"smile", "nod", "concerned", "gentle", "laugh"}
|
| 23 |
+
_TAG_RE = re.compile(r"\[(\w+)\]") # anywhere in text
|
| 24 |
+
_TAG_START = re.compile(r"^\[(\w+)\]") # at start of text
|
| 25 |
+
_THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def parse_structured_output(text: str) -> dict:
|
| 29 |
+
text = text.strip()
|
| 30 |
+
|
| 31 |
+
# Strip think block first — works for both tag-first and think-first formats
|
| 32 |
+
clean = _THINK_RE.sub("", text).strip()
|
| 33 |
+
|
| 34 |
+
# --- avatar tag: prefer at start of clean text, fall back to anywhere ----
|
| 35 |
+
tag_match = _TAG_START.match(clean) or _TAG_RE.search(clean)
|
| 36 |
+
if tag_match and tag_match.group(1).lower() in VALID_TAGS:
|
| 37 |
+
avatar_tag = tag_match.group(1).lower()
|
| 38 |
+
else:
|
| 39 |
+
avatar_tag = "smile"
|
| 40 |
+
|
| 41 |
+
# --- opening line: first line of the clean text after removing the tag ---
|
| 42 |
+
tag_bracket = f"[{avatar_tag}]"
|
| 43 |
+
body = clean.replace(tag_bracket, "").replace(tag_bracket.upper(), "").strip()
|
| 44 |
+
lines = [l.strip() for l in body.split("\n") if l.strip()]
|
| 45 |
+
opening_line = lines[0] if lines else body[:80]
|
| 46 |
+
full_response = body
|
| 47 |
+
|
| 48 |
+
return {
|
| 49 |
+
"avatar_tag": avatar_tag,
|
| 50 |
+
"opening_line": opening_line, # fire TTS after </think> token
|
| 51 |
+
"full_response": full_response, # append to TTS queue
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def extract_facts_from_response(text: str) -> list[str]:
|
| 56 |
+
"""
|
| 57 |
+
Heuristically extract memorable facts from a model response.
|
| 58 |
+
Used to build the patient's persistent memory in ChromaDB.
|
| 59 |
+
"""
|
| 60 |
+
facts = []
|
| 61 |
+
lines = text.split(".")
|
| 62 |
+
for line in lines:
|
| 63 |
+
line = line.strip()
|
| 64 |
+
# keep lines that mention personal details, preferences, family
|
| 65 |
+
keywords = ("granddaughter", "grandson", "daughter", "son", "wife",
|
| 66 |
+
"husband", "likes", "loves", "favourite", "favorite",
|
| 67 |
+
"anniversary", "birthday", "garden", "music", "hymn",
|
| 68 |
+
"lonely", "feels", "misses", "remembers")
|
| 69 |
+
if any(k in line.lower() for k in keywords) and len(line) > 15:
|
| 70 |
+
facts.append(line)
|
| 71 |
+
return facts[:5] # cap at 5 per turn
|