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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Validate the local language-neutral case/trace boundary before grading."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import copy
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ._rag_diagnostics_common import STAGES, digest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ContractError(ValueError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def require(condition, message):
|
|
17
|
+
if not condition:
|
|
18
|
+
raise ContractError(message)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def unique_ids(values, name):
|
|
22
|
+
require(type(values) is list and all(type(v) is str and 0 < len(v) <= 180 for v in values), name+' must be an ID list')
|
|
23
|
+
require(len(values) == len(set(values)), name+' has duplicate IDs')
|
|
24
|
+
return set(values)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def optional_bool(value, name):
|
|
28
|
+
require(value is None or type(value) is bool, name+' must be boolean or null')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _json_domain(value, depth=0, ancestors=None):
|
|
32
|
+
"""Reject non-JSON objects, nonfinite numbers and recursive direct inputs."""
|
|
33
|
+
require(depth <= 64, 'Dataset nesting exceeds the local 64-level limit')
|
|
34
|
+
if value is None or type(value) in (str, bool, int):
|
|
35
|
+
return
|
|
36
|
+
if type(value) is float:
|
|
37
|
+
require(math.isfinite(value), 'Dataset numbers must be finite')
|
|
38
|
+
return
|
|
39
|
+
require(type(value) in (dict, list), 'Dataset values must be JSON objects, lists or scalar values')
|
|
40
|
+
ancestors = set() if ancestors is None else ancestors
|
|
41
|
+
identity = id(value)
|
|
42
|
+
require(identity not in ancestors, 'Dataset must not contain recursive values')
|
|
43
|
+
ancestors.add(identity)
|
|
44
|
+
if type(value) is dict:
|
|
45
|
+
require(all(type(key) is str for key in value), 'JSON object keys must be strings')
|
|
46
|
+
children = value.values()
|
|
47
|
+
else:
|
|
48
|
+
children = value
|
|
49
|
+
for child in children:
|
|
50
|
+
_json_domain(child, depth+1, ancestors)
|
|
51
|
+
ancestors.remove(identity)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def case_input_digest(case):
|
|
55
|
+
"""Bind a semantic review to exact public candidate inputs, not just IDs."""
|
|
56
|
+
return digest({key: case[key] for key in ('id', 'question', 'documents')})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_dataset(data):
|
|
60
|
+
_json_domain(data)
|
|
61
|
+
require(type(data) is dict and data.get('schema_version') == 'sphynx.rag-diagnostics.dataset.v1', 'Unsupported dataset schema')
|
|
62
|
+
require(type(data.get('release')) is str and data['release'], 'Dataset release is required')
|
|
63
|
+
require(type(data.get('suites')) is list and 0 < len(data['suites']) <= 5000, 'Expected 1–5000 suites')
|
|
64
|
+
require(all(type(s) is dict for s in data['suites']), 'Each suite must be an object')
|
|
65
|
+
suite_ids=unique_ids([s.get('id') for s in data['suites']], 'suite IDs')
|
|
66
|
+
require(type(data.get('cases')) is list and 0 < len(data['cases']) <= 5000, 'Expected 1–5000 cases')
|
|
67
|
+
require(all(type(c) is dict for c in data['cases']), 'Each case must be an object')
|
|
68
|
+
unique_ids([c.get('id') for c in data['cases']], 'case IDs')
|
|
69
|
+
for case in data['cases']:
|
|
70
|
+
name=case['id']
|
|
71
|
+
require(case.get('suite_id') in suite_ids, name+': unknown suite')
|
|
72
|
+
require(type(case.get('cluster_id')) is str and case['cluster_id'], name+': independent cluster identity required')
|
|
73
|
+
require(type(case.get('question')) is str and 0 < len(case['question']) <= 20000, name+': question required')
|
|
74
|
+
docs=case.get('documents')
|
|
75
|
+
require(type(docs) is list and len(docs) <= 10000, name+': invalid documents')
|
|
76
|
+
require(all(type(d) is dict for d in docs), name+': each document must be an object')
|
|
77
|
+
ids=unique_ids([d.get('id') for d in docs], name+' document IDs')
|
|
78
|
+
require(all(type(d.get('text')) is str for d in docs), name+': document text required')
|
|
79
|
+
refs=case.get('references')
|
|
80
|
+
require(type(refs) is dict, name+': protected references required')
|
|
81
|
+
reviews=refs.get('reviews', {})
|
|
82
|
+
require(type(reviews) is dict and set(reviews) <= {'baseline','candidate'}, name+': invalid review variants')
|
|
83
|
+
optional_bool(refs.get('corpus_answerable'), name+' corpus answerability')
|
|
84
|
+
qrels=refs.get('qrels')
|
|
85
|
+
require(qrels is None or (type(qrels) is dict and set(qrels) <= ids and all(type(g) is int and 0 <= g <= 100 for g in qrels.values())), name+': invalid qrels')
|
|
86
|
+
for required,mapping in [('required_atoms','atom_support'),('required_relations','relation_support')]:
|
|
87
|
+
needed=refs.get(required)
|
|
88
|
+
if needed is not None:
|
|
89
|
+
unique_ids(needed, name+' '+required)
|
|
90
|
+
support=refs.get(mapping)
|
|
91
|
+
if support is not None:
|
|
92
|
+
require(type(support) is dict, name+': invalid '+mapping)
|
|
93
|
+
for key,values in support.items():
|
|
94
|
+
require(type(key) is str and key, name+': invalid support key')
|
|
95
|
+
require(unique_ids(values, name+' support') <= ids, name+': support references unknown documents')
|
|
96
|
+
traces=case.get('traces')
|
|
97
|
+
require(type(traces) is dict and set(traces)=={'baseline','candidate'}, name+': paired traces required')
|
|
98
|
+
for variant,trace in traces.items():
|
|
99
|
+
require(type(trace) is dict and trace.get('execution_mode') in ('synthetic_replay','recorded_replay'), name+': replay mode required')
|
|
100
|
+
require(type(trace.get('id')) is str and trace['id'], name+': trace identity required')
|
|
101
|
+
stages=trace.get('stages')
|
|
102
|
+
require(type(stages) is dict and set(stages)==set(STAGES), name+': explicit stage lists/nulls required')
|
|
103
|
+
previous=None
|
|
104
|
+
for stage in STAGES:
|
|
105
|
+
values=stages[stage]
|
|
106
|
+
if values is None:
|
|
107
|
+
continue
|
|
108
|
+
found=unique_ids(values, name+' '+stage)
|
|
109
|
+
require(found <= ids, name+': stage references unknown document')
|
|
110
|
+
if previous is not None:
|
|
111
|
+
require(found <= previous, name+': stage cannot invent documents after upstream selection')
|
|
112
|
+
previous=found
|
|
113
|
+
response=trace.get('response')
|
|
114
|
+
require(type(response) is dict and response.get('status') in ('ok','error','timeout'), name+': response status required')
|
|
115
|
+
require(type(response.get('text')) is str, name+': response text required')
|
|
116
|
+
optional_bool(response.get('abstained'), name+' abstained')
|
|
117
|
+
ops=trace.get('operational')
|
|
118
|
+
require(type(ops) is dict and ops.get('origin') in ('synthetic','component_reported','attested'), name+': telemetry origin required')
|
|
119
|
+
require(ops.get('origin')!='attested', name+': SDK cannot validate telemetry attestation')
|
|
120
|
+
for key in ('latency_ms','cost_usd','tokens'):
|
|
121
|
+
value=ops.get(key)
|
|
122
|
+
require(value is None or (type(value) in (int,float) and math.isfinite(value) and value>=0), name+': invalid telemetry '+key)
|
|
123
|
+
if key=='tokens' and value is not None:
|
|
124
|
+
require(type(value) is int, name+': tokens must be integer')
|
|
125
|
+
review=reviews.get(variant)
|
|
126
|
+
if review is None:
|
|
127
|
+
continue
|
|
128
|
+
require(type(review) is dict and review.get('trace_digest')==digest(trace), name+': stale or mismatched review')
|
|
129
|
+
require(review.get('case_input_digest')==case_input_digest(case), name+': stale or mismatched review case inputs')
|
|
130
|
+
provenance=review.get('provenance')
|
|
131
|
+
require(type(provenance) is dict and provenance.get('kind') in ('synthetic','human_adjudicated') and type(provenance.get('reviewer')) is str and provenance['reviewer'].strip(), name+': review provenance required')
|
|
132
|
+
require(trace['execution_mode']!='synthetic_replay' or provenance['kind']=='synthetic', name+': synthetic trace requires synthetic review provenance')
|
|
133
|
+
for field in ('answer_correct','answer_relevant','abstention_expected'):
|
|
134
|
+
optional_bool(review.get(field), name+' '+field)
|
|
135
|
+
suff=review.get('context_sufficiency',{})
|
|
136
|
+
require(type(suff) is dict and set(suff) <= {'retrieved','selected','prompt'}, name+': invalid sufficiency stages')
|
|
137
|
+
for stage,value in suff.items():
|
|
138
|
+
optional_bool(value, name+' sufficient '+stage)
|
|
139
|
+
require(value is None or stages[stage] is not None, name+': cannot judge missing context')
|
|
140
|
+
require(type(review.get('claims_exhaustive')) is bool and type(review.get('claims')) is list, name+': claim audit state required')
|
|
141
|
+
claims=review['claims']
|
|
142
|
+
require(all(type(c) is dict for c in claims), name+': each reviewed claim must be an object')
|
|
143
|
+
unique_ids([c.get('id') for c in claims], name+' claim IDs')
|
|
144
|
+
prompt=set(stages['prompt'] or [])
|
|
145
|
+
for claim in claims:
|
|
146
|
+
require(type(claim.get('text')) is str and claim['text'], name+': claim text required')
|
|
147
|
+
for field in ('supported_by_context','correct','joint_citation_support'):
|
|
148
|
+
optional_bool(claim.get(field), name+' claim '+field)
|
|
149
|
+
require(type(claim.get('requires_citation')) is bool, name+': citation requirement required')
|
|
150
|
+
cited=unique_ids(claim.get('cited_ids'), name+' cited IDs')
|
|
151
|
+
require(cited <= ids, name+': citation ID does not exist')
|
|
152
|
+
# A dangling/out-of-prompt citation is an observed citation validity
|
|
153
|
+
# failure, not an assertion of semantic support.
|
|
154
|
+
if claim.get('joint_citation_support') is True:
|
|
155
|
+
require(cited and cited <= prompt, name+': supported citation leaves handed context')
|
|
156
|
+
unique_ids(claim.get('reference_atoms'), name+' expressed reference atoms')
|
|
157
|
+
if refs.get('required_atoms') is not None:
|
|
158
|
+
require(set(claim['reference_atoms']) <= set(refs['required_atoms']), name+': unknown expressed reference atom')
|
|
159
|
+
for field in ('individual_support','leave_one_out_support'):
|
|
160
|
+
judgments=claim.get(field)
|
|
161
|
+
require(type(judgments) is dict and set(judgments) <= cited, name+': invalid citation judgments')
|
|
162
|
+
for value in judgments.values():
|
|
163
|
+
optional_bool(value, name+' citation judgment')
|
|
164
|
+
return data
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def load_dataset(path):
|
|
168
|
+
raw=Path(path).read_bytes()
|
|
169
|
+
require(len(raw)<=20_000_000, 'Dataset exceeds 20MB local limit')
|
|
170
|
+
def pairs(items):
|
|
171
|
+
out={}
|
|
172
|
+
for key,value in items:
|
|
173
|
+
require(key not in out, 'Duplicate JSON object key: '+key)
|
|
174
|
+
out[key]=value
|
|
175
|
+
return out
|
|
176
|
+
try:
|
|
177
|
+
data=json.loads(raw,object_pairs_hook=pairs,parse_constant=lambda value: (_ for _ in ()).throw(ContractError('Nonfinite JSON number')))
|
|
178
|
+
except (json.JSONDecodeError, UnicodeDecodeError, RecursionError) as error:
|
|
179
|
+
raise ContractError('Invalid or excessively nested JSON dataset') from error
|
|
180
|
+
return validate_dataset(data)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def candidate_view(case):
|
|
184
|
+
"""Protected labels are never part of a candidate payload."""
|
|
185
|
+
return copy.deepcopy({'id':case['id'],'question':case['question'],
|
|
186
|
+
'documents':[{key:doc[key] for key in ('id','text')} for doc in case['documents']]})
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Grade saved traces and compare compatible cases. Never executes candidates."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections import Counter, defaultdict
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import random
|
|
7
|
+
import statistics
|
|
8
|
+
|
|
9
|
+
from ._rag_diagnostics_common import digest, metric
|
|
10
|
+
from ._rag_diagnostics_contracts import validate_dataset
|
|
11
|
+
from ._rag_diagnostics_ranking import ranking_metrics, evidence_metrics
|
|
12
|
+
from ._rag_diagnostics_semantic import semantic_metrics
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def operational_metrics(case,trace):
|
|
16
|
+
rows=[]
|
|
17
|
+
ops=trace['operational']
|
|
18
|
+
for name,unit in [('latency_ms','ms'),('cost_usd','USD'),('tokens','tokens')]:
|
|
19
|
+
value=ops.get(name)
|
|
20
|
+
row=metric('operational.recorded_'+name,case['id'],'operational',value,
|
|
21
|
+
grader_type=ops['origin']+'_telemetry',unit=unit,direction='lower',
|
|
22
|
+
reason='Saved '+ops['origin']+' telemetry; replay grading duration is not model execution time')
|
|
23
|
+
rows.append(row)
|
|
24
|
+
rows.append(metric('operational.failure',case['id'],'operational',float(trace['response']['status']!='ok'),numerator=int(trace['response']['status']!='ok'),denominator=1,direction='lower'))
|
|
25
|
+
rows.append(metric('operational.timeout',case['id'],'operational',float(trace['response']['status']=='timeout'),numerator=int(trace['response']['status']=='timeout'),denominator=1,direction='lower'))
|
|
26
|
+
return rows
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def metric_key(row):
|
|
30
|
+
return row['stage']+'|'+row['metric_id']
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def diagnose(case,trace,rows,review):
|
|
34
|
+
values={row['metric_id']:row['value'] for row in rows}
|
|
35
|
+
evidence={row['stage']:row['value'] for row in rows if row['metric_id']=='evidence.atom_recall'}
|
|
36
|
+
first_loss=None;prior_unknown=False
|
|
37
|
+
for stage in ('indexed','retrieved','reranked','selected','prompt'):
|
|
38
|
+
value=evidence.get(stage)
|
|
39
|
+
if value is None:
|
|
40
|
+
prior_unknown=True
|
|
41
|
+
elif value<1:
|
|
42
|
+
first_loss=stage;break
|
|
43
|
+
result={'first_observed_evidence_loss':first_loss,'earlier_stages_unknown':prior_unknown,
|
|
44
|
+
'interpretation':'Observational localization over saved traces; no causal intervention was executed.'}
|
|
45
|
+
status=trace['response']['status']
|
|
46
|
+
source=case['references'].get('corpus_answerable')
|
|
47
|
+
sufficient=review.get('context_sufficiency',{}).get('prompt') if review else None
|
|
48
|
+
correct=values.get('generation.answer_correctness')
|
|
49
|
+
faithful=values.get('generation.faithfulness')
|
|
50
|
+
cited=values.get('citation.completeness')
|
|
51
|
+
citation_precision=values.get('citation.necessity_precision')
|
|
52
|
+
citation_failed=any(value is not None and value<1 for value in (cited,citation_precision))
|
|
53
|
+
if status!='ok':label='runtime_'+status
|
|
54
|
+
elif source is None:label='unknown_source_answerability'
|
|
55
|
+
elif source is False:
|
|
56
|
+
if trace['response']['abstained'] is True:
|
|
57
|
+
if values.get('outcome.grounded_task_success')==1:
|
|
58
|
+
label='appropriate_abstention'
|
|
59
|
+
elif not review or review.get('claims_exhaustive') is not True:
|
|
60
|
+
label='unknown_claim_support'
|
|
61
|
+
elif any(claim.get('supported_by_context') is False for claim in review['claims']):
|
|
62
|
+
label='unsupported_abstention'
|
|
63
|
+
elif any(claim.get('supported_by_context') is None for claim in review['claims']):
|
|
64
|
+
label='unknown_claim_support'
|
|
65
|
+
elif citation_failed:
|
|
66
|
+
label='citation_failure'
|
|
67
|
+
else:
|
|
68
|
+
label='incomplete_abstention_evidence' if values.get('outcome.grounded_task_success') is None else 'abstention_policy_failure'
|
|
69
|
+
else:
|
|
70
|
+
label='source_gap'
|
|
71
|
+
elif sufficient is None:label='unknown_context_sufficiency'
|
|
72
|
+
elif sufficient is False:
|
|
73
|
+
label='correct_with_insufficient_context' if correct==1 else 'insufficient_delivered_context'
|
|
74
|
+
elif correct is None:label='unknown_answer_correctness'
|
|
75
|
+
elif correct==0:label='incorrect_despite_sufficient_context'
|
|
76
|
+
elif values.get('generation.response_relevance')==0:label='answer_not_relevant'
|
|
77
|
+
elif faithful is None:label='unknown_claim_support'
|
|
78
|
+
elif faithful<1:label='unsupported_answer'
|
|
79
|
+
elif citation_failed:label='citation_failure'
|
|
80
|
+
elif values.get('outcome.grounded_task_success')==1:label='grounded_success'
|
|
81
|
+
else:label='incomplete_success_evidence'
|
|
82
|
+
result['outcome']=label
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def grade_case(case,variant,cutoff):
|
|
87
|
+
trace=case['traces'][variant]
|
|
88
|
+
review=case['references'].get('reviews',{}).get(variant)
|
|
89
|
+
rows=ranking_metrics(case,trace,cutoff)+evidence_metrics(case,trace)+semantic_metrics(case,trace,review)+operational_metrics(case,trace)
|
|
90
|
+
for row in rows:
|
|
91
|
+
row['label_provenance']=review.get('provenance') if row['grader_type']!='deterministic' and review else {'kind':'synthetic' if trace['execution_mode']=='synthetic_replay' else 'owner_supplied_reference'}
|
|
92
|
+
return {'case_id':case['id'],'suite_id':case['suite_id'],'cluster_id':case['cluster_id'],
|
|
93
|
+
'variant':variant,'trace_id':trace['id'],'trace_digest':digest(trace),
|
|
94
|
+
'execution_mode':trace['execution_mode'],'metrics':rows,
|
|
95
|
+
'diagnosis':diagnose(case,trace,rows,review)}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def aggregate(rows):
|
|
99
|
+
grouped=defaultdict(list)
|
|
100
|
+
for case in rows:
|
|
101
|
+
for row in case['metrics']:grouped[metric_key(row)].append(row)
|
|
102
|
+
results={}
|
|
103
|
+
for key,records in grouped.items():
|
|
104
|
+
available=[r for r in records if r['value'] is not None]
|
|
105
|
+
excluded=[r for r in records if r['status']=='unavailable_zero_population']
|
|
106
|
+
missing=len(records)-len(available)-len(excluded)
|
|
107
|
+
observed=statistics.fmean(r['value'] for r in available) if available else None
|
|
108
|
+
sample=records[0]
|
|
109
|
+
results[key]={'metric_id':sample['metric_id'],'stage':sample['stage'],'direction':sample['direction'],'unit':sample['unit'],
|
|
110
|
+
'value':observed if not missing else None,'observed_value':observed,
|
|
111
|
+
'scored_cases':len(available),'eligible_cases':len(records)-len(excluded),
|
|
112
|
+
'missing_cases':missing,'not_applicable_cases':len(excluded),'complete':not missing,
|
|
113
|
+
'aggregation':'macro mean of case metrics; diagnostic cohort only',
|
|
114
|
+
'reason_counts':dict(Counter(r['reason'] for r in records if r['reason']))}
|
|
115
|
+
return results
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _conditional(cases,variant,name,eligible,score):
|
|
119
|
+
n=0;total=0;missing=0;unknown_eligibility=0
|
|
120
|
+
for case in cases:
|
|
121
|
+
trace=case['traces'][variant];review=case['references'].get('reviews',{}).get(variant)
|
|
122
|
+
selected=eligible(case,trace,review)
|
|
123
|
+
if selected is None:unknown_eligibility+=1;continue
|
|
124
|
+
if not selected:continue
|
|
125
|
+
total+=1
|
|
126
|
+
value=score(case,trace,review)
|
|
127
|
+
if value is None:missing+=1
|
|
128
|
+
else:n+=value
|
|
129
|
+
observed_denominator=total-missing
|
|
130
|
+
observed=n/observed_denominator if observed_denominator else None
|
|
131
|
+
complete=not missing and not unknown_eligibility
|
|
132
|
+
return {'metric_id':name,'value':observed if complete else None,'observed_value':observed,
|
|
133
|
+
'numerator':n,'denominator':total,'scored_cases':observed_denominator,
|
|
134
|
+
'missing_cases':missing,'unknown_eligibility_cases':unknown_eligibility,
|
|
135
|
+
'complete':complete,'status':'available' if complete and total else 'unavailable_incomplete_or_zero_population'}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def conditional_metrics(cases,variant):
|
|
139
|
+
def suff(review,stage='prompt'):
|
|
140
|
+
return review.get('context_sufficiency',{}).get(stage) if review else None
|
|
141
|
+
def correct(case,trace,review):
|
|
142
|
+
if not review or trace['response']['status']!='ok' or trace['response']['abstained'] is None:return None
|
|
143
|
+
return 0 if trace['response']['abstained'] is True else review.get('answer_correct')
|
|
144
|
+
def unsupported(case,trace,review):
|
|
145
|
+
if trace['response']['status']!='ok':return None
|
|
146
|
+
if not review or review.get('claims_exhaustive') is not True or trace['stages']['prompt'] is None:return None
|
|
147
|
+
claims=review.get('claims',[])
|
|
148
|
+
if not claims:return 0 if trace['response']['abstained'] is True else None
|
|
149
|
+
if any(c.get('supported_by_context') is False for c in claims):return 1
|
|
150
|
+
if any(c.get('supported_by_context') is None for c in claims):return None
|
|
151
|
+
return 0
|
|
152
|
+
return [
|
|
153
|
+
_conditional(cases,variant,'context.retrieval_sufficiency_recall',lambda c,t,r:c['references'].get('corpus_answerable'),lambda c,t,r:suff(r,'retrieved')),
|
|
154
|
+
_conditional(cases,variant,'generation.success_given_sufficient_context',lambda c,t,r:suff(r),correct),
|
|
155
|
+
_conditional(cases,variant,'generation.unsupported_answer_given_insufficient_context',lambda c,t,r:None if suff(r) is None else not suff(r),unsupported),
|
|
156
|
+
_conditional(cases,variant,'outcome.abstention_recall',lambda c,t,r:r.get('abstention_expected') if r else None,lambda c,t,r:t['response']['abstained'] if t['response']['status']=='ok' else None),
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def paired_comparisons(cases,left,right,samples=1000,seed=17,*,allow_bootstrap=True,interval_scope=None):
|
|
161
|
+
by_left={r['case_id']:r for r in left};by_right={r['case_id']:r for r in right}
|
|
162
|
+
a=aggregate(left);b=aggregate(right);results=[]
|
|
163
|
+
for key in a:
|
|
164
|
+
pairs=[];excluded=0
|
|
165
|
+
for case in cases:
|
|
166
|
+
ar=next(r for r in by_left[case['id']]['metrics'] if metric_key(r)==key)
|
|
167
|
+
br=next(r for r in by_right[case['id']]['metrics'] if metric_key(r)==key)
|
|
168
|
+
if ar['value'] is not None and br['value'] is not None:
|
|
169
|
+
pairs.append({'case_id':case['id'],'cluster_id':case['cluster_id'],
|
|
170
|
+
'baseline':ar['value'],'candidate':br['value'],'delta':br['value']-ar['value']})
|
|
171
|
+
elif ar['status']=='unavailable_zero_population' and br['status']=='unavailable_zero_population':excluded+=1
|
|
172
|
+
clusters=defaultdict(list)
|
|
173
|
+
for pair in pairs:clusters[pair['cluster_id']].append(pair['delta'])
|
|
174
|
+
keys=sorted(clusters);rng=random.Random(seed);draws=[]
|
|
175
|
+
if allow_bootstrap and len(keys)>=2 and samples:
|
|
176
|
+
for _ in range(samples):
|
|
177
|
+
chosen=[d for group in rng.choices(keys,k=len(keys)) for d in clusters[group]]
|
|
178
|
+
draws.append(statistics.fmean(chosen))
|
|
179
|
+
draws.sort()
|
|
180
|
+
interval=[draws[int(.025*(len(draws)-1))],draws[int(.975*(len(draws)-1))]] if draws else None
|
|
181
|
+
missing_pairs=len(cases)-len(pairs)-excluded
|
|
182
|
+
observed_delta=statistics.fmean(p['delta'] for p in pairs) if pairs else None
|
|
183
|
+
task_outcome=a[key]['metric_id'] in ('outcome.task_success','outcome.grounded_task_success')
|
|
184
|
+
row={'key':key,**a[key],'baseline':a[key],'candidate':b[key],
|
|
185
|
+
'paired_delta':observed_delta if not missing_pairs else None,
|
|
186
|
+
'observed_subset_delta':observed_delta,
|
|
187
|
+
'paired_cases':len(pairs),'excluded_zero_population_pairs':excluded,
|
|
188
|
+
'missing_pairs':missing_pairs,'eligible_pairs':len(cases)-excluded,
|
|
189
|
+
'paired_complete':not missing_pairs,'independent_clusters':len(keys),
|
|
190
|
+
'development_cluster_bootstrap_95':interval if not missing_pairs else None,
|
|
191
|
+
'observed_subset_bootstrap_95':interval,'bootstrap_samples':samples if allow_bootstrap else 0,
|
|
192
|
+
'requested_bootstrap_samples':samples,'seed':seed,'bootstrap_allowed':allow_bootstrap,
|
|
193
|
+
'interval_scope':interval_scope or 'Synthetic diagnostic illustration; not population performance evidence. Primary paired claims require all eligible pairs.',
|
|
194
|
+
'observed_subset_scope':'Only complete pairs are included; missing eligible pairs can bias this diagnostic subset.',
|
|
195
|
+
'improved_cases':[p['case_id'] for p in pairs if p['delta']*(1 if a[key]['direction']=='higher' else -1)>0],
|
|
196
|
+
'worsened_cases':[p['case_id'] for p in pairs if p['delta']*(1 if a[key]['direction']=='higher' else -1)<0],
|
|
197
|
+
'task_rescued_cases':[p['case_id'] for p in pairs if task_outcome and p['baseline']==0 and p['candidate']==1],
|
|
198
|
+
'task_regressed_cases':[p['case_id'] for p in pairs if task_outcome and p['baseline']==1 and p['candidate']==0]}
|
|
199
|
+
results.append(row)
|
|
200
|
+
return results
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def evaluate(data,cutoff=3,bootstrap_samples=500):
|
|
204
|
+
validate_dataset(data)
|
|
205
|
+
if type(bootstrap_samples) is not int or not 0<=bootstrap_samples<=5000:raise ValueError('bootstrap_samples must be 0–5000')
|
|
206
|
+
cases=data['cases'];baseline=[grade_case(c,'baseline',cutoff) for c in cases];candidate=[grade_case(c,'candidate',cutoff) for c in cases]
|
|
207
|
+
all_rows=baseline+candidate
|
|
208
|
+
modes={trace['execution_mode'] for case in cases for trace in case['traces'].values()}
|
|
209
|
+
all_synthetic=modes=={'synthetic_replay'}
|
|
210
|
+
provenance=data.get('provenance') if type(data.get('provenance')) is dict else {}
|
|
211
|
+
independence_declared=provenance.get('independent_clusters_verified') is True
|
|
212
|
+
allow_bootstrap=all_synthetic or independence_declared
|
|
213
|
+
if all_synthetic:
|
|
214
|
+
cohort='synthetic diagnostic cases; not representative production traffic'
|
|
215
|
+
interval_scope='Synthetic diagnostic illustration; not population performance evidence. Primary paired claims require all eligible pairs.'
|
|
216
|
+
independence_basis='Synthetic fixture clusters; arithmetic illustration only.'
|
|
217
|
+
elif not allow_bootstrap:
|
|
218
|
+
cohort='mixed synthetic and recorded replay diagnostics' if len(modes)>1 else 'recorded replay diagnostics; representativeness not verified'
|
|
219
|
+
interval_scope='Cluster independence is unknown for recorded traces; no bootstrap interval is computed.'
|
|
220
|
+
independence_basis='No explicit independent-cluster declaration; inference disabled.'
|
|
221
|
+
else:
|
|
222
|
+
cohort='mixed synthetic and recorded replay diagnostics' if len(modes)>1 else 'recorded replay diagnostics; representativeness not verified'
|
|
223
|
+
interval_scope='Independent clusters are caller asserted, not verified. Development diagnostic interval only; not validated population performance evidence. Primary paired claims require all eligible pairs.'
|
|
224
|
+
independence_basis='Caller asserted independent_clusters_verified=true; the prototype does not verify this assertion.'
|
|
225
|
+
return {'schema_version':'sphynx.rag-diagnostics.report.v1','metric_version':'sphynx.rag-diagnostics.metric.v1',
|
|
226
|
+
'generated_at':datetime.now(timezone.utc).isoformat(),'dataset_release':data['release'],
|
|
227
|
+
'dataset_digest':digest(data),'source':'local saved-trace grading; no candidate execution',
|
|
228
|
+
'cohort':cohort,'execution_modes':sorted(modes),
|
|
229
|
+
'bootstrap_independence':{'allowed':allow_bootstrap,'basis':independence_basis,'performance_inference_validated':False},
|
|
230
|
+
'cutoff':cutoff,'case_count':len(cases),'suite_count':len(data['suites']),
|
|
231
|
+
'metric_definition_count':len({r['metric_id'] for c in all_rows for r in c['metrics']}),
|
|
232
|
+
'stage_metric_view_count':len(aggregate(baseline)),
|
|
233
|
+
'suites':data['suites'],'cases':cases,'case_results':all_rows,
|
|
234
|
+
'aggregates':{'baseline':aggregate(baseline),'candidate':aggregate(candidate)},
|
|
235
|
+
'conditional':{v:conditional_metrics(cases,v) for v in ('baseline','candidate')},
|
|
236
|
+
'comparisons':paired_comparisons(cases,baseline,candidate,bootstrap_samples,
|
|
237
|
+
allow_bootstrap=allow_bootstrap,interval_scope=interval_scope),
|
|
238
|
+
'diagnosis_counts':{v:dict(Counter(r['diagnosis']['outcome'] for r in (baseline if v=='baseline' else candidate))) for v in ('baseline','candidate')},
|
|
239
|
+
'limitations':['Semantic judgments are supplied synthetic/adjudicated labels, not automated judges.',
|
|
240
|
+
'No imported public benchmark or customer performance claim follows from these fixtures.',
|
|
241
|
+
'Unjudged documents remain unknown; known-qrel ranking uses zero gain without removing their ranks.',
|
|
242
|
+
'First observed evidence loss is descriptive, not proof of causality.',
|
|
243
|
+
'Recorded telemetry is separate from replay grading time; no verified cost accounting.']}
|