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/fast_search.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Fast Search HTTP smoke adapter.
|
|
2
|
+
|
|
3
|
+
Ordinary search responses contain ranked text but no stable passage identity or
|
|
4
|
+
proof of corpus/label alignment. This adapter deliberately cannot produce a
|
|
5
|
+
``rag.rerank.v1`` quality score or comparison group.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import time
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.request
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Callable, Mapping
|
|
18
|
+
from urllib.parse import quote, urlsplit
|
|
19
|
+
|
|
20
|
+
from .contracts import ContractError, _id, _text, canonical_json
|
|
21
|
+
from .http_transport import open_no_redirect
|
|
22
|
+
|
|
23
|
+
FAST_SEARCH_TRACK = "smoke_latency"
|
|
24
|
+
MAX_RESPONSE_BYTES = 4 * 1024 * 1024
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class FastSearchConfig:
|
|
29
|
+
"""Explicit endpoint and KB identity for one smoke request."""
|
|
30
|
+
|
|
31
|
+
base_url: str
|
|
32
|
+
org: str
|
|
33
|
+
kb: str
|
|
34
|
+
top_k: int = 5
|
|
35
|
+
timeout_seconds: float = 30.0
|
|
36
|
+
source_id: str | None = None
|
|
37
|
+
domain: str | None = None
|
|
38
|
+
|
|
39
|
+
def __post_init__(self) -> None:
|
|
40
|
+
parsed = urlsplit(self.base_url)
|
|
41
|
+
if (
|
|
42
|
+
parsed.scheme != "https"
|
|
43
|
+
or not parsed.hostname
|
|
44
|
+
or parsed.username is not None
|
|
45
|
+
or parsed.password is not None
|
|
46
|
+
or parsed.query
|
|
47
|
+
or parsed.fragment
|
|
48
|
+
):
|
|
49
|
+
raise ContractError("base_url must be an HTTPS origin or path without credentials, query, or fragment")
|
|
50
|
+
_id(self.org, "org")
|
|
51
|
+
_id(self.kb, "kb")
|
|
52
|
+
if type(self.top_k) is not int or not 1 <= self.top_k <= 1000:
|
|
53
|
+
raise ContractError("top_k must be an integer from 1 through 1000")
|
|
54
|
+
if type(self.timeout_seconds) not in (int, float) or not math.isfinite(self.timeout_seconds) or not 0 < self.timeout_seconds <= 120:
|
|
55
|
+
raise ContractError("timeout_seconds must be finite, positive, and at most 120")
|
|
56
|
+
if self.source_id is not None:
|
|
57
|
+
_text(self.source_id, "source_id")
|
|
58
|
+
if self.domain is not None:
|
|
59
|
+
_text(self.domain, "domain")
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def search_url(self) -> str:
|
|
63
|
+
return (
|
|
64
|
+
f"{self.base_url.rstrip('/')}/{quote(self.org, safe='')}/kbs/"
|
|
65
|
+
f"{quote(self.kb, safe='')}/search"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class TransportResponse:
|
|
71
|
+
status_code: int
|
|
72
|
+
body: bytes
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
if type(self.status_code) is not int or not 100 <= self.status_code <= 599:
|
|
76
|
+
raise ContractError("transport status_code must be a valid HTTP status")
|
|
77
|
+
if not isinstance(self.body, bytes):
|
|
78
|
+
raise ContractError("transport body must be bytes")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
Transport = Callable[[str, bytes, Mapping[str, str], float], TransportResponse]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class FastSearchHit:
|
|
86
|
+
"""A hit in server-supplied order, without an invented passage ID."""
|
|
87
|
+
|
|
88
|
+
rank: int
|
|
89
|
+
score: float
|
|
90
|
+
title: str
|
|
91
|
+
section_path: str
|
|
92
|
+
url: str
|
|
93
|
+
text: str
|
|
94
|
+
chunk_index: int
|
|
95
|
+
total_chunks: int
|
|
96
|
+
|
|
97
|
+
def to_dict(self) -> dict[str, object]:
|
|
98
|
+
return {
|
|
99
|
+
"rank": self.rank,
|
|
100
|
+
"score": self.score,
|
|
101
|
+
"title": self.title,
|
|
102
|
+
"section_path": self.section_path,
|
|
103
|
+
"url": self.url,
|
|
104
|
+
"text": self.text,
|
|
105
|
+
"chunk_index": self.chunk_index,
|
|
106
|
+
"total_chunks": self.total_chunks,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class FastSearchSmokeResult:
|
|
112
|
+
"""Explicitly ineligible for labeled frozen-pool quality comparisons."""
|
|
113
|
+
|
|
114
|
+
org: str
|
|
115
|
+
kb: str
|
|
116
|
+
query: str
|
|
117
|
+
top_k: int
|
|
118
|
+
status: str
|
|
119
|
+
http_status: int | None
|
|
120
|
+
latency_ms: float
|
|
121
|
+
hits: tuple[FastSearchHit, ...]
|
|
122
|
+
response_sha256: str | None
|
|
123
|
+
response_size_bytes: int | None
|
|
124
|
+
error: str | None
|
|
125
|
+
reported_latency_ms: float | None = None
|
|
126
|
+
track: str = field(default=FAST_SEARCH_TRACK, init=False)
|
|
127
|
+
trust_tier: str = field(default="local_unverified", init=False)
|
|
128
|
+
identity_capability: str = field(default="stable_source_and_chunk_ids_unavailable", init=False)
|
|
129
|
+
corpus_alignment: str = field(default="unverified", init=False)
|
|
130
|
+
quality_eligible: bool = field(default=False, init=False)
|
|
131
|
+
comparison_group: None = field(default=None, init=False)
|
|
132
|
+
|
|
133
|
+
def to_dict(self) -> dict[str, object]:
|
|
134
|
+
return {
|
|
135
|
+
"track": self.track,
|
|
136
|
+
"trust_tier": self.trust_tier,
|
|
137
|
+
"org": self.org,
|
|
138
|
+
"kb": self.kb,
|
|
139
|
+
"query": self.query,
|
|
140
|
+
"top_k": self.top_k,
|
|
141
|
+
"status": self.status,
|
|
142
|
+
"http_status": self.http_status,
|
|
143
|
+
"latency_ms": self.latency_ms,
|
|
144
|
+
"reported_latency_ms": self.reported_latency_ms,
|
|
145
|
+
"hits": [hit.to_dict() for hit in self.hits],
|
|
146
|
+
"response_sha256": self.response_sha256,
|
|
147
|
+
"response_size_bytes": self.response_size_bytes,
|
|
148
|
+
"error": self.error,
|
|
149
|
+
"identity_capability": self.identity_capability,
|
|
150
|
+
"corpus_alignment": self.corpus_alignment,
|
|
151
|
+
"quality_eligible": self.quality_eligible,
|
|
152
|
+
"comparison_group": self.comparison_group,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _urllib_transport(url: str, body: bytes, headers: Mapping[str, str], timeout_seconds: float) -> TransportResponse:
|
|
157
|
+
request = urllib.request.Request(url, data=body, headers=dict(headers), method="POST")
|
|
158
|
+
try:
|
|
159
|
+
with open_no_redirect(request, timeout=timeout_seconds) as response:
|
|
160
|
+
return TransportResponse(response.status, response.read(MAX_RESPONSE_BYTES + 1))
|
|
161
|
+
except urllib.error.HTTPError as exc:
|
|
162
|
+
return TransportResponse(exc.code, exc.read(MAX_RESPONSE_BYTES + 1))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _require_string(value: object, field: str) -> str:
|
|
166
|
+
if not isinstance(value, str):
|
|
167
|
+
raise ContractError(f"{field} must be a string")
|
|
168
|
+
return value
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _require_int(value: object, field: str, minimum: int) -> int:
|
|
172
|
+
if type(value) is not int or value < minimum:
|
|
173
|
+
raise ContractError(f"{field} must be an integer >= {minimum}")
|
|
174
|
+
return value
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _parse_hits(body: bytes, config: FastSearchConfig, query: str) -> tuple[tuple[FastSearchHit, ...], float | None]:
|
|
178
|
+
try:
|
|
179
|
+
decoded = json.loads(
|
|
180
|
+
body.decode("utf-8"),
|
|
181
|
+
parse_constant=lambda constant: (_ for _ in ()).throw(ValueError(f"nonfinite JSON value {constant}")),
|
|
182
|
+
)
|
|
183
|
+
except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
|
|
184
|
+
raise ContractError(f"Fast Search returned invalid UTF-8 JSON: {exc}") from exc
|
|
185
|
+
if not isinstance(decoded, dict):
|
|
186
|
+
raise ContractError("Fast Search response must be a JSON object")
|
|
187
|
+
if decoded.get("org") != config.org or decoded.get("kb") != config.kb or decoded.get("query") != query:
|
|
188
|
+
raise ContractError("Fast Search response org, kb, or query does not match the request")
|
|
189
|
+
raw_hits = decoded.get("results")
|
|
190
|
+
if not isinstance(raw_hits, list):
|
|
191
|
+
raise ContractError("Fast Search response results must be an array")
|
|
192
|
+
hits: list[FastSearchHit] = []
|
|
193
|
+
for index, raw in enumerate(raw_hits):
|
|
194
|
+
if not isinstance(raw, dict):
|
|
195
|
+
raise ContractError(f"results[{index}] must be an object")
|
|
196
|
+
score = raw.get("score")
|
|
197
|
+
if type(score) not in (int, float) or not math.isfinite(score):
|
|
198
|
+
raise ContractError(f"results[{index}].score must be a finite number")
|
|
199
|
+
hits.append(
|
|
200
|
+
FastSearchHit(
|
|
201
|
+
rank=_require_int(raw.get("rank"), f"results[{index}].rank", 1),
|
|
202
|
+
score=float(score),
|
|
203
|
+
title=_require_string(raw.get("title"), f"results[{index}].title"),
|
|
204
|
+
section_path=_require_string(raw.get("section_path"), f"results[{index}].section_path"),
|
|
205
|
+
url=_require_string(raw.get("url"), f"results[{index}].url"),
|
|
206
|
+
text=_require_string(raw.get("text"), f"results[{index}].text"),
|
|
207
|
+
chunk_index=_require_int(raw.get("chunk_index"), f"results[{index}].chunk_index", 0),
|
|
208
|
+
total_chunks=_require_int(raw.get("total_chunks"), f"results[{index}].total_chunks", 1),
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
reported = decoded.get("latency_ms")
|
|
212
|
+
if reported is not None:
|
|
213
|
+
if type(reported) not in (int, float) or not math.isfinite(reported) or reported < 0:
|
|
214
|
+
raise ContractError("Fast Search reported latency_ms must be a finite nonnegative number")
|
|
215
|
+
reported = float(reported)
|
|
216
|
+
return tuple(hits), reported
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def search_fast_search(
|
|
220
|
+
config: FastSearchConfig,
|
|
221
|
+
query: str,
|
|
222
|
+
*,
|
|
223
|
+
transport: Transport | None = None,
|
|
224
|
+
headers: Mapping[str, str] | None = None,
|
|
225
|
+
clock_ns: Callable[[], int] = time.perf_counter_ns,
|
|
226
|
+
) -> FastSearchSmokeResult:
|
|
227
|
+
"""Call Fast Search once and retain smoke/latency evidence on every outcome.
|
|
228
|
+
|
|
229
|
+
``transport`` receives URL, UTF-8 JSON body, headers, and timeout; the
|
|
230
|
+
default transport uses the Python standard library. Caller-supplied headers
|
|
231
|
+
(including credentials) are sent but never placed in the result.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
if not isinstance(config, FastSearchConfig):
|
|
235
|
+
raise ContractError("config must be FastSearchConfig")
|
|
236
|
+
_text(query, "query")
|
|
237
|
+
payload: dict[str, object] = {"query": query, "top_k": config.top_k}
|
|
238
|
+
if config.source_id is not None:
|
|
239
|
+
payload["source_id"] = config.source_id
|
|
240
|
+
if config.domain is not None:
|
|
241
|
+
payload["domain"] = config.domain
|
|
242
|
+
request_headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
243
|
+
if headers:
|
|
244
|
+
request_headers.update(headers)
|
|
245
|
+
start = clock_ns()
|
|
246
|
+
try:
|
|
247
|
+
response = (transport or _urllib_transport)(
|
|
248
|
+
config.search_url,
|
|
249
|
+
canonical_json(payload).encode("utf-8"),
|
|
250
|
+
request_headers,
|
|
251
|
+
float(config.timeout_seconds),
|
|
252
|
+
)
|
|
253
|
+
if not isinstance(response, TransportResponse):
|
|
254
|
+
raise ContractError("transport must return TransportResponse")
|
|
255
|
+
except Exception as exc:
|
|
256
|
+
latency_ms = max(0.0, (clock_ns() - start) / 1_000_000)
|
|
257
|
+
return FastSearchSmokeResult(
|
|
258
|
+
config.org, config.kb, query, config.top_k,
|
|
259
|
+
"transport_error", None, latency_ms, (), None, None,
|
|
260
|
+
f"transport failed ({type(exc).__name__})",
|
|
261
|
+
)
|
|
262
|
+
latency_ms = max(0.0, (clock_ns() - start) / 1_000_000)
|
|
263
|
+
digest = "sha256:" + hashlib.sha256(response.body).hexdigest()
|
|
264
|
+
size = len(response.body)
|
|
265
|
+
common = (config.org, config.kb, query, config.top_k)
|
|
266
|
+
if size > MAX_RESPONSE_BYTES:
|
|
267
|
+
return FastSearchSmokeResult(
|
|
268
|
+
*common, "invalid_response", response.status_code, latency_ms, (), digest, size,
|
|
269
|
+
f"response exceeds {MAX_RESPONSE_BYTES} bytes",
|
|
270
|
+
)
|
|
271
|
+
if not 200 <= response.status_code < 300:
|
|
272
|
+
return FastSearchSmokeResult(
|
|
273
|
+
*common, "http_error", response.status_code, latency_ms, (), digest, size,
|
|
274
|
+
f"Fast Search returned HTTP {response.status_code}",
|
|
275
|
+
)
|
|
276
|
+
try:
|
|
277
|
+
hits, reported_latency_ms = _parse_hits(response.body, config, query)
|
|
278
|
+
except ContractError as exc:
|
|
279
|
+
return FastSearchSmokeResult(
|
|
280
|
+
*common, "invalid_response", response.status_code, latency_ms, (), digest, size, str(exc),
|
|
281
|
+
)
|
|
282
|
+
return FastSearchSmokeResult(
|
|
283
|
+
*common, "ok", response.status_code, latency_ms, hits, digest, size, None,
|
|
284
|
+
reported_latency_ms=reported_latency_ms,
|
|
285
|
+
)
|
sphynx/full_rag.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Source-parity full-RAG traces, retained grading and fixed-generator diagnostics.
|
|
2
|
+
|
|
3
|
+
Opaque subject traces and componentized diagnostic traces are separate scopes.
|
|
4
|
+
Reference actions, qrels, facts and semantic review are never candidate inputs.
|
|
5
|
+
Local adapters are trusted in-process Python; metadata is not runtime attestation.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from dataclasses import replace
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from time import perf_counter
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
from .contracts import ContractError
|
|
16
|
+
from .integration_contracts import CandidateConfiguration,ComparisonIdentity,TrackCaseCapture,value_digest,require_digest,require_comparable
|
|
17
|
+
from .dataset_v3 import load_dataset_v3
|
|
18
|
+
from ._track_capture import (strict_json,create_run,write_artifact,finish_run,read_artifact,
|
|
19
|
+
verify_track_run,write_evaluation,verify_track_evaluation,byte_digest)
|
|
20
|
+
from .chunking import _policy_for_source,_select_tasks,_verify_approval
|
|
21
|
+
from .pipeline import _detach,_require,_number
|
|
22
|
+
from ._full_rag_trace import Level3Trace,validate_level3_trace,CONTEXT_CONDITIONS
|
|
23
|
+
from ._full_rag_grading import ClaimVerdict,grade_level3,evaluate_counterfactuals,CounterfactualObservation
|
|
24
|
+
|
|
25
|
+
PACK_ID="rag.full_rag.v1"
|
|
26
|
+
INTERFACE="sphynx.rag.full_rag.v1"
|
|
27
|
+
METRIC_RELEASE="rag.full_rag.source-level3.metric.v1"
|
|
28
|
+
GRADER_RELEASE="rag.full_rag.source-level3.grader.v1"
|
|
29
|
+
POLICY_VERSION="sphynx.rag.full_rag.policy.v1"
|
|
30
|
+
JUDGMENTS_SCHEMA="sphynx.rag.full_rag.judgments.v1"
|
|
31
|
+
_SEMANTIC=("answer_required_fact_coverage","answer_completeness","exact_value_unit_date_accuracy","faithfulness","unsupported_claim_rate","citation_support")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_full_rag_request(task,policy,context_ids):
|
|
35
|
+
pool=[{key:row[key] for key in ("candidate_id","input","input_sha256","dense_rank","dense_score")}
|
|
36
|
+
for row in task["candidates"]]
|
|
37
|
+
by_id={row["candidate_id"]:row for row in pool}
|
|
38
|
+
_require(type(context_ids) in (tuple,list) and all(type(cid) is str for cid in context_ids)
|
|
39
|
+
and len(context_ids)==len(set(context_ids)) and set(context_ids)<=set(by_id),"full-RAG context inventory differs")
|
|
40
|
+
body={"schema_version":"sphynx.rag.full_rag.request.v1","case_id":task["task_id"],
|
|
41
|
+
"customer_id":task["customer_id"],"query":task["query"],"conversation_context":task["conversation_context"],
|
|
42
|
+
"candidates":pool,"context_condition":policy["context_condition"],"context_candidate_ids":list(context_ids),
|
|
43
|
+
"context_text":"\n\n".join(by_id[cid]["input"] for cid in context_ids),
|
|
44
|
+
"policy":{key:value for key,value in policy.items() if key!="contexts"}}
|
|
45
|
+
body["input_digest"]=value_digest(body)
|
|
46
|
+
return _detach(body)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_full_rag_response(response,request,*,expected_invocation=False):
|
|
50
|
+
fields={"schema_version","case_id","input_digest","trace"}
|
|
51
|
+
componentized=request["policy"]["execution_shape"]=="componentized"
|
|
52
|
+
if componentized:fields.add("stages")
|
|
53
|
+
_require(type(response) is dict and set(response)==fields,"full-RAG response fields differ")
|
|
54
|
+
_require(response["schema_version"]=="sphynx.rag.full_rag.response.v1" and response["case_id"]==request["case_id"]
|
|
55
|
+
and response["input_digest"]==request["input_digest"],"full-RAG response does not bind its request")
|
|
56
|
+
raw=response["trace"]
|
|
57
|
+
if componentized:
|
|
58
|
+
from ._componentized_full_rag import compose_full_rag
|
|
59
|
+
stages=response["stages"]
|
|
60
|
+
_require(type(stages) is dict and set(stages)=={"router","rewriter","generator"},"full-RAG component outputs differ")
|
|
61
|
+
derived=compose_full_rag(request,lambda kind,operation,payload:deepcopy(stages[kind]))
|
|
62
|
+
_require(_detach(derived)==response,"full-RAG trace differs from retained component outputs")
|
|
63
|
+
_require(type(raw) is dict and "expected_invocation" not in raw,"candidate trace cannot carry a reference action")
|
|
64
|
+
trace=deepcopy(raw)
|
|
65
|
+
_require(trace.get("task_id")==request["case_id"] and trace.get("as_uttered_query")==request["query"],"full-RAG trace request identity drift")
|
|
66
|
+
trace["expected_invocation"]=expected_invocation
|
|
67
|
+
unknown_latency=trace.get("latency_ms")=={}
|
|
68
|
+
if unknown_latency:trace["latency_ms"]={key:0.0 for key in ("routing","rewrite","retrieval","generation","total")}
|
|
69
|
+
else:_require(type(trace.get("latency_ms")) is dict and all(_number(v) and v>=0 for v in trace["latency_ms"].values()),"full-RAG latency values differ")
|
|
70
|
+
citations=trace.get("citations")
|
|
71
|
+
_require(type(citations) is list and all(type(row) is dict and type(row.get("start")) is int and type(row.get("end")) is int
|
|
72
|
+
and type(row.get("citation_id")) is str and row["citation_id"] and type(row.get("evidence_id")) is str and row["evidence_id"] for row in citations),"citation identities/offsets differ")
|
|
73
|
+
_require(len({row["citation_id"] for row in citations})==len(citations),"citation IDs must be unique")
|
|
74
|
+
parsed=validate_level3_trace(trace)
|
|
75
|
+
supplied=list(request["context_candidate_ids"])
|
|
76
|
+
_require(all(cid in supplied for cid in parsed.retrieved_candidate_ids)
|
|
77
|
+
and [cid for cid in supplied if cid in parsed.retrieved_candidate_ids]==list(parsed.retrieved_candidate_ids),
|
|
78
|
+
"trace retrieval is not an ordered subset of the supplied frozen contexts")
|
|
79
|
+
if request["policy"]["execution_shape"]=="componentized":
|
|
80
|
+
expected_context=tuple(supplied) if parsed.actual_rag_call_count else ()
|
|
81
|
+
_require(parsed.handed_context_ids==expected_context,"componentized context handoff differs from its fixed generator input")
|
|
82
|
+
if unknown_latency:parsed=replace(parsed,latency_ms={})
|
|
83
|
+
return parsed
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _policy(dataset,digest,candidate,*,context_condition,contexts,execution_shape,**options):
|
|
87
|
+
_require(context_condition in CONTEXT_CONDITIONS and execution_shape in ("componentized","opaque"),"full-RAG execution boundary differs")
|
|
88
|
+
generator=candidate.parameters.get("generator_digest");prompt=candidate.parameters.get("prompt_digest")
|
|
89
|
+
require_digest(generator,"generator_digest");require_digest(prompt,"prompt_digest")
|
|
90
|
+
policy=_policy_for_source(dataset,digest,**options)
|
|
91
|
+
policy.update(version=POLICY_VERSION,context_condition=context_condition,contexts=_detach(contexts),execution_shape=execution_shape,
|
|
92
|
+
generator_digest=generator,prompt_digest=prompt,execution_mode=candidate.execution_mode,
|
|
93
|
+
timing_authority="component_reported_unattested",semantic_review_policy="capture-bound-owner-judgments.v1")
|
|
94
|
+
return policy
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _identity(dataset,digest,tasks,policy):
|
|
98
|
+
fixed={k:v for k,v in policy.items() if k not in ("context_condition","contexts")}
|
|
99
|
+
semantic_refs=[{key:task[key] for key in ("task_id","should_call_rag","expected_behavior","evidence_groups")} for task in tasks]
|
|
100
|
+
condition="oracle" if policy["context_condition"]=="oracle" else "empty" if policy["context_condition"]=="empty" else "retrieved"
|
|
101
|
+
return ComparisonIdentity("rag_full_rag",PACK_ID,"v1","sable.rag.dataset.v3",digest,
|
|
102
|
+
value_digest([task["task_id"] for task in tasks]),value_digest(semantic_refs),value_digest(fixed),
|
|
103
|
+
METRIC_RELEASE,GRADER_RELEASE,policy["boundary_digest"] or value_digest({"boundary":policy["execution_shape"],"dataset":digest}),
|
|
104
|
+
context_condition=condition,generator_digest=policy["generator_digest"])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_full_rag(candidate,adapter,dataset_path,output_dir,*,expected_sha256,synthetic=False,
|
|
108
|
+
owner_authorized=False,pack_manifest_path=None,trials=1,attempts=1,case_ids=None,boundary_digest=None,
|
|
109
|
+
execution_shape="opaque",context_condition="dense",contexts=None):
|
|
110
|
+
_require(isinstance(candidate,CandidateConfiguration) and candidate.interface_version==INTERFACE,"full-RAG candidate interface differs")
|
|
111
|
+
_require(candidate.execution_mode in ("trusted_local","recorded"),"full-RAG mode requires an explicit local adapter or replay")
|
|
112
|
+
dataset=load_dataset_v3(dataset_path,expected_sha256=expected_sha256);tasks=_select_tasks(dataset,case_ids)
|
|
113
|
+
if contexts is None:
|
|
114
|
+
_require(context_condition in ("dense","empty"),"oracle/A0/challenger diagnostics need explicit frozen contexts")
|
|
115
|
+
contexts={task["task_id"]:[] if context_condition=="empty" else [row["candidate_id"] for row in sorted(task["candidates"],key=lambda row:row["dense_rank"])[:5]] for task in tasks}
|
|
116
|
+
_require(isinstance(contexts,Mapping) and set(contexts)=={task["task_id"] for task in tasks},"full-RAG context cohort differs")
|
|
117
|
+
policy=_policy(dataset,expected_sha256,candidate,context_condition=context_condition,contexts=contexts,execution_shape=execution_shape,
|
|
118
|
+
synthetic=synthetic,owner_authorized=owner_authorized,pack_manifest_path=pack_manifest_path,
|
|
119
|
+
trials=trials,attempts=attempts,boundary_digest=boundary_digest)
|
|
120
|
+
requests={task["task_id"]:build_full_rag_request(task,policy,contexts[task["task_id"]]) for task in tasks}
|
|
121
|
+
if context_condition=="empty":_require(all(not values for values in contexts.values()),"empty context diagnostics must actually be empty")
|
|
122
|
+
if candidate.execution_mode=="recorded":
|
|
123
|
+
_require(isinstance(adapter,Mapping) and candidate.parameters.get("recordings_digest")==value_digest(adapter),"full-RAG replay recording is not pinned")
|
|
124
|
+
adapter=_detach(adapter)
|
|
125
|
+
else:_require(callable(adapter),"full-RAG adapter must be callable")
|
|
126
|
+
observed={"approved_manifest_source":None if synthetic else Path(pack_manifest_path).read_text(encoding="utf-8"),"frozen_policy":policy}
|
|
127
|
+
_verify_approval(dataset,expected_sha256,policy,{"approved_manifest_source":observed["approved_manifest_source"]})
|
|
128
|
+
identity=_identity(dataset,expected_sha256,tasks,policy);ids=[task["task_id"] for task in tasks]
|
|
129
|
+
directory=create_run(output_dir,identity,candidate,ids);captures=[]
|
|
130
|
+
for index,task in enumerate(tasks):
|
|
131
|
+
request=requests[task["task_id"]]
|
|
132
|
+
for trial in range(1,trials+1):
|
|
133
|
+
for attempt in range(1,attempts+1):
|
|
134
|
+
prefix=f"cases/{index}/{trial}/{attempt}/";req=write_artifact(directory,prefix+"request.json",request)
|
|
135
|
+
response=None;failure=None;begin=perf_counter()
|
|
136
|
+
try:
|
|
137
|
+
raw=adapter[value_digest(request)] if candidate.execution_mode=="recorded" else adapter(_detach(request))
|
|
138
|
+
response=write_artifact(directory,prefix+"response.json",raw);validate_full_rag_response(raw,request)
|
|
139
|
+
except Exception:failure="invalid_response" if response is not None else "candidate_failure"
|
|
140
|
+
observation=write_artifact(directory,prefix+"observed.json",observed)
|
|
141
|
+
captures.append(TrackCaseCapture(task["task_id"],trial,attempt,req,response,observation,failure,(perf_counter()-begin)*1000))
|
|
142
|
+
return finish_run(directory,identity,candidate,ids,captures)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _verified(run,dataset_path,digest):
|
|
146
|
+
dataset=load_dataset_v3(dataset_path,expected_sha256=digest);manifest=verify_track_run(run)
|
|
147
|
+
_require(manifest.candidate.interface_version==INTERFACE,"capture is not full-RAG")
|
|
148
|
+
tasks=_select_tasks(dataset,manifest.case_ids)
|
|
149
|
+
_require(manifest.captures[0].observed is not None,"full-RAG frozen control metadata is missing")
|
|
150
|
+
first_observed=read_artifact(run.path,manifest.captures[0].observed)
|
|
151
|
+
_require(type(first_observed) is dict and set(first_observed)=={"approved_manifest_source","frozen_policy"},"full-RAG control metadata fields differ")
|
|
152
|
+
policy=first_observed["frozen_policy"]
|
|
153
|
+
required={"version","synthetic","approved_manifest_digest","trials","attempts","selection","boundary_digest","context_condition",
|
|
154
|
+
"contexts","execution_shape","generator_digest","prompt_digest","execution_mode","timing_authority","semantic_review_policy"}
|
|
155
|
+
_require(type(policy) is dict and set(policy)==required and policy["version"]==POLICY_VERSION
|
|
156
|
+
and type(policy["synthetic"]) is bool and policy["selection"]=="first_success_per_trial"
|
|
157
|
+
and policy["context_condition"] in CONTEXT_CONDITIONS and policy["execution_shape"] in ("opaque","componentized")
|
|
158
|
+
and policy["execution_mode"]==manifest.candidate.execution_mode and policy["timing_authority"]=="component_reported_unattested"
|
|
159
|
+
and policy["semantic_review_policy"]=="capture-bound-owner-judgments.v1","full-RAG policy fields differ")
|
|
160
|
+
_require(all(type(policy[key]) is int and policy[key]>=1 for key in ("trials","attempts")),"full-RAG trial policy differs")
|
|
161
|
+
_require(policy["generator_digest"]==manifest.candidate.parameters["generator_digest"] and
|
|
162
|
+
policy["prompt_digest"]==manifest.candidate.parameters["prompt_digest"],"declared generator/prompt changed")
|
|
163
|
+
requests={task["task_id"]:build_full_rag_request(task,policy,policy["contexts"][task["task_id"]]) for task in tasks}
|
|
164
|
+
require_comparable(_identity(dataset,digest,tasks,policy),manifest.identity)
|
|
165
|
+
verify_track_run(run,expected_case_ids=manifest.case_ids,expected_requests=requests,trials=policy["trials"],attempts=policy["attempts"])
|
|
166
|
+
for capture in manifest.captures:
|
|
167
|
+
_require(capture.observed is not None,"full-RAG approval observation is missing")
|
|
168
|
+
observed=read_artifact(run.path,capture.observed)
|
|
169
|
+
_require(type(observed) is dict and set(observed)=={"approved_manifest_source","frozen_policy"}
|
|
170
|
+
and observed["frozen_policy"]==policy,"full-RAG frozen controls changed between cases")
|
|
171
|
+
_verify_approval(dataset,digest,policy,{"approved_manifest_source":observed["approved_manifest_source"]})
|
|
172
|
+
return manifest,tasks,policy,requests
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def create_full_rag_review_template(run,dataset_path,*,expected_sha256):
|
|
176
|
+
manifest,tasks,policy,requests=_verified(run,dataset_path,expected_sha256)
|
|
177
|
+
rows=[]
|
|
178
|
+
for capture in manifest.captures:
|
|
179
|
+
if capture.failure is None and capture.response is not None:
|
|
180
|
+
rows.append({"case_id":capture.case_id,"trial_index":capture.trial_index,"attempt_index":capture.attempt_index,
|
|
181
|
+
"response_digest":capture.response.sha256,"required_facts":{},"exact_values":[],"claims":[],
|
|
182
|
+
"claims_exhaustive":False,"review_status":"pending"})
|
|
183
|
+
return {"schema_version":JUDGMENTS_SCHEMA,"capture_manifest_digest":run.manifest_sha256,"dataset_digest":expected_sha256,
|
|
184
|
+
"reviewer_id":None,"reviewed_at_utc":None,"authority":"pending","rows":rows}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _reviews(value,run,digest,manifest,tasks):
|
|
188
|
+
if value is None:return {},None,None
|
|
189
|
+
raw=Path(value).read_bytes() if isinstance(value,(str,Path)) else None
|
|
190
|
+
review=strict_json(raw) if raw is not None else _detach(value)
|
|
191
|
+
_require(type(review) is dict and set(review)=={"schema_version","capture_manifest_digest","dataset_digest","reviewer_id","reviewed_at_utc","authority","rows"}
|
|
192
|
+
and review["schema_version"]==JUDGMENTS_SCHEMA and review["capture_manifest_digest"]==run.manifest_sha256
|
|
193
|
+
and review["dataset_digest"]==digest,"semantic judgments are not bound to this capture/dataset")
|
|
194
|
+
_require(type(review["reviewer_id"]) is str and review["reviewer_id"].strip() and type(review["reviewed_at_utc"]) is str
|
|
195
|
+
and re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z",review["reviewed_at_utc"])
|
|
196
|
+
and review["authority"] in ("owner_human_review","synthetic_fixture"),"semantic review identity/authority is incomplete")
|
|
197
|
+
_require(type(review["rows"]) is list,"semantic review rows differ")
|
|
198
|
+
first_policy=read_artifact(run.path,manifest.captures[0].request)["policy"]
|
|
199
|
+
_require(review["authority"]!="synthetic_fixture" or first_policy["synthetic"],
|
|
200
|
+
"synthetic judgments cannot approve a customer capture")
|
|
201
|
+
captures={(row.case_id,row.trial_index,row.attempt_index):row for row in manifest.captures};by_key={}
|
|
202
|
+
for row in review["rows"]:
|
|
203
|
+
fields={"case_id","trial_index","attempt_index","response_digest","required_facts","exact_values","claims","claims_exhaustive","review_status"}
|
|
204
|
+
_require(type(row) is dict and set(row)==fields and row["review_status"]=="reviewed" and type(row["claims_exhaustive"]) is bool,"semantic review row is incomplete")
|
|
205
|
+
key=(row["case_id"],row["trial_index"],row["attempt_index"]);capture=captures.get(key)
|
|
206
|
+
_require(key not in by_key and capture is not None and capture.failure is None and capture.response is not None
|
|
207
|
+
and row["response_digest"]==capture.response.sha256,"semantic review response membership/drift")
|
|
208
|
+
_require(type(row["required_facts"]) is dict and all(type(k) is str and k and type(v) is list and v
|
|
209
|
+
and all(type(phrase) is str and phrase.strip() for phrase in v) for k,v in row["required_facts"].items())
|
|
210
|
+
and type(row["exact_values"]) is list and all(type(v) is str and v.strip() for v in row["exact_values"])
|
|
211
|
+
and type(row["claims"]) is list,"semantic review facts/values/claims differ")
|
|
212
|
+
by_key[key]=row
|
|
213
|
+
truth={}
|
|
214
|
+
for row in review["rows"]:
|
|
215
|
+
criteria={"required_facts":row["required_facts"],"exact_values":row["exact_values"]}
|
|
216
|
+
previous=truth.get(row["case_id"])
|
|
217
|
+
_require(previous is None or previous==criteria,"semantic truth changed between trials/attempts")
|
|
218
|
+
truth[row["case_id"]]=criteria
|
|
219
|
+
policy_digest=value_digest({"schema_version":"sphynx.rag.full_rag.semantic-policy.v1",
|
|
220
|
+
"metric_release":METRIC_RELEASE,"authority":review["authority"],"truth":truth})
|
|
221
|
+
return by_key,value_digest(review),policy_digest
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _grade_metrics(run,dataset_path,digest,judgments=None):
|
|
225
|
+
manifest,tasks,policy,requests=_verified(run,dataset_path,digest)
|
|
226
|
+
reviews,review_digest,review_policy=_reviews(judgments,run,digest,manifest,tasks);rows=[]
|
|
227
|
+
for task in tasks:
|
|
228
|
+
for trial in range(1,policy["trials"]+1):
|
|
229
|
+
score=None;selected=None;reviewed=False
|
|
230
|
+
for capture in sorted(manifest.captures,key=lambda row:row.attempt_index):
|
|
231
|
+
if capture.case_id!=task["task_id"] or capture.trial_index!=trial or capture.failure is not None:continue
|
|
232
|
+
_require(capture.response is not None,"successful full-RAG capture lacks response")
|
|
233
|
+
trace=validate_full_rag_response(read_artifact(run.path,capture.response),requests[task["task_id"]],expected_invocation=task["should_call_rag"])
|
|
234
|
+
review=reviews.get((capture.case_id,trial,capture.attempt_index));claims=[]
|
|
235
|
+
if review is not None:
|
|
236
|
+
for raw in review["claims"]:
|
|
237
|
+
_require(type(raw) is dict and set(raw)=={"start","end","supported_by_evidence_ids"}
|
|
238
|
+
and type(raw["start"]) is int and type(raw["end"]) is int and type(raw["supported_by_evidence_ids"]) is list
|
|
239
|
+
and all(type(cid) is str for cid in raw["supported_by_evidence_ids"])
|
|
240
|
+
and len(set(raw["supported_by_evidence_ids"]))==len(raw["supported_by_evidence_ids"])
|
|
241
|
+
and set(raw["supported_by_evidence_ids"])<=set(trace.handed_context_ids),"claim support leaves exact handed evidence")
|
|
242
|
+
claims.append(ClaimVerdict(raw["start"],raw["end"],tuple(raw["supported_by_evidence_ids"])))
|
|
243
|
+
ordered=sorted(claims,key=lambda claim:(claim.start,claim.end))
|
|
244
|
+
_require(all(left.end<=right.start for left,right in zip(ordered,ordered[1:])),
|
|
245
|
+
"reviewed claims overlap or repeat answer spans")
|
|
246
|
+
current=grade_level3(task,trace,required_facts={} if review is None else review["required_facts"],
|
|
247
|
+
exact_values=[] if review is None else review["exact_values"],claims=claims)
|
|
248
|
+
if review is None:
|
|
249
|
+
for key in _SEMANTIC:current[key]=None
|
|
250
|
+
elif not review["claims_exhaustive"]:
|
|
251
|
+
for key in ("faithfulness","unsupported_claim_rate"):current[key]=None
|
|
252
|
+
if selected is None:score,selected,reviewed=current,capture,review is not None
|
|
253
|
+
rows.append({"case_id":task["task_id"],"trial_index":trial,"status":"failed" if selected is None else "measured",
|
|
254
|
+
"metrics":score,"semantic_reviewed":reviewed,"runner_wall_ms":None if selected is None else selected.runner_latency_ms,
|
|
255
|
+
"response_digest":None if selected is None else selected.response.sha256})
|
|
256
|
+
keys=("rag_call_true_positive","rag_call_false_positive","rag_call_false_negative","rewrite_preservation","evidence_handoff_completeness",
|
|
257
|
+
"answer_required_fact_coverage","exact_value_unit_date_accuracy","faithfulness","unsupported_claim_rate","citation_precision","citation_recall",
|
|
258
|
+
"citation_support","correct_abstention","answer_completeness","subject_error")
|
|
259
|
+
summary={}
|
|
260
|
+
for key in keys:
|
|
261
|
+
values=[row["metrics"][key] for row in rows if row["metrics"] is not None and row["metrics"][key] is not None]
|
|
262
|
+
summary[key]={"value":sum(values)/len(values) if len(values)==len(rows) else None,
|
|
263
|
+
"observed_mean":sum(values)/len(values) if values else None,"observed_count":len(values),
|
|
264
|
+
"eligible_count":len(rows),"missing_count":len(rows)-len(values)}
|
|
265
|
+
complete=all(row["metrics"] is not None for row in rows)
|
|
266
|
+
counts={key:sum(row["metrics"][key] for row in rows if row["metrics"] is not None)
|
|
267
|
+
for key in ("rag_call_true_positive","rag_call_false_positive","rag_call_false_negative")}
|
|
268
|
+
for key,denominator in (("rag_call_precision",counts["rag_call_true_positive"]+counts["rag_call_false_positive"]),
|
|
269
|
+
("rag_call_recall",counts["rag_call_true_positive"]+counts["rag_call_false_negative"])):
|
|
270
|
+
summary[key]={"value":counts["rag_call_true_positive"]/denominator if complete and denominator else None,
|
|
271
|
+
"numerator":counts["rag_call_true_positive"],"denominator":denominator,
|
|
272
|
+
"complete":complete,"undefined":complete and not denominator,
|
|
273
|
+
"eligible_count":len(rows),"observed_count":sum(row["metrics"] is not None for row in rows),
|
|
274
|
+
"missing_count":sum(row["metrics"] is None for row in rows)}
|
|
275
|
+
return manifest,{"summary":summary,"cases":rows,"coverage":sum(row["metrics"] is not None for row in rows)/len(rows),
|
|
276
|
+
"execution_shape":policy["execution_shape"],"context_condition":policy["context_condition"],
|
|
277
|
+
"generator_digest":policy["generator_digest"],"prompt_digest":policy["prompt_digest"],
|
|
278
|
+
"execution_mode":policy["execution_mode"],"timing_authority":"component_reported_unattested",
|
|
279
|
+
"quality_scope":"synthetic_fixture" if policy["synthetic"] else "owner_approved",
|
|
280
|
+
"review_digest":review_digest,"judgment_policy_digest":review_policy},review_digest,review_policy
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def grade_full_rag(run,dataset_path,*,expected_sha256,judgments=None):
|
|
284
|
+
manifest,metrics,review_digest,review_policy=_grade_metrics(run,dataset_path,expected_sha256,judgments)
|
|
285
|
+
if review_digest is not None:
|
|
286
|
+
value=strict_json(Path(judgments).read_bytes()) if isinstance(judgments,(str,Path)) else _detach(judgments)
|
|
287
|
+
relative="semantic-reviews/"+review_digest.removeprefix("sha256:")+".json"
|
|
288
|
+
if not (run.path/relative).exists():write_artifact(run.path,relative,value)
|
|
289
|
+
metrics["review_artifact"]=relative
|
|
290
|
+
return write_evaluation(run,replace(manifest.identity,judgment_policy_digest=review_policy),metrics,
|
|
291
|
+
judgment_artifact_digest=review_digest)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def report_full_rag(evaluation,dataset_path,*,expected_sha256):
|
|
295
|
+
report=verify_track_evaluation(evaluation).to_dict();review=None
|
|
296
|
+
if report["metrics"].get("review_artifact"):
|
|
297
|
+
from ._track_capture import safe_path
|
|
298
|
+
path=safe_path(evaluation.run.path,report["metrics"]["review_artifact"]);review=strict_json(path.read_bytes())
|
|
299
|
+
_require(value_digest(review)==report["judgment_artifact_digest"],"retained semantic review digest differs")
|
|
300
|
+
_,expected,review_digest,review_policy=_grade_metrics(evaluation.run,dataset_path,expected_sha256,review)
|
|
301
|
+
if review_digest is not None:expected["review_artifact"]=report["metrics"]["review_artifact"]
|
|
302
|
+
_require(report["metrics"]==expected,"full-RAG evaluation differs from retained trace/review")
|
|
303
|
+
_require(report["identity"]["judgment_policy_digest"]==review_policy,"semantic policy identity differs")
|
|
304
|
+
return report
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def compare_full_rag(baseline,candidate,dataset_path,*,expected_sha256):
|
|
308
|
+
a=report_full_rag(baseline,dataset_path,expected_sha256=expected_sha256)
|
|
309
|
+
b=report_full_rag(candidate,dataset_path,expected_sha256=expected_sha256)
|
|
310
|
+
require_comparable(ComparisonIdentity.from_dict(a["identity"]),ComparisonIdentity.from_dict(b["identity"]))
|
|
311
|
+
_require(a["metrics"]["context_condition"]==b["metrics"]["context_condition"],"full-RAG context diagnostic differs")
|
|
312
|
+
_require([(row["case_id"],row["trial_index"]) for row in a["metrics"]["cases"]]==
|
|
313
|
+
[(row["case_id"],row["trial_index"]) for row in b["metrics"]["cases"]],"full-RAG paired cohort differs")
|
|
314
|
+
return {"schema_version":"sphynx.rag.full_rag.comparison.v1","baseline":a["evaluation_id"],"candidate":b["evaluation_id"],
|
|
315
|
+
"delta":{key:None if value["value"] is None or b["metrics"]["summary"][key]["value"] is None
|
|
316
|
+
else b["metrics"]["summary"][key]["value"]-value["value"] for key,value in a["metrics"]["summary"].items()},
|
|
317
|
+
"timing_comparable":False,"notice":"Review scopes and opaque/componentized boundaries are held fixed; local timings are not attested."}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def compare_full_rag_contexts(evaluations,dataset_path,*,expected_sha256):
|
|
321
|
+
_require(isinstance(evaluations,Mapping) and set(evaluations)==CONTEXT_CONDITIONS,"counterfactuals require exactly five fixed contexts")
|
|
322
|
+
reports={key:report_full_rag(value,dataset_path,expected_sha256=expected_sha256) for key,value in evaluations.items()}
|
|
323
|
+
base=reports["oracle"];identity=ComparisonIdentity.from_dict(base["identity"])
|
|
324
|
+
for condition,report in reports.items():
|
|
325
|
+
_require(report["metrics"]["context_condition"]==condition,"counterfactual condition alias changed")
|
|
326
|
+
require_comparable(identity,ComparisonIdentity.from_dict(report["identity"]),context_conditions=True)
|
|
327
|
+
_require(report["metrics"]["generator_digest"]==base["metrics"]["generator_digest"] and
|
|
328
|
+
report["metrics"]["prompt_digest"]==base["metrics"]["prompt_digest"],"counterfactual generator/prompt changed")
|
|
329
|
+
# This diagnostic uses retained coverage only. No generator/candidate is rerun.
|
|
330
|
+
rows={key:{(row["case_id"],row["trial_index"]):row for row in report["metrics"]["cases"]} for key,report in reports.items()}
|
|
331
|
+
_require(all(set(value)==set(rows["oracle"]) for value in rows.values()),"counterfactual task/trial cohorts differ")
|
|
332
|
+
result=[]
|
|
333
|
+
for case in rows["oracle"]:
|
|
334
|
+
coverage={key:rows[key][case]["metrics"]["answer_required_fact_coverage"] if rows[key][case]["metrics"] else None for key in CONTEXT_CONDITIONS}
|
|
335
|
+
def gap(a,b):return None if coverage[a] is None or coverage[b] is None else coverage[a]-coverage[b]
|
|
336
|
+
result.append({"case_id":case[0],"trial_index":case[1],"condition_coverage":coverage,
|
|
337
|
+
"retrieval_ranking_gap":gap("oracle","challenger"),"challenger_vs_a0":gap("challenger","a0"),
|
|
338
|
+
"a0_ranking_contribution":gap("a0","dense"),"rag_dependency":gap("challenger","empty")})
|
|
339
|
+
return {"schema_version":"sphynx.rag.full_rag.context-comparison.v1","cases":result,
|
|
340
|
+
"generator_digest":base["metrics"]["generator_digest"],"prompt_digest":base["metrics"]["prompt_digest"],
|
|
341
|
+
"notice":"Fixed-generator context diagnostic, separate from architecture selection; missing semantic truth remains unavailable."}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Hosted submission preparation. Remote execution is not connected yet."""
|
|
2
|
+
|
|
3
|
+
from .contracts import (
|
|
4
|
+
CandidateConfig, CandidateFamily, HostedError, SelectedCandidateRepository,
|
|
5
|
+
parse_candidate_config, parse_candidate_family, parse_candidate_repository,
|
|
6
|
+
verify_candidate_repository,
|
|
7
|
+
)
|
|
8
|
+
from .registry import HostedRegistry, RepositoryRegistration, load_registry
|
|
9
|
+
from .submission import FrozenSubmission, GitHubReader, prepare_submission
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CandidateConfig", "HostedError", "parse_candidate_config", "HostedRegistry",
|
|
13
|
+
"RepositoryRegistration", "load_registry", "FrozenSubmission", "GitHubReader",
|
|
14
|
+
"prepare_submission",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
__all__ += ["CandidateFamily", "SelectedCandidateRepository", "parse_candidate_family",
|
|
18
|
+
"parse_candidate_repository", "verify_candidate_repository"]
|