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/model_adapters.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""Lazy, CPU-only model scorers. No third-party package is imported at discovery.
|
|
2
|
+
|
|
3
|
+
A caller supplies an existing pinned snapshot and its complete file digest.
|
|
4
|
+
Preflight never resolves a remote model name. Scores are raw ranking utilities,
|
|
5
|
+
not calibrated relevance probabilities. Local inference timings exclude load.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import importlib.util
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Mapping, Sequence
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from types import MappingProxyType
|
|
20
|
+
|
|
21
|
+
from .contracts import ContractError, canonical_json
|
|
22
|
+
from .integration_contracts import CandidateConfiguration, require_digest, value_digest
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ModelSpec:
|
|
27
|
+
model_id: str
|
|
28
|
+
checkpoint: str
|
|
29
|
+
revision: str
|
|
30
|
+
max_length: int
|
|
31
|
+
batch_size: int
|
|
32
|
+
backend: str = "cross_encoder"
|
|
33
|
+
trust_remote_code: bool = False
|
|
34
|
+
onnx_file: str | None = None
|
|
35
|
+
|
|
36
|
+
def to_dict(self):
|
|
37
|
+
return dict(vars(self))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
MODEL_SPECS = MappingProxyType({spec.model_id: spec for spec in (
|
|
41
|
+
ModelSpec("minilm-l6-v2", "cross-encoder/ms-marco-MiniLM-L-6-v2", "233902d25c440f23af6f7d6e94d2946bac0bee0a", 512, 16),
|
|
42
|
+
ModelSpec("jina-turbo-v1", "jinaai/jina-reranker-v1-turbo-en", "b8c14f4e723d9e0aab4732a7b7b93741eeeb77c2", 192, 16, trust_remote_code=True),
|
|
43
|
+
ModelSpec("gte-modernbert-base", "Alibaba-NLP/gte-reranker-modernbert-base", "f7481e6055501a30fb19d090657df9ec1f79ab2c", 512, 8, trust_remote_code=True),
|
|
44
|
+
ModelSpec("gte-modernbert-int8", "Alibaba-NLP/gte-reranker-modernbert-base", "f7481e6055501a30fb19d090657df9ec1f79ab2c", 512, 16, backend="onnx_int8", onnx_file="onnx/model_int8.onnx"),
|
|
45
|
+
ModelSpec("mxbai-xsmall-v1", "mixedbread-ai/mxbai-rerank-xsmall-v1", "b5c6e9da73abc3711f593f705371cdbe9e0fe422", 512, 16),
|
|
46
|
+
)})
|
|
47
|
+
MODEL_IMPLEMENTATION_DIGEST = "sha256:" + hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ModelUnavailableError(ContractError):
|
|
51
|
+
"""A required local snapshot, runtime or backend is unavailable."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_model_spec(model_id: str) -> ModelSpec:
|
|
55
|
+
try:
|
|
56
|
+
return MODEL_SPECS[model_id]
|
|
57
|
+
except (KeyError, TypeError) as exc:
|
|
58
|
+
raise ContractError("unsupported model ID; inspect MODEL_SPECS") from exc
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _file_digest(path: Path) -> str:
|
|
62
|
+
h = hashlib.sha256()
|
|
63
|
+
with path.open("rb") as stream:
|
|
64
|
+
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
65
|
+
h.update(block)
|
|
66
|
+
return "sha256:" + h.hexdigest()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _directory_alias(path: Path) -> bool:
|
|
70
|
+
"""Recognize directory links without following them, including NT junctions."""
|
|
71
|
+
info = path.lstat()
|
|
72
|
+
attributes = getattr(info, "st_file_attributes", 0)
|
|
73
|
+
# Windows directory reparse points include junctions and other directory
|
|
74
|
+
# aliases; file blob symlinks deliberately retain their content-bound policy.
|
|
75
|
+
return (path.is_symlink() and path.is_dir()) or bool(attributes & 0x400 and attributes & 0x10)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _snapshot_root(snapshot_path: str | Path) -> Path:
|
|
79
|
+
path = Path(snapshot_path).expanduser().absolute()
|
|
80
|
+
if not path.is_dir():
|
|
81
|
+
raise ModelUnavailableError("pinned local snapshot directory missing")
|
|
82
|
+
if _directory_alias(path):
|
|
83
|
+
raise ModelUnavailableError("snapshot directory symlinks/reparse aliases are unsupported")
|
|
84
|
+
return path.resolve()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def snapshot_identity(snapshot_path: str | Path) -> dict:
|
|
88
|
+
"""Bind every relative filename and file contents, including HF blob links.
|
|
89
|
+
|
|
90
|
+
Inspect each directory before descending: rglob can conceal linked trees on
|
|
91
|
+
some platforms. No directory symlink, junction or directory reparse alias is
|
|
92
|
+
allowed, even when it contains no referenced Python module.
|
|
93
|
+
"""
|
|
94
|
+
path = _snapshot_root(snapshot_path)
|
|
95
|
+
files, directories = {}, [path]
|
|
96
|
+
while directories:
|
|
97
|
+
for item in sorted(directories.pop().iterdir()):
|
|
98
|
+
if _directory_alias(item):
|
|
99
|
+
raise ModelUnavailableError("snapshot directory symlinks/reparse aliases are unsupported")
|
|
100
|
+
if item.is_dir():
|
|
101
|
+
directories.append(item)
|
|
102
|
+
elif item.is_file():
|
|
103
|
+
files[item.relative_to(path).as_posix()] = _file_digest(item)
|
|
104
|
+
else:
|
|
105
|
+
raise ModelUnavailableError("snapshot contains a missing file target or unsupported file")
|
|
106
|
+
if not files:
|
|
107
|
+
raise ModelUnavailableError("pinned local snapshot is empty")
|
|
108
|
+
return {"snapshot_files": files, "snapshot_digest": value_digest(files)}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _validate_custom_code(path: Path) -> None:
|
|
112
|
+
"""Resolve static model/tokenizer imports using one local package policy.
|
|
113
|
+
|
|
114
|
+
This is a conservative syntax-based closure check, not a Python sandbox or
|
|
115
|
+
proof of arbitrary program behavior. Local packages require __init__.py;
|
|
116
|
+
wildcard imports, known dynamic loaders and undeclared external imports
|
|
117
|
+
are unavailable. Known loader/execution helper modules are excluded as a
|
|
118
|
+
whole at import/reference boundaries, so renamed imports cannot evade the
|
|
119
|
+
policy. Direct loader primitives and package hooks are also excluded.
|
|
120
|
+
Remaining stdlib and declared backend package roots are external.
|
|
121
|
+
This finite policy does not detect every way Python can construct or execute
|
|
122
|
+
code. Source is parsed, never imported or executed.
|
|
123
|
+
"""
|
|
124
|
+
pending, visited, trees = [], set(), {}
|
|
125
|
+
external = sys.stdlib_module_names | {"torch", "transformers", "sentence_transformers", "onnxruntime", "numpy"}
|
|
126
|
+
# Whole-module exclusions deliberately include benign members: accepting a
|
|
127
|
+
# loader module under an alias would require proving all later uses safe.
|
|
128
|
+
# pkgutil/pydoc wrap imports, importlib/imp expose loader machinery,
|
|
129
|
+
# runpy executes modules, and builtins exposes import/compile/eval/exec.
|
|
130
|
+
loader_modules = {"pkgutil", "pydoc", "importlib", "runpy", "builtins", "imp"}
|
|
131
|
+
loader_primitives = {
|
|
132
|
+
"__import__", "compile", "eval", "exec", "import_module", "resolve_name",
|
|
133
|
+
"locate", "safeimport", "run_module", "run_path", "load_module",
|
|
134
|
+
"load_source", "load_compiled", "load_dynamic", "exec_module",
|
|
135
|
+
"get_importer", "find_loader", "iter_importers",
|
|
136
|
+
"module_from_spec", "spec_from_loader", "spec_from_file_location",
|
|
137
|
+
"SourceFileLoader", "SourcelessFileLoader", "ExtensionFileLoader",
|
|
138
|
+
"LazyLoader",
|
|
139
|
+
}
|
|
140
|
+
package_hooks = {"__getattr__", "__path__"}
|
|
141
|
+
unsupported_references = loader_modules | loader_primitives | package_hooks
|
|
142
|
+
|
|
143
|
+
def parse(source):
|
|
144
|
+
if source not in trees:
|
|
145
|
+
relative = source.relative_to(path).as_posix()
|
|
146
|
+
try:
|
|
147
|
+
trees[source] = ast.parse(source.read_text(encoding="utf-8"), filename=relative)
|
|
148
|
+
except (SyntaxError, UnicodeError) as exc:
|
|
149
|
+
raise ModelUnavailableError("pinned local custom-code cannot be parsed: " + relative) from exc
|
|
150
|
+
return trees[source]
|
|
151
|
+
|
|
152
|
+
def resolve(parts, *, required=True):
|
|
153
|
+
"""Queue package parents and return the unique local module/package."""
|
|
154
|
+
if not parts or any(not part.isidentifier() for part in parts):
|
|
155
|
+
raise ModelUnavailableError("unsafe or outside pinned local custom-code path")
|
|
156
|
+
for depth in range(1, len(parts) + 1):
|
|
157
|
+
base = path.joinpath(*parts[:depth])
|
|
158
|
+
module, initializer = base.with_suffix(".py"), base / "__init__.py"
|
|
159
|
+
if module.is_file() and initializer.is_file():
|
|
160
|
+
raise ModelUnavailableError("ambiguous pinned local custom-code module/package: " + ".".join(parts[:depth]))
|
|
161
|
+
if depth < len(parts):
|
|
162
|
+
if not initializer.is_file():
|
|
163
|
+
if required:
|
|
164
|
+
raise ModelUnavailableError("pinned local custom-code module missing: " + ".".join(parts))
|
|
165
|
+
return None
|
|
166
|
+
pending.append(initializer)
|
|
167
|
+
elif initializer.is_file():
|
|
168
|
+
pending.append(initializer)
|
|
169
|
+
return initializer
|
|
170
|
+
elif module.is_file():
|
|
171
|
+
pending.append(module)
|
|
172
|
+
return module
|
|
173
|
+
elif base.is_dir():
|
|
174
|
+
raise ModelUnavailableError("local namespace package lacks a provable initializer: " + ".".join(parts))
|
|
175
|
+
if required:
|
|
176
|
+
raise ModelUnavailableError("pinned local custom-code module missing: " + ".".join(parts))
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
def local(parts, *, relative=False):
|
|
180
|
+
root = path / parts[0]
|
|
181
|
+
if relative or root.exists() or root.with_suffix(".py").exists():
|
|
182
|
+
return resolve(parts)
|
|
183
|
+
if parts[0] not in external:
|
|
184
|
+
raise ModelUnavailableError("undeclared external or missing local custom-code module: " + ".".join(parts))
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
def binds(source, name):
|
|
188
|
+
# A package may export a class/function/value rather than a child module.
|
|
189
|
+
# Existing child code always takes precedence for closure traversal.
|
|
190
|
+
for node in parse(source).body:
|
|
191
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and node.name == name:
|
|
192
|
+
return True
|
|
193
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
194
|
+
if any((alias.asname or (alias.name.split(".")[0] if isinstance(node, ast.Import) else alias.name)) == name for alias in node.names):
|
|
195
|
+
return True
|
|
196
|
+
targets = node.targets if isinstance(node, ast.Assign) else [node.target] if isinstance(node, ast.AnnAssign) else []
|
|
197
|
+
if any(isinstance(target, ast.Name) and target.id == name for target in targets):
|
|
198
|
+
return True
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
def children(parts, source, names):
|
|
202
|
+
if source is not None and source.name != "__init__.py":
|
|
203
|
+
return # from a_module import an_attribute does not load a child.
|
|
204
|
+
for alias in names:
|
|
205
|
+
child = resolve(parts + [alias.name], required=False)
|
|
206
|
+
if child is None and (source is None or not binds(source, alias.name)):
|
|
207
|
+
raise ModelUnavailableError("pinned local custom-code module missing: " + ".".join(parts + [alias.name]))
|
|
208
|
+
|
|
209
|
+
root_init = path / "__init__.py"
|
|
210
|
+
for filename in ("config.json", "tokenizer_config.json"):
|
|
211
|
+
try:
|
|
212
|
+
config = json.loads((path / filename).read_text(encoding="utf-8"))
|
|
213
|
+
auto_map = config.get("auto_map", {})
|
|
214
|
+
if not isinstance(auto_map, dict):
|
|
215
|
+
raise TypeError("auto_map")
|
|
216
|
+
except (ValueError, TypeError, AttributeError, UnicodeError) as exc:
|
|
217
|
+
raise ContractError("invalid pinned custom-code configuration: " + filename) from exc
|
|
218
|
+
for references in auto_map.values():
|
|
219
|
+
for reference in references if isinstance(references, list) else [references]:
|
|
220
|
+
if reference is None:
|
|
221
|
+
continue
|
|
222
|
+
if not isinstance(reference, str) or "--" in reference:
|
|
223
|
+
raise ModelUnavailableError("custom-code reference requires another repository; complete pinned local code closure required")
|
|
224
|
+
module, separator, class_name = reference.rpartition(".")
|
|
225
|
+
if not separator or not class_name.isidentifier():
|
|
226
|
+
raise ModelUnavailableError("unsafe pinned local custom-code reference")
|
|
227
|
+
resolve(module.split("."))
|
|
228
|
+
if root_init.is_file():
|
|
229
|
+
pending.append(root_init)
|
|
230
|
+
|
|
231
|
+
while pending:
|
|
232
|
+
source = pending.pop()
|
|
233
|
+
if source in visited:
|
|
234
|
+
continue
|
|
235
|
+
visited.add(source)
|
|
236
|
+
package = list(source.relative_to(path).parts[:-1])
|
|
237
|
+
for node in ast.walk(parse(source)):
|
|
238
|
+
# Reject direct references even when stored for a later call.
|
|
239
|
+
# Import exclusions below use original module/symbol names, before
|
|
240
|
+
# aliasing, and apply to every parsed transitive local source.
|
|
241
|
+
if ((isinstance(node, ast.Name) and node.id in unsupported_references)
|
|
242
|
+
or (isinstance(node, ast.Attribute) and node.attr in unsupported_references)
|
|
243
|
+
or (isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in unsupported_references)):
|
|
244
|
+
raise ModelUnavailableError("dynamic custom-code loading is unsupported; static local closure required")
|
|
245
|
+
if not isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
246
|
+
continue
|
|
247
|
+
names = [alias.name for alias in node.names] if isinstance(node, ast.Import) else [node.module or ""]
|
|
248
|
+
if (any(name.split(".")[0] in loader_modules for name in names)
|
|
249
|
+
or any(alias.name.rsplit(".", 1)[-1] in loader_primitives | package_hooks for alias in node.names)):
|
|
250
|
+
raise ModelUnavailableError("dynamic custom-code loading is unsupported; static local closure required")
|
|
251
|
+
if any(alias.name == "*" for alias in node.names):
|
|
252
|
+
raise ModelUnavailableError("custom-code wildcard lacks a provable module closure")
|
|
253
|
+
if isinstance(node, ast.Import):
|
|
254
|
+
for alias in node.names:
|
|
255
|
+
local(alias.name.split("."))
|
|
256
|
+
continue
|
|
257
|
+
if node.level:
|
|
258
|
+
if node.level - 1 > len(package):
|
|
259
|
+
raise ModelUnavailableError("relative custom-code import escapes pinned snapshot")
|
|
260
|
+
parent = package[:len(package) - (node.level - 1)]
|
|
261
|
+
parts = parent + (node.module.split(".") if node.module else [])
|
|
262
|
+
dependency = resolve(parts) if parts else root_init if root_init.is_file() else None
|
|
263
|
+
if dependency is not None:
|
|
264
|
+
pending.append(dependency)
|
|
265
|
+
else:
|
|
266
|
+
parts = node.module.split(".")
|
|
267
|
+
dependency = local(parts)
|
|
268
|
+
if dependency is None:
|
|
269
|
+
continue
|
|
270
|
+
children(parts, dependency, node.names)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def preflight_model(model_id: str, snapshot_path: str | Path, expected_snapshot_digest: str | None = None, *, check_backend: bool = True) -> dict:
|
|
274
|
+
"""Check snapshot completeness and optional runtime without loading models."""
|
|
275
|
+
spec = get_model_spec(model_id)
|
|
276
|
+
path = _snapshot_root(snapshot_path)
|
|
277
|
+
if path.name != spec.revision:
|
|
278
|
+
raise ModelUnavailableError("snapshot directory must name the exact pinned model revision")
|
|
279
|
+
if not path.is_dir() or not (path / "config.json").is_file():
|
|
280
|
+
raise ModelUnavailableError("complete pinned local snapshot required: config.json missing")
|
|
281
|
+
if not (path / "tokenizer_config.json").is_file() or not any((path / name).is_file() for name in ("tokenizer.json", "vocab.txt", "vocab.json", "spiece.model", "sentencepiece.bpe.model")):
|
|
282
|
+
raise ModelUnavailableError("complete pinned tokenizer files required")
|
|
283
|
+
identity = snapshot_identity(path)
|
|
284
|
+
_validate_custom_code(path)
|
|
285
|
+
if spec.backend == "onnx_int8":
|
|
286
|
+
if not (path / spec.onnx_file).is_file():
|
|
287
|
+
raise ModelUnavailableError("pinned official INT8 ONNX graph missing")
|
|
288
|
+
else:
|
|
289
|
+
weights = list(path.glob("*.safetensors")) + list(path.glob("pytorch_model*.bin"))
|
|
290
|
+
if not weights:
|
|
291
|
+
raise ModelUnavailableError("pinned model weights missing")
|
|
292
|
+
# Reject partial sharded snapshots before the optional loader is invoked.
|
|
293
|
+
import json
|
|
294
|
+
for index in path.glob("*.index.json"):
|
|
295
|
+
try:
|
|
296
|
+
shards = set(json.loads(index.read_text(encoding="utf-8"))["weight_map"].values())
|
|
297
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
298
|
+
raise ContractError("invalid model weight index") from exc
|
|
299
|
+
if any(not isinstance(name, str) or Path(name).is_absolute() or ".." in Path(name).parts or not (path / name).is_file() for name in shards):
|
|
300
|
+
raise ModelUnavailableError("pinned sharded weights incomplete")
|
|
301
|
+
if expected_snapshot_digest is not None:
|
|
302
|
+
require_digest(expected_snapshot_digest, "model_snapshot_digest")
|
|
303
|
+
if identity["snapshot_digest"] != expected_snapshot_digest:
|
|
304
|
+
raise ContractError("pinned model snapshot digest differs")
|
|
305
|
+
if check_backend:
|
|
306
|
+
if sys.version_info[:2] != (3, 12):
|
|
307
|
+
raise ModelUnavailableError("optional model execution requires Python 3.12; backend compatibility is unverified")
|
|
308
|
+
packages = ("onnxruntime", "numpy", "transformers") if spec.backend == "onnx_int8" else ("torch", "sentence_transformers", "transformers")
|
|
309
|
+
missing = [name for name in packages if importlib.util.find_spec(name) is None]
|
|
310
|
+
if missing:
|
|
311
|
+
raise ModelUnavailableError("optional backend packages missing: " + ", ".join(missing))
|
|
312
|
+
return {**spec.to_dict(), **identity, "device": "cpu", "activation": "identity", "truncation": "longest_first", "tokenizer_revision": spec.revision}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _scores(values, count: int) -> tuple[float, ...]:
|
|
316
|
+
try:
|
|
317
|
+
result = tuple(float(value) for value in values)
|
|
318
|
+
except (TypeError, ValueError, OverflowError) as exc:
|
|
319
|
+
raise ContractError("model output must contain scalar ranking utilities") from exc
|
|
320
|
+
if len(result) != count or any(not math.isfinite(value) for value in result):
|
|
321
|
+
raise ContractError("model score coverage differs or contains nonfinite values")
|
|
322
|
+
return result
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def request_pairs(request: Mapping) -> tuple[tuple[str, str], ...]:
|
|
326
|
+
if not isinstance(request, Mapping) or request.get("schema_version") != "sphynx.rag.rerank.request.v1":
|
|
327
|
+
raise ContractError("unsupported model rerank request")
|
|
328
|
+
query, candidates = request.get("query"), request.get("candidates")
|
|
329
|
+
if not isinstance(query, str) or not query.strip() or not isinstance(candidates, (tuple, list)) or not candidates:
|
|
330
|
+
raise ContractError("model request requires query and nonempty candidates")
|
|
331
|
+
ids, pairs = [], []
|
|
332
|
+
for item in candidates:
|
|
333
|
+
if not isinstance(item, Mapping) or not isinstance(item.get("candidate_id"), str) or not isinstance(item.get("input"), str) or not item["candidate_id"] or not item["input"].strip():
|
|
334
|
+
raise ContractError("model candidate requires ID and input text")
|
|
335
|
+
ids.append(item["candidate_id"])
|
|
336
|
+
pairs.append((query, item["input"]))
|
|
337
|
+
if len(set(ids)) != len(ids):
|
|
338
|
+
raise ContractError("duplicate model candidate IDs")
|
|
339
|
+
for name in ("case_id", "query_id", "pool_digest"):
|
|
340
|
+
if not isinstance(request.get(name), str) or not request[name]:
|
|
341
|
+
raise ContractError("model request identity missing")
|
|
342
|
+
return tuple(pairs)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class OfflineModelScorer:
|
|
346
|
+
"""Lazy frozen scorer; construction/configuration do not import ML packages.
|
|
347
|
+
|
|
348
|
+
``score_pairs`` supports fusion/retrieval callers; ``__call__`` preserves the
|
|
349
|
+
existing rerank response protocol and ordered pool coverage. Full request,
|
|
350
|
+
dataset, pool and configuration identities belong in the caller's capture.
|
|
351
|
+
"""
|
|
352
|
+
def __init__(self, model_id: str, snapshot_path: str | Path, expected_snapshot_digest: str, *, threads: int = 4):
|
|
353
|
+
self.spec = get_model_spec(model_id)
|
|
354
|
+
self.snapshot_path = Path(snapshot_path).expanduser().absolute()
|
|
355
|
+
require_digest(expected_snapshot_digest, "model_snapshot_digest")
|
|
356
|
+
if type(threads) is not int or not 1 <= threads <= 64:
|
|
357
|
+
raise ContractError("threads must be an integer between 1 and 64")
|
|
358
|
+
self.snapshot_digest, self.threads = expected_snapshot_digest, threads
|
|
359
|
+
self._model = self._tokenizer = self._torch = None
|
|
360
|
+
self._loaded_identity = None
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def configuration(self) -> CandidateConfiguration:
|
|
364
|
+
return CandidateConfiguration(self.spec.model_id + "-offline-v1", "sphynx.offline-model.v1", MODEL_IMPLEMENTATION_DIGEST,
|
|
365
|
+
{**self.spec.to_dict(), "device": "cpu", "threads": self.threads, "seed": 0, "activation": "identity", "truncation": "longest_first", "tokenizer_revision": self.spec.revision, "offline": True},
|
|
366
|
+
"trusted_local", self.spec.checkpoint, self.spec.revision, self.snapshot_digest)
|
|
367
|
+
|
|
368
|
+
def preflight(self) -> dict:
|
|
369
|
+
return preflight_model(self.spec.model_id, self.snapshot_path, self.snapshot_digest)
|
|
370
|
+
|
|
371
|
+
def _load(self):
|
|
372
|
+
self._loaded_identity = self.preflight()
|
|
373
|
+
# Force offline flags before importing packages. Never restore a permissive
|
|
374
|
+
# network setting while this scorer or another lazy adapter can execute.
|
|
375
|
+
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
376
|
+
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
|
377
|
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
378
|
+
if self.spec.backend == "onnx_int8":
|
|
379
|
+
import onnxruntime as ort
|
|
380
|
+
from transformers import AutoTokenizer
|
|
381
|
+
self._tokenizer = AutoTokenizer.from_pretrained(str(self.snapshot_path), local_files_only=True, trust_remote_code=self.spec.trust_remote_code)
|
|
382
|
+
options = ort.SessionOptions()
|
|
383
|
+
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
|
384
|
+
options.intra_op_num_threads = self.threads
|
|
385
|
+
self._model = ort.InferenceSession(str(self.snapshot_path / self.spec.onnx_file), sess_options=options, providers=["CPUExecutionProvider"])
|
|
386
|
+
else:
|
|
387
|
+
import inspect
|
|
388
|
+
import torch
|
|
389
|
+
from sentence_transformers import CrossEncoder
|
|
390
|
+
self._torch = torch
|
|
391
|
+
torch.set_num_threads(self.threads)
|
|
392
|
+
torch.manual_seed(0)
|
|
393
|
+
kwargs = {"model_name_or_path": str(self.snapshot_path), "device": "cpu", "max_length": self.spec.max_length, "trust_remote_code": self.spec.trust_remote_code}
|
|
394
|
+
accepted = inspect.signature(CrossEncoder).parameters
|
|
395
|
+
if "local_files_only" in accepted:
|
|
396
|
+
kwargs["local_files_only"] = True
|
|
397
|
+
elif "model_kwargs" in accepted and "tokenizer_kwargs" in accepted:
|
|
398
|
+
kwargs["model_kwargs"] = {"local_files_only": True}
|
|
399
|
+
kwargs["tokenizer_kwargs"] = {"local_files_only": True}
|
|
400
|
+
else:
|
|
401
|
+
raise ModelUnavailableError("CrossEncoder backend lacks enforceable local_files_only support")
|
|
402
|
+
if "trust_remote_code" not in accepted and self.spec.trust_remote_code:
|
|
403
|
+
raise ModelUnavailableError("CrossEncoder backend lacks pinned local custom-code support")
|
|
404
|
+
self._model = CrossEncoder(**kwargs)
|
|
405
|
+
|
|
406
|
+
def score_pairs(self, pairs: Sequence[tuple[str, str]]) -> tuple[float, ...]:
|
|
407
|
+
pairs = tuple(pairs)
|
|
408
|
+
if not pairs or any(not isinstance(pair, (tuple, list)) or len(pair) != 2 or any(not isinstance(text, str) or not text.strip() for text in pair) for pair in pairs):
|
|
409
|
+
raise ContractError("score_pairs requires nonempty query/document text pairs")
|
|
410
|
+
if self._model is None:
|
|
411
|
+
self._load()
|
|
412
|
+
else:
|
|
413
|
+
# Recheck the file closure even when model weights are in memory.
|
|
414
|
+
# This keeps later captures from claiming a changed local snapshot.
|
|
415
|
+
preflight_model(self.spec.model_id, self.snapshot_path, self.snapshot_digest, check_backend=False)
|
|
416
|
+
if self.spec.backend == "cross_encoder":
|
|
417
|
+
values = self._model.predict(list(pairs), batch_size=self.spec.batch_size, show_progress_bar=False, convert_to_numpy=True, activation_fn=self._torch.nn.Identity())
|
|
418
|
+
return _scores(values.reshape(-1).tolist(), len(pairs))
|
|
419
|
+
import numpy as np
|
|
420
|
+
values = []
|
|
421
|
+
input_names = {item.name for item in self._model.get_inputs()}
|
|
422
|
+
for start in range(0, len(pairs), self.spec.batch_size):
|
|
423
|
+
batch = pairs[start:start + self.spec.batch_size]
|
|
424
|
+
encoded = self._tokenizer([p[0] for p in batch], [p[1] for p in batch], padding=True, truncation="longest_first", max_length=self.spec.max_length, return_tensors="np")
|
|
425
|
+
if not input_names <= set(encoded):
|
|
426
|
+
raise ModelUnavailableError("tokenizer does not supply all frozen ONNX inputs")
|
|
427
|
+
feed = {name: np.asarray(encoded[name], dtype=np.int64) for name in input_names}
|
|
428
|
+
values.extend(np.asarray(self._model.run(None, feed)[0], dtype=float).reshape(-1).tolist())
|
|
429
|
+
return _scores(values, len(pairs))
|
|
430
|
+
|
|
431
|
+
def __call__(self, request: Mapping) -> dict:
|
|
432
|
+
values = self.score_pairs(request_pairs(request))
|
|
433
|
+
return {"schema_version": "sphynx.rag.rerank.response.v1", "query_id": request["query_id"], "pool_digest": request["pool_digest"], "scores": [{"candidate_id": item["candidate_id"], "ranking_score": value} for item, value in zip(request["candidates"], values)]}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def create_model_scorer(model_id: str, snapshot_path: str | Path, expected_snapshot_digest: str, *, threads: int = 4) -> OfflineModelScorer:
|
|
437
|
+
return OfflineModelScorer(model_id, snapshot_path, expected_snapshot_digest, threads=threads)
|
|
438
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Content-free optional model capability records; no cache or ML imports."""
|
|
2
|
+
from .catalog import CandidateDescriptor
|
|
3
|
+
from .integration_contracts import ScoreField
|
|
4
|
+
|
|
5
|
+
_LOCAL_SOURCE = 'sha256:619729f450a24430bbe2c1641a6fe47723cc9f616385ec7814f3fcb52e2249c1'
|
|
6
|
+
_RECORDED_SOURCE = 'sha256:352086665411a7f44429ccd73b9f778d001f5ed727a09ea560cb740a9533f437'
|
|
7
|
+
_FIELDS = (ScoreField("ranking_score", "utility", "higher", "model_score"),)
|
|
8
|
+
_MODELS = ("minilm-l6-v2", "jina-turbo-v1", "gte-modernbert-base", "gte-modernbert-int8", "mxbai-xsmall-v1")
|
|
9
|
+
CANDIDATES = tuple(CandidateDescriptor(
|
|
10
|
+
candidate_id="model-" + model_id + "-offline-v1", track="rerank",
|
|
11
|
+
interface_version="sphynx.offline-model.v1", source_digest=_LOCAL_SOURCE,
|
|
12
|
+
supported_dataset_ids=(), execution_modes=("trusted_local",),
|
|
13
|
+
implementation_status="available",
|
|
14
|
+
reason="Offline implementation available; runtime support unverified. Matching optional ML packages and complete pinned snapshots require caller preflight; fresh backend smoke for every profile remains unverified.",
|
|
15
|
+
local_prerequisites=("complete revision-named local snapshot and SHA-256 file closure", "matching pinned tokenizer/config/weights", "Python 3.12 required intended runtime; optional backend compatibility unverified"),
|
|
16
|
+
dependencies=("onnxruntime", "numpy", "transformers") if model_id == "gte-modernbert-int8" else ("torch", "sentence-transformers", "transformers"),
|
|
17
|
+
score_fields=_FIELDS, model_identity_required=True, supported_python=(),
|
|
18
|
+
) for model_id in _MODELS) + tuple(CandidateDescriptor(
|
|
19
|
+
candidate_id="model-" + model_id + "-recorded-v2", track="rerank",
|
|
20
|
+
interface_version="sphynx.recorded.rerank.v2", source_digest=_RECORDED_SOURCE,
|
|
21
|
+
supported_dataset_ids=(), execution_modes=("recorded",), implementation_status="available",
|
|
22
|
+
local_prerequisites=("exact complete dataset-bound score artifact and SHA-256", "dataset/cohort/reference/policy identity", "pinned original pools for expanded source import"),
|
|
23
|
+
score_fields=_FIELDS, supported_python=("3.11", "3.12", "3.13", "3.14"),
|
|
24
|
+
) for model_id in _MODELS) + tuple(CandidateDescriptor(
|
|
25
|
+
candidate_id="model-" + model_id + "-prescreen", track="rerank",
|
|
26
|
+
interface_version="sphynx.model-prescreen.v1", source_digest=None,
|
|
27
|
+
supported_dataset_ids=(), execution_modes=("recorded",), implementation_status="blocked",
|
|
28
|
+
reason="Historical prescreen has only partial query coverage; no complete validated confirmation recording or supported lazy adapter. Experimental/incomplete.",
|
|
29
|
+
local_prerequisites=("separately approved complete recording and implementation",), score_fields=_FIELDS,
|
|
30
|
+
) for model_id in ("qwen3-reranker-0.6b", "mxbai-base-v2", "bge-v2-m3"))
|