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