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
sphynx/demo.py ADDED
@@ -0,0 +1,130 @@
1
+ """Run the complete synthetic beginner demo from an ordinary installed SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .contracts import INTERFACE_VERSION, CandidateSpec
10
+ from .metric_views import summarize_metrics
11
+ from .packs import check_candidate, list_packs
12
+ from .results import Scorecard, compare, grade, report
13
+ from .runner import run
14
+
15
+ _PACK_ID = "rag.rerank.v1"
16
+ _BASELINE_LABEL = "Supplied-score baseline (A)"
17
+ _CANDIDATE_LABEL = "Word-overlap example (B)"
18
+ _COMPARISON_LABEL = (
19
+ "A = supplied-score baseline (supplied-order-example). "
20
+ "B = word-overlap example (word-overlap-example).\n"
21
+ "Deltas are B - A: candidate minus baseline."
22
+ )
23
+
24
+
25
+ def _save(path: Path, text: str) -> Path:
26
+ # Exclusive creation keeps earlier reports intact, including on collisions.
27
+ with path.open("x", encoding="utf-8") as stream:
28
+ stream.write(text)
29
+ return path
30
+
31
+
32
+ def _save_scorecard(reports: Path, run_id: str, label: str, scorecard: Scorecard) -> dict[str, Path]:
33
+ summary = summarize_metrics(scorecard)
34
+ return {
35
+ "scorecard_json_path": _save(reports / f"{run_id}-scorecard.json", scorecard.to_json() + "\n"),
36
+ "scorecard_path": _save(reports / f"{run_id}-scorecard.md", scorecard.to_markdown()),
37
+ "summary_path": _save(reports / f"{run_id}-summary.md", f"# {label}\n\n" + summary.to_markdown()),
38
+ "summary_text_path": _save(reports / f"{run_id}-summary.txt", label + "\n\n" + summary.to_text()),
39
+ }
40
+
41
+
42
+ def _print_summary(label: str, scorecard: Scorecard) -> None:
43
+ counts = scorecard.to_dict()["counts"]
44
+ print(f"\n{label}")
45
+ print(
46
+ f"Cases: {counts['attempted_count']} | "
47
+ f"Valid: {counts['success_count']} | Failed: {counts['failure_count']}\n"
48
+ )
49
+ print(summarize_metrics(scorecard).to_text())
50
+
51
+
52
+ def run_demo(output_dir: str | Path | None = None) -> dict[str, Any]:
53
+ """Evaluate both packaged examples and save durable local reports.
54
+
55
+ Each method runs the six installed synthetic cases in fresh subprocesses
56
+ using ``sys.executable``. No model, provider key, or cloud setup is needed.
57
+ The default output is ``Path.home() / ".sphynx" / "beginner-quickstart"``.
58
+ Every invocation keeps earlier runs, evaluations, and reports. Returned
59
+ references include the original ``run_quickstart`` mapping keys; comparison
60
+ A is the supplied-score baseline and B is the word-overlap example.
61
+ """
62
+ output = (
63
+ Path(output_dir) if output_dir is not None
64
+ else Path.home() / ".sphynx" / "beginner-quickstart"
65
+ ).expanduser().resolve()
66
+ reports = output / "reports"
67
+ reports.mkdir(parents=True, exist_ok=True)
68
+ candidate = CandidateSpec(
69
+ name="word-overlap-example", version="example-v1",
70
+ argv=(sys.executable, "-I", "-m", "sphynx.demo_candidates.lexical"),
71
+ interface_version=INTERFACE_VERSION,
72
+ )
73
+ baseline = CandidateSpec(
74
+ name="supplied-order-example", version="example-v1",
75
+ argv=(sys.executable, "-I", "-m", "sphynx.demo_candidates.dense"),
76
+ interface_version=INTERFACE_VERSION,
77
+ )
78
+ for method in (candidate, baseline):
79
+ check = check_candidate(method, _PACK_ID)
80
+ if not check.compatible or not check.pack_runnable:
81
+ raise RuntimeError("; ".join(check.reasons))
82
+
83
+ print("SPHYNX - LOCAL RERANKING EXAMPLE")
84
+ print("Installed packs:")
85
+ for pack in list_packs():
86
+ print(f" {pack.pack_id}: {pack.status}")
87
+ print("\nRunning six synthetic reranking cases for each method...")
88
+ first_run = run(candidate, _PACK_ID, output)
89
+ first_evaluation = grade(first_run)
90
+ if first_evaluation.path is None:
91
+ raise RuntimeError(first_evaluation.reason)
92
+ scorecard = report(first_evaluation)
93
+ _print_summary(_CANDIDATE_LABEL, scorecard)
94
+ candidate_paths = _save_scorecard(reports, first_run.run_id, _CANDIDATE_LABEL, scorecard)
95
+
96
+ baseline_run = run(baseline, _PACK_ID, output)
97
+ baseline_evaluation = grade(baseline_run)
98
+ if baseline_evaluation.path is None:
99
+ raise RuntimeError(baseline_evaluation.reason)
100
+ baseline_scorecard = report(baseline_evaluation)
101
+ _print_summary(_BASELINE_LABEL, baseline_scorecard)
102
+ baseline_paths = _save_scorecard(reports, baseline_run.run_id, _BASELINE_LABEL, baseline_scorecard)
103
+
104
+ comparison = compare(baseline_evaluation, first_evaluation)
105
+ if not comparison.compatible:
106
+ raise RuntimeError(comparison.reason)
107
+ comparison_json_path = _save(
108
+ reports / f"{first_run.run_id}-comparison.json", comparison.to_json() + "\n"
109
+ )
110
+ comparison_path = _save(
111
+ reports / f"{first_run.run_id}-comparison.md",
112
+ _COMPARISON_LABEL + "\n\n" + comparison.to_markdown(),
113
+ )
114
+ print("COMPARISON")
115
+ print(_COMPARISON_LABEL)
116
+ print(f"\nReports: {reports}")
117
+ print(f"Candidate summary: {candidate_paths['summary_path'].name}")
118
+ print(f"Baseline summary: {baseline_paths['summary_path'].name}")
119
+ print(f"Comparison: {comparison_path.name}")
120
+ print("\nThese are synthetic demonstration results, not an official benchmark.")
121
+ print("No cloud sync was requested.")
122
+ return {
123
+ "run": first_run, "evaluation": first_evaluation, "scorecard": scorecard,
124
+ "baseline_run": baseline_run, "baseline_evaluation": baseline_evaluation,
125
+ "baseline_scorecard": baseline_scorecard, "comparison": comparison,
126
+ **candidate_paths,
127
+ **{f"baseline_{key}": path for key, path in baseline_paths.items()},
128
+ "comparison_path": comparison_path, "comparison_json_path": comparison_json_path,
129
+ "reports_dir": reports,
130
+ }
@@ -0,0 +1 @@
1
+ """Packaged synthetic demo candidates, executed by the installed interpreter."""
@@ -0,0 +1,11 @@
1
+ """Illustrative baseline: return each frozen passage's supplied dense score."""
2
+
3
+ from sphynx.candidates import serve_reranker
4
+
5
+
6
+ def score(query: str, passage: str, supplied_score: float) -> float:
7
+ return supplied_score
8
+
9
+
10
+ if __name__ == "__main__":
11
+ serve_reranker(score)
@@ -0,0 +1,21 @@
1
+ """Illustrative query/passage word overlap; no model or provider required."""
2
+
3
+ import re
4
+
5
+ from sphynx.candidates import serve_reranker
6
+
7
+ _TOKENS = re.compile(r"[a-z0-9]+")
8
+ _STOP = frozenset({"a", "an", "and", "are", "can", "do", "how", "i", "is", "of", "the", "to", "what", "where"})
9
+
10
+
11
+ def tokens(text: str) -> set[str]:
12
+ return set(_TOKENS.findall(text.lower())) - _STOP
13
+
14
+
15
+ def score(query: str, passage: str, supplied_score: float) -> float:
16
+ overlap = len(tokens(query) & tokens(passage))
17
+ return float(overlap) + float(supplied_score) / 100.0
18
+
19
+
20
+ if __name__ == "__main__":
21
+ serve_reranker(score)
sphynx/diagnostics.py ADDED
@@ -0,0 +1,207 @@
1
+ """Safe guidance over existing exceptions; never retry, refresh or execute work.
2
+
3
+ The original exception and its code stay unchanged. Default guidance copies no
4
+ exception message, command, token, request payload or private reference content.
5
+ Recovery values are explicit operation identifiers, not authentication grants.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Mapping
10
+ from dataclasses import dataclass, field
11
+ import json
12
+ from types import MappingProxyType
13
+ from typing import Literal, TypeGuard, cast
14
+
15
+ Retryability = Literal["fix_required", "wait_for_review", "read_original_handle",
16
+ "reconcile_before_retry", "unknown"]
17
+ _RETRY = {"fix_required", "wait_for_review", "read_original_handle", "reconcile_before_retry", "unknown"}
18
+ _RECOVERY = {"submission_id", "experiment_id", "run_id", "grading_id", "evaluation_id", "idempotency_key", "record_id"}
19
+
20
+
21
+ def _identity_value(value: object) -> TypeGuard[str]:
22
+ # Existing workflow identities use the workflow text contract. Presentation
23
+ # must not narrow keys after a driver has accepted the original operation.
24
+ if type(value) is not str or not value.strip() or '\x00' in value:
25
+ return False
26
+ try:
27
+ value.encode('utf-8')
28
+ except UnicodeError:
29
+ return False
30
+ return True
31
+
32
+
33
+ def _safe_text(value: str) -> str:
34
+ text = ''.join(char if char.isprintable() else ' ' for char in value)
35
+ return text if len(text) <= 256 else text[:256] + '... (full value in to_dict())'
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class Diagnostic:
40
+ """A readable hint. It neither grants access nor decides an evaluation."""
41
+
42
+ code: str
43
+ summary: str
44
+ next_actions: tuple[str, ...]
45
+ retryability: Retryability = "unknown"
46
+ recovery: Mapping[str, str] = field(default_factory=dict)
47
+
48
+ def __post_init__(self) -> None:
49
+ if (not isinstance(self.code, str) or not self.code or not isinstance(self.summary, str)
50
+ or not self.summary or self.retryability not in _RETRY
51
+ or not isinstance(self.next_actions, (tuple, list))
52
+ or any(type(action) is not str or not action for action in self.next_actions)):
53
+ raise ValueError("diagnostic needs a code, summary and valid guidance")
54
+ values = dict(self.recovery)
55
+ if any(key not in _RECOVERY or not _identity_value(value)
56
+ for key, value in values.items()):
57
+ raise ValueError("diagnostic recovery contains an invalid operation identity")
58
+ object.__setattr__(self, "next_actions", tuple(self.next_actions))
59
+ object.__setattr__(self, "recovery", MappingProxyType(values))
60
+
61
+ def to_dict(self) -> dict:
62
+ return {"schema_version": "sphynx.diagnostic.v1", "code": self.code, "summary": self.summary,
63
+ "next_actions": list(self.next_actions), "retryability": self.retryability,
64
+ "recovery": dict(self.recovery)}
65
+
66
+ def to_json(self) -> str:
67
+ return json.dumps(self.to_dict(), sort_keys=True, allow_nan=False)
68
+
69
+ def to_text(self) -> str:
70
+ lines = [f"{_safe_text(self.code)}: {_safe_text(self.summary)}", *["- " + _safe_text(action) for action in self.next_actions],
71
+ "Recovery: " + self.retryability]
72
+ lines.extend(f"{name}: {_safe_text(value)}" for name, value in self.recovery.items())
73
+ return "\n".join(lines)
74
+
75
+ def __str__(self) -> str:
76
+ return self.to_text()
77
+
78
+
79
+ # These statements describe next actions, not suite-specific acceptance rules.
80
+ _CONFIG = ("The configuration needs attention.",
81
+ "Check the selected suite, schemas, versions and supported options before starting work.", "fix_required")
82
+ _ACCESS = ("The selected record or access grant is unavailable.",
83
+ "Check the record identity and ask its operator for the required access.", "fix_required")
84
+ _AUTH = ("The hosted client needs valid configuration or sign-in.",
85
+ "Check the trusted API origin and restore your operator-issued Sphynx credentials.", "fix_required")
86
+ _INTEGRITY = ("The saved identity or response could not be verified.",
87
+ "Inspect the original saved record and ask the operator to resolve its identity mismatch.", "fix_required")
88
+ _UNCERTAIN = ("The request outcome needs to be reconciled.",
89
+ "Read the original work status or replay the exact original request and key before a new submission.", "reconcile_before_retry")
90
+ _WAIT = ("Waiting ended; the original work may still be active.",
91
+ "Refresh or wait on the original handle and keep its operation identity.", "read_original_handle")
92
+ _REVIEW = ("The selected bank still needs review.",
93
+ "Read its current review status and follow that bank's approved review process.", "wait_for_review")
94
+ _COMPARISON = ("These results cannot provide the requested comparison.",
95
+ "Check source, bank/cohort, grading versions and the declared comparison conditions.", "fix_required")
96
+ _KNOWN: dict[str, tuple[str, str, str]] = {}
97
+ for _codes, _hint in [
98
+ (("client_not_configured", "invalid_client_config", "invalid_api_url", "invalid_client_token",
99
+ "authentication_required", "authentication_failed"), _AUTH),
100
+ (("submission_not_authorized", "bank_release_unavailable", "reference_access_denied"), _ACCESS),
101
+ (("invalid_server_response", "bank_review_integrity", "artifact_identity_mismatch", "comparison_identity_mismatch"), _INTEGRITY),
102
+ (("hosted_transport_failed", "store_unavailable", "service_unavailable", "submission_failed", "grading_request_uncertain"), _UNCERTAIN),
103
+ (("wait_timeout",), _WAIT), (("bank_review_not_ready",), _REVIEW),
104
+ (("comparison_unavailable", "comparison_incompatible"), _COMPARISON),
105
+ (("invalid_bank_review", "invalid_bank_review_page", "bank_review_limit"), _CONFIG),
106
+ ]:
107
+ _KNOWN.update({code: _hint for code in _codes})
108
+ _KNOWN["bank_review_conflict"] = ("The review version, current set or operation key changed.",
109
+ "Recover an uncertain original request first; for a new judgment, read the current release and target.", "reconcile_before_retry")
110
+ _KNOWN["bank_release_inactive"] = ("The release is held or withdrawn.",
111
+ "Read its current state; an operator can resume a held release, while withdrawal needs a new release.", "fix_required")
112
+
113
+
114
+ def _fields(value: object) -> dict:
115
+ try:
116
+ return object.__getattribute__(value, "__dict__")
117
+ except (AttributeError, TypeError):
118
+ return {}
119
+
120
+
121
+ def _recovery(error: BaseException) -> dict[str, str]:
122
+ fields = _fields(error)
123
+ sources = [fields]
124
+ handle = fields.get("handle")
125
+ if handle is not None:
126
+ saved = _fields(handle)
127
+ sources.append(saved)
128
+ if type(saved.get("_record")) is dict:
129
+ sources.append(saved["_record"])
130
+ if saved.get("snapshot") is not None:
131
+ snapshot = _fields(saved["snapshot"])
132
+ sources.append(snapshot)
133
+ # GradingSnapshot stores its identity in a ReferenceBinding. Read
134
+ # cached fields only; its convenience properties need no invocation.
135
+ if snapshot.get("reference") is not None:
136
+ reference = _fields(snapshot["reference"])
137
+ sources.append({"grading_id": reference.get("record_id"),
138
+ "run_id": reference.get("run_id")})
139
+ identity = saved.get("id")
140
+ if _identity_value(identity):
141
+ key = ("experiment_id" if identity.startswith("exp_") else "evaluation_id"
142
+ if identity.startswith("eval_") else "run_id" if identity.startswith(("run_", "run-")) else "record_id")
143
+ sources.append({key: identity})
144
+ if fields.get("intent") is not None:
145
+ sources.append(_fields(fields["intent"]))
146
+ result: dict[str, str] = {}
147
+ for source in sources:
148
+ for key in sorted(_RECOVERY):
149
+ value = source.get(key)
150
+ if _identity_value(value):
151
+ result.setdefault(key, value)
152
+ return result
153
+
154
+
155
+ def explain_error(error: BaseException) -> Diagnostic:
156
+ """Explain an existing error without reading data or changing remote work.
157
+
158
+ Retryability names the suggested recovery action. It never authorizes a
159
+ candidate retry. Unknown errors keep unknown guidance; inspect the original
160
+ exception explicitly when more detail is needed.
161
+ """
162
+ if not isinstance(error, BaseException):
163
+ raise TypeError("explain_error needs an exception")
164
+ fields = _fields(error)
165
+ raw_code = fields.get("code")
166
+ code = raw_code if type(raw_code) is str and raw_code in _KNOWN else "unknown_error"
167
+ hint = _KNOWN.get(code)
168
+ names = {cls.__name__ for cls in type(error).__mro__}
169
+ sdk_error = type(error).__module__.startswith("sphynx.")
170
+ if hint is None and sdk_error:
171
+ if "WorkflowTimeoutError" in names:
172
+ code, hint = "wait_timeout", _WAIT
173
+ elif "GradingRequestError" in names or "SubmissionError" in names:
174
+ code, hint = "grading_request_uncertain" if "GradingRequestError" in names else "submission_failed", _UNCERTAIN
175
+ elif "PendingWorkError" in names or "EvaluationExecutionError" in names:
176
+ code, hint = "execution_incomplete", ("Execution or grading did not complete.",
177
+ "Inspect the original run and failure evidence before selecting recovery supported by its driver.", "unknown")
178
+ elif "EvaluationCheckFailed" in names:
179
+ code, hint = "check_failed", ("The test's explicit report check failed.",
180
+ "Inspect the saved assessments and the selected check before changing the candidate.", "fix_required")
181
+ elif type(raw_code) is str:
182
+ hint = ("The failure has no registered safe guidance.",
183
+ "Keep the original error and work handle, then inspect the permitted details.", "unknown")
184
+ else:
185
+ # Match only plain SDK message text; never render it or call __str__.
186
+ args = BaseException.__dict__['args'].__get__(error, BaseException)
187
+ message = args[0].lower() if args and type(args[0]) is str else ""
188
+ if any(word in message for word in ("unsupported", "does not support", "capabilit", "does not implement")):
189
+ code, hint = "unsupported_controls", ("This execution path does not support the requested controls.",
190
+ "Check its declared scope/mode/capabilities and select an implementing driver or supported settings.", "fix_required")
191
+ elif "compar" in message:
192
+ code, hint = "comparison_unavailable", _COMPARISON
193
+ else:
194
+ code, hint = "workflow_configuration", _CONFIG
195
+ if hint is None:
196
+ if isinstance(error, PermissionError):
197
+ code, hint = "access_required", _ACCESS
198
+ elif isinstance(error, FileNotFoundError):
199
+ code, hint = "asset_or_path_unavailable", ("A required asset or local path is unavailable.",
200
+ "Check the approved assets and output directory; use a short writable output root on Windows.", "fix_required")
201
+ elif isinstance(error, TimeoutError):
202
+ code, hint = "request_outcome_unknown", _UNCERTAIN
203
+ else:
204
+ hint = ("The failure has no registered safe guidance.",
205
+ "Keep the original error and work handle, then inspect the permitted details.", "unknown")
206
+ summary, action, retryability = hint
207
+ return Diagnostic(code, summary, (action,), cast(Retryability, retryability), _recovery(error))