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.
Files changed (174) hide show
  1. sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
  2. sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
  3. sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
  4. sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
  5. sphynx/__init__.py +469 -0
  6. sphynx/__init__.pyi +376 -0
  7. sphynx/_analytical_client.py +77 -0
  8. sphynx/_analytical_values.py +252 -0
  9. sphynx/_audit_client.py +173 -0
  10. sphynx/_comparison_adapters.py +113 -0
  11. sphynx/_componentized_full_rag.py +34 -0
  12. sphynx/_composed_pipeline.py +109 -0
  13. sphynx/_full_rag_grading.py +218 -0
  14. sphynx/_full_rag_trace.py +221 -0
  15. sphynx/_local_eval.py +268 -0
  16. sphynx/_pipeline_core.py +243 -0
  17. sphynx/_pr_client.py +237 -0
  18. sphynx/_rag_diagnostics_calibration.py +285 -0
  19. sphynx/_rag_diagnostics_catalog.py +39 -0
  20. sphynx/_rag_diagnostics_common.py +26 -0
  21. sphynx/_rag_diagnostics_contracts.py +186 -0
  22. sphynx/_rag_diagnostics_engine.py +243 -0
  23. sphynx/_rag_diagnostics_importers.py +242 -0
  24. sphynx/_rag_diagnostics_ranking.py +81 -0
  25. sphynx/_rag_diagnostics_semantic.py +325 -0
  26. sphynx/_rag_ranking.py +149 -0
  27. sphynx/_speech_benchmark_scoring.py +140 -0
  28. sphynx/_speech_benchmark_sources.py +275 -0
  29. sphynx/_speech_complete_scoring.py +218 -0
  30. sphynx/_speech_complete_source.py +118 -0
  31. sphynx/_speech_kido.py +294 -0
  32. sphynx/_speech_scoring.py +77 -0
  33. sphynx/_track_capture.py +213 -0
  34. sphynx/analytical_views.py +515 -0
  35. sphynx/assessment_contracts.py +378 -0
  36. sphynx/assessment_selection.py +57 -0
  37. sphynx/audit_access.py +245 -0
  38. sphynx/audit_contracts.py +204 -0
  39. sphynx/audit_requests.py +180 -0
  40. sphynx/authoring.py +188 -0
  41. sphynx/benchmarks.py +568 -0
  42. sphynx/candidates.py +62 -0
  43. sphynx/catalog.py +272 -0
  44. sphynx/chunking.py +372 -0
  45. sphynx/clickhouse_reader.py +169 -0
  46. sphynx/client.py +486 -0
  47. sphynx/comparison_core.py +149 -0
  48. sphynx/comparison_views.py +223 -0
  49. sphynx/components.py +1661 -0
  50. sphynx/contracts.py +514 -0
  51. sphynx/customer_rerank.py +299 -0
  52. sphynx/customer_retrieval.py +146 -0
  53. sphynx/data/benchmark-releases.v1.json +164 -0
  54. sphynx/data/public-benchmark-release.v1.json +354 -0
  55. sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
  56. sphynx/data/rag-paper-references.v1.json +176 -0
  57. sphynx/data/rag-paper-schemas.v1.json +1069 -0
  58. sphynx/data/rag-rerank-v1.json +507 -0
  59. sphynx/dataset_catalog_data.py +1214 -0
  60. sphynx/dataset_readiness.py +138 -0
  61. sphynx/dataset_v3.py +999 -0
  62. sphynx/demo.py +130 -0
  63. sphynx/demo_candidates/__init__.py +1 -0
  64. sphynx/demo_candidates/dense.py +11 -0
  65. sphynx/demo_candidates/lexical.py +21 -0
  66. sphynx/diagnostics.py +207 -0
  67. sphynx/documentation.py +737 -0
  68. sphynx/documentation_candidate.py +939 -0
  69. sphynx/evals.py +538 -0
  70. sphynx/evaluation_protocols.py +124 -0
  71. sphynx/evaluator_bindings.py +370 -0
  72. sphynx/evidence.py +352 -0
  73. sphynx/execution_capabilities.py +109 -0
  74. sphynx/fast_search.py +285 -0
  75. sphynx/full_rag.py +341 -0
  76. sphynx/hosted/__init__.py +18 -0
  77. sphynx/hosted/benchmark_intake.py +218 -0
  78. sphynx/hosted/contracts.py +361 -0
  79. sphynx/hosted/model_admission.py +130 -0
  80. sphynx/hosted/model_artifacts.py +298 -0
  81. sphynx/hosted/model_contracts.py +63 -0
  82. sphynx/hosted/model_gcs.py +181 -0
  83. sphynx/hosted/model_history.py +84 -0
  84. sphynx/hosted/model_session.py +233 -0
  85. sphynx/hosted/public_suite.py +74 -0
  86. sphynx/hosted/registry.py +97 -0
  87. sphynx/hosted/revisions.py +126 -0
  88. sphynx/hosted/sandbox.py +105 -0
  89. sphynx/hosted/submission.py +287 -0
  90. sphynx/hosted_workflow.py +451 -0
  91. sphynx/http_transport.py +86 -0
  92. sphynx/integrated_rag.py +352 -0
  93. sphynx/integrated_workflow.py +552 -0
  94. sphynx/integration_contracts.py +474 -0
  95. sphynx/invocation.py +791 -0
  96. sphynx/invocation_candidates.py +392 -0
  97. sphynx/invocation_catalog_data.py +40 -0
  98. sphynx/learned_catalog_data.py +30 -0
  99. sphynx/learned_rerank.py +267 -0
  100. sphynx/legacy_full_rag.py +67 -0
  101. sphynx/lifecycle.py +885 -0
  102. sphynx/lifecycle_workflow.py +490 -0
  103. sphynx/metric_views.py +474 -0
  104. sphynx/metrics.py +282 -0
  105. sphynx/model_adapters.py +438 -0
  106. sphynx/model_catalog_data.py +30 -0
  107. sphynx/official_benchmarks.py +385 -0
  108. sphynx/packs.py +258 -0
  109. sphynx/pipeline.py +313 -0
  110. sphynx/pipeline_workflow.py +551 -0
  111. sphynx/pr_evaluations.py +560 -0
  112. sphynx/progress.py +89 -0
  113. sphynx/public_bundle.py +173 -0
  114. sphynx/public_eval.py +62 -0
  115. sphynx/public_rerank.py +355 -0
  116. sphynx/py.typed +0 -0
  117. sphynx/pytest_plugin.py +62 -0
  118. sphynx/question_robustness.py +437 -0
  119. sphynx/rag.py +329 -0
  120. sphynx/rag_diagnostics.py +309 -0
  121. sphynx/rag_papers/__init__.py +29 -0
  122. sphynx/rag_papers/__main__.py +45 -0
  123. sphynx/rag_papers/candidates.py +19 -0
  124. sphynx/rag_papers/contracts.py +282 -0
  125. sphynx/rag_papers/datasets.py +291 -0
  126. sphynx/rag_papers/experiments.py +116 -0
  127. sphynx/rag_papers/judges.py +308 -0
  128. sphynx/rag_papers/registry.py +33 -0
  129. sphynx/rag_papers/runner.py +176 -0
  130. sphynx/rag_papers/sable.py +135 -0
  131. sphynx/rag_papers/schemas.py +12 -0
  132. sphynx/rag_papers/scoring.py +249 -0
  133. sphynx/rag_papers/semantic.py +288 -0
  134. sphynx/rag_papers/trec.py +80 -0
  135. sphynx/recorded_scores.py +352 -0
  136. sphynx/records.py +124 -0
  137. sphynx/reflex_traces.py +193 -0
  138. sphynx/report_rendering.py +71 -0
  139. sphynx/rerank_family_contracts.py +150 -0
  140. sphynx/rerank_workflow.py +424 -0
  141. sphynx/research_candidate.py +190 -0
  142. sphynx/results.py +852 -0
  143. sphynx/retrieval.py +750 -0
  144. sphynx/retrieval_candidates.py +295 -0
  145. sphynx/retrieval_catalog_data.py +19 -0
  146. sphynx/retrieval_recordings.py +125 -0
  147. sphynx/reviewer_audit.py +266 -0
  148. sphynx/routing_policies.py +375 -0
  149. sphynx/runner.py +544 -0
  150. sphynx/sable_rerank.py +489 -0
  151. sphynx/score_reliability.py +393 -0
  152. sphynx/speech_benchmarks.py +204 -0
  153. sphynx/speech_campaigns.py +378 -0
  154. sphynx/speech_complete.py +236 -0
  155. sphynx/speech_contracts.py +225 -0
  156. sphynx/speech_evidence.py +429 -0
  157. sphynx/speech_listening.py +115 -0
  158. sphynx/speech_multilingual.py +71 -0
  159. sphynx/speech_tts.py +479 -0
  160. sphynx/sql/001_sync_v1.sql +30 -0
  161. sphynx/stage_contracts.py +199 -0
  162. sphynx/statistical_catalog_data.py +53 -0
  163. sphynx/statistical_rerank.py +232 -0
  164. sphynx/sync.py +797 -0
  165. sphynx/workbench.py +378 -0
  166. sphynx/workbench_catalog_data.py +13 -0
  167. sphynx/workbench_tracks.py +78 -0
  168. sphynx/workflow_async.py +197 -0
  169. sphynx/workflow_contracts.py +594 -0
  170. sphynx/workflow_decisions.py +211 -0
  171. sphynx/workflow_defaults.py +16 -0
  172. sphynx/workflow_extensions.py +212 -0
  173. sphynx/workflow_registry.py +513 -0
  174. sphynx/workflows.py +792 -0
@@ -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"))