#!/usr/bin/env python3 """ reply_formatter.py -- extract the user-visible answer from a Heartly Qwen-Code (v3) grammar output. The v3 model is trained to emit: thinking {reasoning} responsespeak|stopknown|unknown {answer} (or `stop` for the silence case). The user should only ever see {answer}, never the scaffolding. In practice the model -- especially through a GUI like LM Studio -- produces *noisy* variants: the tags are NOT registered as tokenizer special tokens, so Qwen's BPE shatters `` into subwords that can re-decode mangled (e.g. ````), the `` end-marker can come back truncated (no closing `>`), and verify values can carry junk (`known, true`). Qwen chat-template tokens also leak as plain text (` `). This parser is tolerant: it canonicalises the tags, extracts the structured fields, and has an aggressive fallback so NO grammar token ever reaches the user. Modes: "chat" -- only the clean answer (default) "debug" -- answer + [decide=X verify=Y] metadata per turn "raw" -- the original raw output, untouched Usage: from reply_formatter import format_reply, clean_reply shown = format_reply(raw_model_output) """ from __future__ import annotations import re from dataclasses import dataclass # --------------------------------------------------------------------------- # 1. Junk tokens -- decoded-to-plain-text Qwen special tokens that are never # answer content. Stripped first so they cannot confuse grammar parsing. # --------------------------------------------------------------------------- _JUNK_TOKENS = ( "<|im_start|>", "<|im_end|>", "im_start", "im_end", "<|object_ref_start|>", "<|object_ref_end|>", "<|box_start|>", "<|box_end|>", "<|quad_start|>", "<|quad_end|>", "<|vision_start|>", "<|vision_end|>", "<|vision_pad|>", "<|image_pad|>", "<|endoftext|>", "endoftext", "<|tool_call_begin|>", "<|tool_call_end|>", "<|tool_call_argument_begin|>", "<|tool_call_argument_end|>", "<|tool_call_argument_name|>", "<|tool_call_argument|>", "<|tool_calls_section_begin|>", "<|tool_calls_section_end|>", "", "", "tool_call", "tool_calls", "", "", ) # --------------------------------------------------------------------------- # 2. Meta-commentary patterns -- the model's self-talk about what it knows / # doesn't know. If any bleeds into the answer zone, strip it. Sourced from # the reasoning / refusal / silence templates in render_code_sft_v3.py. # (The think block is removed entirely in the common case, so this is a # safety net, not the primary mechanism.) # --------------------------------------------------------------------------- _META_PATTERNS = [ r"\bI (?:do )?know (?:how )?to do this\b[^.]*\.", r"\bI (?:do )?know how to write this\b[^.]*\.", r"\bI (?:do )?know this function well\b[^.]*\.", r"\bI (?:can|will) (?:write|produce|implement|respond|answer|write clean code)\b[^.]*\.", r"\bThis is a standard programming task\b[^.]*\.", r"\bI recognise this programming problem\b[^.]*\.", r"\bI (?:recognise|recognize) this (?:programming|pattern|algorithm)\b[^.]*\.", r"\bStandard problem\b[^.]*\.", r"\bClear task\b[^.]*\.", r"\bI've seen this pattern before\b[^.]*\.", r"\bThis is straightforward\b[^.]*\.", r"\bI (?:do )?not know this API or library\b[^.]*\.", r"\bI should not invent a solution\b[^.]*\.", r"\bI have no knowledge of this framework\b[^.]*\.", r"\bThe honest response is\b[^.]*\.", r"\bI cannot verify the correct implementation\b[^.]*\.", r"\bMaking something up would be worse than admitting it\b[^.]*\.", r"\bI'll say so rather than produce fake code\b[^.]*\.", r"\bThe input is empty or not a real question\b[^.]*\.", r"\bNo meaningful request was made\b[^.]*\.", r"\bSpeaking would add nothing\b[^.]*\.", r"\bSocial turn\b[^.]*\.", r"\bGreeting[ —-]\s*respond\b[^.]*\.", r"\bNot a factual question\b[^.]*\.", r"\bCasual conversation\b[^.]*\.", r"\bFollow-up question\b[^.]*\.", r"\bThey need motivation\b[^.]*\.", r"\bEmotional context\b[^.]*\.", r"\bMeta-conversation about how we work together\b[^.]*\.", r"\bBe warm\b[^.]*\.", r"\bThey (?:want|are|need|'re|'ve|gave) ", r"\bThey're (?:just saying|opening up) ", r"\bThey gave more detail\b[^.]*\.", ] _META_RE = re.compile("|".join(_META_PATTERNS), re.IGNORECASE) # Stray control words left behind after a mangled tag is stripped. # Any leftover angle-bracket control construct, tolerant of truncation/mangles # (e.g. ``, or ``). _TAG_RE = re.compile(r"]*>?", re.IGNORECASE) # Stray control words left behind after a mangled tag is stripped (used inline # in _clean_text as leading/trailing guards). _CONTROL_WORDS = r"(?:speak|stop|silent|done|response|known|unknown|precise)" # Collapse runs of whitespace. _MULTI_SPACE_RE = re.compile(r"\s{2,}") # --------------------------------------------------------------------------- # 3. Normalization -- turn messy real-world tokens into canonical tags so the # structured parser can run cleanly. # --------------------------------------------------------------------------- def _normalize(raw: str) -> str: t = raw or "" # 3a. Drop Qwen / decoded-token litter. for junk in _JUNK_TOKENS: t = t.replace(junk, "") # 3b. Decide tags. Canonical is ```` / ```` but tokenization # can drop a letter () or add spaces, and the close can be # truncated (``]*>?>?", lambda m: "" if "/" in m.group(0) else "", t, flags=re.IGNORECASE, ) # 3c. Verify tags -- same tolerance. t = re.sub( r"<\s*/?\s*verify\b[^>]*>?>?", lambda m: "" if "/" in m.group(0) else "", t, flags=re.IGNORECASE, ) # 3d. Stop tags -- ````, ``?>`` handles all of these. t = re.sub(r"<\s*stop\b[^>]*>?>?", "", t, flags=re.IGNORECASE) # 3e. Strip a leading noise run (`` `` ...) that some # outputs carry before any thinking/decide content. This is junk, # not the Heartly end-of-turn marker (which is mid/trailing). t = re.sub( r"^\s*((?:||)\s*)*", "", t, flags=re.IGNORECASE ).lstrip() return t # --------------------------------------------------------------------------- # 4. Value normalization helpers. # --------------------------------------------------------------------------- _DECIDE_VAL_RE = re.compile(r"\b(speak|stop|silent)\b", re.IGNORECASE) def _decide_value(raw_val: str) -> str: """Reduce a decide tag body to canonical ``speak`` / ``stop``.""" m = _DECIDE_VAL_RE.search(raw_val or "") if m: v = m.group(1).lower() return "stop" if v == "silent" else v return "" # unparseable _VERIFY_VAL_RE = re.compile(r"\b(known|unknown)\b", re.IGNORECASE) def _verify_value(raw_val: str) -> str: m = _VERIFY_VAL_RE.search(raw_val or "") return m.group(1).lower() if m else "" # --------------------------------------------------------------------------- # 5. Parsed turn + segment parser. # --------------------------------------------------------------------------- @dataclass class ParsedTurn: reasoning: str = "" decide: str = "" # speak | stop | "" verify: str = "" # known | unknown | "" answer: str = "" # ``thinking {reasoning} response...`` -- the word "response" is # OPTIONAL (the model drops it sometimes; seen in the 2026-08-01 log). The # lookahead ``(?=|\\Z)`` bounds the reasoning block at the decide tag # (or end of string) so the (non-greedy) body doesn't greedily swallow the # answer, and it does NOT consume the decide opener (leaving it for the decide # parser to read the decide value). _THINK_RE = re.compile( r"\bthinking\b\s*(.*?)(\s*\bresponse\b)?\s*(?=|\Z)", re.DOTALL | re.IGNORECASE, ) # After normalization, tags are canonical. _DECIDE_CLOSE_RE = re.compile(r"(.*?)", re.DOTALL | re.IGNORECASE) _VERIFY_CLOSE_RE = re.compile(r"(.*?)", re.DOTALL | re.IGNORECASE) # Tolerant open-tag grabs (for mangled/missing close). _DECIDE_OPEN_RE = re.compile(r" ParsedTurn: """Parse one turn (already split on ````).""" seg = segment.strip() if not seg: return ParsedTurn() # --- thinking block (word "response" optional) --- reasoning = "" m = _THINK_RE.search(seg) if m: reasoning = (m.group(1) or "").strip() seg = seg[m.end():] # --- decide --- decide = "" m = _DECIDE_CLOSE_RE.search(seg) if m: decide = _decide_value(m.group(1)) seg = seg[m.end():] else: mo = _DECIDE_OPEN_RE.search(seg) if mo: decide = _decide_value(mo.group(1)) seg = seg[mo.end():] # --- verify --- verify = "" m = _VERIFY_CLOSE_RE.search(seg) if m: verify = _verify_value(m.group(1)) seg = seg[m.end():] else: mo = _VERIFY_OPEN_RE.search(seg) if mo: verify = _verify_value(mo.group(1)) seg = seg[mo.end():] # --- answer zone: everything left, up to any stray stop --- answer = seg sm = re.search(r" list[ParsedTurn]: """Parse raw model output into one or more turns (multi-turn aware).""" text = _normalize(raw) turns: list[ParsedTurn] = [] for segment in re.split(r"", text): seg = segment.strip() if not seg: continue # A real turn must carry a decide marker; stray fence/backtick noise # emitted between repeated markers is skipped. if not _DECIDE_OPEN_RE.search(seg): continue turns.append(_parse_segment(seg)) if not turns: # No structured grammar found at all -- treat the whole output as one # best-effort turn. turns.append(_parse_segment(text)) return turns # --------------------------------------------------------------------------- # 6. Answer sanitizers. # --------------------------------------------------------------------------- def _strip_meta(text: str) -> str: """Remove the model's reasoning self-talk from a piece of text.""" return _META_RE.sub("", text).strip() def _dedupe(text: str) -> str: """Drop verbatim-repeated sentences, keep first occurrence.""" out, seen = [], set() for sent in re.split(r"(?<=[.!?])\s+", text): s = sent.strip() if not s: continue key = s.lower() if key not in seen: seen.add(key) out.append(s) return " ".join(out) def _clean_text(text: str) -> str: """Final pass on an answer zone: strip residual tags/junk, meta, dedupe. Whitespace collapse and sentence de-duplication run on the PROSE regions only -- fenced code blocks (``` ``` ```) are preserved verbatim, because their indentation is significant and must not be collapsed to a single space. """ t = text or "" # Qwen litter first (never answer content). for junk in _JUNK_TOKENS: t = t.replace(junk, "") # Fence out code blocks so their whitespace survives intact. re.split with # a capturing group yields [prose, code, prose, code, ...] -- code blocks # land at odd indices and are emitted untouched. pieces = re.split(r"(```[^\n]*\n.*?```)", t, flags=re.DOTALL) out = [] for i, chunk in enumerate(pieces): if not chunk: continue if i % 2 == 1: # captured fenced block -> keep verbatim # The model emits literal backslash-n / backslash-t as plain text in # code (training artifact). Restore real newlines/tabs so multi-line # code renders instead of collapsing to a single line. chunk = chunk.replace(chr(92) + "n", chr(10)).replace(chr(92) + "t", chr(9)) out.append(chunk) continue # prose region chunk = _TAG_RE.sub(" ", chunk) chunk = re.sub(rf"^\s*{_CONTROL_WORDS}\b\s*", "", chunk, flags=re.IGNORECASE) chunk = re.sub(rf"\s*{_CONTROL_WORDS}\s*$", "", chunk, flags=re.IGNORECASE) chunk = _strip_meta(chunk) chunk = _MULTI_SPACE_RE.sub(" ", chunk).strip() chunk = _dedupe(chunk) if chunk: out.append(chunk) return " ".join(out).strip() def _answer_from_reasoning(reasoning: str) -> str: """Last-resort: if the answer zone is empty, try to harvest a real answer from the reasoning block (e.g. 'The answer is X' phrasing). Returns "" when the reasoning only holds placeholder / meta chatter -- in that case the caller maps the empty answer to the silence sentinel ("...").""" if not reasoning: return "" m = re.search( r"(?:the answer is|answer is|it's|it is)\s*:?\s*(.+?)(?:\.\s*$|\.\s*<|$)", reasoning, re.IGNORECASE | re.DOTALL, ) if m: return _clean_text(m.group(1)) return "" # --------------------------------------------------------------------------- # 7. Resolution -- decide what the user actually sees. # --------------------------------------------------------------------------- def _resolve_chat(turns: list[ParsedTurn]) -> str: parts = [] for t in turns: if t.decide == "stop": parts.append("...") continue ans = _clean_text(t.answer) if not ans and t.verify == "unknown": parts.append(ans or "I don't have that information.") continue if not ans: ans = _answer_from_reasoning(t.reasoning) parts.append(ans if ans else "...") return "\n".join(parts) def _resolve_debug(turns): parts = [] for t in turns: ans = _clean_text(t.answer) if not ans: ans = _answer_from_reasoning(t.reasoning) meta = " ".join( f"[{k}={v}]" for k, v in (("decide", t.decide), ("verify", t.verify)) if v ) shown = f"{ans} {meta}".strip() if ans else (meta or "(empty)") parts.append(shown) return "\n".join(parts) # 6b. Last-resort legacy cleaner -- used only if structured parsing leaves a # grammar token in the chat output. Aggressively strips every angle-bracket # construct + control word, guaranteeing a clean result. def _legacy_clean(text): t = _normalize(text) t = re.split(r"