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/_pr_client.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Durable PR operation handles over the existing authenticated transport."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any, TYPE_CHECKING
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
import math
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from .client import HostedClientError, _retry_key
|
|
10
|
+
from .pr_evaluations import PrEvaluationRequest, PrEvaluationStatus, PrEvaluationResult, _saved
|
|
11
|
+
from ._analytical_values import identifier, require, MetricProjectionSummary, ReportProjectionSummary, ComparisonProjectionSummary
|
|
12
|
+
from .analytical_views import ProjectionPage
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .client import Client
|
|
16
|
+
from .diagnostics import Diagnostic
|
|
17
|
+
from .progress import ProgressView
|
|
18
|
+
from .pr_evaluations import PrCaseChangePage
|
|
19
|
+
|
|
20
|
+
_READ_RETRY={'hosted_transport_failed','hosted_request_failed','service_unavailable','store_unavailable',
|
|
21
|
+
'projection_unavailable','pr_evaluation_conflict','result_not_ready','evaluation_projection_pending'}
|
|
22
|
+
_PARSE_ERRORS=(ValueError,TypeError,KeyError,AttributeError,RecursionError,UnicodeError)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PrEvaluationError(HostedClientError):
|
|
26
|
+
"""Keep the original request/key or known handle after an uncertain response."""
|
|
27
|
+
def __init__(self,cause:HostedClientError,*,pr_evaluation_id:str|None=None,request:PrEvaluationRequest|None=None,
|
|
28
|
+
idempotency_key:str|None=None,handle:PrEvaluation|None=None,submission:bool=False,operation_failed:bool=False):
|
|
29
|
+
self.pr_evaluation_id,self.request,self.idempotency_key=pr_evaluation_id,request,idempotency_key
|
|
30
|
+
self._submission=submission
|
|
31
|
+
self._operation_failed=operation_failed
|
|
32
|
+
message=('PR submission needs reconciliation; retain the original request and key' if submission
|
|
33
|
+
else 'PR evaluation failed; read result() on the original handle to inspect retained coverage' if operation_failed
|
|
34
|
+
else 'PR evaluation read did not complete; retain the original handle and inspect its status')
|
|
35
|
+
super().__init__(cause.code,message,handle=handle)
|
|
36
|
+
@property
|
|
37
|
+
def diagnostic(self)->Diagnostic:
|
|
38
|
+
from .diagnostics import Diagnostic
|
|
39
|
+
recovery={} if self.pr_evaluation_id is None else {'record_id':self.pr_evaluation_id}
|
|
40
|
+
if self._operation_failed:
|
|
41
|
+
return Diagnostic(self.code,'The PR operation failed; its permitted results remain readable.',
|
|
42
|
+
('Read result() on the same handle to inspect coverage and errors before choosing a new operation.',),
|
|
43
|
+
retryability='fix_required',recovery=recovery)
|
|
44
|
+
if self._submission:
|
|
45
|
+
return Diagnostic(self.code,'The PR submission outcome needs reconciliation.',
|
|
46
|
+
('Replay the original immutable request with the original idempotency key; retain any returned handle.',),
|
|
47
|
+
retryability='reconcile_before_retry',recovery=recovery)
|
|
48
|
+
if self.code in _READ_RETRY or self.code=='wait_timeout':
|
|
49
|
+
return Diagnostic(self.code,'Read the original PR evaluation handle again.',
|
|
50
|
+
('Resume waiting or read result() on the same handle; use get_pr_evaluation() after restarting.',),
|
|
51
|
+
retryability='read_original_handle',recovery=recovery)
|
|
52
|
+
return Diagnostic(self.code,'The selected PR evaluation needs an access or input check.',
|
|
53
|
+
('Inspect the retained handle; have the operator check current access and private profile admission.',),
|
|
54
|
+
retryability='fix_required',recovery=recovery)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _invalid()->HostedClientError:
|
|
58
|
+
return HostedClientError('invalid_server_response','the PR operation response could not be verified')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _path(identity:str)->str:return '/v1/pr-evaluations/'+_saved(identity,'preval')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _budget(deadline:float|None)->None:
|
|
65
|
+
if deadline is not None and time.monotonic()>=deadline:raise HostedClientError('wait_timeout','the read budget expired')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _bound(previous:PrEvaluationStatus,fresh:PrEvaluationStatus)->None:
|
|
69
|
+
require((fresh.id,fresh.request,fresh.template,fresh.expected_children,fresh.created_at,fresh.deadline_at)==
|
|
70
|
+
(previous.id,previous.request,previous.template,previous.expected_children,previous.created_at,previous.deadline_at))
|
|
71
|
+
def roster(status):
|
|
72
|
+
return {r.requirement_id:(r.suite_id,r.scope,r.mode,r.bank_id,r.release_id,r.cohort) for r in status.requirements}
|
|
73
|
+
require(roster(previous)==roster(fresh))
|
|
74
|
+
if previous.revisions is not None:require((previous.revisions,previous.plan_digest)==(fresh.revisions,fresh.plan_digest))
|
|
75
|
+
if previous.decision_id is not None:require(fresh.decision_id==previous.decision_id)
|
|
76
|
+
if previous.status in ('completed','failed'):require(previous.status==fresh.status)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _status(client:Client,identity:str,*,handle:PrEvaluation|None=None,deadline:float|None=None)->PrEvaluationStatus:
|
|
80
|
+
try:
|
|
81
|
+
try:
|
|
82
|
+
_budget(deadline)
|
|
83
|
+
options={} if deadline is None else {'deadline':deadline}
|
|
84
|
+
status=PrEvaluationStatus.from_dict(client._request('GET',_path(identity),**options))
|
|
85
|
+
require(status.id==identity)
|
|
86
|
+
if handle is not None:_bound(handle._status,status)
|
|
87
|
+
return status
|
|
88
|
+
except _PARSE_ERRORS:raise _invalid() from None
|
|
89
|
+
except HostedClientError as error:
|
|
90
|
+
raise PrEvaluationError(error,pr_evaluation_id=identity,handle=handle) from None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def submit(client:Client,request:PrEvaluationRequest,*,idempotency_key:str|None=None)->PrEvaluation:
|
|
94
|
+
require(isinstance(request,PrEvaluationRequest),'submit a typed PrEvaluationRequest')
|
|
95
|
+
original=PrEvaluationRequest.from_dict(request.to_dict());key=_retry_key(idempotency_key)
|
|
96
|
+
body={'schema_version':'sphynx.pr-evaluation-submission.v1','request':original.to_dict(),'idempotency_key':key}
|
|
97
|
+
try:
|
|
98
|
+
raw=client._request('POST','/v1/pr-evaluations',body,retry_key=key)
|
|
99
|
+
try:
|
|
100
|
+
status=PrEvaluationStatus.from_dict(raw);require(status.request==original)
|
|
101
|
+
except _PARSE_ERRORS:raise _invalid() from None
|
|
102
|
+
return PrEvaluation(client,status,idempotency_key=key)
|
|
103
|
+
except HostedClientError as error:
|
|
104
|
+
raise PrEvaluationError(error,request=original,idempotency_key=key,submission=True) from None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get(client:Client,identity:str)->PrEvaluation:
|
|
108
|
+
_saved(identity,'preval');return PrEvaluation(client,_status(client,identity))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class PrEvaluation:
|
|
112
|
+
"""One durable private PR operation. Polling never submits candidate work."""
|
|
113
|
+
def __init__(self,client:Client,status:PrEvaluationStatus,*,idempotency_key:str|None=None):
|
|
114
|
+
require(isinstance(status,PrEvaluationStatus))
|
|
115
|
+
self._client,self._status,self.id,self.idempotency_key=client,status,status.id,idempotency_key
|
|
116
|
+
self._last_result:PrEvaluationResult|None=None
|
|
117
|
+
@property
|
|
118
|
+
def status(self)->str:return self._status.status
|
|
119
|
+
@property
|
|
120
|
+
def request(self)->PrEvaluationRequest:return self._status.request
|
|
121
|
+
@property
|
|
122
|
+
def observation(self)->PrEvaluationStatus:return self._status
|
|
123
|
+
@property
|
|
124
|
+
def progress(self)->ProgressView:
|
|
125
|
+
from .progress import _progress
|
|
126
|
+
return _progress('submission',self.status,{'submission_id':self.id})
|
|
127
|
+
def refresh(self)->PrEvaluation:
|
|
128
|
+
return self._refresh()
|
|
129
|
+
def _refresh(self,deadline:float|None=None)->PrEvaluation:
|
|
130
|
+
fresh=_status(self._client,self.id,handle=self,deadline=deadline)
|
|
131
|
+
self._status=fresh;return self
|
|
132
|
+
def result(self)->PrEvaluationResult:
|
|
133
|
+
"""Read finished reports or terminal coverage without executing work."""
|
|
134
|
+
return self._result()
|
|
135
|
+
def _result(self,deadline:float|None=None)->PrEvaluationResult:
|
|
136
|
+
try:
|
|
137
|
+
options={} if deadline is None else {'deadline':deadline}
|
|
138
|
+
_budget(deadline)
|
|
139
|
+
raw=self._client._request('GET',_path(self.id)+'/result',**options)
|
|
140
|
+
try:
|
|
141
|
+
result=PrEvaluationResult.from_dict(raw);_bound(self._status,result.operation)
|
|
142
|
+
except _PARSE_ERRORS:raise _invalid() from None
|
|
143
|
+
self._status,self._last_result=result.operation,result
|
|
144
|
+
return result
|
|
145
|
+
except HostedClientError as error:raise PrEvaluationError(error,pr_evaluation_id=self.id,handle=self) from None
|
|
146
|
+
def projection(self,requirement_id:str)->ProjectionPage:
|
|
147
|
+
"""Read one authorized existing v2 projection; it grants no acceptance."""
|
|
148
|
+
requirement=next((r for r in self._status.requirements if r.requirement_id==identifier(requirement_id)),None)
|
|
149
|
+
require(requirement is not None,'requirement_id must belong to the original operation')
|
|
150
|
+
assert requirement is not None
|
|
151
|
+
try:
|
|
152
|
+
raw=self._client._request('GET',_path(self.id)+'/requirements/'+quote(requirement_id,safe='')+'/projection')
|
|
153
|
+
try:
|
|
154
|
+
page=ProjectionPage.from_dict(raw)
|
|
155
|
+
require(page.selection is None and len(page.items)==1 and
|
|
156
|
+
(page.bank_id,page.release_id,page.cohort_id)==(requirement.bank_id,requirement.release_id,requirement.cohort.identity_id))
|
|
157
|
+
if page.items[0].availability=='available':
|
|
158
|
+
reports=[r.summary for r in page.rows if r.row_kind=='report'];metrics=[r.summary for r in page.rows if r.row_kind=='metric']
|
|
159
|
+
comparisons=[r.summary for r in page.rows if r.row_kind=='comparison']
|
|
160
|
+
require(len(reports)==1 and len(comparisons)<=1 and len(metrics)+len(comparisons)+1==len(page.rows) and isinstance(reports[0],ReportProjectionSummary))
|
|
161
|
+
report=reports[0];assert isinstance(report,ReportProjectionSummary)
|
|
162
|
+
require((report.scope,report.mode)==(requirement.scope,requirement.mode))
|
|
163
|
+
typed=[m for m in metrics if isinstance(m,MetricProjectionSummary)];require(len(typed)==len(metrics))
|
|
164
|
+
require(len({m.metric_id for m in typed})==len(typed) and all((m.run_id,m.grading_id)==(report.run_id,report.grading_id) for m in typed))
|
|
165
|
+
require(all(isinstance(c,ComparisonProjectionSummary) and c.candidate_grading_id==report.grading_id for c in comparisons))
|
|
166
|
+
if self._last_result is not None:
|
|
167
|
+
observed=next(r for r in self._last_result.requirements if r.status.requirement_id==requirement_id)
|
|
168
|
+
if observed.report is not None:
|
|
169
|
+
proof=observed.report.projection;require(page.team_id==observed.report.team_id and
|
|
170
|
+
(page.items[0].event_id,page.items[0].source_digest)==(proof.event_id,proof.source_digest))
|
|
171
|
+
if page.items[0].availability=='available':
|
|
172
|
+
require(report==observed.report.report)
|
|
173
|
+
require({m.metric_id:m for m in typed}=={m.metric_id:m for m in observed.report.metrics})
|
|
174
|
+
if observed.comparison.regressions is not None:
|
|
175
|
+
require(len(comparisons)==1 and isinstance(comparisons[0],ComparisonProjectionSummary)
|
|
176
|
+
and comparisons[0].artifact_digest==observed.comparison.regressions.comparison_digest)
|
|
177
|
+
return page
|
|
178
|
+
except _PARSE_ERRORS:raise _invalid() from None
|
|
179
|
+
except HostedClientError as error:raise PrEvaluationError(error,pr_evaluation_id=self.id,handle=self) from None
|
|
180
|
+
def case_changes(self,requirement_id:str,*,cursor:str|None=None,limit:int=100)->PrCaseChangePage:
|
|
181
|
+
"""Read case scores from a committed comparison. This starts no work."""
|
|
182
|
+
from .pr_evaluations import PrCaseChangePage, _case_cursor
|
|
183
|
+
require(any(r.requirement_id==identifier(requirement_id) for r in self._status.requirements),
|
|
184
|
+
'requirement_id must belong to the original operation')
|
|
185
|
+
require(type(limit) is int and 1<=limit<=500,'limit must be an integer from 1 to 500')
|
|
186
|
+
query='?limit='+str(limit)
|
|
187
|
+
if cursor is not None:query+='&cursor='+quote(_case_cursor(cursor),safe='')
|
|
188
|
+
try:
|
|
189
|
+
raw=self._client._request('GET',_path(self.id)+'/requirements/'+quote(requirement_id,safe='')+'/case-changes'+query)
|
|
190
|
+
try:
|
|
191
|
+
page=PrCaseChangePage.from_dict(raw)
|
|
192
|
+
require((page.plan_id,page.requirement_id)==(self.id,requirement_id) and len(page.items)<=limit)
|
|
193
|
+
if cursor is None:require(page.offset==0)
|
|
194
|
+
if self._last_result is not None:
|
|
195
|
+
observed=next(r for r in self._last_result.requirements if r.status.requirement_id==requirement_id)
|
|
196
|
+
require(observed.report is not None and observed.comparison.regressions is not None)
|
|
197
|
+
assert observed.report is not None and observed.comparison.regressions is not None
|
|
198
|
+
require((page.source_digest,page.comparison_digest,page.total)==(observed.report.projection.source_digest,
|
|
199
|
+
observed.comparison.regressions.comparison_digest,observed.comparison.regressions.case_change_count))
|
|
200
|
+
return page
|
|
201
|
+
except _PARSE_ERRORS:raise _invalid() from None
|
|
202
|
+
except HostedClientError as error:raise PrEvaluationError(error,pr_evaluation_id=self.id,handle=self) from None
|
|
203
|
+
def wait(self,*,timeout:float=900,poll_interval:float=2,on_progress:Callable[[str],None]|None=None)->PrEvaluationResult:
|
|
204
|
+
"""Wait on this handle; socket/retry phases use the remaining read budget."""
|
|
205
|
+
return self._wait(timeout=timeout,poll_interval=poll_interval,on_progress=on_progress,reports_only=False)
|
|
206
|
+
def wait_for_reports(self,*,timeout:float=900,poll_interval:float=2,on_progress:Callable[[str],None]|None=None)->PrEvaluationResult:
|
|
207
|
+
"""Read completed required reports while human review or policy is pending.
|
|
208
|
+
|
|
209
|
+
A returned report is not an acceptance decision. Use current_acceptance
|
|
210
|
+
and the bank review status in the returned result for that distinction.
|
|
211
|
+
"""
|
|
212
|
+
return self._wait(timeout=timeout,poll_interval=poll_interval,on_progress=on_progress,reports_only=True)
|
|
213
|
+
def _wait(self,*,timeout:float,poll_interval:float,on_progress:Callable[[str],None]|None,reports_only:bool)->PrEvaluationResult:
|
|
214
|
+
for value in (timeout,poll_interval):
|
|
215
|
+
if type(value) not in (int,float) or not math.isfinite(value) or value<=0:raise ValueError('timeout and poll_interval must be finite positive seconds')
|
|
216
|
+
deadline=time.monotonic()+timeout;previous=None
|
|
217
|
+
while True:
|
|
218
|
+
if self.status!=previous:
|
|
219
|
+
if on_progress is not None:on_progress(self.status)
|
|
220
|
+
previous=self.status
|
|
221
|
+
if self.status=='failed':
|
|
222
|
+
code=self._status.error.code if self._status.error is not None else 'pr_evaluation_failed'
|
|
223
|
+
raise PrEvaluationError(HostedClientError(code,'PR evaluation failed; inspect result() for retained coverage'),pr_evaluation_id=self.id,handle=self,operation_failed=True)
|
|
224
|
+
ready=bool(self._status.requirements) and all(r.state in ('completed','incomplete') for r in self._status.requirements)
|
|
225
|
+
if self.status=='completed' or reports_only and ready:
|
|
226
|
+
try:return self._result(deadline)
|
|
227
|
+
except PrEvaluationError as error:
|
|
228
|
+
if error.code not in _READ_RETRY:raise
|
|
229
|
+
remaining=deadline-time.monotonic()
|
|
230
|
+
if remaining<=0:raise PrEvaluationError(HostedClientError('wait_timeout','waiting expired'),pr_evaluation_id=self.id,handle=self)
|
|
231
|
+
time.sleep(min(poll_interval,remaining))
|
|
232
|
+
try:self._refresh(deadline)
|
|
233
|
+
except PrEvaluationError as error:
|
|
234
|
+
if error.code not in _READ_RETRY:raise
|
|
235
|
+
if time.monotonic()>=deadline:raise PrEvaluationError(HostedClientError('wait_timeout','waiting expired'),pr_evaluation_id=self.id,handle=self) from None
|
|
236
|
+
def to_dict(self)->dict[str,Any]:return self._status.to_dict()
|
|
237
|
+
def __repr__(self)->str:return f'PrEvaluation(id={self.id!r}, status={self.status!r})'
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Offline judge audit and gated PPI arithmetic over supplied labels.
|
|
2
|
+
|
|
3
|
+
This module does not run a model or check that a declared sampling design is
|
|
4
|
+
true. Descriptive audit scores apply only to the supplied records. PPI output is
|
|
5
|
+
an arithmetic demonstration, never an automatically validated performance claim
|
|
6
|
+
or ARES parity result.
|
|
7
|
+
|
|
8
|
+
``judge_audit`` rows contain ``id``, ``human_label`` (bool or None),
|
|
9
|
+
``judge_label`` (bool or None) and ``judge_status`` (judged, abstained, missing).
|
|
10
|
+
An omitted status means judged for a boolean label and missing otherwise.
|
|
11
|
+
|
|
12
|
+
``ppi_mean`` uses a separate prediction cohort and labeled audit cohort. Rows
|
|
13
|
+
contain ``id``, ``cluster_id``, numeric ``prediction`` in [0, 1], and the same
|
|
14
|
+
frozen ``predictor_digest``. Audit rows also need a boolean ``human_label``.
|
|
15
|
+
|
|
16
|
+
Required PPI declarations are ``predictor_digest`` (sha256:<64 hex digits>),
|
|
17
|
+
``predictor_frozen``, ``audit_representative``, ``prediction_representative``,
|
|
18
|
+
``same_target_population``, ``cohorts_independent``, ``cohorts_disjoint``,
|
|
19
|
+
``iid_rows``, ``audit_held_out_from_predictor_tuning`` (all True),
|
|
20
|
+
``target_population``, ``sampling_protocol``, ``predictor_model``,
|
|
21
|
+
``predictor_revision``, ``prompt_version`` and ``metric_version`` (nonempty strings), and
|
|
22
|
+
``provenance_kind`` (synthetic or independent_human_audit).
|
|
23
|
+
|
|
24
|
+
Repeated or shared cluster IDs need a different variance estimator and are not
|
|
25
|
+
accepted. Unknown labels, overlapping IDs and undeclared designs are not
|
|
26
|
+
silently filtered. At least two complete observations in each cohort are needed
|
|
27
|
+
for sample variances. Small synthetic samples still do not support inference.
|
|
28
|
+
|
|
29
|
+
Method source: https://arxiv.org/html/2301.09633v3 (independent-samples PPI
|
|
30
|
+
mean estimator). ARES: https://aclanthology.org/2024.naacl-long.20/ . This module
|
|
31
|
+
does not reproduce ARES judge training or establish finite-sample coverage.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import math
|
|
37
|
+
import re
|
|
38
|
+
from statistics import NormalDist, fmean, variance
|
|
39
|
+
from typing import Any, Mapping, Sequence
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CalibrationError(ValueError):
|
|
43
|
+
"""The supplied audit records do not follow the explicit label contract."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_DECLARATIONS = (
|
|
47
|
+
"predictor_frozen",
|
|
48
|
+
"audit_representative",
|
|
49
|
+
"prediction_representative",
|
|
50
|
+
"same_target_population",
|
|
51
|
+
"cohorts_independent",
|
|
52
|
+
"cohorts_disjoint",
|
|
53
|
+
"iid_rows",
|
|
54
|
+
"audit_held_out_from_predictor_tuning",
|
|
55
|
+
)
|
|
56
|
+
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _ratio(numerator: int, denominator: int, reason: str) -> dict[str, Any]:
|
|
60
|
+
return {
|
|
61
|
+
"value": numerator / denominator if denominator else None,
|
|
62
|
+
"numerator": numerator,
|
|
63
|
+
"denominator": denominator,
|
|
64
|
+
"status": "observed" if denominator else "undefined",
|
|
65
|
+
"reason": None if denominator else reason,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _rows(value: Sequence[Mapping[str, Any]]) -> list[Mapping[str, Any]]:
|
|
70
|
+
if not isinstance(value, (list, tuple)) or any(not isinstance(row, Mapping) for row in value):
|
|
71
|
+
raise CalibrationError("records_must_be_an_array_of_objects")
|
|
72
|
+
result = list(value)
|
|
73
|
+
ids = [row.get("id") for row in result]
|
|
74
|
+
if any(type(value) is not str or not value.strip() for value in ids):
|
|
75
|
+
raise CalibrationError("record_ids_must_be_nonempty_strings")
|
|
76
|
+
if len(set(ids)) != len(ids):
|
|
77
|
+
raise CalibrationError("duplicate_record_ids")
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def judge_audit(
|
|
82
|
+
records: Sequence[Mapping[str, Any]], *, provenance_kind: str = "unspecified"
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""Return descriptive confusion metrics with explicit missing populations.
|
|
85
|
+
|
|
86
|
+
Accuracy and disagreement use only decisive judge labels paired with known
|
|
87
|
+
human labels. Abstention and missing-judge rates use all supplied records.
|
|
88
|
+
Neither unknown human labels nor judge abstentions become negative labels.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
rows = _rows(records)
|
|
92
|
+
tp = fp = tn = fn = 0
|
|
93
|
+
missing_human = missing_judge = abstained = decisive = 0
|
|
94
|
+
for row in rows:
|
|
95
|
+
human, judge = row.get("human_label"), row.get("judge_label")
|
|
96
|
+
if human is not None and type(human) is not bool:
|
|
97
|
+
raise CalibrationError("human_label_must_be_boolean_or_null")
|
|
98
|
+
if judge is not None and type(judge) is not bool:
|
|
99
|
+
raise CalibrationError("judge_label_must_be_boolean_or_null")
|
|
100
|
+
status = row.get("judge_status", "judged" if judge is not None else "missing")
|
|
101
|
+
if status not in {"judged", "abstained", "missing"}:
|
|
102
|
+
raise CalibrationError("unsupported_judge_status")
|
|
103
|
+
if (status == "judged") != (judge is not None):
|
|
104
|
+
raise CalibrationError("judge_status_and_label_disagree")
|
|
105
|
+
missing_human += human is None
|
|
106
|
+
missing_judge += status == "missing"
|
|
107
|
+
abstained += status == "abstained"
|
|
108
|
+
decisive += status == "judged"
|
|
109
|
+
if human is None or judge is None:
|
|
110
|
+
continue
|
|
111
|
+
tp += human and judge
|
|
112
|
+
fp += not human and judge
|
|
113
|
+
tn += not human and not judge
|
|
114
|
+
fn += human and not judge
|
|
115
|
+
paired = tp + fp + tn + fn
|
|
116
|
+
count = len(rows)
|
|
117
|
+
metrics = {
|
|
118
|
+
"accuracy": _ratio(tp + tn, paired, "no_paired_decisive_judgments"),
|
|
119
|
+
"precision": _ratio(tp, tp + fp, "no_predicted_positives"),
|
|
120
|
+
"recall": _ratio(tp, tp + fn, "no_human_positives"),
|
|
121
|
+
"specificity": _ratio(tn, tn + fp, "no_human_negatives"),
|
|
122
|
+
"disagreement": _ratio(fp + fn, paired, "no_paired_decisive_judgments"),
|
|
123
|
+
"judge_abstention_rate": _ratio(abstained, count, "empty_audit"),
|
|
124
|
+
"judge_missing_rate": _ratio(missing_judge, count, "empty_audit"),
|
|
125
|
+
"human_label_coverage": _ratio(count - missing_human, count, "empty_audit"),
|
|
126
|
+
"paired_decisive_coverage": _ratio(paired, count, "empty_audit"),
|
|
127
|
+
}
|
|
128
|
+
chance_agreement = None
|
|
129
|
+
kappa = None
|
|
130
|
+
kappa_reason = "no_paired_decisive_judgments"
|
|
131
|
+
if paired:
|
|
132
|
+
chance_agreement = ((tp + fp) * (tp + fn) + (tn + fn) * (tn + fp)) / paired**2
|
|
133
|
+
if chance_agreement < 1:
|
|
134
|
+
kappa = ((tp + tn) / paired - chance_agreement) / (1 - chance_agreement)
|
|
135
|
+
kappa_reason = None
|
|
136
|
+
else:
|
|
137
|
+
kappa_reason = "constant_labels_make_chance_agreement_one"
|
|
138
|
+
metrics["cohen_kappa"] = {
|
|
139
|
+
"value": kappa,
|
|
140
|
+
"denominator": paired,
|
|
141
|
+
"expected_agreement": chance_agreement,
|
|
142
|
+
"status": "observed" if kappa is not None else "undefined",
|
|
143
|
+
"reason": kappa_reason,
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
"schema_version": "sphynx.rag-diagnostics.judge-audit.v1",
|
|
147
|
+
"scope": "supplied_records_descriptive_only",
|
|
148
|
+
"provenance_kind": provenance_kind,
|
|
149
|
+
"counts": {
|
|
150
|
+
"records": count,
|
|
151
|
+
"human_labeled": count - missing_human,
|
|
152
|
+
"missing_human_label": missing_human,
|
|
153
|
+
"judge_decisive": decisive,
|
|
154
|
+
"judge_abstained": abstained,
|
|
155
|
+
"missing_judge_label": missing_judge,
|
|
156
|
+
"paired_decisive": paired,
|
|
157
|
+
},
|
|
158
|
+
"confusion": {"true_positive": tp, "false_positive": fp, "true_negative": tn, "false_negative": fn},
|
|
159
|
+
"metrics": metrics,
|
|
160
|
+
"performance_inference_validated": False,
|
|
161
|
+
"ares_parity": False,
|
|
162
|
+
"notice": "Scores describe the supplied records. Unknown, abstained and missing labels are not negatives. Independent sampling and population transfer are not established.",
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _unavailable(reason: str, predictions: int, audit: int) -> dict[str, Any]:
|
|
167
|
+
return {
|
|
168
|
+
"schema_version": "sphynx.rag-diagnostics.ppi-mean-arithmetic.v1",
|
|
169
|
+
"status": "unavailable",
|
|
170
|
+
"reason": reason,
|
|
171
|
+
"prediction_count": predictions,
|
|
172
|
+
"audit_count": audit,
|
|
173
|
+
"raw_point_estimate": None,
|
|
174
|
+
"raw_normal_interval": None,
|
|
175
|
+
"performance_confidence_interval": None,
|
|
176
|
+
"performance_inference_validated": False,
|
|
177
|
+
"ares_parity": False,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def ppi_mean(
|
|
182
|
+
prediction_cohort: Sequence[Mapping[str, Any]],
|
|
183
|
+
audit_cohort: Sequence[Mapping[str, Any]],
|
|
184
|
+
*,
|
|
185
|
+
declarations: Mapping[str, Any] | None = None,
|
|
186
|
+
confidence: float = 0.95,
|
|
187
|
+
) -> dict[str, Any]:
|
|
188
|
+
"""Compute a raw rectified-mean example under declared disjoint IID design.
|
|
189
|
+
|
|
190
|
+
Point estimate = mean(prediction cohort) + mean(human - audit prediction).
|
|
191
|
+
Variance = s²(prediction cohort)/N + s²(audit residuals)/n.
|
|
192
|
+
|
|
193
|
+
The normal interval is intentionally unbounded and separately named. This
|
|
194
|
+
helper never clips the estimate or interval to [0, 1], never claims a finite
|
|
195
|
+
sample coverage guarantee, and never upgrades declarations into verified
|
|
196
|
+
independence. Synthetic output remains demonstration arithmetic.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
predictions, audit = _rows(prediction_cohort), _rows(audit_cohort)
|
|
201
|
+
except CalibrationError as error:
|
|
202
|
+
return _unavailable(str(error), len(prediction_cohort) if isinstance(prediction_cohort, (list, tuple)) else 0,
|
|
203
|
+
len(audit_cohort) if isinstance(audit_cohort, (list, tuple)) else 0)
|
|
204
|
+
unavailable = lambda reason: _unavailable(reason, len(predictions), len(audit))
|
|
205
|
+
if not isinstance(declarations, Mapping):
|
|
206
|
+
return unavailable("sampling_and_provenance_declarations_required")
|
|
207
|
+
missing = [name for name in _DECLARATIONS if declarations.get(name) is not True]
|
|
208
|
+
if missing:
|
|
209
|
+
return unavailable("sampling_declarations_not_affirmed:" + ",".join(missing))
|
|
210
|
+
for name in ("target_population", "sampling_protocol", "predictor_model",
|
|
211
|
+
"predictor_revision", "prompt_version", "metric_version"):
|
|
212
|
+
if type(declarations.get(name)) is not str or not declarations[name].strip():
|
|
213
|
+
return unavailable(name + "_required")
|
|
214
|
+
digest = declarations.get("predictor_digest")
|
|
215
|
+
if type(digest) is not str or _DIGEST.fullmatch(digest) is None:
|
|
216
|
+
return unavailable("frozen_predictor_digest_required")
|
|
217
|
+
if declarations.get("provenance_kind") not in {"synthetic", "independent_human_audit"}:
|
|
218
|
+
return unavailable("explicit_provenance_kind_required")
|
|
219
|
+
if type(confidence) not in (int, float) or not math.isfinite(confidence) or not 0 < confidence < 1:
|
|
220
|
+
return unavailable("invalid_confidence")
|
|
221
|
+
quantile_probability = 0.5 + confidence / 2
|
|
222
|
+
if not 0 < quantile_probability < 1:
|
|
223
|
+
return unavailable("normal_quantile_not_representable")
|
|
224
|
+
if len(predictions) < 2 or len(audit) < 2:
|
|
225
|
+
return unavailable("at_least_two_observations_per_cohort_required")
|
|
226
|
+
if {row["id"] for row in predictions} & {row["id"] for row in audit}:
|
|
227
|
+
return unavailable("cohorts_overlap")
|
|
228
|
+
clusters = []
|
|
229
|
+
for row in predictions + audit:
|
|
230
|
+
if row.get("predictor_digest") != digest:
|
|
231
|
+
return unavailable("predictor_identity_mismatch")
|
|
232
|
+
value = row.get("prediction")
|
|
233
|
+
if type(value) not in (int, float) or not math.isfinite(value) or not 0 <= value <= 1:
|
|
234
|
+
return unavailable("prediction_must_be_finite_probability")
|
|
235
|
+
cluster = row.get("cluster_id")
|
|
236
|
+
if type(cluster) is not str or not cluster.strip():
|
|
237
|
+
return unavailable("explicit_independent_cluster_ids_required")
|
|
238
|
+
clusters.append(cluster)
|
|
239
|
+
if len(set(clusters)) != len(clusters):
|
|
240
|
+
return unavailable("shared_or_repeated_clusters_need_different_variance")
|
|
241
|
+
if any(type(row.get("human_label")) is not bool for row in audit):
|
|
242
|
+
return unavailable("complete_boolean_human_audit_labels_required")
|
|
243
|
+
prediction_values = [float(row["prediction"]) for row in predictions]
|
|
244
|
+
residuals = [float(row["human_label"]) - float(row["prediction"]) for row in audit]
|
|
245
|
+
prediction_mean = fmean(prediction_values)
|
|
246
|
+
correction = fmean(residuals)
|
|
247
|
+
point = prediction_mean + correction
|
|
248
|
+
prediction_variance = variance(prediction_values) / len(predictions)
|
|
249
|
+
residual_variance = variance(residuals) / len(audit)
|
|
250
|
+
standard_error = math.sqrt(prediction_variance + residual_variance)
|
|
251
|
+
quantile = NormalDist().inv_cdf(quantile_probability)
|
|
252
|
+
synthetic = declarations["provenance_kind"] == "synthetic"
|
|
253
|
+
return {
|
|
254
|
+
"schema_version": "sphynx.rag-diagnostics.ppi-mean-arithmetic.v1",
|
|
255
|
+
"status": "arithmetic_only",
|
|
256
|
+
"reason": "synthetic_demonstration_no_performance_inference" if synthetic else "sampling_declarations_not_independently_verified",
|
|
257
|
+
"prediction_count": len(predictions),
|
|
258
|
+
"audit_count": len(audit),
|
|
259
|
+
"raw_prediction_mean": prediction_mean,
|
|
260
|
+
"raw_human_residual_correction": correction,
|
|
261
|
+
"raw_point_estimate": point,
|
|
262
|
+
"raw_normal_interval": {
|
|
263
|
+
"lower_unbounded": point - quantile * standard_error,
|
|
264
|
+
"upper_unbounded": point + quantile * standard_error,
|
|
265
|
+
"confidence": float(confidence),
|
|
266
|
+
"standard_error": standard_error,
|
|
267
|
+
"prediction_variance_component": prediction_variance,
|
|
268
|
+
"audit_residual_variance_component": residual_variance,
|
|
269
|
+
"method": "independent_disjoint_iid_normal_approximation",
|
|
270
|
+
"finite_sample_coverage_guarantee": False,
|
|
271
|
+
},
|
|
272
|
+
"performance_confidence_interval": None,
|
|
273
|
+
"performance_inference_validated": False,
|
|
274
|
+
"sampling_design_verified": False,
|
|
275
|
+
"synthetic_demonstration": synthetic,
|
|
276
|
+
"ares_parity": False,
|
|
277
|
+
"predictor_digest": digest,
|
|
278
|
+
"declared_predictor_identity": {
|
|
279
|
+
key: declarations[key]
|
|
280
|
+
for key in ("predictor_model", "predictor_revision", "prompt_version", "metric_version")
|
|
281
|
+
},
|
|
282
|
+
"declared_target_population": declarations["target_population"],
|
|
283
|
+
"declared_sampling_protocol": declarations["sampling_protocol"],
|
|
284
|
+
"notice": "Raw PPI mean arithmetic only. The normal approximation is not validated performance confidence. Synthetic or tiny samples do not establish population quality. Sampling, independent human review and frozen predictor provenance require separate verification.",
|
|
285
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Readable metric definitions for the local review surface."""
|
|
2
|
+
INFO={
|
|
3
|
+
'retrieval.recall':('Document recall',r'R_k / R','Known positive qrels found in the first k results / all known positive qrels. The full-reference denominator stays fixed when selection drops documents.'),
|
|
4
|
+
'retrieval.precision':('Document precision',r'R_k / k','Known positive qrels in the first k positions / k, including unfilled positions. Unjudged entries keep their original ranks.'),
|
|
5
|
+
'retrieval.average_precision':('Average precision',r'\frac{1}{R}\sum_{r=1}^{k}P@r\,\mathbf{1}[r\text{ relevant}]','AP per case. Macro-averaging eligible case AP values produces MAP. Denominator is all known positives, not min(R,k).'),
|
|
6
|
+
'retrieval.ndcg_linear':('nDCG · linear gain',r'\frac{\sum_{r=1}^{k}g_r/\log_2(r+1)}{\operatorname{IDCG}@k}','trec_eval-style linear relevance gain. This is a distinct metric from the existing Sphynx exponential-gain nDCG.'),
|
|
7
|
+
'retrieval.reciprocal_rank':('Reciprocal rank',r'1/r_{\mathrm{first\ relevant}}','Zero when no known positive appears by k. Macro-averaging the per-case values produces MRR.'),
|
|
8
|
+
'retrieval.unjudged_fraction':('Unjudged fraction',r'U_k / |L_{:k}|','Unknown judgments / returned items by k. An unknown contributes zero gain to the compatibility projection, but remains unknown, never a negative label.'),
|
|
9
|
+
'evidence.atom_recall':('Required-atom recall',r'|A_{\mathrm{covered}}|/|A_{\mathrm{required}}|','Coverage of distinct required evidence atoms at each recorded stage. This is an evidence proxy, not context sufficiency.'),
|
|
10
|
+
'evidence.relation_recall':('Required-relation recall',r'|E_{\mathrm{covered}}|/|E_{\mathrm{required}}|','Coverage of explicitly labeled required relations; independent from individual atom coverage.'),
|
|
11
|
+
'evidence.complete_required_atoms':('All required atoms',r'\mathbf{1}[A_{\mathrm{required}}\subseteq A_{\mathrm{covered}}]','One only if every required atom is covered. It does not establish independently judged sufficiency.'),
|
|
12
|
+
'context.sufficient':('Context sufficiency',r'\mathbf{1}[\text{reviewed context is sufficient}]','Independent question/context judgment for the exact saved stage. A plausible but stale answer can have sufficient context and still be wrong.'),
|
|
13
|
+
'generation.answer_correctness':('Answer correctness',r'\mathbf{1}[\text{answer is correct}]','Independent explicit review of the answer. This is not computed from phrase overlap or context faithfulness.'),
|
|
14
|
+
'generation.response_relevance':('Response relevance',r'\mathbf{1}[\text{answer addresses the question}]','Explicit reviewed relevance, not embedding similarity or keyword matching.'),
|
|
15
|
+
'generation.required_fact_coverage':('Required-fact coverage',r'|A_{\mathrm{expressed\ correctly}}|/|A_{\mathrm{required}}|','Unique required atoms linked to correct reviewed answer claims. Omissions and unresolved claim truth remain visible.'),
|
|
16
|
+
'generation.faithfulness':('Claim faithfulness',r'|C_{\mathrm{supported}}|/|C_{\mathrm{all}}|','Requires an exhaustive response claim audit and resolved context-support judgments. It says nothing by itself about source truth.'),
|
|
17
|
+
'generation.unsupported_claim_fraction':('Unsupported-claim fraction',r'|C_{\mathrm{unsupported}}|/|C_{\mathrm{all}}|','All response claims must be audited. Unknown support is unavailable; it is not counted as a supported or unsupported claim.'),
|
|
18
|
+
'generation.claim_correctness':('Claim correctness',r'|C_{\mathrm{correct}}|/|C_{\mathrm{all}}|','Claim truth reviewed independently of its support by retrieved material.'),
|
|
19
|
+
'citation.reference_validity':('Citation ID validity',r'|\mathrm{valid\ references}|/|\mathrm{references}|','Checks known document identities. It does not establish citation entailment.'),
|
|
20
|
+
'citation.necessity_precision':('Citation necessity precision',r'|\mathrm{supporting\ or\ necessary\ citations}|/|\mathrm{citations}|','ALCE-inspired: joint support must hold; a citation earns credit if it supports alone or its removal breaks joint support. All judgments are supplied labels.'),
|
|
21
|
+
'citation.completeness':('Citation completeness',r'|C_{\mathrm{jointly\ cited\ and\ supported}}|/|C_{\mathrm{citation\ required}}|','An uncited citation-required claim scores zero. No citations makes precision undefined, not perfect. Not official ALCE segmentation/model parity.'),
|
|
22
|
+
'outcome.abstention_correctness':('Abstention policy correctness',r'\mathbf{1}[\mathrm{actual}=\mathrm{expected}]','Expected abstention is an explicit independent label; source answerability is not substituted for it.'),
|
|
23
|
+
'outcome.task_success':('Task success',r'\mathbf{1}[\text{correct relevant answer or expected abstention}]','Conjunctive endpoint gate. A runtime failure counts as failure; missing semantic truth is unavailable.'),
|
|
24
|
+
'outcome.grounded_task_success':('Grounded task success',r'\mathbf{1}[\text{task success}\land\text{support}\land\text{required citations}]','Requires exhaustive faithfulness, required facts and citation requirements, or an explicitly appropriate abstention. No weighted overall RAG score.'),
|
|
25
|
+
'operational.recorded_latency_ms':('Recorded latency',r'\mathrm{saved\ latency}_{\mathrm{ms}}','Saved synthetic/component telemetry, never the elapsed time used to replay grading. This local view reports case values and their macro mean; it does not benchmark fresh inference.'),
|
|
26
|
+
'operational.recorded_cost_usd':('Recorded cost',r'\mathrm{saved\ USD}','Fixture/component-reported cost only. No pricing model or verified cost attestation is implemented.'),
|
|
27
|
+
'operational.recorded_tokens':('Recorded tokens',r'\mathrm{saved\ token\ count}','Fixture/component-reported count, not tokenization performed by the prototype.'),
|
|
28
|
+
'operational.failure':('Execution failure',r'\mathbf{1}[\mathrm{status}\ne\mathrm{ok}]','Explicit saved error/timeout status, including failed cases in the operational denominator.'),
|
|
29
|
+
'operational.timeout':('Execution timeout',r'\mathbf{1}[\mathrm{status}=\mathrm{timeout}]','A timeout is an execution failure, not an empty successful response.'),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def describe(metric_id):
|
|
34
|
+
base=metric_id.split('@')[0]
|
|
35
|
+
if base.startswith('context.sufficient.'):
|
|
36
|
+
base='context.sufficient'
|
|
37
|
+
title,formula,meaning=INFO.get(base,(metric_id,'','See the recorded numerator, denominator and unavailable reason.'))
|
|
38
|
+
if '@' in metric_id:title+=' @'+metric_id.split('@')[1]
|
|
39
|
+
return {'title':title,'formula':formula,'meaning':meaning}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Shared wire records; no model/provider or Sphynx imports."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
VERSION = 'sphynx.rag-diagnostics.metric.v1'
|
|
8
|
+
STAGES = ('indexed', 'retrieved', 'reranked', 'selected', 'prompt')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def canonical(value):
|
|
12
|
+
return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False, allow_nan=False).encode('utf-8')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def digest(value):
|
|
16
|
+
return hashlib.sha256(canonical(value)).hexdigest()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def metric(metric_id, case_id, stage, value=None, *, numerator=None,
|
|
20
|
+
denominator=None, status=None, reason=None, grader_type='deterministic',
|
|
21
|
+
evidence_refs=None, population='this case', direction='higher', unit='ratio'):
|
|
22
|
+
return {'metric_id': metric_id, 'metric_version': VERSION, 'case_id': case_id,
|
|
23
|
+
'stage': stage, 'value': value, 'numerator': numerator,
|
|
24
|
+
'denominator': denominator, 'status': status or ('available' if value is not None else 'unavailable_missing_input'),
|
|
25
|
+
'reason': reason, 'grader_type': grader_type, 'evidence_refs': evidence_refs or [],
|
|
26
|
+
'population': population, 'direction': direction, 'unit': unit}
|