sable-sphynx 0.7.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
- sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
- sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
- sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
- sphynx/__init__.py +469 -0
- sphynx/__init__.pyi +376 -0
- sphynx/_analytical_client.py +77 -0
- sphynx/_analytical_values.py +252 -0
- sphynx/_audit_client.py +173 -0
- sphynx/_comparison_adapters.py +113 -0
- sphynx/_componentized_full_rag.py +34 -0
- sphynx/_composed_pipeline.py +109 -0
- sphynx/_full_rag_grading.py +218 -0
- sphynx/_full_rag_trace.py +221 -0
- sphynx/_local_eval.py +268 -0
- sphynx/_pipeline_core.py +243 -0
- sphynx/_pr_client.py +237 -0
- sphynx/_rag_diagnostics_calibration.py +285 -0
- sphynx/_rag_diagnostics_catalog.py +39 -0
- sphynx/_rag_diagnostics_common.py +26 -0
- sphynx/_rag_diagnostics_contracts.py +186 -0
- sphynx/_rag_diagnostics_engine.py +243 -0
- sphynx/_rag_diagnostics_importers.py +242 -0
- sphynx/_rag_diagnostics_ranking.py +81 -0
- sphynx/_rag_diagnostics_semantic.py +325 -0
- sphynx/_rag_ranking.py +149 -0
- sphynx/_speech_benchmark_scoring.py +140 -0
- sphynx/_speech_benchmark_sources.py +275 -0
- sphynx/_speech_complete_scoring.py +218 -0
- sphynx/_speech_complete_source.py +118 -0
- sphynx/_speech_kido.py +294 -0
- sphynx/_speech_scoring.py +77 -0
- sphynx/_track_capture.py +213 -0
- sphynx/analytical_views.py +515 -0
- sphynx/assessment_contracts.py +378 -0
- sphynx/assessment_selection.py +57 -0
- sphynx/audit_access.py +245 -0
- sphynx/audit_contracts.py +204 -0
- sphynx/audit_requests.py +180 -0
- sphynx/authoring.py +188 -0
- sphynx/benchmarks.py +568 -0
- sphynx/candidates.py +62 -0
- sphynx/catalog.py +272 -0
- sphynx/chunking.py +372 -0
- sphynx/clickhouse_reader.py +169 -0
- sphynx/client.py +486 -0
- sphynx/comparison_core.py +149 -0
- sphynx/comparison_views.py +223 -0
- sphynx/components.py +1661 -0
- sphynx/contracts.py +514 -0
- sphynx/customer_rerank.py +299 -0
- sphynx/customer_retrieval.py +146 -0
- sphynx/data/benchmark-releases.v1.json +164 -0
- sphynx/data/public-benchmark-release.v1.json +354 -0
- sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
- sphynx/data/rag-paper-references.v1.json +176 -0
- sphynx/data/rag-paper-schemas.v1.json +1069 -0
- sphynx/data/rag-rerank-v1.json +507 -0
- sphynx/dataset_catalog_data.py +1214 -0
- sphynx/dataset_readiness.py +138 -0
- sphynx/dataset_v3.py +999 -0
- sphynx/demo.py +130 -0
- sphynx/demo_candidates/__init__.py +1 -0
- sphynx/demo_candidates/dense.py +11 -0
- sphynx/demo_candidates/lexical.py +21 -0
- sphynx/diagnostics.py +207 -0
- sphynx/documentation.py +737 -0
- sphynx/documentation_candidate.py +939 -0
- sphynx/evals.py +538 -0
- sphynx/evaluation_protocols.py +124 -0
- sphynx/evaluator_bindings.py +370 -0
- sphynx/evidence.py +352 -0
- sphynx/execution_capabilities.py +109 -0
- sphynx/fast_search.py +285 -0
- sphynx/full_rag.py +341 -0
- sphynx/hosted/__init__.py +18 -0
- sphynx/hosted/benchmark_intake.py +218 -0
- sphynx/hosted/contracts.py +361 -0
- sphynx/hosted/model_admission.py +130 -0
- sphynx/hosted/model_artifacts.py +298 -0
- sphynx/hosted/model_contracts.py +63 -0
- sphynx/hosted/model_gcs.py +181 -0
- sphynx/hosted/model_history.py +84 -0
- sphynx/hosted/model_session.py +233 -0
- sphynx/hosted/public_suite.py +74 -0
- sphynx/hosted/registry.py +97 -0
- sphynx/hosted/revisions.py +126 -0
- sphynx/hosted/sandbox.py +105 -0
- sphynx/hosted/submission.py +287 -0
- sphynx/hosted_workflow.py +451 -0
- sphynx/http_transport.py +86 -0
- sphynx/integrated_rag.py +352 -0
- sphynx/integrated_workflow.py +552 -0
- sphynx/integration_contracts.py +474 -0
- sphynx/invocation.py +791 -0
- sphynx/invocation_candidates.py +392 -0
- sphynx/invocation_catalog_data.py +40 -0
- sphynx/learned_catalog_data.py +30 -0
- sphynx/learned_rerank.py +267 -0
- sphynx/legacy_full_rag.py +67 -0
- sphynx/lifecycle.py +885 -0
- sphynx/lifecycle_workflow.py +490 -0
- sphynx/metric_views.py +474 -0
- sphynx/metrics.py +282 -0
- sphynx/model_adapters.py +438 -0
- sphynx/model_catalog_data.py +30 -0
- sphynx/official_benchmarks.py +385 -0
- sphynx/packs.py +258 -0
- sphynx/pipeline.py +313 -0
- sphynx/pipeline_workflow.py +551 -0
- sphynx/pr_evaluations.py +560 -0
- sphynx/progress.py +89 -0
- sphynx/public_bundle.py +173 -0
- sphynx/public_eval.py +62 -0
- sphynx/public_rerank.py +355 -0
- sphynx/py.typed +0 -0
- sphynx/pytest_plugin.py +62 -0
- sphynx/question_robustness.py +437 -0
- sphynx/rag.py +329 -0
- sphynx/rag_diagnostics.py +309 -0
- sphynx/rag_papers/__init__.py +29 -0
- sphynx/rag_papers/__main__.py +45 -0
- sphynx/rag_papers/candidates.py +19 -0
- sphynx/rag_papers/contracts.py +282 -0
- sphynx/rag_papers/datasets.py +291 -0
- sphynx/rag_papers/experiments.py +116 -0
- sphynx/rag_papers/judges.py +308 -0
- sphynx/rag_papers/registry.py +33 -0
- sphynx/rag_papers/runner.py +176 -0
- sphynx/rag_papers/sable.py +135 -0
- sphynx/rag_papers/schemas.py +12 -0
- sphynx/rag_papers/scoring.py +249 -0
- sphynx/rag_papers/semantic.py +288 -0
- sphynx/rag_papers/trec.py +80 -0
- sphynx/recorded_scores.py +352 -0
- sphynx/records.py +124 -0
- sphynx/reflex_traces.py +193 -0
- sphynx/report_rendering.py +71 -0
- sphynx/rerank_family_contracts.py +150 -0
- sphynx/rerank_workflow.py +424 -0
- sphynx/research_candidate.py +190 -0
- sphynx/results.py +852 -0
- sphynx/retrieval.py +750 -0
- sphynx/retrieval_candidates.py +295 -0
- sphynx/retrieval_catalog_data.py +19 -0
- sphynx/retrieval_recordings.py +125 -0
- sphynx/reviewer_audit.py +266 -0
- sphynx/routing_policies.py +375 -0
- sphynx/runner.py +544 -0
- sphynx/sable_rerank.py +489 -0
- sphynx/score_reliability.py +393 -0
- sphynx/speech_benchmarks.py +204 -0
- sphynx/speech_campaigns.py +378 -0
- sphynx/speech_complete.py +236 -0
- sphynx/speech_contracts.py +225 -0
- sphynx/speech_evidence.py +429 -0
- sphynx/speech_listening.py +115 -0
- sphynx/speech_multilingual.py +71 -0
- sphynx/speech_tts.py +479 -0
- sphynx/sql/001_sync_v1.sql +30 -0
- sphynx/stage_contracts.py +199 -0
- sphynx/statistical_catalog_data.py +53 -0
- sphynx/statistical_rerank.py +232 -0
- sphynx/sync.py +797 -0
- sphynx/workbench.py +378 -0
- sphynx/workbench_catalog_data.py +13 -0
- sphynx/workbench_tracks.py +78 -0
- sphynx/workflow_async.py +197 -0
- sphynx/workflow_contracts.py +594 -0
- sphynx/workflow_decisions.py +211 -0
- sphynx/workflow_defaults.py +16 -0
- sphynx/workflow_extensions.py +212 -0
- sphynx/workflow_registry.py +513 -0
- sphynx/workflows.py +792 -0
sphynx/reviewer_audit.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Audit a recorded Jev non-call reviewer against its own initial decisions.
|
|
2
|
+
|
|
3
|
+
Stage evidence is supplied by the candidate. Capture hashes and consistency are
|
|
4
|
+
verified locally; this does not promote a run beyond LOCAL_UNVERIFIED.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .invocation import (InvocationError, InvocationRunRef, _percentile,
|
|
18
|
+
_verified_run, load_invocation_pack)
|
|
19
|
+
|
|
20
|
+
_ORDER = ("call_rag", "clarify_first", "do_not_call_rag")
|
|
21
|
+
_ACTION = {"call_rag": "CALL_RAG", "clarify_first": "ASK_BUYER",
|
|
22
|
+
"do_not_call_rag": "ANSWER_FROM_CONTEXT"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _hash(value: object, *, ensure_ascii: bool = True) -> str:
|
|
26
|
+
raw = json.dumps(value, sort_keys=True, separators=(",", ":"),
|
|
27
|
+
ensure_ascii=ensure_ascii, allow_nan=False).encode()
|
|
28
|
+
return hashlib.sha256(raw).hexdigest()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _events(capture: dict[str, Any]) -> list[dict[str, Any]]:
|
|
32
|
+
if capture["status"] != "success":
|
|
33
|
+
raise InvocationError("reviewer audit requires complete successful stage evidence")
|
|
34
|
+
|
|
35
|
+
def unique(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
36
|
+
obj: dict[str, Any] = {}
|
|
37
|
+
for key, value in pairs:
|
|
38
|
+
if key in obj:
|
|
39
|
+
raise ValueError("duplicate event field")
|
|
40
|
+
obj[key] = value
|
|
41
|
+
return obj
|
|
42
|
+
|
|
43
|
+
def invalid_constant(value: str) -> None:
|
|
44
|
+
raise ValueError("non-finite event value")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
events = [json.loads(line, object_pairs_hook=unique,
|
|
48
|
+
parse_constant=invalid_constant)
|
|
49
|
+
for line in capture["stderr_utf8"].splitlines() if line]
|
|
50
|
+
if len(events) not in (1, 2):
|
|
51
|
+
raise ValueError("wrong stage count")
|
|
52
|
+
for event in events:
|
|
53
|
+
if (not isinstance(event, dict)
|
|
54
|
+
or event.get("schema_version") != "sphynx.rag.invoke.candidate_stage.v1"
|
|
55
|
+
or event.get("case_id") != capture["case_id"]
|
|
56
|
+
or not isinstance(event.get("request"), dict)
|
|
57
|
+
or not isinstance(event.get("response"), dict)
|
|
58
|
+
or event.get("request_sha256") != _hash(event["request"],
|
|
59
|
+
ensure_ascii=event.get("stage") != "jev")
|
|
60
|
+
or event.get("response_sha256") != _hash(event["response"])):
|
|
61
|
+
raise ValueError("stage identity or digest differs")
|
|
62
|
+
timing = event.get("candidate_reported_latency_ms")
|
|
63
|
+
if type(timing) not in (int, float) or not math.isfinite(timing) or timing < 0:
|
|
64
|
+
raise ValueError("stage timing invalid")
|
|
65
|
+
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
66
|
+
raise InvocationError("candidate stage evidence is invalid") from exc
|
|
67
|
+
return events
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _initial(capture: dict[str, Any], events: list[dict[str, Any]]) -> str:
|
|
71
|
+
first = events[0]
|
|
72
|
+
request, response = first["request"], first["response"]
|
|
73
|
+
state = request.get("state", {})
|
|
74
|
+
if not isinstance(state, dict):
|
|
75
|
+
raise InvocationError("Jev stage state is invalid")
|
|
76
|
+
question = capture["request"]
|
|
77
|
+
if (first.get("stage") != "jev" or first.get("model") != "jev-1.13.0"
|
|
78
|
+
or request.get("model") != "jev-1.13.0" or response.get("model") != "jev-1.13.0"
|
|
79
|
+
or state.get("latest_user_message") != question["user_turn"]
|
|
80
|
+
or state.get("recent_conversation") != question["conversation_context"]
|
|
81
|
+
or state.get("assistant_context") !=
|
|
82
|
+
f"An enterprise agent can optionally retrieve from the frozen {question['company_id']} company knowledge base."):
|
|
83
|
+
raise InvocationError("Jev stage does not match the recorded candidate input")
|
|
84
|
+
answers = response.get("answers", {})
|
|
85
|
+
if not isinstance(answers, dict) or not isinstance(answers.get("invocation_action"), dict):
|
|
86
|
+
raise InvocationError("Jev stage answer is invalid")
|
|
87
|
+
answer = answers["invocation_action"]
|
|
88
|
+
probabilities = answer.get("probabilities")
|
|
89
|
+
if (answer.get("type") != "choice" or not isinstance(probabilities, dict)
|
|
90
|
+
or set(probabilities) != set(_ORDER)
|
|
91
|
+
or any(type(p) not in (int, float) or not math.isfinite(p) or not 0 <= p <= 1
|
|
92
|
+
for p in probabilities.values())
|
|
93
|
+
or abs(sum(probabilities.values()) - 1) > 0.031
|
|
94
|
+
or probabilities != first.get("probabilities")):
|
|
95
|
+
raise InvocationError("Jev stage probability vector is invalid")
|
|
96
|
+
choice = max(_ORDER, key=lambda action: probabilities[action])
|
|
97
|
+
if first.get("decision") != choice:
|
|
98
|
+
raise InvocationError("Jev stage decision differs from its probability vector")
|
|
99
|
+
return _ACTION[choice]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _review(capture: dict[str, Any], events: list[dict[str, Any]], first: str) -> str:
|
|
103
|
+
if first == "CALL_RAG":
|
|
104
|
+
if len(events) != 1 or capture["action"] != first:
|
|
105
|
+
raise InvocationError("a Jev call must bypass the non-call reviewer")
|
|
106
|
+
return first
|
|
107
|
+
if len(events) != 2:
|
|
108
|
+
raise InvocationError("a Jev non-call requires a recorded reviewer stage")
|
|
109
|
+
event = events[1]
|
|
110
|
+
request, response = event["request"], event["response"]
|
|
111
|
+
question = capture["request"]
|
|
112
|
+
messages = request.get("input", [])
|
|
113
|
+
expected = [dict(turn) for turn in question["conversation_context"]]
|
|
114
|
+
expected.append({"role": "user", "content": question["user_turn"]})
|
|
115
|
+
try:
|
|
116
|
+
observed = [{"role": turn["role"], "content": turn["content"][0]["text"]}
|
|
117
|
+
for turn in messages[1:]]
|
|
118
|
+
output = [item.get("text", "") for message in response.get("output", [])
|
|
119
|
+
if isinstance(message, dict) and message.get("type") == "message"
|
|
120
|
+
for item in message.get("content", [])
|
|
121
|
+
if isinstance(item, dict) and item.get("type") == "output_text"]
|
|
122
|
+
def unique(pairs):
|
|
123
|
+
result = {}
|
|
124
|
+
for key, value in pairs:
|
|
125
|
+
if key in result:
|
|
126
|
+
raise ValueError("duplicate reviewer field")
|
|
127
|
+
result[key] = value
|
|
128
|
+
return result
|
|
129
|
+
# The source reviewer joins output_text parts before parsing its JSON.
|
|
130
|
+
result = json.loads("".join(output), object_pairs_hook=unique)
|
|
131
|
+
except (AttributeError, KeyError, IndexError, TypeError, ValueError) as exc:
|
|
132
|
+
raise InvocationError("reviewer response or input structure is invalid") from exc
|
|
133
|
+
if (event.get("stage") != "noncall_reviewer" or event.get("model") != "gpt-6-sol"
|
|
134
|
+
or request.get("model") != "gpt-6-sol" or response.get("model") != "gpt-6-sol"
|
|
135
|
+
or response.get("status") != "completed" or not messages
|
|
136
|
+
or messages[0].get("role") != "developer" or observed != expected
|
|
137
|
+
or not isinstance(result, dict) or set(result) != {"route", "brief_reason"}
|
|
138
|
+
or result["route"] not in {"REQUIRE_RAG", "KEEP_NONCALL"}
|
|
139
|
+
or not isinstance(result["brief_reason"], str)
|
|
140
|
+
or result["route"] != event.get("decision")):
|
|
141
|
+
raise InvocationError("reviewer stage does not match its recorded request or output")
|
|
142
|
+
final = "CALL_RAG" if result["route"] == "REQUIRE_RAG" else first
|
|
143
|
+
if final != capture["action"]:
|
|
144
|
+
raise InvocationError("final candidate action differs from the two-stage composition")
|
|
145
|
+
return final
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _quality(gold: list[str], actions: list[str]) -> dict[str, Any]:
|
|
149
|
+
def rate(numerator: int, denominator: int, direction: str = "higher") -> dict[str, Any]:
|
|
150
|
+
return {"value": numerator / denominator if denominator else None,
|
|
151
|
+
"numerator": numerator, "denominator": denominator, "direction": direction}
|
|
152
|
+
|
|
153
|
+
required = sum(g == "CALL_RAG" for g in gold)
|
|
154
|
+
noncalls = len(gold) - required
|
|
155
|
+
return {
|
|
156
|
+
"three_way_accuracy": rate(sum(a == g for a, g in zip(actions, gold)), len(gold)),
|
|
157
|
+
"binary_rag_accuracy": rate(sum((a == "CALL_RAG") == (g == "CALL_RAG")
|
|
158
|
+
for a, g in zip(actions, gold)), len(gold)),
|
|
159
|
+
"required_lookup_recall": rate(sum(g == "CALL_RAG" and a == "CALL_RAG"
|
|
160
|
+
for a, g in zip(actions, gold)), required),
|
|
161
|
+
"unnecessary_call_rate": rate(sum(g != "CALL_RAG" and a == "CALL_RAG"
|
|
162
|
+
for a, g in zip(actions, gold)), noncalls, "lower"),
|
|
163
|
+
"clarification_accuracy": rate(sum(g == "ASK_BUYER" and a == g
|
|
164
|
+
for a, g in zip(actions, gold)), sum(g == "ASK_BUYER" for g in gold)),
|
|
165
|
+
"answer_from_context_accuracy": rate(sum(g == "ANSWER_FROM_CONTEXT" and a == g
|
|
166
|
+
for a, g in zip(actions, gold)),
|
|
167
|
+
sum(g == "ANSWER_FROM_CONTEXT" for g in gold)),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class ReviewerAudit:
|
|
173
|
+
"""Aggregate report without case IDs, question text, or source content."""
|
|
174
|
+
|
|
175
|
+
data: dict[str, Any]
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> dict[str, Any]:
|
|
178
|
+
return json.loads(self.to_json())
|
|
179
|
+
|
|
180
|
+
def to_json(self) -> str:
|
|
181
|
+
return json.dumps(self.data, sort_keys=True, ensure_ascii=False, allow_nan=False)
|
|
182
|
+
|
|
183
|
+
def to_markdown(self) -> str:
|
|
184
|
+
lines = ["# Jev reviewer audit", "", "LOCAL_UNVERIFIED; paired initial and final decisions from one recorded run.", "",
|
|
185
|
+
"| Metric | Initial Jev | After reviewer |", "| --- | ---: | ---: |"]
|
|
186
|
+
for name, item in self.data["initial_metrics"].items():
|
|
187
|
+
after = self.data["final_metrics"][name]
|
|
188
|
+
def label(value: dict[str, Any]) -> str:
|
|
189
|
+
return ("unavailable" if value["value"] is None else
|
|
190
|
+
f"{value['numerator']}/{value['denominator']} ({value['value']:.1%})")
|
|
191
|
+
lines.append(f"| {name} | {label(item)} | {label(after)} |")
|
|
192
|
+
lines += ["", f"Reviews: {self.data['reviewer_call_count']}; corrections: {self.data['corrected_count']}; regressions: {self.data['regressed_count']}." ]
|
|
193
|
+
if "independent_raw_run" in self.data:
|
|
194
|
+
independent = self.data["independent_raw_run"]
|
|
195
|
+
lines += ["", f"The independent raw run differs on {independent['initial_decision_difference_count']} Jev initial decisions. Its difference from the final result includes sampling variation."]
|
|
196
|
+
return "\n".join(lines) + "\n"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def audit_invocation_reviewer(
|
|
200
|
+
run_ref: InvocationRunRef | str | os.PathLike[str],
|
|
201
|
+
data_dir: str | os.PathLike[str],
|
|
202
|
+
*, raw_run_ref: InvocationRunRef | str | os.PathLike[str] | None = None,
|
|
203
|
+
) -> ReviewerAudit:
|
|
204
|
+
"""Measure the reviewer intervention using the same recorded Jev decisions.
|
|
205
|
+
|
|
206
|
+
Optionally show why a separately sampled raw run differs. No provider calls
|
|
207
|
+
are made. Requires successful, complete Jev and non-call reviewer evidence.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
pack = load_invocation_pack(data_dir)
|
|
211
|
+
manifest, captures, _ = _verified_run(run_ref, pack)
|
|
212
|
+
gold = [pack.gold[case.case_id] for case in pack.cases]
|
|
213
|
+
initial, final, stages = [], [], []
|
|
214
|
+
for capture in captures:
|
|
215
|
+
events = _events(capture)
|
|
216
|
+
first = _initial(capture, events)
|
|
217
|
+
initial.append(first)
|
|
218
|
+
final.append(_review(capture, events, first))
|
|
219
|
+
stages.append(events)
|
|
220
|
+
corrected = sum(a != g and b == g for a, b, g in zip(initial, final, gold))
|
|
221
|
+
regressed = sum(a == g and b != g for a, b, g in zip(initial, final, gold))
|
|
222
|
+
stage_totals = sorted(sum(event["candidate_reported_latency_ms"] for event in events)
|
|
223
|
+
for events in stages)
|
|
224
|
+
result: dict[str, Any] = {
|
|
225
|
+
"schema_version": "sphynx.rag.invoke.reviewer_audit.v1",
|
|
226
|
+
"scope": "same_recorded_jev_first_stage_before_and_after_noncall_review",
|
|
227
|
+
"trust_tier": "local_unverified", "development_only": True,
|
|
228
|
+
"evidence_source": "hash_verified_candidate_reported_stage_events",
|
|
229
|
+
"run_id": manifest["run_id"], "candidate": manifest["candidate"],
|
|
230
|
+
"input_sha256": pack.input_sha256, "gold_sha256": pack.gold_sha256,
|
|
231
|
+
"case_count": len(gold), "reviewer_call_count": sum(len(events) == 2 for events in stages),
|
|
232
|
+
"changed_count": sum(a != b for a, b in zip(initial, final)),
|
|
233
|
+
"corrected_count": corrected, "regressed_count": regressed,
|
|
234
|
+
"net_correct_count_delta": corrected - regressed,
|
|
235
|
+
"required_lookup_rescued_count": sum(g == "CALL_RAG" and a != "CALL_RAG" and b == "CALL_RAG"
|
|
236
|
+
for a, b, g in zip(initial, final, gold)),
|
|
237
|
+
"new_unnecessary_call_count": sum(g != "CALL_RAG" and a != "CALL_RAG" and b == "CALL_RAG"
|
|
238
|
+
for a, b, g in zip(initial, final, gold)),
|
|
239
|
+
"initial_metrics": _quality(gold, initial), "final_metrics": _quality(gold, final),
|
|
240
|
+
"candidate_reported_serial_stage_latency_ms": {
|
|
241
|
+
"p50": _percentile(stage_totals, .5), "p95": _percentile(stage_totals, .95),
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
if raw_run_ref is not None:
|
|
245
|
+
raw_manifest, raw_captures, _ = _verified_run(raw_run_ref, pack)
|
|
246
|
+
for key in ("execution", "runner"):
|
|
247
|
+
if raw_manifest[key] != manifest[key]:
|
|
248
|
+
raise InvocationError(f"independent raw run differs in {key}")
|
|
249
|
+
raw_actions = []
|
|
250
|
+
for capture in raw_captures:
|
|
251
|
+
events = _events(capture)
|
|
252
|
+
first = _initial(capture, events)
|
|
253
|
+
if len(events) != 1 or capture["action"] != first:
|
|
254
|
+
raise InvocationError("independent raw run must contain exactly one Jev stage")
|
|
255
|
+
raw_actions.append(first)
|
|
256
|
+
raw_correct = sum(a == g for a, g in zip(raw_actions, gold))
|
|
257
|
+
initial_correct = sum(a == g for a, g in zip(initial, gold))
|
|
258
|
+
result["independent_raw_run"] = {
|
|
259
|
+
"run_id": raw_manifest["run_id"], "candidate": raw_manifest["candidate"],
|
|
260
|
+
"metrics": _quality(gold, raw_actions),
|
|
261
|
+
"initial_decision_difference_count": sum(a != b for a, b in zip(raw_actions, initial)),
|
|
262
|
+
"sampling_correct_count_delta": initial_correct - raw_correct,
|
|
263
|
+
"final_correct_count_delta_vs_independent_raw": initial_correct + corrected - regressed - raw_correct,
|
|
264
|
+
"scope": "independent_jev_samples_same_questions_does_not_isolate_reviewer_effect",
|
|
265
|
+
}
|
|
266
|
+
return ReviewerAudit(result)
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""Frozen routing rules and provider request builders, ported from verified sources.
|
|
2
|
+
|
|
3
|
+
No source checkout is imported at runtime. Binary counterfactual cascades retain
|
|
4
|
+
binary semantics and are not promoted to three-way invocation decisions.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from types import MappingProxyType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SOURCE_DIGESTS = MappingProxyType({'rule': 'sha256:7436e619e247905df92b74ef921d61171aec146c9ae6907fe73231d1d5202660', 'questions': 'sha256:116c6958e9a4558a2209c9c708702915389d7f751e9c15227efa02e63d73eb86', 'jev': 'sha256:1b56234bc39b8052e64e32c97750152760461f2ec2eff4e1abf1e0de34570200', 'typed': 'sha256:3d56b6c154f056c2c2c5afb9934988bfc335bdd5621cfe79a0483bb97e673964', 'narrow': 'sha256:ecc78a7b685ebde8f93953813447f04d532e02e6415192ba38c1a142d12dbb20', 'hybrid': 'sha256:d14af4ae252b8aa9d645a4cbdc0b6ea0ca7861cfb24b432718954aacb375fbf2', 'reviewer': 'sha256:102876c0ce907572d9dbc42e11258684b46108da8bebff1f6ff55d67bdedb4ee', 'cascade': 'sha256:a1cd3170e59f448abf5db157b9c012317c8398a3fbd22a02b2121d22be2bfce2'})
|
|
16
|
+
|
|
17
|
+
ORDER = ("call_rag", "clarify_first", "do_not_call_rag")
|
|
18
|
+
ACTION = {"call_rag": "CALL_RAG", "clarify_first": "ASK_BUYER", "do_not_call_rag": "ANSWER_FROM_CONTEXT"}
|
|
19
|
+
MODEL = "gpt-6-sol"
|
|
20
|
+
JEV_MODEL = "jev-1.13.0"
|
|
21
|
+
|
|
22
|
+
COMPANIES = {'tipalti': 'Tipalti', 'vanta': 'Vanta', 'monday': 'Monday.com', 'notion': 'Notion', 'figma': 'Figma'}
|
|
23
|
+
|
|
24
|
+
_CLARIFY_PATTERN_TEXT = ('\\bwhich one\\b', '\\bdoes this apply\\b', '\\bwhen is it due\\b', '\\bwhy was it rejected\\b', '\\bcan they approve\\b', '\\bwhat changed\\b', '\\bhow do i fix it\\b', '\\bsame way as before\\b', '\\bis this allowed\\b', '\\bwhich policy\\b', '\\bcheck the exception\\b', '\\bgive me the steps\\b', '\\bwhat is the limit for it\\b')
|
|
25
|
+
|
|
26
|
+
_NO_CALL_PATTERN_TEXT = ('\\brewrite\\b', '\\bsummarize\\b', '\\bcalculate\\b', '\\bformat\\b', '\\bextract\\b', '\\btranslate\\b', '\\bdraft\\b', '\\bcompare\\b', '\\bbrainstorm\\b', '\\bthanks\\b', '\\bwhat did you just complete\\b', '\\bmultiplied by\\b', '\\bturn that json\\b')
|
|
27
|
+
|
|
28
|
+
_CLARIFY_PATTERNS = tuple(re.compile(p, re.IGNORECASE) for p in _CLARIFY_PATTERN_TEXT)
|
|
29
|
+
_NO_CALL_PATTERNS = tuple(re.compile(p, re.IGNORECASE) for p in _NO_CALL_PATTERN_TEXT)
|
|
30
|
+
|
|
31
|
+
def simple_rule_action(latest_user_message: str) -> str:
|
|
32
|
+
"""Execute the exact frozen T-022 keyword/context rule."""
|
|
33
|
+
|
|
34
|
+
if any(pattern.search(latest_user_message) for pattern in _CLARIFY_PATTERNS):
|
|
35
|
+
return "clarify_first"
|
|
36
|
+
if any(pattern.search(latest_user_message) for pattern in _NO_CALL_PATTERNS):
|
|
37
|
+
return "do_not_call_rag"
|
|
38
|
+
return "call_rag"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def jev_080_action(direct: float, clarify: float) -> str:
|
|
42
|
+
"""Apply the frozen inclusive 0.80 high-confidence non-call policy."""
|
|
43
|
+
|
|
44
|
+
_validate_score(direct, "direct")
|
|
45
|
+
_validate_score(clarify, "clarify")
|
|
46
|
+
if max(direct, clarify) < 0.8:
|
|
47
|
+
return "call_rag"
|
|
48
|
+
return "do_not_call_rag" if direct >= clarify else "clarify_first"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def t023_asymmetric_action(direct: float, clarify: float) -> str:
|
|
52
|
+
"""Apply the exact T-023 arbitration, including simultaneous-boundary behavior."""
|
|
53
|
+
|
|
54
|
+
_validate_score(direct, "direct")
|
|
55
|
+
_validate_score(clarify, "clarify")
|
|
56
|
+
if direct >= 0.65 and (clarify < 0.75 or direct >= clarify):
|
|
57
|
+
return "do_not_call_rag"
|
|
58
|
+
if clarify >= 0.75:
|
|
59
|
+
return "clarify_first"
|
|
60
|
+
return "call_rag"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _validate_score(value: float, name: str) -> None:
|
|
64
|
+
if isinstance(value, bool) or not isinstance(value, float | int):
|
|
65
|
+
raise ValueError(f"{name} score must be numeric")
|
|
66
|
+
if not math.isfinite(value) or not 0 <= value <= 1:
|
|
67
|
+
raise ValueError(f"{name} score must be finite and in [0, 1]")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
TYPED_POLICY = "You are a decision-only router for an enterprise agent's next response.\nReturn one route; do not answer the user's question. A company knowledge base is\navailable, but no company documents or product facts are preloaded here.\n\nPriority order:\n1. Resolve references using the prior conversation. Do not call a detail\n missing if the user already supplied it.\n2. ASK_BUYER when a specific, buyer-owned detail is absent and different\n values of that detail would materially change which answer is useful. A\n product document cannot reveal the buyer's requirements, environment,\n intended use, or acceptance threshold. Ask one focused question naming the\n missing detail. This takes precedence over a lookup that would merely give\n generic product facts while leaving the user's actual question unanswered.\n3. Otherwise CALL_RAG when answering requires an unverified fact about the\n company's products, integrations, capabilities, limitations, pricing, or\n competitors. Do not answer such facts from model memory. Name the fact and\n give a targeted search query.\n4. Otherwise ANSWER_FROM_CONTEXT when the request is conversational or the\n information needed is already present in the conversation.\n\nDo not ask the buyer for details that are optional to answer a concrete product\nquestion. When both buyer context and a product fact are missing, clarify first\nonly if the buyer detail changes the search target or answer; otherwise retrieve.\nBe concise. Use empty strings for fields that do not apply to the chosen route."
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
ROUTE_SCHEMA = {'type': 'object', 'properties': {'route': {'type': 'string', 'enum': ['CALL_RAG', 'ASK_BUYER', 'ANSWER_FROM_CONTEXT']}, 'missing_buyer_detail': {'type': 'string'}, 'needed_product_fact': {'type': 'string'}, 'rag_query': {'type': 'string'}, 'clarifying_question': {'type': 'string'}, 'brief_reason': {'type': 'string'}}, 'required': ['route', 'missing_buyer_detail', 'needed_product_fact', 'rag_query', 'clarifying_question', 'brief_reason'], 'additionalProperties': False}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
NARROW_POLICY = "You are a decision-only router for an enterprise agent's next response.\nReturn one route; do not answer the user's question. A company knowledge base is\navailable, but no company documents or product facts are preloaded here.\n\nPriority order:\n1. Resolve references using the prior conversation. Do not call a detail\n missing if the user already supplied it.\n2. CALL_RAG when a named company product, capability, integration, procedure,\n limitation, price, or competitor gives a useful factual lookup target, even\n if the buyer has not finalized their own configuration. Retrieve the\n general evidence first; the agent may ask a follow-up afterward. Do not\n answer unverified product facts from model memory. Name the fact and give\n a targeted search query.\n3. ASK_BUYER when the missing buyer-owned detail is necessary to identify any\n useful retrieval target, or the user requests a buyer-specific choice or\n judgment that cannot be made from general product evidence alone. Ask one\n focused question naming that detail. Do not call RAG solely to collect\n generic facts that cannot advance the actual request.\n4. Otherwise ANSWER_FROM_CONTEXT when the request is conversational or the\n information needed is already present in the conversation.\n\nDo not ask the buyer for details that are optional to answer a concrete product\nquestion. When both buyer context and a product fact are missing, retrieve if\nthe named product fact could advance the request without that context; otherwise\nclarify. Be concise. Use empty strings for fields that do not apply to the\nchosen route."
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
HYBRID_POLICY = "You are the first-decision router for an enterprise agent. Select exactly one route, and do not answer the user. A company knowledge base is configured, but no company documents or product facts are preloaded.\n\nDecide what information the user is asking for, not merely whether the conversation mentions an unfinished buyer setup.\n1. ANSWER_FROM_CONTEXT if prior conversation already supplies the requested fact, or the next response needs no company lookup. Resolve references in the conversation before deciding.\n2. ASK_BUYER if the requested fact is a buyer-owned choice, approval, requirement, or event that the buyer has not stated. Product documentation cannot reveal which option this particular buyer selected. Ask one focused question. Also clarify if a missing buyer detail makes every plausible lookup target useless.\n3. CALL_RAG if the user asks for a company-product fact or documented procedure that is not in the conversation. A named product, feature, integration, limitation, or procedure is a sufficient retrieval target. Incomplete buyer configuration does not block a useful general lookup; retrieve now and clarify later if the answer still needs buyer context. Do not invent product facts from model memory.\n\nWhen product facts and buyer context are both missing, choose CALL_RAG if the named documented fact or procedure would advance the user's actual request; choose ASK_BUYER when the user is asking for their own unstated decision. Give a targeted search query only for CALL_RAG. Use empty strings for nonapplicable fields."
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
REVIEWER_POLICY = "You are a safety reviewer of an enterprise agent's proposed decision NOT to search its company knowledge base. Choose one action. Do not answer the user.\n\nREQUIRE_RAG when the user's requested deliverable includes a concrete, company-specific product fact, documented procedure, API operation, setup step, capability, limitation, or integration fact that can usefully be looked up. A buyer's unfinished configuration does not block retrieving general documentation when the user asks how the named feature works or is set up.\n\nKEEP_NONCALL when the user asks whether a product will work for their particular needs but has not provided the requirements, environment, or acceptance criteria necessary to judge suitability. Merely finding general feature information would not settle that buyer-specific question; the next action should clarify. Also keep non-call if the requested fact is already supplied in the conversation or the request needs no company-specific evidence.\n\nDecide from what the user actually asks to know, not simply whether a feature is named or a buyer detail is missing. A general how/what/where question about a documented operation differs from a fit-for-us judgment. If a useful company lookup is required to answer the stated request, require RAG; otherwise preserve the proposed non-call."
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
REVIEWER_SCHEMA = {'type': 'object', 'properties': {'route': {'type': 'string', 'enum': ['REQUIRE_RAG', 'KEEP_NONCALL']}, 'brief_reason': {'type': 'string'}}, 'required': ['route', 'brief_reason'], 'additionalProperties': False}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _jev_questions() -> dict[str, Any]:
|
|
89
|
+
return {
|
|
90
|
+
"invocation_action": {
|
|
91
|
+
"type": "choice",
|
|
92
|
+
"instructions": (
|
|
93
|
+
"Before retrieving anything, choose the enterprise agent's best next action. "
|
|
94
|
+
"Judge whether private workspace knowledge is needed, whether the supplied "
|
|
95
|
+
"conversation is already sufficient, or whether missing information prevents a "
|
|
96
|
+
"useful retrieval query. A knowledge-seeking request may still require retrieval "
|
|
97
|
+
"even when the knowledge base could ultimately lack the answer."
|
|
98
|
+
),
|
|
99
|
+
"criteria": {
|
|
100
|
+
"call_rag": (
|
|
101
|
+
"Retrieval from the private workspace knowledge base is needed before "
|
|
102
|
+
"a useful, "
|
|
103
|
+
"grounded answer or action can be produced."
|
|
104
|
+
),
|
|
105
|
+
"do_not_call_rag": (
|
|
106
|
+
"The request can be handled from supplied conversation, general reasoning, "
|
|
107
|
+
"calculation, transformation, or ordinary conversation without private "
|
|
108
|
+
"retrieval."
|
|
109
|
+
),
|
|
110
|
+
"clarify_first": (
|
|
111
|
+
"A key entity, referent, scope, role, record, process, or constraint is "
|
|
112
|
+
"missing or "
|
|
113
|
+
"ambiguous, so the agent should ask a clarifying question before retrieval."
|
|
114
|
+
),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
"primary_basis": {
|
|
118
|
+
"type": "choice",
|
|
119
|
+
"instructions": "Choose the primary basis for the invocation decision.",
|
|
120
|
+
"criteria": {
|
|
121
|
+
"internal_knowledge_required": (
|
|
122
|
+
"The requested answer or action depends on facts, policies, procedures, "
|
|
123
|
+
"records, "
|
|
124
|
+
"or state from the private workspace knowledge base."
|
|
125
|
+
),
|
|
126
|
+
"provided_context_sufficient": (
|
|
127
|
+
"The user or conversation already supplies the facts needed for the requested "
|
|
128
|
+
"answer, extraction, calculation, or transformation."
|
|
129
|
+
),
|
|
130
|
+
"general_or_transformative": (
|
|
131
|
+
"The request is conversational or can be completed with general reasoning or "
|
|
132
|
+
"language transformation and no private facts."
|
|
133
|
+
),
|
|
134
|
+
"underspecified_or_ambiguous": (
|
|
135
|
+
"The request lacks enough identifying or scoping information to form a useful "
|
|
136
|
+
"private-knowledge retrieval query."
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _typed_body(case: dict) -> bytes:
|
|
144
|
+
company = COMPANIES[case["company_id"]]
|
|
145
|
+
context = (
|
|
146
|
+
f"Company: {company}. The company's RAG fact source is configured, "
|
|
147
|
+
"but no company facts are preloaded. No shared screen is supplied."
|
|
148
|
+
)
|
|
149
|
+
messages = [{
|
|
150
|
+
"role": "developer",
|
|
151
|
+
"content": [{"type": "input_text", "text": TYPED_POLICY + "\n\n" + context}],
|
|
152
|
+
}]
|
|
153
|
+
for prior in case["conversation_context"]:
|
|
154
|
+
content_type = "output_text" if prior["role"] == "assistant" else "input_text"
|
|
155
|
+
messages.append({
|
|
156
|
+
"role": prior["role"],
|
|
157
|
+
"content": [{"type": content_type, "text": prior["content"]}],
|
|
158
|
+
})
|
|
159
|
+
messages.append({
|
|
160
|
+
"role": "user",
|
|
161
|
+
"content": [{"type": "input_text", "text": case["user_turn"]}],
|
|
162
|
+
})
|
|
163
|
+
body = {
|
|
164
|
+
"model": MODEL,
|
|
165
|
+
"input": messages,
|
|
166
|
+
"text": {
|
|
167
|
+
"format": {
|
|
168
|
+
"type": "json_schema",
|
|
169
|
+
"name": "sphynx_rag_invocation_route",
|
|
170
|
+
"strict": True,
|
|
171
|
+
"schema": ROUTE_SCHEMA,
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"reasoning": {"effort": "none"},
|
|
175
|
+
"service_tier": "fast",
|
|
176
|
+
"max_output_tokens": 800,
|
|
177
|
+
"store": False,
|
|
178
|
+
}
|
|
179
|
+
return json.dumps(body, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def jev_body(request: dict) -> bytes:
|
|
183
|
+
body = {"model": JEV_MODEL, "state": {
|
|
184
|
+
"assistant_context": f"An enterprise agent can optionally retrieve from the frozen {request['company_id']} company knowledge base.",
|
|
185
|
+
"recent_conversation": request["conversation_context"],
|
|
186
|
+
"latest_user_message": request["user_turn"],
|
|
187
|
+
}, "questions": _jev_questions()}
|
|
188
|
+
return json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False).encode("utf-8")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def route_body(request: dict, policy: str = "typed") -> bytes:
|
|
192
|
+
body = json.loads(_typed_body(request))
|
|
193
|
+
policies = {"typed": TYPED_POLICY, "narrow": NARROW_POLICY, "hybrid": HYBRID_POLICY, "reviewer": REVIEWER_POLICY}
|
|
194
|
+
original = body["input"][0]["content"][0]["text"]
|
|
195
|
+
body["input"][0]["content"][0]["text"] = policies[policy] + original[len(TYPED_POLICY):]
|
|
196
|
+
if policy == "reviewer":
|
|
197
|
+
body["text"]["format"]["name"] = "sphynx_jev_noncall_safety_review"
|
|
198
|
+
body["text"]["format"]["schema"] = REVIEWER_SCHEMA
|
|
199
|
+
body["max_output_tokens"] = 300
|
|
200
|
+
return json.dumps(body, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def jev_legacy(probs: dict[str, float]) -> bool:
|
|
204
|
+
"""Existing Jev 0.80 binary policy, not a newly fitted threshold."""
|
|
205
|
+
return max(probs["do_not_call_rag"], probs["clarify_first"]) < 0.8
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def jev_confident(probs: dict[str, float]) -> bool | None:
|
|
209
|
+
"""First gate exits only when one three-way action reaches 0.80."""
|
|
210
|
+
action = max(probs, key=probs.get)
|
|
211
|
+
if probs[action] < 0.8:
|
|
212
|
+
return None
|
|
213
|
+
return action == "call_rag"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def jev_confident_clarification(probs: dict[str, float]) -> bool:
|
|
217
|
+
return probs["clarify_first"] >= 0.8 and probs["clarify_first"] == max(probs.values())
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def binary_cascade(name: str, full: bool | None, cheap: bool | None, probs: dict[str, float]) -> tuple[bool | None, tuple[str, ...]]:
|
|
221
|
+
if name == "incumbent":
|
|
222
|
+
return full, ("full",)
|
|
223
|
+
if name == "jev_080":
|
|
224
|
+
return jev_legacy(probs), ("jev",)
|
|
225
|
+
if name == "gpt6sol_only":
|
|
226
|
+
return cheap, ("cheap",)
|
|
227
|
+
if name == "jev_first":
|
|
228
|
+
stages = ["jev"]
|
|
229
|
+
confident = jev_confident(probs)
|
|
230
|
+
if confident is not None:
|
|
231
|
+
return confident, tuple(stages)
|
|
232
|
+
stages.append("cheap")
|
|
233
|
+
if cheap is not None:
|
|
234
|
+
return cheap, tuple(stages)
|
|
235
|
+
stages.append("full")
|
|
236
|
+
return (full if full is not None else True), tuple(stages)
|
|
237
|
+
if name == "cheap_first":
|
|
238
|
+
stages = ["cheap"]
|
|
239
|
+
if cheap is False:
|
|
240
|
+
return False, tuple(stages)
|
|
241
|
+
stages.append("jev")
|
|
242
|
+
if cheap is None:
|
|
243
|
+
confident = jev_confident(probs)
|
|
244
|
+
if confident is not None:
|
|
245
|
+
return confident, tuple(stages)
|
|
246
|
+
stages.append("full")
|
|
247
|
+
return (full if full is not None else jev_legacy(probs)), tuple(stages)
|
|
248
|
+
if jev_legacy(probs):
|
|
249
|
+
return True, tuple(stages)
|
|
250
|
+
stages.append("full")
|
|
251
|
+
return (full if full is not None else True), tuple(stages)
|
|
252
|
+
if name == "consensus_escalate":
|
|
253
|
+
stages = ["cheap", "jev"]
|
|
254
|
+
j = jev_legacy(probs)
|
|
255
|
+
if cheap is None:
|
|
256
|
+
return j, tuple(stages)
|
|
257
|
+
if cheap == j:
|
|
258
|
+
return j, tuple(stages)
|
|
259
|
+
stages.append("full")
|
|
260
|
+
return (full if full is not None else True), tuple(stages)
|
|
261
|
+
if name == "either_calls":
|
|
262
|
+
stages = ("cheap", "jev")
|
|
263
|
+
return (jev_legacy(probs) if cheap is None else cheap or jev_legacy(probs)), stages
|
|
264
|
+
if name == "jev_clarify_only_posthoc":
|
|
265
|
+
stages = ["jev"]
|
|
266
|
+
if jev_confident_clarification(probs):
|
|
267
|
+
return False, tuple(stages)
|
|
268
|
+
stages.append("cheap")
|
|
269
|
+
if cheap is not None:
|
|
270
|
+
return cheap, tuple(stages)
|
|
271
|
+
stages.append("full")
|
|
272
|
+
return (full if full is not None else jev_legacy(probs)), tuple(stages)
|
|
273
|
+
if name == "cheap_jev_clarify_posthoc":
|
|
274
|
+
stages = ["cheap"]
|
|
275
|
+
if cheap is not None:
|
|
276
|
+
if not cheap:
|
|
277
|
+
return False, tuple(stages)
|
|
278
|
+
stages.append("jev")
|
|
279
|
+
return (not jev_confident_clarification(probs)), tuple(stages)
|
|
280
|
+
stages.append("jev")
|
|
281
|
+
if jev_confident_clarification(probs):
|
|
282
|
+
return False, tuple(stages)
|
|
283
|
+
stages.append("full")
|
|
284
|
+
return (full if full is not None else jev_legacy(probs)), tuple(stages)
|
|
285
|
+
raise ValueError(f"Unknown policy: {name}")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def jev_probabilities(response: dict) -> dict[str, float]:
|
|
289
|
+
if not isinstance(response, dict) or response.get("model") != JEV_MODEL:
|
|
290
|
+
raise ValueError("Jev model identity differs")
|
|
291
|
+
answers = response.get("answers")
|
|
292
|
+
if not isinstance(answers, dict) or not isinstance(answers.get("invocation_action"), dict):
|
|
293
|
+
raise ValueError("Jev answer is invalid")
|
|
294
|
+
_extract_jev_scores(response)
|
|
295
|
+
answer = answers["invocation_action"]
|
|
296
|
+
p = answer.get("probabilities")
|
|
297
|
+
if (answer.get("type") != "choice" or not isinstance(p, dict) or set(p) != set(ORDER)
|
|
298
|
+
or any(type(v) not in (int, float) or not math.isfinite(v) or not 0 <= v <= 1 for v in p.values())
|
|
299
|
+
or abs(sum(p.values()) - 1) > 0.031):
|
|
300
|
+
raise ValueError("Jev probabilities are invalid")
|
|
301
|
+
return dict(p)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def route_result(response: dict, *, reviewer: bool = False) -> dict:
|
|
305
|
+
if not isinstance(response, dict) or response.get("model") != MODEL or response.get("status") != "completed":
|
|
306
|
+
raise ValueError("router model or completion identity differs")
|
|
307
|
+
texts = [c.get("text", "") for o in response.get("output", []) if isinstance(o, dict) and o.get("type") == "message"
|
|
308
|
+
for c in o.get("content", []) if isinstance(c, dict) and c.get("type") == "output_text"]
|
|
309
|
+
def unique(pairs):
|
|
310
|
+
d = {}
|
|
311
|
+
for k, v in pairs:
|
|
312
|
+
if k in d: raise ValueError("duplicate router field")
|
|
313
|
+
d[k] = v
|
|
314
|
+
return d
|
|
315
|
+
value = json.loads("".join(texts), object_pairs_hook=unique)
|
|
316
|
+
schema = REVIEWER_SCHEMA if reviewer else ROUTE_SCHEMA
|
|
317
|
+
if not isinstance(value, dict) or set(value) != set(schema["properties"]) or not all(isinstance(v, str) for v in value.values()):
|
|
318
|
+
raise ValueError("router schema differs")
|
|
319
|
+
if value["route"] not in schema["properties"]["route"]["enum"]:
|
|
320
|
+
raise ValueError("router route differs")
|
|
321
|
+
if not reviewer:
|
|
322
|
+
required = {"ASK_BUYER": ("missing_buyer_detail", "clarifying_question"), "CALL_RAG": ("needed_product_fact", "rag_query")}.get(value["route"], ())
|
|
323
|
+
if any(not value[k].strip() for k in required): raise ValueError("router required route details absent")
|
|
324
|
+
return value
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
JEV_BASIS_OPTIONS = ('internal_knowledge_required', 'provided_context_sufficient', 'general_or_transformative', 'underspecified_or_ambiguous')
|
|
328
|
+
ACTIONS = ORDER
|
|
329
|
+
|
|
330
|
+
def _extract_jev_scores(response: Any | None) -> tuple[tuple[float, float], str | None]:
|
|
331
|
+
if not isinstance(response, Mapping):
|
|
332
|
+
raise ValueError("Jev response must be an object")
|
|
333
|
+
if response.get("model") != JEV_MODEL:
|
|
334
|
+
raise ValueError("Jev response model mismatch")
|
|
335
|
+
answers = response.get("answers")
|
|
336
|
+
if not isinstance(answers, Mapping):
|
|
337
|
+
raise ValueError("Jev answers must be an object")
|
|
338
|
+
action = answers.get("invocation_action")
|
|
339
|
+
if (
|
|
340
|
+
not isinstance(action, Mapping)
|
|
341
|
+
or set(action) != {"type", "choice", "confidence", "probabilities"}
|
|
342
|
+
or action.get("type") != "choice"
|
|
343
|
+
or action.get("choice") not in ACTIONS
|
|
344
|
+
):
|
|
345
|
+
raise ValueError("Jev invocation_action must be a choice")
|
|
346
|
+
_validate_score(cast("float", action.get("confidence")), "invocation_action confidence")
|
|
347
|
+
probabilities = action.get("probabilities")
|
|
348
|
+
if not isinstance(probabilities, Mapping) or set(probabilities) != set(ACTIONS):
|
|
349
|
+
raise ValueError("Jev action probabilities have the wrong identity set")
|
|
350
|
+
values = {name: cast("float", probabilities[name]) for name in ACTIONS}
|
|
351
|
+
for name, value in values.items():
|
|
352
|
+
_validate_score(value, name)
|
|
353
|
+
if abs(sum(values.values()) - 1) > 0.031:
|
|
354
|
+
raise ValueError("Jev action probabilities do not sum to one")
|
|
355
|
+
basis = answers.get("primary_basis")
|
|
356
|
+
if (
|
|
357
|
+
not isinstance(basis, Mapping)
|
|
358
|
+
or set(basis) != {"type", "choice", "confidence", "probabilities"}
|
|
359
|
+
or basis.get("type") != "choice"
|
|
360
|
+
or basis.get("choice") not in JEV_BASIS_OPTIONS
|
|
361
|
+
):
|
|
362
|
+
raise ValueError("Jev primary_basis must be a registered choice")
|
|
363
|
+
_validate_score(cast("float", basis.get("confidence")), "primary_basis confidence")
|
|
364
|
+
basis_probabilities = basis.get("probabilities")
|
|
365
|
+
if not isinstance(basis_probabilities, Mapping) or set(basis_probabilities) != set(
|
|
366
|
+
JEV_BASIS_OPTIONS
|
|
367
|
+
):
|
|
368
|
+
raise ValueError("Jev basis probabilities have the wrong identity set")
|
|
369
|
+
basis_values = {name: cast("float", basis_probabilities[name]) for name in JEV_BASIS_OPTIONS}
|
|
370
|
+
for basis_name, value in basis_values.items():
|
|
371
|
+
_validate_score(value, basis_name)
|
|
372
|
+
if abs(sum(basis_values.values()) - 1) > 0.031:
|
|
373
|
+
raise ValueError("Jev basis probabilities do not sum to one")
|
|
374
|
+
basis_value = cast("str", basis["choice"])
|
|
375
|
+
return (values["do_not_call_rag"], values["clarify_first"]), basis_value
|