Files
bikoret/kushya_generator/inference/kushya_api.py
T
Scott KarpandClaude Opus 4.6 e223ab15b3 Phase 4 complete: Steps 21, 22, 24 — Korban external integration, L2 Urim v'Tumim, Kushya generator
Vayikra 9:24 — vatetzei esh milifnei Hashem vatokhal al hamizbeach.
Phase 4 of the architecture is now complete. The remaining three steps
that were marked "blocked on external infrastructure" are built —
discipline-respecting, eish-zarah-aware, and tested.

## Step 22 — Korban Processor external model integration
   (korban_processor/src/korban_processor/external_offering.py)

   The single legitimate entry point for external model consultation.
   Wraps Claude API calls through the offering protocol so external
   output is treated as korban (raw material), never authoritative:

   - semichah  — provenance is tagged (avodah zarah Form 1 prevention)
   - shechitah — assistant voice, hedging, formatting stripped
   - zerikah   — vital content extracted
   - haktarah  — converted to Prolog propositions
   - route     — sent to internal component per offering type

   The raw Korban is preserved on the ProcessedKorban (gilui arayot
   Form 3 prevention — input/output coupling never severed).
   No internal component should call call_claude() directly.

   11 tests, mocked external calls. Live API test opt-in via
   SHORASHIM_LIVE_API=1.

## Step 24 — L2 Urim v'Tumim interface
   (vav_pipeline/lib/vav_pipeline/urim_v_tumim.ex)

   PRD §4.7: housed in the kohen gadol's breastplate; consulted only
   on high-stakes decisions where normal deliberation is insufficient.
   Bamidbar 27:21 — al-piv yetz'u v'al-piv yavo'u.

   Three escalation steps, three honest outcomes:
     1. resolved_by_specification — re-read produced an answer
     2. resolved_by_external — external testimony recorded (NOT binding)
     3. deferred_to_human — the system stops, asks for human judgment

   Pre-condition gating refuses cases that didn't reach Sanhedrin teiku
   first (no preempting deliberation) or that aren't flagged high-stakes
   (ordinary teiku stays teiku). Records every consultation with steps
   taken — there is no quiet escalation.

   Per PRD: "The Urim v'Tumim never fabricates certainty where none
   exists." When external also fails, the system stops. It does not
   proceed on a guess.

   Added to VavPipeline supervision tree. 14 tests.

## Step 21 — Kushya generator scaffolding
   (kushya_generator/)

   Directory layout per Architecture §3.6 with three layers:

   - data/training_pairs/seed_pairs.jsonl — 10 hand-curated bootstrap
     pairs covering 9 kushya types (scope_narrowing, internal_tension,
     harmonization, exception_test, edge_case, scope_application,
     rabbinic_constraint, method_question, subject_identification)
   - data/prepare_training.py — validates the four kushya requirements
     (specific Torah passage, identified weakness, question form,
     non-trivial), produces train.jsonl + eval.jsonl with stratified
     80/20 split by type
   - fine_tune/config.yaml + train.py — QLoRA configuration and
     training script. Operator's task — needs GPU. Script is
     complete and self-contained.
   - inference/kushya_api.py — three-backend interface
     (fine_tuned → external → fallback). When fine-tuned model isn't
     deployed, falls back to external via Korban Processor (Step 22).
     The fallback is honest: returns no kushya rather than inventing
     one. The Talmudic Engine's rule-based challenge.pl remains the
     primary kushya source.

   17 tests covering dataset validation, backend selection, and
   reference extraction.

## .gitignore fix

   The earlier "data/" line was too broad — it caught
   kushya_generator/data/. Scoped to /data/ (root-level only).

## Test counts

   - Elixir:           274 (was 260; +14 for UrimVTumim)
   - Korban Python:    196 + 1 skipped (was 195; +1 module = 11 tests)
   - Kushya Python:     17 (new)
   - Haskell:           33 (unchanged)
   - Prolog:           195 (unchanged)
   - Total:            715 + 19 L3 excluded

## Phase 4 status

   All 12 steps now have implementations:
   - Steps 23, 25, 26, 27, 28, 29, 30, 31, 32: complete
   - Steps 21, 22, 24: complete with discipline preserved

   Step 21's fine-tuning run remains the operator's GPU task; the
   pipeline is built such that a deployed adapter slots into the
   inference backend without touching the Talmudic Engine or anything
   above.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 17:12:03 -04:00

266 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Kushya API — the in-process interface the Talmudic Engine calls.
Architecture §3.6 / §4 Step 22.
Three backends are supported, in priority order:
1. Fine-tuned local model (when available — operator's deployed adapter)
2. External model via Korban Processor (eish-zarah-disciplined)
3. Rule-based fallback that returns a structured "no kushya" indicator
The caller (Talmudic Engine via socket, or vav_pipeline.SanhedrinClient)
decides which backend to use; this module surfaces all three behind a
single function.
"""
from dataclasses import dataclass
from typing import Optional
# Import korban_processor at module level so tests can patch
# `kushya_api.consult_external_model`. If the package isn't on the
# path (e.g., someone only wants the dataset tools), these bindings
# are set to None and _try_external returns a no-backend result.
try:
from korban_processor.external_offering import consult_external_model
from korban_processor.korban import OfferingType, Provenance
except ImportError: # pragma: no cover
consult_external_model = None
OfferingType = None
Provenance = None
@dataclass
class KushyaResult:
"""The kushya generator's reply to an interpretation."""
interpretation: str
kushya: Optional[str]
backend: str # "fine_tuned", "external", "fallback"
torah_refs: list[str]
raw_response: Optional[str] = None # the full model output, before extraction
notes: Optional[str] = None # backend-specific notes (e.g., warning)
def generate_kushya(
interpretation: str,
backend: str = "auto",
fine_tuned_endpoint: Optional[str] = None,
) -> KushyaResult:
"""Generate a kushya for the given interpretation.
Args:
interpretation: The proposed Torah interpretation.
backend: One of "auto", "fine_tuned", "external", "fallback".
"auto" tries fine_tuned, then external, then fallback.
fine_tuned_endpoint: HTTP URL of the local fine-tuned model server.
When None and backend is "auto" or "fine_tuned", this backend
is skipped.
Returns:
A KushyaResult. The kushya field may be None if no backend produced
a substantive challenge — the caller must handle that case
(typically by treating the interpretation as un-challenged).
"""
if backend == "fine_tuned":
return _try_fine_tuned(interpretation, fine_tuned_endpoint, allow_fallback=False)
if backend == "external":
return _try_external(interpretation, allow_fallback=False)
if backend == "fallback":
return _fallback(interpretation)
# auto: try in priority order
if fine_tuned_endpoint:
result = _try_fine_tuned(interpretation, fine_tuned_endpoint, allow_fallback=True)
if result.kushya:
return result
result = _try_external(interpretation, allow_fallback=True)
if result.kushya:
return result
return _fallback(interpretation)
def _try_fine_tuned(
interpretation: str,
endpoint: Optional[str],
*,
allow_fallback: bool,
) -> KushyaResult:
"""Call the fine-tuned local kushya model over HTTP.
The serve.py script wraps llama.cpp behind a small HTTP API at
POST /generate that accepts {"prompt": "..."} and returns
{"completion": "..."}.
"""
if not endpoint:
if allow_fallback:
return KushyaResult(
interpretation=interpretation,
kushya=None,
backend="fine_tuned",
torah_refs=[],
notes="fine-tuned endpoint not configured",
)
raise ValueError("fine_tuned backend requires fine_tuned_endpoint")
try:
import httpx
except ImportError:
return KushyaResult(
interpretation=interpretation,
kushya=None,
backend="fine_tuned",
torah_refs=[],
notes="httpx not installed",
)
prompt = (
"Generate a kushya (challenge) for this interpretation. "
"Cite a specific Torah passage. End with a question mark.\n\n"
f"Proposed interpretation: {interpretation}\n\n"
"Kushya: "
)
try:
with httpx.Client(timeout=60.0) as client:
response = client.post(endpoint, json={"prompt": prompt})
response.raise_for_status()
completion = response.json().get("completion", "").strip()
except httpx.RequestError as e:
if allow_fallback:
return KushyaResult(
interpretation=interpretation,
kushya=None,
backend="fine_tuned",
torah_refs=[],
notes=f"endpoint unreachable: {e}",
)
raise
if not completion:
return KushyaResult(
interpretation=interpretation,
kushya=None,
backend="fine_tuned",
torah_refs=[],
raw_response=completion,
notes="empty completion",
)
return KushyaResult(
interpretation=interpretation,
kushya=completion,
backend="fine_tuned",
torah_refs=_extract_refs(completion),
raw_response=completion,
)
def _try_external(interpretation: str, *, allow_fallback: bool) -> KushyaResult:
"""Call an external model (Claude API) via Korban Processor.
The output flows through the offering protocol so external response is
treated as testimony, not law (eish_zarah_reading.md). Provenance is
preserved on the resulting consultation record.
"""
if consult_external_model is None:
if allow_fallback:
return KushyaResult(
interpretation=interpretation,
kushya=None,
backend="external",
torah_refs=[],
notes="korban_processor not on sys.path",
)
raise RuntimeError("korban_processor not on sys.path")
system_prompt = (
"You are a Talmudic challenger. Generate exactly one kushya (challenge) "
"to the proposed interpretation. The kushya must:\n"
" 1. Reference a specific Torah passage that creates tension\n"
" 2. Identify the logical or textual weakness\n"
" 3. Be expressed as a question, not a statement\n"
" 4. Not be resolvable by trivial clarification\n\n"
"Return only the kushya question. No preamble, no closing."
)
query = f"Proposed interpretation: {interpretation}"
try:
consultation = consult_external_model(
query=query,
source_query=f"kushya_request:{interpretation[:80]}",
provenance=Provenance.CLAUDE,
offering_type=OfferingType.OLAH,
system_prompt=system_prompt,
)
except Exception as e:
if allow_fallback:
return KushyaResult(
interpretation=interpretation,
kushya=None,
backend="external",
torah_refs=[],
notes=f"external consultation failed: {type(e).__name__}: {e}",
)
raise
raw = consultation.raw_response.content
vital = consultation.processed.vital_content
# The vital content already passed through shechitah (preamble strip)
# and zerikah (vital extraction). Use it as the kushya.
if not vital.strip():
return KushyaResult(
interpretation=interpretation,
kushya=None,
backend="external",
torah_refs=[],
raw_response=raw,
notes="zerikah found no vital content in external response",
)
return KushyaResult(
interpretation=interpretation,
kushya=vital,
backend="external",
torah_refs=_extract_refs(vital),
raw_response=raw,
notes=(
"External testimony — recorded under offering protocol. "
"NOT binding ruling."
),
)
def _fallback(interpretation: str) -> KushyaResult:
"""No external backend available. Return a structured no-kushya result.
The Talmudic Engine has its own rule-based kushya generator
(challenge.pl). This fallback is used when even external testimony
is unavailable — we do NOT invent a kushya, we honestly report
that no challenge was generated.
"""
return KushyaResult(
interpretation=interpretation,
kushya=None,
backend="fallback",
torah_refs=[],
notes=(
"No backend available. Rule-based kushyot from the Talmudic "
"Engine should be used; the system does not invent challenges."
),
)
def _extract_refs(text: str) -> list[str]:
"""Extract Torah verse references from kushya text.
Returns the full match strings (e.g. "Shemot 35:3", "Devarim 21:1-9"),
not just the captured book names.
"""
import re
# Non-capturing group so re.findall returns the full match.
pattern = r"\b(?:Bereishit|Shemot|Vayikra|Bamidbar|Devarim)\s+\d+:\d+(?:[-–]\d+)?"
return re.findall(pattern, text)