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,169 @@
|
|
|
1
|
+
"""Optional fixed-view snapshot reads with explicit separate credentials.
|
|
2
|
+
|
|
3
|
+
No environment, API-token, cloud-secret, raw-SQL or writer integration exists.
|
|
4
|
+
The operator must provision a fixed-scope read-only role before using this API.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
import http.client
|
|
12
|
+
import ipaddress
|
|
13
|
+
import math
|
|
14
|
+
import re
|
|
15
|
+
import ssl
|
|
16
|
+
from urllib.parse import SplitResult, urlencode, urlsplit
|
|
17
|
+
|
|
18
|
+
from ._analytical_values import identifier, parse_json, require
|
|
19
|
+
from .analytical_views import ProjectionEventRef, ProjectionPage, ProjectionResult, ProjectionRow
|
|
20
|
+
from .workflow_contracts import WorkflowContractError
|
|
21
|
+
|
|
22
|
+
_COLUMNS = ('team_id', 'event_id', 'row_id', 'row_hash', 'row_kind', 'record_id',
|
|
23
|
+
'bank_id', 'release_id', 'cohort_id', 'source_version', 'summary_json',
|
|
24
|
+
'source_digest', 'plan_digest', 'manifest_digest', 'row_count', 'commit_hash')
|
|
25
|
+
_MAX_RESPONSE = 16 * 1024 * 1024
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ReadOnlyClickHouseError(RuntimeError):
|
|
29
|
+
"""Safe connector failure. Credentials and raw response bodies stay hidden."""
|
|
30
|
+
def __init__(self, code: str):
|
|
31
|
+
self.code = code
|
|
32
|
+
super().__init__(code + ': the fixed-view snapshot read could not be verified')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ReadOnlyClickHouseConfig:
|
|
37
|
+
endpoint: str = field(repr=False)
|
|
38
|
+
database: str
|
|
39
|
+
view: str
|
|
40
|
+
username: str = field(repr=False)
|
|
41
|
+
password: str = field(repr=False)
|
|
42
|
+
team_id: str
|
|
43
|
+
bank_id: str
|
|
44
|
+
release_id: str
|
|
45
|
+
cohort_id: str
|
|
46
|
+
connect_proxy: str | None = field(default=None, repr=False)
|
|
47
|
+
timeout: float = 30
|
|
48
|
+
allow_local_http: bool = False
|
|
49
|
+
_target: SplitResult = field(init=False, repr=False)
|
|
50
|
+
_port: int = field(init=False, repr=False)
|
|
51
|
+
_proxy: tuple[str, int] | None = field(init=False, repr=False)
|
|
52
|
+
|
|
53
|
+
def __post_init__(self) -> None:
|
|
54
|
+
try:
|
|
55
|
+
require(type(self.endpoint) is str and 1 <= len(self.endpoint) <= 2048
|
|
56
|
+
and not any(char.isspace() or ord(char) < 32 for char in self.endpoint))
|
|
57
|
+
target = urlsplit(self.endpoint)
|
|
58
|
+
require(target.hostname and target.path in ('', '/') and not target.username
|
|
59
|
+
and not target.password and not target.query and not target.fragment)
|
|
60
|
+
require(type(self.allow_local_http) is bool)
|
|
61
|
+
require(target.scheme == 'https' or self.allow_local_http and target.scheme == 'http'
|
|
62
|
+
and target.hostname in ('localhost', '127.0.0.1', '::1'))
|
|
63
|
+
port = target.port or (443 if target.scheme == 'https' else 80)
|
|
64
|
+
require(1 <= port <= 65535)
|
|
65
|
+
require(type(self.database) is str and re.fullmatch('[a-z][a-z0-9_]{0,63}', self.database))
|
|
66
|
+
require(type(self.view) is str and re.fullmatch('sphynx_ro_[a-z][a-z0-9_]{0,47}', self.view))
|
|
67
|
+
for name in ('team_id', 'bank_id', 'release_id', 'cohort_id'):
|
|
68
|
+
identifier(getattr(self, name))
|
|
69
|
+
require(type(self.timeout) in (int, float) and math.isfinite(self.timeout) and 0 < self.timeout <= 120)
|
|
70
|
+
require(type(self.username) is str and type(self.password) is str
|
|
71
|
+
and bool(self.username) and bool(self.password) and ':' not in self.username
|
|
72
|
+
and len(self.username.encode('utf-8')) <= 256 and len(self.password.encode('utf-8')) <= 4096
|
|
73
|
+
and not any(ord(char) < 32 or ord(char) == 127 for char in self.username + self.password))
|
|
74
|
+
proxy_address = None
|
|
75
|
+
if self.connect_proxy is not None:
|
|
76
|
+
require(type(self.connect_proxy) is str and len(self.connect_proxy) <= 2048
|
|
77
|
+
and not any(char.isspace() or ord(char) < 32 for char in self.connect_proxy))
|
|
78
|
+
proxy = urlsplit(self.connect_proxy)
|
|
79
|
+
address = ipaddress.ip_address(proxy.hostname or '')
|
|
80
|
+
require(proxy.scheme == 'http' and address.version == 4
|
|
81
|
+
and any(address in ipaddress.ip_network(net) for net in
|
|
82
|
+
('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'))
|
|
83
|
+
and proxy.port and proxy.path in ('', '/') and not proxy.username
|
|
84
|
+
and not proxy.password and not proxy.query and not proxy.fragment and target.scheme == 'https')
|
|
85
|
+
assert proxy.port is not None
|
|
86
|
+
proxy_address = (str(address), proxy.port)
|
|
87
|
+
object.__setattr__(self, '_target', target)
|
|
88
|
+
object.__setattr__(self, '_port', port)
|
|
89
|
+
object.__setattr__(self, '_proxy', proxy_address)
|
|
90
|
+
except (WorkflowContractError, ValueError, TypeError, UnicodeError, AttributeError):
|
|
91
|
+
raise ReadOnlyClickHouseError('invalid_readonly_clickhouse_config') from None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ClickHouseReader:
|
|
95
|
+
"""Read only known references through one operator-approved fixed view."""
|
|
96
|
+
def __init__(self, config: ReadOnlyClickHouseConfig):
|
|
97
|
+
if not isinstance(config, ReadOnlyClickHouseConfig):
|
|
98
|
+
raise ReadOnlyClickHouseError('invalid_readonly_clickhouse_config')
|
|
99
|
+
self._config = config
|
|
100
|
+
|
|
101
|
+
def __repr__(self) -> str:
|
|
102
|
+
return f'ClickHouseReader(database={self._config.database!r}, view={self._config.view!r})'
|
|
103
|
+
|
|
104
|
+
def _read(self, refs: tuple[ProjectionEventRef, ...]) -> str:
|
|
105
|
+
config = self._config
|
|
106
|
+
sql = ('SELECT DISTINCT ' + ','.join(_COLUMNS) + ' FROM ' + config.database + '.' + config.view
|
|
107
|
+
+ ' WHERE team_id={team:String} AND bank_id={bank:String} AND release_id={release:String}'
|
|
108
|
+
+ " AND (cohort_id={cohort:String} OR (row_kind='audit' AND cohort_id=''))"
|
|
109
|
+
+ " AND event_id IN splitByChar(',',{events:String}) LIMIT 4097 FORMAT JSONEachRow")
|
|
110
|
+
query = {'database': config.database, 'readonly': '1', 'max_execution_time': '15',
|
|
111
|
+
'max_result_rows': '4097', 'output_format_json_quote_64bit_integers': '0',
|
|
112
|
+
'param_team': config.team_id, 'param_bank': config.bank_id,
|
|
113
|
+
'param_release': config.release_id, 'param_cohort': config.cohort_id,
|
|
114
|
+
'param_events': ','.join(ref.event_id for ref in refs)}
|
|
115
|
+
connection: http.client.HTTPConnection | None = None
|
|
116
|
+
try:
|
|
117
|
+
assert config._target.hostname is not None
|
|
118
|
+
if config._target.scheme == 'https':
|
|
119
|
+
host, port = config._proxy or (config._target.hostname, config._port)
|
|
120
|
+
connection = http.client.HTTPSConnection(host, port, timeout=config.timeout,
|
|
121
|
+
context=ssl.create_default_context())
|
|
122
|
+
if config._proxy:
|
|
123
|
+
connection.set_tunnel(config._target.hostname, config._port)
|
|
124
|
+
else:
|
|
125
|
+
connection = http.client.HTTPConnection(config._target.hostname, config._port, timeout=config.timeout)
|
|
126
|
+
authorization = 'Basic ' + base64.b64encode((config.username + ':' + config.password).encode('utf-8')).decode('ascii')
|
|
127
|
+
connection.request('POST', '/?' + urlencode(query), body=sql.encode('utf-8'),
|
|
128
|
+
headers={'Authorization': authorization, 'Content-Type': 'text/plain; charset=utf-8'})
|
|
129
|
+
with connection.getresponse() as response:
|
|
130
|
+
if response.status != 200:
|
|
131
|
+
raise ReadOnlyClickHouseError('readonly_clickhouse_unavailable')
|
|
132
|
+
raw = response.read(_MAX_RESPONSE + 1)
|
|
133
|
+
if len(raw) > _MAX_RESPONSE:
|
|
134
|
+
raise ReadOnlyClickHouseError('readonly_clickhouse_response_too_large')
|
|
135
|
+
return raw.decode('utf-8')
|
|
136
|
+
except (OSError, ValueError, UnicodeError, http.client.HTTPException):
|
|
137
|
+
raise ReadOnlyClickHouseError('readonly_clickhouse_unavailable') from None
|
|
138
|
+
finally:
|
|
139
|
+
if connection is not None:
|
|
140
|
+
connection.close()
|
|
141
|
+
|
|
142
|
+
def read_events(self, refs: Sequence[ProjectionEventRef]) -> ProjectionPage:
|
|
143
|
+
"""Return historical snapshots. No current review/acceptance authority."""
|
|
144
|
+
require(isinstance(refs, (tuple, list)) and 1 <= len(refs) <= 25,
|
|
145
|
+
'read_events needs from 1 to 25 known source references')
|
|
146
|
+
require(all(isinstance(ref, ProjectionEventRef) for ref in refs))
|
|
147
|
+
frozen = tuple(refs)
|
|
148
|
+
require(len({ref.event_id for ref in frozen}) == len(frozen), 'source references must be unique')
|
|
149
|
+
raw = self._read(frozen)
|
|
150
|
+
try:
|
|
151
|
+
expected = {ref.event_id: ref.source_digest for ref in frozen}
|
|
152
|
+
groups: dict[str, list[ProjectionRow]] = {ref.event_id: [] for ref in frozen}
|
|
153
|
+
seen: set[tuple[str, str]] = set()
|
|
154
|
+
for line in raw.splitlines():
|
|
155
|
+
if not line.strip():
|
|
156
|
+
continue
|
|
157
|
+
require(len(seen) < 4096)
|
|
158
|
+
row = ProjectionRow.from_dict(parse_json(line, maximum=_MAX_RESPONSE))
|
|
159
|
+
pair = (row.event_id, row.row_id)
|
|
160
|
+
require(row.event_id in expected and row.source_digest == expected[row.event_id] and pair not in seen)
|
|
161
|
+
groups[row.event_id].append(row); seen.add(pair)
|
|
162
|
+
items = tuple(ProjectionResult(ref.event_id, ref.source_digest,
|
|
163
|
+
'available' if groups[ref.event_id] else 'unavailable',
|
|
164
|
+
None if groups[ref.event_id] else 'projection_not_committed', tuple(groups[ref.event_id])) for ref in frozen)
|
|
165
|
+
config = self._config
|
|
166
|
+
return ProjectionPage(config.team_id, config.bank_id, config.release_id, config.cohort_id,
|
|
167
|
+
'committed_snapshot_requires_current_service_check', items)
|
|
168
|
+
except (WorkflowContractError, ValueError, TypeError, KeyError, UnicodeError, RecursionError):
|
|
169
|
+
raise ReadOnlyClickHouseError('invalid_readonly_clickhouse_response') from None
|
sphynx/client.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Thin authenticated client for hosted submissions and saved grading passes.
|
|
2
|
+
|
|
3
|
+
Configuration: explicit arguments, SPHYNX_API_URL / SPHYNX_TOKEN, or the private
|
|
4
|
+
~/.sphynx/client.json file. A hosted server must be deployed before this can run.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
import urllib.error
|
|
13
|
+
import urllib.request
|
|
14
|
+
import uuid
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .diagnostics import Diagnostic
|
|
20
|
+
from .progress import ProgressView
|
|
21
|
+
from .audit_access import ReviewHistoryPage, ReviewOperationReceipt, ReviewTargetPage, TargetReviewPage
|
|
22
|
+
from .audit_contracts import BankRelease, ReviewContribution
|
|
23
|
+
from .audit_requests import BankReleaseDeclaration, BankReleaseTransition, ReviewAdjudication
|
|
24
|
+
from .analytical_views import EvaluationBoardPage, ProjectionPage
|
|
25
|
+
from .pr_evaluations import PrEvaluationRequest
|
|
26
|
+
from ._pr_client import PrEvaluation
|
|
27
|
+
from urllib.parse import quote, urlencode, urlsplit
|
|
28
|
+
from .http_transport import open_no_redirect
|
|
29
|
+
|
|
30
|
+
_MAX_RESPONSE = 4_194_304
|
|
31
|
+
_ID = re.compile(r'^exp_[0-9a-f]{32}$')
|
|
32
|
+
|
|
33
|
+
def _unique_pairs(pairs):
|
|
34
|
+
value = {}
|
|
35
|
+
for key, item in pairs:
|
|
36
|
+
if key in value: raise ValueError('duplicate JSON field')
|
|
37
|
+
value[key] = item
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
def _release_ref(value):
|
|
41
|
+
if not isinstance(value, str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._:@/-]{0,255}', value):
|
|
42
|
+
raise ValueError('release must be a stable catalog identifier')
|
|
43
|
+
return quote(value, safe='')
|
|
44
|
+
def _saved_id(value, prefix):
|
|
45
|
+
if not isinstance(value, str) or not re.fullmatch(prefix + r'_[0-9a-f]{32}', value):
|
|
46
|
+
raise ValueError(f'invalid {prefix} ID')
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
def _retry_key(value):
|
|
50
|
+
key = uuid.uuid4().hex if value is None else value
|
|
51
|
+
if not isinstance(key, str) or not re.fullmatch('[A-Za-z0-9_-]{16,100}', key):
|
|
52
|
+
raise ValueError('idempotency_key must contain 16 to 100 letters, numbers, underscores or hyphens')
|
|
53
|
+
return key
|
|
54
|
+
|
|
55
|
+
def _response_matches(actual, expected, kind):
|
|
56
|
+
if actual != expected:
|
|
57
|
+
raise HostedClientError('invalid_server_response', f'the server returned a different {kind}')
|
|
58
|
+
|
|
59
|
+
def _unchanged_links(previous, fresh):
|
|
60
|
+
for name in ('run_id', 'evaluation_id', 'experiment_id'):
|
|
61
|
+
if previous.get(name) is not None and fresh.get(name) != previous[name]:
|
|
62
|
+
raise HostedClientError('invalid_server_response', 'the server changed a bound record identity')
|
|
63
|
+
|
|
64
|
+
def _validate_links(payload):
|
|
65
|
+
for name, prefix in (('run_id', 'run'), ('evaluation_id', 'eval'), ('experiment_id', 'exp')):
|
|
66
|
+
if payload.get(name) is not None:
|
|
67
|
+
try: _saved_id(payload[name], prefix)
|
|
68
|
+
except ValueError:
|
|
69
|
+
raise HostedClientError('invalid_server_response', 'the server returned an invalid linked identity') from None
|
|
70
|
+
|
|
71
|
+
class HostedClientError(RuntimeError):
|
|
72
|
+
def __init__(self, code: str, message: str, *, experiment_id: str | None = None, handle: object | None = None):
|
|
73
|
+
self.code, self.experiment_id, self.handle = code, experiment_id, handle
|
|
74
|
+
super().__init__(f'{code}: {message}')
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def diagnostic(self) -> Diagnostic:
|
|
78
|
+
from .diagnostics import explain_error
|
|
79
|
+
return explain_error(self)
|
|
80
|
+
|
|
81
|
+
class HostedResult:
|
|
82
|
+
"""Native hosted report presentation; also available as HostedReport."""
|
|
83
|
+
def __init__(self, payload: dict): self._payload = payload
|
|
84
|
+
def to_dict(self): return json.loads(json.dumps(self._payload, allow_nan=False))
|
|
85
|
+
def summary(self):
|
|
86
|
+
text = self._payload.get('summary')
|
|
87
|
+
if not isinstance(text, str): raise HostedClientError('invalid_server_response', 'the report has no readable summary')
|
|
88
|
+
return text
|
|
89
|
+
|
|
90
|
+
class Experiment:
|
|
91
|
+
"""Native hosted submission handle; also available as HostedSubmission.
|
|
92
|
+
|
|
93
|
+
Native IDs, wire fields, polling, and representations keep their existing
|
|
94
|
+
identity. This handle coordinates work; it is not a run or an assessment.
|
|
95
|
+
"""
|
|
96
|
+
def __init__(self, client: Client, payload: dict):
|
|
97
|
+
if not isinstance(payload, dict) or not isinstance(payload.get('id'), str) or not _ID.fullmatch(payload['id']):
|
|
98
|
+
raise HostedClientError('invalid_server_response', 'the server did not return an experiment ID')
|
|
99
|
+
_validate_links(payload)
|
|
100
|
+
self._client, self._record = client, payload
|
|
101
|
+
self.id = payload['id']
|
|
102
|
+
@property
|
|
103
|
+
def progress(self) -> ProgressView:
|
|
104
|
+
from .progress import _hosted_progress
|
|
105
|
+
return _hosted_progress(self._record, 'submission')
|
|
106
|
+
@property
|
|
107
|
+
def status(self): return self._record.get('status')
|
|
108
|
+
@property
|
|
109
|
+
def head_sha(self): return self._record.get('head_sha')
|
|
110
|
+
@property
|
|
111
|
+
def run_id(self): return self._record.get('run_id')
|
|
112
|
+
@property
|
|
113
|
+
def evaluation_id(self): return self._record.get('evaluation_id')
|
|
114
|
+
def refresh(self):
|
|
115
|
+
fresh = self._client.get_experiment(self.id)
|
|
116
|
+
_response_matches(fresh.id, self.id, 'submission')
|
|
117
|
+
_unchanged_links(self._record, fresh._record)
|
|
118
|
+
self._record = fresh._record
|
|
119
|
+
return self
|
|
120
|
+
def wait(self, *, timeout: float = 900, poll_interval: float = 2, on_progress=None):
|
|
121
|
+
for value in (timeout, poll_interval):
|
|
122
|
+
if type(value) not in (int, float) or not math.isfinite(value) or value <= 0:
|
|
123
|
+
raise ValueError('timeout and poll_interval must be finite positive seconds')
|
|
124
|
+
deadline, previous = time.monotonic() + timeout, None
|
|
125
|
+
while True:
|
|
126
|
+
state = self.status
|
|
127
|
+
if state != previous:
|
|
128
|
+
if on_progress is not None: on_progress(state)
|
|
129
|
+
previous = state
|
|
130
|
+
if state == 'completed':
|
|
131
|
+
return HostedResult(self._client._request('GET', f'/v1/experiments/{self.id}/result'))
|
|
132
|
+
if state == 'failed':
|
|
133
|
+
error = self._record.get('error') or {}
|
|
134
|
+
raise HostedClientError(error.get('code', 'experiment_failed'),
|
|
135
|
+
f"experiment {self.id} failed at {error.get('stage', 'unknown')}; inspect its status for details", experiment_id=self.id)
|
|
136
|
+
remaining = deadline - time.monotonic()
|
|
137
|
+
if remaining <= 0:
|
|
138
|
+
raise HostedClientError('wait_timeout', f'experiment {self.id} continues remotely; use get_experiment() to resume waiting', experiment_id=self.id, handle=self)
|
|
139
|
+
time.sleep(min(poll_interval, remaining))
|
|
140
|
+
self.refresh()
|
|
141
|
+
def to_dict(self): return json.loads(json.dumps(self._record, allow_nan=False))
|
|
142
|
+
def __repr__(self): return f'Experiment(id={self.id!r}, status={self.status!r})'
|
|
143
|
+
|
|
144
|
+
class HostedRun:
|
|
145
|
+
"""Captured remote execution. Grading this run does not execute its candidate."""
|
|
146
|
+
def __init__(self, client: Client, payload: dict):
|
|
147
|
+
try: identity = _saved_id(payload.get('id'), 'run')
|
|
148
|
+
except (AttributeError, ValueError):
|
|
149
|
+
raise HostedClientError('invalid_server_response', 'the server did not return a run ID') from None
|
|
150
|
+
_validate_links(payload)
|
|
151
|
+
self._client, self._record, self.id = client, payload, identity
|
|
152
|
+
@property
|
|
153
|
+
def progress(self) -> ProgressView:
|
|
154
|
+
from .progress import _hosted_progress
|
|
155
|
+
return _hosted_progress(self._record, 'execution')
|
|
156
|
+
@property
|
|
157
|
+
def status(self): return self._record.get('status')
|
|
158
|
+
def refresh(self):
|
|
159
|
+
fresh = self._client.get_run(self.id)
|
|
160
|
+
if fresh.id != self.id:
|
|
161
|
+
raise HostedClientError('invalid_server_response', 'the server returned a different run')
|
|
162
|
+
_unchanged_links(self._record, fresh._record)
|
|
163
|
+
self._record = fresh._record
|
|
164
|
+
return self
|
|
165
|
+
def grade(self, *, idempotency_key=None):
|
|
166
|
+
return self._client.grade(self.id, idempotency_key=idempotency_key)
|
|
167
|
+
def to_dict(self): return json.loads(json.dumps(self._record, allow_nan=False))
|
|
168
|
+
def __repr__(self): return f'HostedRun(id={self.id!r}, status={self.status!r})'
|
|
169
|
+
|
|
170
|
+
class HostedEvaluation:
|
|
171
|
+
"""A durable remote grading pass; also available as HostedGradingPass."""
|
|
172
|
+
def __init__(self, client: Client, payload: dict):
|
|
173
|
+
try: identity = _saved_id(payload.get('id'), 'eval')
|
|
174
|
+
except (AttributeError, ValueError):
|
|
175
|
+
raise HostedClientError('invalid_server_response', 'the server did not return an evaluation ID') from None
|
|
176
|
+
_validate_links(payload)
|
|
177
|
+
self._client, self._record, self.id = client, payload, identity
|
|
178
|
+
@property
|
|
179
|
+
def progress(self) -> ProgressView:
|
|
180
|
+
from .progress import _hosted_progress
|
|
181
|
+
return _hosted_progress(self._record, 'grading')
|
|
182
|
+
@property
|
|
183
|
+
def status(self): return self._record.get('status')
|
|
184
|
+
@property
|
|
185
|
+
def run_id(self): return self._record.get('run_id')
|
|
186
|
+
def refresh(self):
|
|
187
|
+
fresh = self._client.get_evaluation(self.id)
|
|
188
|
+
if fresh.id != self.id:
|
|
189
|
+
raise HostedClientError('invalid_server_response', 'the server returned a different evaluation')
|
|
190
|
+
_unchanged_links(self._record, fresh._record)
|
|
191
|
+
self._record = fresh._record
|
|
192
|
+
return self
|
|
193
|
+
def wait(self, *, timeout=900, poll_interval=2, on_progress=None):
|
|
194
|
+
for value in (timeout, poll_interval):
|
|
195
|
+
if type(value) not in (int, float) or not math.isfinite(value) or value <= 0:
|
|
196
|
+
raise ValueError('timeout and poll_interval must be finite positive seconds')
|
|
197
|
+
deadline, previous = time.monotonic() + timeout, None
|
|
198
|
+
while True:
|
|
199
|
+
state = self.status
|
|
200
|
+
if state != previous:
|
|
201
|
+
if on_progress is not None: on_progress(state)
|
|
202
|
+
previous = state
|
|
203
|
+
if state == 'completed':
|
|
204
|
+
return HostedResult(self._client._request('GET', f'/v1/evaluations/{self.id}/result'))
|
|
205
|
+
if state == 'failed':
|
|
206
|
+
error = self._record.get('error') or {}
|
|
207
|
+
raise HostedClientError(error.get('code', 'evaluation_failed'),
|
|
208
|
+
f'evaluation {self.id} failed during grading; inspect its status for details')
|
|
209
|
+
remaining = deadline - time.monotonic()
|
|
210
|
+
if remaining <= 0:
|
|
211
|
+
raise HostedClientError('wait_timeout',
|
|
212
|
+
f'evaluation {self.id} continues remotely; use get_evaluation() to resume waiting', handle=self)
|
|
213
|
+
time.sleep(min(poll_interval, remaining))
|
|
214
|
+
self.refresh()
|
|
215
|
+
def to_dict(self): return json.loads(json.dumps(self._record, allow_nan=False))
|
|
216
|
+
def __repr__(self): return f'HostedEvaluation(id={self.id!r}, status={self.status!r})'
|
|
217
|
+
|
|
218
|
+
class ComparisonRef:
|
|
219
|
+
"""Saved comparison of owned reports. Retrieval never runs or grades a candidate."""
|
|
220
|
+
def __init__(self, client: Client, payload: dict):
|
|
221
|
+
try: identity = _saved_id(payload.get('id'), 'cmp')
|
|
222
|
+
except (AttributeError, ValueError):
|
|
223
|
+
raise HostedClientError('invalid_server_response', 'the server did not return a comparison ID') from None
|
|
224
|
+
self._client, self._record, self.id = client, payload, identity
|
|
225
|
+
@property
|
|
226
|
+
def status(self): return self._record.get('status')
|
|
227
|
+
def result(self):
|
|
228
|
+
payload = self._client._request('GET', f'/v1/comparisons/{self.id}/result')
|
|
229
|
+
if payload.get('comparison_id') != self.id or payload.get('schema_version') != 'sphynx.hosted.saved-comparison.v1':
|
|
230
|
+
raise HostedClientError('invalid_server_response', 'the server returned a different comparison')
|
|
231
|
+
return HostedResult(payload)
|
|
232
|
+
def summary(self): return self.result().summary()
|
|
233
|
+
def to_dict(self): return json.loads(json.dumps(self._record, allow_nan=False))
|
|
234
|
+
def __repr__(self): return f'ComparisonRef(id={self.id!r}, status={self.status!r})'
|
|
235
|
+
|
|
236
|
+
class Client:
|
|
237
|
+
def __init__(self, api_url: str | None = None, token: str | None = None, *, timeout: float = 120,
|
|
238
|
+
config_path: str | Path | None = None, allow_local_http: bool = False, retries: int = 2):
|
|
239
|
+
config = {}
|
|
240
|
+
path = Path(config_path) if config_path else Path.home() / '.sphynx' / 'client.json'
|
|
241
|
+
if (api_url is None or token is None) and path.exists():
|
|
242
|
+
try:
|
|
243
|
+
if path.stat().st_size > 16_384: raise ValueError('size')
|
|
244
|
+
config = json.loads(path.read_bytes())
|
|
245
|
+
if not isinstance(config, dict) or set(config) != {'api_url', 'token'}: raise ValueError('shape')
|
|
246
|
+
except (OSError, ValueError):
|
|
247
|
+
raise HostedClientError('invalid_client_config', 'the private client configuration is invalid') from None
|
|
248
|
+
api_url = api_url or os.environ.get('SPHYNX_API_URL') or config.get('api_url')
|
|
249
|
+
token = token or os.environ.get('SPHYNX_TOKEN') or config.get('token')
|
|
250
|
+
if not isinstance(api_url, str) or not isinstance(token, str) or not token:
|
|
251
|
+
raise HostedClientError('client_not_configured', 'configure a hosted API URL and your Sphynx token')
|
|
252
|
+
try: parsed = urlsplit(api_url)
|
|
253
|
+
except ValueError: raise HostedClientError('invalid_api_url', 'use a trusted HTTPS API origin') from None
|
|
254
|
+
if (parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in ('', '/')
|
|
255
|
+
or not parsed.hostname or any(c.isspace() or ord(c) < 32 for c in api_url)):
|
|
256
|
+
raise HostedClientError('invalid_api_url', 'use a trusted HTTPS API origin without a path or credentials')
|
|
257
|
+
if parsed.scheme != 'https' and not (allow_local_http and parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', '::1', 'localhost')):
|
|
258
|
+
raise HostedClientError('invalid_api_url', 'HTTPS is required; local HTTP must be explicitly enabled for development')
|
|
259
|
+
if not 32 <= len(token) <= 256 or any(c.isspace() for c in token):
|
|
260
|
+
raise HostedClientError('invalid_client_token', 'use an operator-issued Sphynx token')
|
|
261
|
+
if type(timeout) not in (int, float) or not math.isfinite(timeout) or timeout <= 0:
|
|
262
|
+
raise ValueError('timeout must be finite positive seconds')
|
|
263
|
+
if type(retries) is not int or not 0 <= retries <= 4: raise ValueError('retries must be between zero and four')
|
|
264
|
+
self.api_url, self._token, self.timeout, self.retries = api_url.rstrip('/'), token, timeout, retries
|
|
265
|
+
def __repr__(self): return f'Client(api_url={self.api_url!r})'
|
|
266
|
+
def _request(self, method, path, payload=None, *, retry_key=None, deadline=None):
|
|
267
|
+
if deadline is not None and (type(deadline) not in (int, float) or not math.isfinite(deadline)):
|
|
268
|
+
raise ValueError('request deadline must be a finite monotonic time')
|
|
269
|
+
def remaining():
|
|
270
|
+
if deadline is None: return None
|
|
271
|
+
value = deadline - time.monotonic()
|
|
272
|
+
if value <= 0: raise HostedClientError('wait_timeout', 'the read budget expired; retain the original operation handle')
|
|
273
|
+
return value
|
|
274
|
+
def pause(seconds):
|
|
275
|
+
budget = remaining()
|
|
276
|
+
time.sleep(seconds if budget is None else min(seconds, budget))
|
|
277
|
+
body = None if payload is None else json.dumps(payload, separators=(',', ':'), allow_nan=False).encode()
|
|
278
|
+
headers = {'Authorization': 'Bearer ' + self._token, 'Accept': 'application/json', 'Content-Type': 'application/json'}
|
|
279
|
+
if retry_key: headers['Idempotency-Key'] = retry_key
|
|
280
|
+
request = urllib.request.Request(self.api_url + path, data=body, headers=headers, method=method)
|
|
281
|
+
for attempt in range(self.retries + 1):
|
|
282
|
+
budget = remaining()
|
|
283
|
+
socket_timeout = self.timeout if budget is None else min(self.timeout, budget)
|
|
284
|
+
try:
|
|
285
|
+
with open_no_redirect(request, timeout=socket_timeout) as response:
|
|
286
|
+
raw = response.read(_MAX_RESPONSE + 1)
|
|
287
|
+
except urllib.error.HTTPError as exc:
|
|
288
|
+
status = exc.code
|
|
289
|
+
try: raw = exc.read(16_385)
|
|
290
|
+
finally: exc.close()
|
|
291
|
+
if status in (429, 500, 502, 503, 504) and attempt < self.retries:
|
|
292
|
+
pause(min(2 ** attempt, 4)); continue
|
|
293
|
+
try: error = json.loads(raw, object_pairs_hook=_unique_pairs).get('error', {})
|
|
294
|
+
except (ValueError, AttributeError, RecursionError): error = {}
|
|
295
|
+
code = error.get('code') if isinstance(error, dict) else None
|
|
296
|
+
safe = code if isinstance(code, str) and re.fullmatch('[a-z_]{1,64}', code) else 'hosted_request_failed'
|
|
297
|
+
message = ('authentication failed; provision or renew your Sphynx token' if status == 401
|
|
298
|
+
else 'submission or access was rejected; check repository and suite registration' if status == 403
|
|
299
|
+
else f'the hosted API returned HTTP {status}; inspect the experiment status or retry')
|
|
300
|
+
raise HostedClientError(safe, message) from None
|
|
301
|
+
except (urllib.error.URLError, OSError, TimeoutError):
|
|
302
|
+
if attempt < self.retries: pause(min(2 ** attempt, 4)); continue
|
|
303
|
+
raise HostedClientError('hosted_transport_failed', 'the hosted API could not be reached; retry with the same submission key') from None
|
|
304
|
+
remaining()
|
|
305
|
+
if len(raw) > _MAX_RESPONSE: raise HostedClientError('invalid_server_response', 'the hosted response exceeds its size limit')
|
|
306
|
+
try:
|
|
307
|
+
value = json.loads(raw, object_pairs_hook=_unique_pairs, parse_constant=lambda _: (_ for _ in ()).throw(ValueError('nonfinite')))
|
|
308
|
+
if not isinstance(value, dict): raise ValueError('shape')
|
|
309
|
+
except (ValueError, RecursionError): raise HostedClientError('invalid_server_response', 'the hosted API returned invalid JSON') from None
|
|
310
|
+
return value
|
|
311
|
+
@staticmethod
|
|
312
|
+
def list_evals():
|
|
313
|
+
"""Installed contract discovery, offline; not hosted availability/access."""
|
|
314
|
+
from .evals import list_evals
|
|
315
|
+
return list_evals()
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def describe_eval(evaluation_id: str):
|
|
319
|
+
"""Describe an installed definition without any authenticated request."""
|
|
320
|
+
from .evals import describe_eval
|
|
321
|
+
return describe_eval(evaluation_id)
|
|
322
|
+
|
|
323
|
+
def list_benchmarks(self):
|
|
324
|
+
"""Authenticated public metadata discovery; does not run a candidate."""
|
|
325
|
+
return self._request('GET', '/v1/benchmarks')
|
|
326
|
+
|
|
327
|
+
def get_benchmark(self, release: str):
|
|
328
|
+
"""Inspect release membership and eligibility, without private inputs or labels."""
|
|
329
|
+
return self._request('GET', '/v1/benchmarks/' + _release_ref(release))
|
|
330
|
+
|
|
331
|
+
def get_label_release(self, release: str):
|
|
332
|
+
"""Inspect declared label provenance; metadata does not prove review or approval."""
|
|
333
|
+
return self._request('GET', '/v1/label-releases/' + _release_ref(release))
|
|
334
|
+
def evaluate(self, *, candidate_pr: str, suite: str = 'rag.rerank.v1', baseline: str = 'supplied-score:v1',
|
|
335
|
+
idempotency_key: str | None = None, parameters: dict | None = None,
|
|
336
|
+
expected_head_sha: str | None = None, candidate_method: str | None = None,
|
|
337
|
+
expected_baseline_sha: str | None = None) -> Experiment:
|
|
338
|
+
"""Submit a PR and optional named method; a full commit SHA can guard its head."""
|
|
339
|
+
if candidate_method is not None:
|
|
340
|
+
if not isinstance(candidate_method, str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._:-]{0,127}', candidate_method):
|
|
341
|
+
raise ValueError('candidate_method must be a bounded ASCII stable identifier')
|
|
342
|
+
if expected_head_sha is not None:
|
|
343
|
+
if not isinstance(expected_head_sha, str) or not re.fullmatch(r'[0-9a-fA-F]{40}', expected_head_sha):
|
|
344
|
+
raise ValueError('expected_head_sha must be a complete 40-character hexadecimal commit SHA')
|
|
345
|
+
expected_head_sha = expected_head_sha.lower()
|
|
346
|
+
if expected_baseline_sha is not None:
|
|
347
|
+
if baseline != 'registered-main-reflex-lexical:v1':
|
|
348
|
+
raise ValueError('expected_baseline_sha requires the registered main baseline')
|
|
349
|
+
if not isinstance(expected_baseline_sha, str) or not re.fullmatch(r'[0-9a-fA-F]{40}', expected_baseline_sha):
|
|
350
|
+
raise ValueError('expected_baseline_sha must be a complete 40-character hexadecimal commit SHA')
|
|
351
|
+
expected_baseline_sha = expected_baseline_sha.lower()
|
|
352
|
+
key = _retry_key(idempotency_key)
|
|
353
|
+
payload = {'candidate_pr': candidate_pr, 'suite': suite, 'baseline': baseline}
|
|
354
|
+
if parameters is not None:
|
|
355
|
+
if not isinstance(parameters, dict): raise ValueError('parameters must be an object')
|
|
356
|
+
payload['parameters'] = parameters
|
|
357
|
+
if expected_head_sha is not None:
|
|
358
|
+
payload['expected_head_sha'] = expected_head_sha
|
|
359
|
+
if candidate_method is not None:
|
|
360
|
+
payload['candidate_method'] = candidate_method
|
|
361
|
+
if expected_baseline_sha is not None:
|
|
362
|
+
payload['expected_baseline_sha'] = expected_baseline_sha
|
|
363
|
+
return Experiment(self, self._request('POST', '/v1/experiments', payload, retry_key=key))
|
|
364
|
+
def get_experiment(self, experiment_id: str) -> Experiment:
|
|
365
|
+
if not isinstance(experiment_id, str) or not _ID.fullmatch(experiment_id): raise ValueError('invalid experiment ID')
|
|
366
|
+
result = Experiment(self, self._request('GET', '/v1/experiments/' + experiment_id))
|
|
367
|
+
_response_matches(result.id, experiment_id, 'submission')
|
|
368
|
+
return result
|
|
369
|
+
# Equivalent names retain exact signatures, validation, and native wire
|
|
370
|
+
# behavior. They do not add a shared-workflow hosted execution adapter.
|
|
371
|
+
submit = evaluate
|
|
372
|
+
get_submission = get_experiment
|
|
373
|
+
def get_run(self, run_id: str) -> HostedRun:
|
|
374
|
+
identity = _saved_id(run_id, 'run')
|
|
375
|
+
result = HostedRun(self, self._request('GET', '/v1/runs/' + identity))
|
|
376
|
+
_response_matches(result.id, identity, 'run')
|
|
377
|
+
return result
|
|
378
|
+
def grade(self, run_id: str, *, idempotency_key: str | None = None) -> HostedEvaluation:
|
|
379
|
+
"""Grade an owned saved capture again with its pinned grader; no candidate run."""
|
|
380
|
+
identity = _saved_id(run_id, 'run')
|
|
381
|
+
result = HostedEvaluation(self, self._request('POST', '/v1/runs/' + identity + '/evaluations',
|
|
382
|
+
{}, retry_key=_retry_key(idempotency_key)))
|
|
383
|
+
if result.run_id is not None:
|
|
384
|
+
_response_matches(result.run_id, identity, 'grading run')
|
|
385
|
+
return result
|
|
386
|
+
def get_evaluation(self, evaluation_id: str) -> HostedEvaluation:
|
|
387
|
+
identity = _saved_id(evaluation_id, 'eval')
|
|
388
|
+
result = HostedEvaluation(self, self._request('GET', '/v1/evaluations/' + identity))
|
|
389
|
+
_response_matches(result.id, identity, 'grading pass')
|
|
390
|
+
return result
|
|
391
|
+
def compare_saved(self, evaluation_a: str, evaluation_b: str, *, lane_a='candidate', lane_b='candidate') -> ComparisonRef:
|
|
392
|
+
"""Compare two existing owned evaluations; no candidate run or new grading."""
|
|
393
|
+
a, b = _saved_id(evaluation_a, 'eval'), _saved_id(evaluation_b, 'eval')
|
|
394
|
+
if lane_a not in ('candidate', 'baseline') or lane_b not in ('candidate', 'baseline'):
|
|
395
|
+
raise ValueError('comparison lanes must be candidate or baseline')
|
|
396
|
+
return ComparisonRef(self, self._request('POST', '/v1/comparisons', {
|
|
397
|
+
'evaluation_a': a, 'evaluation_b': b, 'lane_a': lane_a, 'lane_b': lane_b}))
|
|
398
|
+
def get_comparison(self, comparison_id: str) -> ComparisonRef:
|
|
399
|
+
identity = _saved_id(comparison_id, 'cmp')
|
|
400
|
+
record = self._request('GET', '/v1/comparisons/' + identity)
|
|
401
|
+
result = ComparisonRef(self, record)
|
|
402
|
+
if result.id != identity:
|
|
403
|
+
raise HostedClientError('invalid_server_response', 'the server returned a different comparison')
|
|
404
|
+
return result
|
|
405
|
+
def get_bank_release(self, bank_id: str, release_id: str) -> BankRelease:
|
|
406
|
+
"""Read current permitted metadata/eligibility for a known selected bank."""
|
|
407
|
+
from ._audit_client import release
|
|
408
|
+
return release(self, bank_id, release_id)
|
|
409
|
+
|
|
410
|
+
def get_bank_review_targets(self, bank_id: str, release_id: str, *, after: str | None = None,
|
|
411
|
+
through_version: int | None = None, limit: int = 20) -> ReviewTargetPage:
|
|
412
|
+
"""Read one protected page. No global catalog or automatic pagination."""
|
|
413
|
+
from ._audit_client import targets
|
|
414
|
+
return targets(self, bank_id, release_id, after=after, through_version=through_version, limit=limit)
|
|
415
|
+
|
|
416
|
+
def get_bank_review_history(self, bank_id: str, release_id: str, *, after: int = 0,
|
|
417
|
+
through_version: int | None = None, limit: int | None = None,
|
|
418
|
+
protected: bool = False) -> ReviewHistoryPage:
|
|
419
|
+
"""Read metadata, or explicitly request operator-only private history."""
|
|
420
|
+
from ._audit_client import history
|
|
421
|
+
return history(self, bank_id, release_id, after=after, through_version=through_version,
|
|
422
|
+
limit=limit, protected=protected)
|
|
423
|
+
|
|
424
|
+
def get_bank_review_details(self, bank_id: str, release_id: str, target_id: str, *, after: int = 0,
|
|
425
|
+
through_version: int | None = None, limit: int = 10) -> TargetReviewPage:
|
|
426
|
+
"""Read authorized judgments/protocol and the current contribution-set digest."""
|
|
427
|
+
from ._audit_client import details
|
|
428
|
+
return details(self, bank_id, release_id, target_id, after=after, through_version=through_version, limit=limit)
|
|
429
|
+
|
|
430
|
+
def create_bank_release(self, bank_id: str, request: BankReleaseDeclaration) -> ReviewOperationReceipt:
|
|
431
|
+
"""Submit an operator declaration with its original explicit key."""
|
|
432
|
+
from ._audit_client import write
|
|
433
|
+
return write(self, bank_id, None, 'create', request)
|
|
434
|
+
|
|
435
|
+
def contribute_bank_review(self, bank_id: str, release_id: str, request: ReviewContribution) -> ReviewOperationReceipt:
|
|
436
|
+
"""Submit a version-guarded review; the service supplies reviewer identity."""
|
|
437
|
+
from ._audit_client import write
|
|
438
|
+
return write(self, bank_id, release_id, 'contribute', request)
|
|
439
|
+
|
|
440
|
+
def adjudicate_bank_review(self, bank_id: str, release_id: str, request: ReviewAdjudication) -> ReviewOperationReceipt:
|
|
441
|
+
"""Submit an independent decision bound to the exact current review set."""
|
|
442
|
+
from ._audit_client import write
|
|
443
|
+
return write(self, bank_id, release_id, 'adjudicate', request)
|
|
444
|
+
|
|
445
|
+
def transition_bank_release(self, bank_id: str, release_id: str, request: BankReleaseTransition) -> ReviewOperationReceipt:
|
|
446
|
+
"""Request operator hold/resume/withdraw with the original version/key."""
|
|
447
|
+
from ._audit_client import write
|
|
448
|
+
return write(self, bank_id, release_id, 'transition', request)
|
|
449
|
+
|
|
450
|
+
def get_evaluation_board(self, bank_id: str, board_id: str, *, limit: int = 20,
|
|
451
|
+
cursor: str | None = None) -> EvaluationBoardPage:
|
|
452
|
+
"""Read a selected private board. Retain its cursor with its snapshot."""
|
|
453
|
+
from ._analytical_client import board
|
|
454
|
+
return board(self, bank_id, board_id, limit=limit, cursor=cursor)
|
|
455
|
+
def submit_pr_evaluation(self, request: PrEvaluationRequest, *, idempotency_key: str | None = None) -> PrEvaluation:
|
|
456
|
+
"""Submit one server-selected private evaluation plan for exact base/head revisions."""
|
|
457
|
+
from ._pr_client import submit
|
|
458
|
+
return submit(self, request, idempotency_key=idempotency_key)
|
|
459
|
+
def get_pr_evaluation(self, evaluation_id: str) -> PrEvaluation:
|
|
460
|
+
"""Read the original owned PR operation without creating more candidate work."""
|
|
461
|
+
from ._pr_client import get
|
|
462
|
+
return get(self, evaluation_id)
|
|
463
|
+
|
|
464
|
+
def get_evaluation_projection(self, bank_id: str, board_id: str, entry_id: str) -> ProjectionPage:
|
|
465
|
+
"""Read permitted source summaries for one selected board entry."""
|
|
466
|
+
from ._analytical_client import projection
|
|
467
|
+
return projection(self, bank_id, board_id, entry_id)
|
|
468
|
+
|
|
469
|
+
def leaderboard(self, suite: str = 'rag.rerank.v1', *, include_baseline: bool = False, limit: int = 200):
|
|
470
|
+
"""Read owner-scoped saved rankings, grouped by quality and actual runtime."""
|
|
471
|
+
if suite not in ('rag.rerank.v1', 'rag.rerank.public.v1'):
|
|
472
|
+
raise ValueError('suite has no admitted hosted leaderboard')
|
|
473
|
+
if type(include_baseline) is not bool or type(limit) is not int or not 1 <= limit <= 200:
|
|
474
|
+
raise ValueError('include_baseline must be boolean and limit must be from 1 to 200')
|
|
475
|
+
query = urlencode({'suite': suite, 'include_baseline': str(include_baseline).lower(), 'limit': limit})
|
|
476
|
+
board = self._request('GET', '/v1/leaderboards?' + query)
|
|
477
|
+
if board.get('schema_version') != 'sphynx.hosted.leaderboard.v1' or board.get('official') is not False:
|
|
478
|
+
raise HostedClientError('invalid_server_response', 'the server returned an invalid development leaderboard')
|
|
479
|
+
return board
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# Direct aliases preserve native type identity and saved/wire compatibility.
|
|
483
|
+
# Existing imports remain supported; no new remote capability is implied.
|
|
484
|
+
HostedSubmission = Experiment
|
|
485
|
+
HostedGradingPass = HostedEvaluation
|
|
486
|
+
HostedReport = HostedResult
|