171 lines
5.1 KiB
Python
171 lines
5.1 KiB
Python
import json
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OutputCleanConfig:
|
|
mode: str = "prompt"
|
|
strip_think: bool = True
|
|
strip_code_fences: bool = True
|
|
strip_role_prefixes: bool = True
|
|
strip_json_wrappers: bool = True
|
|
strip_leading_preamble: bool = True
|
|
strip_planning: bool = True
|
|
keep_first_paragraph_only: bool = False
|
|
|
|
|
|
_ROLE_PREFIX_RE = re.compile(r"^\s*(assistant|final|output|response|result|prompt)\s*:\s*", re.IGNORECASE)
|
|
_CODE_FENCE_RE = re.compile(r"^\s*```[\w-]*\s*$", re.IGNORECASE)
|
|
_THINK_BLOCK_RE = re.compile(r"<think[^>]*>.*?</think>", flags=re.IGNORECASE | re.DOTALL)
|
|
_THINK_OPEN_RE = re.compile(r"<think[^>]*>", flags=re.IGNORECASE)
|
|
_THINK_CLOSE_RE = re.compile(r"</think\s*>", flags=re.IGNORECASE)
|
|
_MARKER_RE = re.compile(
|
|
r"(?im)^\s*(final|final answer|answer|output|result|prompt)\s*[:\-]\s*",
|
|
)
|
|
_IM_TOKEN_RE = re.compile(
|
|
r"(?i)<\|?im_(start|end)\|?>|<im_(start|end)>|<\|endoftext\|>",
|
|
)
|
|
_PLANNING_RE = re.compile(
|
|
r"(?is)\b("
|
|
r"final\s+plan\b|"
|
|
r"final\s+check\b|"
|
|
r"i\s+(should|need|must|will|want|am\s+going\s+to|have\s+to)\b|"
|
|
r"let's\b|"
|
|
r"first\b|next\b|then\b|"
|
|
r"wait\b|"
|
|
r"ready\s+to\s+write\b|"
|
|
r"writing\s+the\s+prompt\b|"
|
|
r"so\s+i\s+need\s+to\b|"
|
|
r"i\s+should\s+focus\s+on\b"
|
|
r")"
|
|
)
|
|
|
|
|
|
def clean_model_output(text: str, config: OutputCleanConfig | None = None) -> str:
|
|
if not text:
|
|
return ""
|
|
|
|
cfg = config or OutputCleanConfig()
|
|
cleaned = (text or "").strip()
|
|
|
|
# Remove common chat template tokens that sometimes leak into output.
|
|
cleaned = _IM_TOKEN_RE.sub("", cleaned).strip()
|
|
|
|
if cfg.strip_think:
|
|
cleaned = _THINK_BLOCK_RE.sub("", cleaned)
|
|
cleaned = _THINK_CLOSE_RE.sub("", cleaned)
|
|
if _THINK_OPEN_RE.search(cleaned):
|
|
cleaned = _THINK_OPEN_RE.sub("", cleaned)
|
|
parts = re.split(r"\n\s*\n", cleaned, maxsplit=1)
|
|
if len(parts) == 2:
|
|
cleaned = parts[1]
|
|
cleaned = cleaned.strip()
|
|
|
|
cleaned = _IM_TOKEN_RE.sub("", cleaned).strip()
|
|
|
|
if cfg.strip_code_fences and "```" in cleaned:
|
|
lines = [ln for ln in cleaned.splitlines() if not _CODE_FENCE_RE.match(ln)]
|
|
cleaned = "\n".join(lines).strip()
|
|
|
|
if cfg.strip_json_wrappers:
|
|
maybe = _extract_from_json(cleaned, mode=cfg.mode)
|
|
if maybe is not None:
|
|
cleaned = maybe.strip()
|
|
|
|
if cfg.strip_leading_preamble:
|
|
cleaned = _drop_preamble(cleaned).strip()
|
|
|
|
if cfg.strip_planning and cfg.mode == "prompt":
|
|
without_planning = _strip_planning_paragraphs(cleaned)
|
|
if without_planning:
|
|
cleaned = without_planning
|
|
|
|
if cfg.strip_role_prefixes:
|
|
lines = cleaned.splitlines()
|
|
if lines:
|
|
lines[0] = _ROLE_PREFIX_RE.sub("", lines[0])
|
|
cleaned = "\n".join(lines).strip()
|
|
|
|
cleaned = _MARKER_RE.sub("", cleaned).strip()
|
|
|
|
if cfg.keep_first_paragraph_only:
|
|
parts = re.split(r"\n\s*\n", cleaned, maxsplit=1)
|
|
cleaned = parts[0].strip()
|
|
|
|
return cleaned
|
|
|
|
|
|
def _extract_from_json(text: str, mode: str) -> str | None:
|
|
candidate = text.strip()
|
|
if not candidate:
|
|
return None
|
|
|
|
if not (candidate.startswith("{") and candidate.endswith("}")):
|
|
return None
|
|
|
|
try:
|
|
payload = json.loads(candidate)
|
|
except Exception:
|
|
return None
|
|
|
|
if not isinstance(payload, dict):
|
|
return None
|
|
|
|
preferred_keys = []
|
|
if mode == "prompt":
|
|
preferred_keys = ["prompt", "final", "output", "text", "content"]
|
|
else:
|
|
preferred_keys = ["text", "content", "output", "final"]
|
|
|
|
for key in preferred_keys:
|
|
value = payload.get(key)
|
|
if isinstance(value, str) and value.strip():
|
|
return value
|
|
|
|
return None
|
|
|
|
|
|
def _drop_preamble(text: str) -> str:
|
|
lines = text.splitlines()
|
|
if not lines:
|
|
return text
|
|
|
|
keep_from = 0
|
|
for i, ln in enumerate(lines[:20]):
|
|
if _MARKER_RE.match(ln):
|
|
keep_from = i
|
|
if re.search(r"(?i)\bhere(?:'s| is)\b", ln) and i < 6:
|
|
keep_from = max(keep_from, i + 1)
|
|
|
|
return "\n".join(lines[keep_from:]).strip()
|
|
|
|
|
|
def _strip_planning_paragraphs(text: str) -> str:
|
|
paragraphs = [p.strip() for p in re.split(r"\n\s*\n", (text or "").strip()) if p.strip()]
|
|
if not paragraphs:
|
|
return ""
|
|
|
|
kept: list[str] = []
|
|
dropping = True
|
|
for p in paragraphs:
|
|
is_planning = bool(_PLANNING_RE.search(p))
|
|
if dropping and is_planning:
|
|
continue
|
|
dropping = False
|
|
kept.append(p)
|
|
|
|
# If everything looked like planning, don't destroy the output.
|
|
if not kept:
|
|
return text.strip()
|
|
return "\n\n".join(kept).strip()
|
|
|
|
|
|
def prompt_output_guard() -> str:
|
|
return (
|
|
"Output rules that override all style preferences: return only the final usable prompt text. "
|
|
"Do not include analysis, planning, bullet points, headings, markdown, JSON, explanations, alternatives, self-corrections, or notes. "
|
|
"Do not write phrases such as Final Plan, Final Check, Wait, Okay, First, Next, Then, I will, or I need. "
|
|
"Start directly with the required prompt text."
|
|
)
|