YahBible / code /taviel_decide.py
OhBeOneKeyNoBe's picture
Correct embed(): the index is CLS-pooled, not mean -- a port trusting the old docstring would silently land in a different vector space
999c9f2 verified
Raw History Blame Contribute Delete
30.8 kB
r"""TAV'IEL-DECIDE -- single-pass typed decisions for YahBible's AI ("Jev mode").
The current pipeline (taviel_reason.py) makes every decision INSIDE prose
generation: which groundings to pull, what kind of question this is, which
gauntlet round matches, whether a cited verse is real -- all decided while the
model is already writing, across two 360-token autoregressive generations.
This module moves those decisions in front of generation, using the
parallel-decision fork of llama.cpp (D:\llama.cpp-decision, branch
parallel-decision): every field of a finite schema is scored as token paths
forking from ONE shared KV cache, so all questions are answered in a single
llama_decode with a calibrated probability each, and the JSON is assembled by
code -- it cannot be malformed. Same mechanism harshatheg benchmarked at
5.6-7.0x over autoregressive JSON on this exact base model
(Qwen2.5-1.5B-Instruct).
Server: the fork's llama-server with --decision-seqs, port 8110 (root tier,
Qwen2.5-1.5B-Instruct-Q4_K_M -- Tav'iel's existing mobile-tier GGUF; one
resident model serves BOTH /v1/chat/completions and /v1/decision).
Machine-first: nothing here loads a model into this process; it is an HTTP
client. Start/stop the server with serve_up()/serve_down().
"""
from __future__ import annotations
import json
import os
import subprocess
import time
import urllib.request
PORT = int(os.environ.get("TAV_DECIDE_PORT", "8110"))
URL = f"http://127.0.0.1:{PORT}/v1/decision"
SERVER = r"D:\llama.cpp-decision\build-cuda-mmq\bin\llama-server.exe"
GGUF = os.environ.get("TAV_DECIDE_GGUF",
r"D:\0000_Raw_LLM Models\Qwen2.5-1.5B-Instruct-Q4_K_M.gguf")
NGL = int(os.environ.get("TAV_DECIDE_NGL", "99"))
EMB_PORT = int(os.environ.get("TAV_EMB_PORT", "8111"))
EMB_SERVER = r"D:\0000_Raw_LLM Models\llama.cpp-tools\llama.cpp\llama-server.exe"
# a REAL contrastive embedder: causal-LM mean-pooling (Qwen 0.6B) proved
# recall-blind -- 4/20 paraphrases, the true round absent from top-8. bge-small
# is 33M params, CPU-instant, trained for exactly this.
# bge-BASE (110M) over bge-small (33M): measured on 630 held-out askings,
# R@8 86% -> 88%, spatial 53->59, R@3 83->84. Still CPU-only, still zero
# VRAM taken from the decision model.
EMB_GGUF = os.environ.get(
"TAV_EMB_GGUF", r"D:\0000_Raw_LLM Models\bge-base-en-v1.5-q8_0.gguf")
_proc = None
_emb_proc = None
# ---------------------------------------------------------------- server ----
def serve_up(ngl=NGL, ctx=4096, seqs=16, wait_s=180):
"""Start the decision server (one resident model, 1.5B fits the 1650 whole)."""
global _proc
if _up():
return True
# the fork asserts (ggml_can_mul_mat) when --embeddings meets the unified
# decision KV, so recall gets its own tiny embedder: the third_eye tier
# (0.6B) on CPU ONLY -- zero VRAM taken from the decision model.
global _emb_proc
_emb_proc = subprocess.Popen(
[EMB_SERVER, "-m", EMB_GGUF, "-ngl", "0", "-c", "512",
"--embeddings", "--port", str(EMB_PORT)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# ctx 8192 x 24 unified decision sequences proved unstable on the 4 GB
# card across long runs; 4096 x 16 comfortably covers the router's 8
# candidates plus instructions and has not died since.
_proc = subprocess.Popen(
[SERVER, "-m", GGUF, "-ngl", str(ngl), "-fa", "on", "-c", str(ctx),
"--decision-seqs", str(seqs), "--port", str(PORT)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
t0 = time.time()
while time.time() - t0 < wait_s:
if _up() and _emb_up():
return True
time.sleep(1.0)
raise RuntimeError("server(s) did not come up in %ss (decision=%s embed=%s)"
% (wait_s, _up(), _emb_up()))
_AVAIL = {"at": 0.0, "ok": False}
AVAIL_TTL = 20.0
def available(timeout=0.4, ttl=AVAIL_TTL):
"""Is Jev actually here? A CHEAP probe that never starts anything.
This exists because of what serve_up() costs when the answer is no. Its
retry path waits up to 180 seconds for a server to come up, and decide()
calls it three times -- so on a machine without the fork binary, or with
one that cannot allocate, a single user question could hang for minutes
before falling back. A speedup whose absence costs six minutes is not a
speedup.
So every caller that would LIKE to use the decision server asks here
first. The probe is a HEAD-weight /health with a short timeout, cached for
a few seconds so a chat turn does not pay for it repeatedly, and it never
spawns a process. A machine without Jev behaves exactly as it did before
Jev existed, at the cost of one refused connection every ttl seconds.
Both servers are required: the router needs the decision endpoint to judge
and the embedder to retrieve, and a half-present Jev routes worse than no
Jev at all.
"""
now = time.time()
if now - _AVAIL["at"] < ttl:
return _AVAIL["ok"]
ok = False
try:
urllib.request.urlopen(f"http://127.0.0.1:{PORT}/health", timeout=timeout)
urllib.request.urlopen(f"http://127.0.0.1:{EMB_PORT}/health",
timeout=timeout)
ok = True
except Exception:
ok = False
_AVAIL.update(at=now, ok=ok)
return ok
def unavailable():
"""Mark Jev as gone without waiting for the cache to lapse.
Called when a decision actually fails mid-turn, so the next question does
not repeat a doomed round-trip.
"""
_AVAIL.update(at=time.time(), ok=False)
def serve_down():
global _proc, _emb_proc
for p in (_proc, _emb_proc):
if p:
p.terminate()
_proc = _emb_proc = None
def _up():
try:
urllib.request.urlopen(f"http://127.0.0.1:{PORT}/health", timeout=2)
return True
except Exception:
return False
def _emb_up():
try:
urllib.request.urlopen(f"http://127.0.0.1:{EMB_PORT}/health", timeout=2)
return True
except Exception:
return False
# ---------------------------------------------------------------- client ----
def decide(schema, contexts, instructions="", timeout=120, _tries=3):
"""POST /v1/decision. contexts: str or list of 1-256 strings sharing one
schema + one cached instruction prefix. Returns the parsed response.
RESILIENT BY DESIGN: the fork's server has died mid-run three times
across long evaluations, and each time the numbers it corrupted looked
like a model failure rather than a dead socket (the always-answer law
once scored 11/40 purely from connection-refused). A refused connection
now restarts the server and retries, so a measurement reports the system
rather than the server's mood."""
if isinstance(contexts, str):
contexts = [contexts]
body = json.dumps({"instructions": instructions, "schema": schema,
"contexts": contexts}).encode()
last = None
for attempt in range(_tries):
try:
req = urllib.request.Request(
URL, data=body, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read())
except Exception as ex:
last = ex
if attempt == _tries - 1:
break
import sys
import time as _t
print(f"[taviel_decide] decision server unreachable "
f"({type(ex).__name__}); restarting", file=sys.stderr)
try:
if _proc:
_proc.terminate()
except Exception:
pass
globals()["_proc"] = None
_t.sleep(2)
serve_up()
raise last
def decisions_of(resp):
"""[{field: value}] per context, in order."""
return [r["decision"] for r in resp["results"]]
def fields_of(resp, i=0):
"""{field: {value, probability, ...}} for context i."""
return resp["results"][i]["fields"]
# ---------------------------------------------------------------- schemas ---
# 1) THE ANSWER PLAN -- the full shape of the answer, decided before the first
# generated token. One decode answers all nine.
# There is deliberately NO "route" question. v1 asked the model to route with
# bare labels and "decline" won at p=0.96 on a core objection; v2's descriptive
# phrases still lost at p=0.61. Routing is a COMPOUND judgment a 1.5B fumbles,
# while the primitive judgments below measure strong (genre=objection p=0.97).
# So the model answers primitives, and plan() COMPOSES the route in code --
# "with code owning the action", as the aeon-pack doctrine has it.
ANSWER_PLAN = {
"wants_passage": {"type": "boolean",
"description": "Do they only want a Bible passage's text "
"quoted to them, not an explanation?"},
"genre": {"type": "enum",
"choices": ["objection", "word_study", "doctrine", "comfort",
"history", "practice"],
"description": "What kind of question is this?"},
"register": {"type": "enum", "choices": ["skeptic", "believer", "child"],
"description": "Who is asking, by their wording?"},
"depth": {"type": "enum", "choices": ["brief", "moderate", "full"],
"description": "How thorough must the answer be to satisfy the asker?"},
"need_scripture": {"type": "boolean",
"description": "Would verse quotations strengthen the answer?"},
"need_hebrew_greek": {"type": "boolean",
"description": "Does answering require the original "
"Hebrew or Greek behind a word?"},
"need_apologetics": {"type": "boolean",
"description": "Is this an objection or challenge to the "
"faith that a recorded argument answers?"},
"christ_first": {"type": "boolean",
"description": "Should the answer open with what Yeshua the "
"Christ Himself said or did on this?"},
"in_scope": {"type": "boolean",
"description": "Is this within Scripture study, doctrine, or the "
"life of faith at all?"},
}
PLAN_INSTRUCTIONS = ("You are Tav'iel, the study guide of YahBible, a Scripture "
"study app. Judge the user's question below and answer each "
"field from the question's own wording.")
def _true(v):
return v in (True, "true")
def plan(question):
"""One decode of primitive judgments; the route is COMPOSED in code."""
r = decide(ANSWER_PLAN, "Question: " + question, PLAN_INSTRUCTIONS)
dec, fl = decisions_of(r)[0], fields_of(r)
if not _true(dec.get("in_scope")) and fl["in_scope"]["probability"] >= 0.6:
dec["route"] = "decline"
elif _true(dec.get("wants_passage")):
dec["route"] = "verse_lookup"
elif dec.get("genre") == "objection" or _true(dec.get("need_apologetics")):
dec["route"] = "vetted" # the router confirms; its NONE -> reason
else:
dec["route"] = "reason"
return dec, fl, r.get("timings", {})
# 2) THE GAUNTLET ROUTER -- which of the 100 vetted rounds answers this
# question. v1 (a 100-way enum of round IDS) proved 9/20: id tokens carry no
# meaning. v2 (100 independent booleans) proved 1/20 at 4.7s: "covers" says
# yes to everything topical, and independent judgments cannot discriminate.
# v3: the existing LEXICAL net shortlists a handful, and the decision is one
# enum whose CHOICES ARE THE CANDIDATE QUESTIONS THEMSELVES (plus NONE) --
# the candidates compete inside one softmax, which is where a decision
# model's discrimination actually lives.
_KB = None
def _kb():
global _KB
if _KB is None:
p = os.path.join(os.path.dirname(os.path.abspath(__file__)), "gauntlet_kb.json")
_KB = json.load(open(p, encoding="utf-8"))
return _KB
_STOP = set("a an the is are was be been do does did what why how who when where "
"isn didn doesn can could would should i you it they of in on to for "
"with about that this if not no or and but there s t re".split())
def _shortlist(question, k=8):
qw = {w for w in "".join(c if c.isalnum() else " " for c in question.lower())
.split() if w not in _STOP}
scored = []
for e in _kb():
ew = set()
for field in ("words", "keys"):
v = e.get(field) or []
ew |= {str(x).lower() for x in (v if isinstance(v, list) else [v])}
ew |= {w for w in "".join(c if c.isalnum() else " " for c in
e["question"].lower()).split() if w not in _STOP}
s = len(qw & ew)
if s:
scored.append((s, e))
scored.sort(key=lambda x: -x[0])
return [e for _, e in scored[:k]]
def embed(texts):
"""CLS-pooled embeddings from the bge server (/v1/embeddings).
NOT mean-pooled -- this docstring said so and was wrong. The GGUF declares
`bert.pooling_type = 2`, which is CLS, and serve_up passes no --pooling
flag, so the model's own default stands. Every vector in the precomputed
index was therefore built with CLS.
This matters far beyond a docstring: transformers.js and most ONNX
examples default to MEAN pooling. A port that accepts that default puts
its queries in a different space from the keys and recall collapses with
no error and no exception -- just quietly worse answers. Any
reimplementation must pool CLS, must truncate at 512 tokens (`-c 512`),
and must NOT prepend bge's "Represent this sentence for searching
relevant passages: " instruction, because llama-server does not.
"""
if isinstance(texts, str):
texts = [texts]
body = json.dumps({"input": texts}).encode()
last = None
for attempt in range(3):
try:
req = urllib.request.Request(
f"http://127.0.0.1:{EMB_PORT}/v1/embeddings", data=body,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=180) as r:
data = json.loads(r.read())["data"]
return [d["embedding"] for d in sorted(data, key=lambda d: d["index"])]
except Exception as ex:
# same lesson as decide(): building the key cache issues ~180
# batched calls, and one dropped connection used to abort a whole
# proof run with a bare URLError.
last = ex
if attempt == 2:
break
import sys
import time as _t
print(f"[taviel_decide] embedder unreachable ({type(ex).__name__}); "
"restarting", file=sys.stderr)
try:
if _emb_proc:
_emb_proc.terminate()
except Exception:
pass
globals()["_emb_proc"] = None
_t.sleep(2)
serve_up()
raise last
def _cos(a, b):
num = sum(x * y for x, y in zip(a, b))
da = sum(x * x for x in a) ** 0.5
db = sum(x * x for x in b) ** 0.5
return num / (da * db + 1e-9)
_ROUND_EMB = None
def _round_embs():
"""Views per round: the question, its key words, and -- when the variant
file exists -- every generated way of asking it (registers, synonyms,
antonym framings, symbolic/metaphorical, ballpark). A user question
routes by its NEAREST view, so nobody needs the exact wording. Variant
embeddings are cached to disk keyed on the variant file's mtime."""
global _ROUND_EMB
if _ROUND_EMB is None:
base = os.path.dirname(os.path.abspath(__file__))
views = {} # n -> [text, ...]
for e in _kb():
words = e.get("words") or []
views[e["n"]] = [e["question"].strip('"'),
" ".join(str(w) for w in words[:16]) or
e["question"].strip('"')]
vfile = os.path.join(base, "gauntlet_variants.json")
if os.path.exists(vfile):
var = json.load(open(vfile, encoding="utf-8"))
for k_, vs in var.items():
views.setdefault(int(k_), []).extend(vs)
# flat embed with a disk cache (numpy npz keyed by content signature)
import hashlib
import numpy as np
flat, owners, isvar = [], [], []
nbase = {n: 0 for n in views}
for n in sorted(views):
for i, t in enumerate(views[n]):
flat.append(t)
owners.append(n)
isvar.append(0 if i < 2 else 1) # first two are question+words
sig = hashlib.sha1("\x1f".join(flat).encode("utf-8")).hexdigest()[:16]
cache = os.path.join(base, f"round_embs_{sig}.npz")
if os.path.exists(cache):
z = np.load(cache)
arr = z["e"]
else:
out = []
for i in range(0, len(flat), 64):
out.extend(embed(flat[i:i + 64]))
arr = np.asarray(out, dtype="float32")
arr /= (np.linalg.norm(arr, axis=1, keepdims=True) + 1e-9)
np.savez_compressed(cache, e=arr)
_ROUND_EMB = (np.asarray(owners), np.asarray(isvar), arr)
return _ROUND_EMB
# MEASURED on the paraphrase set + off-scope traps (tune_recall.py):
# knn k10 p6 ranks best (19/20 R@1) but its scores do NOT separate scope
# (worst on-topic 0.202 BELOW best off-scope 0.417);
# base_max (question+words only) ranks worse (17/20) but is the only
# strategy with positive separation (0.616 on-topic vs 0.583 off-scope).
# So the two jobs take the two signals: BASE-MAX GATES, KNN RANKS. Variants
# then help recall without letting one noisy generated line admit an
# off-scope question.
KNN_K, KNN_POWER = 10, 6
def _semantic_shortlist(question, k=8):
"""[(knn_score, entry)] best-first. The gate score rides on the list as
.scope (see _scope_of) so callers can judge in/out of scope separately."""
import numpy as np
owners, isvar, arr = _round_embs()
q = np.asarray(embed(question)[0], dtype="float32")
q /= (np.linalg.norm(q) + 1e-9)
sims = arr @ q
base_mask = isvar == 0
scope = float(sims[base_mask].max()) if base_mask.any() else 0.0
kk = min(KNN_K, sims.shape[0])
idx = np.argpartition(-sims, kk - 1)[:kk]
acc = {}
for i in idx:
acc[int(owners[i])] = acc.get(int(owners[i]), 0.0) + \
float(sims[i]) ** KNN_POWER
byn = {e["n"]: e for e in _kb()}
top = sorted(acc.items(), key=lambda x: -x[1])[:k]
out = [(s, byn[n]) for n, s in top]
_semantic_shortlist.last_scope = scope
return out
def _scope_of(question=None):
"""The gate score of the most recent shortlist (base-view max cosine)."""
return getattr(_semantic_shortlist, "last_scope", 0.0)
def suggest(question, k=3, exclude=None):
"""Better questions near this one -- ALWAYS k of them, from k DISTINCT
rounds. 'Poor answering without better suggestions for questions is poor
answering.'
It cannot reuse the router's shortlist: that ranks by a knn vote over
individual vectors, so a near-verbatim question fills all ten neighbours
with ONE round's own variants. Excluding the answered round then left
nothing to suggest -- measured, seven of forty perfect vetted answers
arrived with no follow-up questions at all, the best matches being the
emptiest. Suggestions therefore rank whole rounds by their best view,
which always yields distinct alternatives."""
import numpy as np
try:
owners, isvar, arr = _round_embs()
q = np.asarray(embed(question)[0], dtype="float32")
q /= (np.linalg.norm(q) + 1e-9)
sims = arr @ q
best = {}
for n, s in zip(owners, sims):
n = int(n)
if s > best.get(n, -1.0):
best[n] = float(s)
except Exception:
return []
byn = {e["n"]: e for e in _kb()}
out = []
for n, _ in sorted(best.items(), key=lambda x: -x[1]):
if n == exclude or n not in byn:
continue
out.append((n, byn[n]["question"].strip('"')))
if len(out) >= k:
break
return out
def route(question, threshold=0.60, cos_gate=0.60, k=8, direct_floor=0.70):
"""(round_number|None, probability, timings). None = fall through to reason().
v6 -- two successive questions, each GATED where its signal lives:
recall asks "what is this ABOUT" (embeddings -- lexical overlap capped it
at 3/20 and causal-LM pooling was recall-blind); the judge asks "which one
EXACTLY" among the top-k candidates' own text. A NONE choice no longer
sits in the judge's softmax: as candidates grow plausible the mass SPLITS
among them and NONE wins by default (measured -- the refined suffering
question lost to NONE at p=0.43 with round 2 sitting at recall rank 1).
Out-of-scope is the RECALL's signal: an off-topic question embeds far from
every round, so the cosine gate rejects it before any judging. The judge's
threshold is a margin over uniform (1/k), not a majority bar."""
try:
scored = _semantic_shortlist(question, k)
except Exception as ex:
# fall back, but NEVER silently -- a masked recall failure cost two
# proof runs that were secretly re-measuring the lexical net.
import sys
print(f"[taviel_decide] semantic recall failed ({ex!r}); lexical fallback",
file=sys.stderr)
scored = [(1.0, e) for e in _shortlist(question, k)]
# the GATE is the base-view cosine (the only separating signal); the
# ORDER is the knn vote. Two jobs, two signals.
scope = _scope_of()
if not scored or scope < cos_gate:
return None, scope, {}
bynq = {e["question"].strip('"'): e["n"] for _, e in scored}
schema = {"same": {"type": "enum", "choices": list(bynq),
"description": "Which recorded objection is asking the "
"same thing as the user, in other words?"}}
r = decide(schema, "User's question: " + question,
"The user's question is a REWORDING of one recorded objection. "
"Pick the objection that means the same thing even when the "
"words differ.")
f = fields_of(r)["same"]
n = bynq.get(f["value"])
p = f["probability"]
# TWO WITNESSES. To commit is to hand the inquirer a RECORDED answer --
# and if it is the wrong one, an answer to a question they never asked.
# So retrieval and the judge must INDEPENDENTLY name the same round, and
# the judge must be sure. Measured on 225 held-out askings
# (sweep_thresholds.py): the old permissive rule (p>=0.30, pick anywhere
# in the top 8, with a dominance override) answered 70.2% correctly but
# was OFF TOPIC 17.8% of the time -- one asking in six got another
# question's answer. Requiring agreement at p>=0.60 gives 54.2% correct
# and 1.8% wrong: a tenfold cut in the only truly bad outcome, at 96.8%
# precision. Deferral is not silence -- the front door reasons from
# scripture grounding and offers better questions, which under his law
# is a good answer of a different kind. Bounding WRONG is the objective;
# maximising CORRECT is not.
if n is None or n != scored[0][1]["n"]:
return None, p, r.get("timings", {})
if p < threshold:
return None, p, r.get("timings", {})
# A THIRD WITNESS THAT NEVER SAW THE SHORTLIST.
#
# The two-witness rule assumed retrieval and the judge were independent.
# They are not: retrieval hands the judge its candidate list. Measured on
# 600 held-out askings, 10 of the 14 worst wrong commits had the true
# round ABSENT from the top-8 entirely -- so the judge chose confidently,
# several at p=1.00, among candidates that could not contain the right
# answer, and both witnesses agreed on the same wrong page. No threshold
# on the judge can reach that failure.
#
# So the last check compares the asking DIRECTLY against the chosen
# round's own text, which depends on nothing the shortlist did. Measured
# separation: correct commits median 0.761, wrong 0.663. A floor of 0.70
# keeps 77% of correct commits and blocks 73% of wrong ones, taking the
# off-topic rate from 3.5% to about 1%. The commits it blocks are not
# lost -- they become deferrals, which answer by reasoning with better
# questions offered, and under his law that is a good answer of a
# different kind.
if not _resembles(question, scored[0][1], floor=direct_floor):
return None, p, {**(r.get("timings") or {}), "blocked": "direct_floor"}
return n, p, r.get("timings", {})
def _resembles(question, entry, floor=0.70):
"""Does the CHOSEN round actually look like what was asked?"""
try:
qv, cv = embed([question, entry["question"].strip('"')])
except Exception:
return True # no embedder: do not block on a check we cannot run
num = sum(x * y for x, y in zip(qv, cv))
na = sum(x * x for x in qv) ** .5
nb = sum(x * x for x in cv) ** .5
return (num / (na * nb + 1e-9)) >= floor
def literalize(question, timeout=120):
"""Say plainly what is being asked underneath a figurative asking."""
prompt = ("Someone asked this in a figurative or roundabout way:\n"
f"\"{question}\"\n\n"
"Restate ONLY the plain question underneath it, in one short "
"line, using ordinary words about God, the Bible, or faith. "
"No metaphors, no preamble, no quotation marks.")
body = json.dumps({
"messages": [{"role": "system",
"content": "You restate questions plainly. One line."},
{"role": "user", "content": prompt}],
"max_tokens": 48, "temperature": 0.1}).encode()
req = urllib.request.Request(
f"http://127.0.0.1:{PORT}/v1/chat/completions", data=body,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as r:
t = json.loads(r.read())["choices"][0]["message"]["content"]
return t.strip().strip('"').split("\n")[0][:200]
def route_best(question, k=8, anchor=3, lit_threshold=0.70):
"""route(), and when it defers, ASK A BETTER QUESTION OF THE QUESTION.
His principle turned inward: "asking better questions, or questions that
build on questions, is what leads to the best answer." Metaphorical
askings fail retrieval because their words sit far from every recorded
question -- musical 44%, spatial 58% recall@1 -- so the system restates
the asking plainly and routes on the restatement, the way a librarian
says "so what you're really asking is whether God is just."
Three attempts to close this gap by moving the KEYS toward the question
all failed (intelligence-field variants, the KB's own prose as views, a
bigger embedder). Moving the QUESTION toward the keys works.
THE THIRD WITNESS: a restatement may only land on a round the ORIGINAL
asking already had in view. Restating is interpretation, and
interpretation can invent -- unanchored, it turned 7 deferrals into
confident wrong answers. Anchored, it keeps 22 of 24 rescues and breaks
2. Measured on 160 held-out askings from the four weakest fields:
correct 34.4% -> 48.1%, WRONG 2.5% -> 3.8%, with spatial 22% -> 40% at
zero wrong.
Returns (round|None, p, {source}). Fires only on deferral, so a question
both witnesses already agreed on is never second-guessed.
"""
n, p, t = route(question, k=k)
if n is not None:
return n, p, {"via": "direct", **(t or {})}
try:
# ANCHOR DEPTH 3, not 8. Swept offline over 300 held-out askings
# (collect_literalize + sweep_literalize): a top-8 anchor at p>=0.60
# scored correct 58.0% / WRONG 2.7%, while top-3 at p>=0.70 scores
# 57.7% / 2.3% -- and 2.3% is EXACTLY the baseline wrongness without
# literalization at all. So restating buys +7 points of correct
# answers at ZERO added off-topic risk, which is the only version of
# this feature his standard permits. Giving up 0.3 points of correct
# to add no risk is the right trade when the rule is "never off
# topic"; the 3% bar also sits near the sampling noise floor, so the
# rule that ties baseline is the robust one, not the nominal best.
near = [e["n"] for _, e in _semantic_shortlist(question, k)][:anchor]
plain = literalize(question)
n2, p2, t2 = route(plain, k=k)
if n2 is not None and n2 in near and p2 >= lit_threshold:
return n2, p2, {"via": "literalized", "plain": plain, **(t2 or {})}
except Exception:
pass
return None, p, {"via": "deferred"}
# 3) VERSE VERIFICATION -- every candidate ref of a draft, one batched round.
# Replaces the second 360-token "refine" generation on drafts whose only sin
# is a doubtful citation.
VERIFY_SCHEMA = {
"supports": {"type": "boolean",
"description": "Does the verse, as quoted, actually say what the "
"claim uses it to say?"},
"quoted_exactly": {"type": "boolean",
"description": "Is the quotation word-for-word from the "
"verse text given?"},
}
def verify_refs(claims_and_verses):
"""claims_and_verses: [(claim, ref, verse_text)] -> [{ref, supports, exact, p}].
All pairs are scored in ONE decision round (shared schema + cached prefix).
NOT SHIPPED as the refine replacement: prove_taviel_decide2 measured 3/6
discrimination at the 1.5B tier (embedding cosine also 3/6) -- the refine
generation stays until a bigger tier or a true NLI cross-encoder retries."""
contexts = [f"Claim: {c}\nVerse ({ref}): {v}" for c, ref, v in claims_and_verses]
r = decide(VERIFY_SCHEMA, contexts,
"Judge each claim against the verse quoted with it.")
out = []
for (c, ref, v), res in zip(claims_and_verses, r["results"]):
f = res["fields"]
out.append({"ref": ref,
"supports": f["supports"]["value"] in (True, "true"),
"exact": f["quoted_exactly"]["value"] in (True, "true"),
"p": min(f["supports"]["probability"],
f["quoted_exactly"]["probability"])})
return out