114 lines
4.6 KiB
Python
114 lines
4.6 KiB
Python
"""Deterministic analysis blocks (segmentation, alignment, numerals) for the
|
|
prompt: the full scaffold and the optional light hint."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Dict, List, Optional, Sequence, Tuple
|
|
|
|
from .align import align as build_align
|
|
from .numerals import extract_attested, induce
|
|
from .preprocess import Puzzle, strip_punct, tokenize
|
|
from .segment import Segmenter
|
|
|
|
MAX_SEG_LINES = 40
|
|
MAX_ALIGN_LINES = 30
|
|
MAX_VOCAB = 60
|
|
|
|
|
|
def analysis_blocks(puzzle: Puzzle) -> Tuple[str, str]:
|
|
"""(segmentation block, alignment block) for the prompt. Also used by the
|
|
CEGIS proposer (synth.py)."""
|
|
amap = build_align(puzzle.pairs)
|
|
align_lines = []
|
|
for tok, cands in sorted(amap.items()):
|
|
top = [f"{t} ({s:.1f})" for t, s in cands[:2] if s > 0.2]
|
|
if top:
|
|
align_lines.append(f" {tok} ~ {', '.join(top)}")
|
|
|
|
vocab, groups = [], {}
|
|
for p in puzzle.pairs:
|
|
for t in tokenize(p.src):
|
|
t = strip_punct(t).casefold()
|
|
if t and t not in vocab:
|
|
vocab.append(t)
|
|
vocab = vocab[:MAX_VOCAB]
|
|
for tok, cands in amap.items():
|
|
if cands:
|
|
groups.setdefault(cands[0][0], set()).add(tok)
|
|
seg = Segmenter().fit(vocab, share_groups=[g for g in groups.values() if len(g) > 1])
|
|
seg_lines = []
|
|
for w in vocab:
|
|
parts = seg.segment(w)
|
|
if len(parts) > 1:
|
|
seg_lines.append(f" {w} = {'-'.join(parts)}")
|
|
return ("\n".join(seg_lines[:MAX_SEG_LINES]) or " (none found)",
|
|
"\n".join(align_lines[:MAX_ALIGN_LINES]) or " (none found)")
|
|
|
|
|
|
def light_hint(puzzle: Puzzle, max_lines: int = 10) -> str:
|
|
"""A minimal, optional hint for the lean prompt: a few morpheme segmentation
|
|
guesses, framed as fallible. Off by default; enabled via a toggle."""
|
|
try:
|
|
seg_block, _align = analysis_blocks(puzzle)
|
|
except Exception:
|
|
return ""
|
|
lines = [l for l in seg_block.splitlines() if l.strip() and "none found" not in l]
|
|
if not lines:
|
|
return ""
|
|
body = "\n".join(lines[:max_lines])
|
|
return ("Optional hint (an automatic guess at word parts; it may be wrong, "
|
|
"so rely on the data itself):\n" + body)
|
|
|
|
|
|
def numeral_block(puzzle: Puzzle) -> str:
|
|
"""Induced numeral-system values, when the CSP solved and round-trip
|
|
verified them — the strongest kind of hint we can give."""
|
|
if puzzle.task_type not in ("text_to_num", "num_to_text"):
|
|
return ""
|
|
attested = extract_attested(puzzle.pairs)
|
|
system = induce(attested) if attested else None
|
|
if system is None:
|
|
return ""
|
|
vals = ", ".join(f"{t}={v}" for t, v in sorted(system.values.items(), key=lambda kv: kv[1]))
|
|
return (f"Numeral analysis (verified against every attested example):\n {vals}\n"
|
|
f" combination rule: a smaller value directly before a larger one multiplies it; "
|
|
f"otherwise values add.")
|
|
|
|
|
|
def candidate_block(items_answers: Sequence[Tuple[str, Optional[str], float, str]]) -> str:
|
|
"""Symbolic candidate answers per item: (item label, answer, confidence,
|
|
method). Only candidates with real evidence are shown — a low-confidence
|
|
echo would anchor the model on garbage."""
|
|
lines = []
|
|
for label, ans, conf, method in items_answers:
|
|
if ans and conf >= 0.4:
|
|
lines.append(f" item {label}: '{ans}' (source: {method}, fit {conf:.2f})")
|
|
if not lines:
|
|
return ""
|
|
return ("Candidate answers from mechanical analysis (adopt if consistent with "
|
|
"the data, correct if not):\n" + "\n".join(lines))
|
|
|
|
|
|
def build_scaffold(puzzle: Puzzle,
|
|
answers: Optional[Sequence[Optional[str]]] = None,
|
|
confs: Optional[Sequence[float]] = None,
|
|
methods: Optional[Sequence[str]] = None) -> str:
|
|
"""Full scaffold block for one puzzle's prompt."""
|
|
seg_block, align_block = analysis_blocks(puzzle)
|
|
parts = [
|
|
"## Mechanical analysis (computed from the data above; may contain errors — "
|
|
"the attested data always wins)",
|
|
f"Morpheme segmentation hypotheses:\n{seg_block}",
|
|
f"Word alignment hypotheses (task-language token ~ likely meaning):\n{align_block}",
|
|
]
|
|
nb = numeral_block(puzzle)
|
|
if nb:
|
|
parts.append(nb)
|
|
if answers is not None and confs is not None:
|
|
labels = [it.number or str(i + 1) for i, it in enumerate(puzzle.items)]
|
|
meths = list(methods) if methods else ["symbolic"] * len(labels)
|
|
cb = candidate_block(list(zip(labels, answers, confs, meths)))
|
|
if cb:
|
|
parts.append(cb)
|
|
return "\n\n".join(parts)
|