#!/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"?\s*[a-z_][^>]*>?", 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"