"""Centralized sanitizer for LLM responses. CCAI conversations don't work well when thinking traces leak into chat or into orchestrator/summarizer/Credential-Summary inputs, so every LLM response funnels through `strip_thinking` before being stored, displayed, or forwarded to another LLM. """ from __future__ import annotations import re # Top-level reasoning blocks emitted as XML-ish tags. DOTALL so we catch # multi-line reasoning blocks; non-greedy so adjacent blocks don't merge. _THINK_TAG_RE = re.compile(r".*?", re.DOTALL | re.IGNORECASE) _REASONING_BLOCK_RE = re.compile( r"<(reasoning|reflection|inner_thoughts|scratchpad|analysis|plan)>.*?", re.DOTALL | re.IGNORECASE, ) # Bare "thought:" / "reasoning:" prologues some models emit before content # (only at the very start of the response, otherwise we'd nuke the body). _PROLOGUE_RE = re.compile( r"^\s*(thought|thinking|reasoning|analysis|scratchpad)\s*:\s*" r".*?(?=\n\n|\Z)", re.DOTALL | re.IGNORECASE, ) # Some providers wrap thinking in special framing tokens. We try to strip # the *paired* form (open ... close) first so the body in between is # removed, and fall back to stripping any leftover bare markers. _PAIRED_FRAMING_RES = [ re.compile(r"<\|reasoning\|>.*?<\|/reasoning\|>", re.DOTALL | re.IGNORECASE), re.compile(r"<\|think\|>.*?<\|/think\|>", re.DOTALL | re.IGNORECASE), ] _FRAMING_TOKENS = ["<|reasoning|>", "<|/reasoning|>", "<|think|>", "<|/think|>"] def strip_thinking(text: str | None) -> str: """Return `text` with all reasoning artifacts removed. Safe to call on empty, whitespace-only, or None inputs (returns empty string in those cases). Idempotent: calling twice yields the same result. """ if not text: return "" out = _THINK_TAG_RE.sub("", text) out = _REASONING_BLOCK_RE.sub("", out) for paired in _PAIRED_FRAMING_RES: out = paired.sub("", out) for tok in _FRAMING_TOKENS: out = out.replace(tok, "") out = _PROLOGUE_RE.sub("", out) return out.strip() def response_has_thinking(text: str | None, msg: dict | None = None) -> bool: """Return True if the raw response had any thinking artifact. Checks both the textual content and any `reasoning_content` / `reasoning` fields the OpenAI-compat client may have surfaced. """ if msg is not None: if msg.get("reasoning_content") or msg.get("reasoning"): return True if not text: return False if _THINK_TAG_RE.search(text): return True if _REASONING_BLOCK_RE.search(text): return True if any(tok in text for tok in _FRAMING_TOKENS): return True if _PROLOGUE_RE.match(text): return True return False