146 lines
7.9 KiB
Python
146 lines
7.9 KiB
Python
"""One paid reasoning pass; two independently accepted phase artifacts."""
|
|
import copy
|
|
import hashlib
|
|
from importlib.resources import files
|
|
|
|
from viano_bug_analysis.case.contracts import read_json
|
|
from viano_bug_analysis.lifecycle.storage import json_bytes
|
|
from .errors import InterpretationError
|
|
from .hypothesis_config import HypothesisError
|
|
|
|
VERSION = "case_plan_v1"
|
|
REPAIR = "case_plan_interpretation_repair_v1"
|
|
|
|
|
|
def digest(value):
|
|
return hashlib.sha256(json_bytes(value)).hexdigest()
|
|
|
|
|
|
def schemas(context):
|
|
from .contracts import output_schema
|
|
from .hypothesis_contracts import output_schema as hypothesis_schema
|
|
interpretation = output_schema(context)
|
|
hypotheses = hypothesis_schema(context["joint_generation"]["policy"])
|
|
proposal = hypotheses["$defs"]["proposal"]
|
|
proposal["properties"].pop("interpretation_ids")
|
|
proposal["properties"]["interpretation_paths"] = {
|
|
"type": "array", "minItems": 1, "uniqueItems": True,
|
|
"items": {"type": "string", "pattern": "^/interpretation/[a-z_]+(/[0-9]+)?$"}}
|
|
proposal["required"] = ["interpretation_paths" if f == "interpretation_ids" else f for f in proposal["required"]]
|
|
return interpretation, hypotheses
|
|
|
|
|
|
def build_request(context):
|
|
from .phase_selection import select_context
|
|
from .model_context import compact_context, model_json
|
|
from .json_consolidation import consolidate
|
|
view = select_context(context, "interpretation")
|
|
settings = view.pop("joint_generation")
|
|
interpretation, hypotheses = schemas(context)
|
|
# Full child contracts occur ONCE in prompt. Permissive transport fields allow
|
|
# one malformed semantic block to survive for separate local validation.
|
|
return {"prompt_version": VERSION, "privacy": "configured",
|
|
"output_schema": {"type": "object", "properties": {"interpretation": {}, "hypotheses": {}},
|
|
"additionalProperties": False},
|
|
"messages": [{"role": "system", "content": files("viano_bug_analysis.llm.prompts").joinpath(VERSION + ".txt").read_text(encoding="utf-8")},
|
|
{"role": "user", "content": model_json({"task": "interpret_and_propose", "input": consolidate(compact_context(view)),
|
|
"policy": settings["policy"], "prior_generations": settings["prior_generations"],
|
|
"block_contracts": {"interpretation": interpretation, "hypotheses": hypotheses}})}]}
|
|
|
|
|
|
def normalize_block(payload, schema):
|
|
from viano_bug_analysis.llm.answer_recovery import normalize_schema, parse_complete, CAPTURE_LIMIT
|
|
if isinstance(payload, str) and len(payload.encode("utf-8")) <= CAPTURE_LIMIT:
|
|
decoded, _ = parse_complete(payload)
|
|
if decoded is not None:
|
|
payload = decoded
|
|
return normalize_schema(payload, schema)[0]
|
|
|
|
|
|
def interpretation_payload(call, context):
|
|
from .contracts import output_schema
|
|
payload = call["outcome"].get("document")
|
|
if call["outcome"].get("metadata", {}).get("prompt_version") == VERSION:
|
|
payload = payload.get("interpretation") if isinstance(payload, dict) else None
|
|
payload = normalize_block(payload, output_schema(context))
|
|
return payload
|
|
|
|
|
|
def hypothesis_payload(reader, call, context):
|
|
"""Bind the candidate to the exact accepted interpretation, never a repaired one."""
|
|
accepted = read_json(reader, context["interpretation_ref"], "case-interpretation", 2 * 1024**2)
|
|
case_context = context["case_context"]
|
|
if (call["result_ref"] != accepted["llm_result_ref"] or call["status"] != "SUCCESS"
|
|
or call["phase"] != "INTERPRETING_CASE"
|
|
or any(call[f] != accepted[f] for f in ("case_id", "package_id", "analysis_revision"))
|
|
or call["outcome"].get("metadata", {}).get("prompt_version") != VERSION
|
|
or interpretation_payload(call, case_context) != accepted["interpretation"]
|
|
or accepted["llm_metadata"] != call["outcome"].get("metadata")
|
|
or accepted["prompt_version"] != VERSION
|
|
or accepted["case_ref"] != case_context["case_ref"]
|
|
or accepted["case_sha256"] != case_context["case_sha256"]
|
|
or digest(accepted) != context["interpretation_sha256"]
|
|
or case_context["joint_generation"]["policy"] != context["policy"]
|
|
or case_context["joint_generation"]["prior_generations"] != context["prior_generations"]):
|
|
raise HypothesisError("HYPOTHESIS_LLM_BINDING")
|
|
stored_context = read_json(reader, accepted["context_ref"], "case-interpretation-context", 2 * 1024**2)
|
|
if stored_context != case_context or accepted["context_sha256"] != digest(case_context):
|
|
raise HypothesisError("HYPOTHESIS_LLM_BINDING")
|
|
from .contracts import validate_interpretation
|
|
validate_interpretation(accepted["interpretation"], case_context)
|
|
request = read_request(reader, call)
|
|
expected = build_request(case_context)
|
|
from .correction import replay_request
|
|
expected = replay_request(reader, expected, call, "interpretation")
|
|
expected["privacy"] = request.get("privacy")
|
|
if request != expected:
|
|
raise HypothesisError("HYPOTHESIS_LLM_BINDING")
|
|
raw = call["outcome"]["document"].get("hypotheses")
|
|
_, schema = schemas(case_context)
|
|
raw = normalize_block(raw, schema)
|
|
from jsonschema import Draft202012Validator
|
|
if not Draft202012Validator(schema).is_valid(raw):
|
|
raise HypothesisError("HYPOTHESIS_SCHEMA")
|
|
result = copy.deepcopy(raw)
|
|
pointers = {v["interpretation_pointer"]: k for k,v in context["interpretation_sources"].items()}
|
|
for proposal in result["hypotheses"]:
|
|
paths = proposal.pop("interpretation_paths")
|
|
if any(p not in pointers for p in paths):
|
|
raise HypothesisError("HYPOTHESIS_SOURCE_UNKNOWN")
|
|
proposal["interpretation_ids"] = [pointers[p] for p in paths]
|
|
return result
|
|
|
|
|
|
def read_request(reader, call):
|
|
from viano_bug_analysis.package.manifest import parse_manifest
|
|
from viano_bug_analysis.case.contracts import require_file
|
|
require_file(reader, call["request_ref"])
|
|
with reader._archive.open(call["request_ref"]) as stream:
|
|
raw = reader.validator._read_bounded(stream, min(4 * 1024**2, reader.validator.limits.max_file_bytes))
|
|
if hashlib.sha256(raw).hexdigest() != call["input_request_hash"]:
|
|
raise InterpretationError("INTERPRETATION_LLM_BINDING")
|
|
return parse_manifest(raw)
|
|
|
|
|
|
def joint_call(reader, context):
|
|
accepted = read_json(reader, context["interpretation_ref"], "case-interpretation", 2 * 1024**2)
|
|
call = read_json(reader, accepted["llm_result_ref"], "llm-call", 4 * 1024**2)
|
|
return call if call["outcome"].get("metadata", {}).get("prompt_version") == VERSION else None
|
|
|
|
|
|
def repair_interpretation(request, reader, failure, feedback):
|
|
"""Replace only interpretation; hypotheses from the rejected parent are invalidated."""
|
|
from .contracts import output_schema
|
|
context = read_json(reader, failure[1]["context_ref"], "case-interpretation-context", 2 * 1024**2)
|
|
from .phase_selection import select_context
|
|
from .model_context import compact_context, model_json
|
|
from .json_consolidation import consolidate
|
|
view = select_context(context, "interpretation")
|
|
view.pop("joint_generation", None)
|
|
encoding = files("viano_bug_analysis.llm.prompts").joinpath(VERSION + ".txt").read_text(encoding="utf-8").split("\nENCODING\n", 1)[1]
|
|
return {"prompt_version": REPAIR, "privacy": request["privacy"], "output_schema": output_schema(context),
|
|
"messages": [{"role": "system", "content": files("viano_bug_analysis.llm.prompts").joinpath(REPAIR + ".txt").read_text(encoding="utf-8") + "\n" + encoding},
|
|
{"role": "user", "content": model_json({"task": "replace_interpretation_only",
|
|
"input": consolidate(compact_context(view)), "rejected_interpretation": interpretation_payload(failure[2],context),
|
|
"validation_feedback": feedback, "output_schema": output_schema(context)})}]}
|