pretensor 0.1.0__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 (198) hide show
  1. pretensor/__init__.py +50 -0
  2. pretensor/benchmark/__init__.py +54 -0
  3. pretensor/benchmark/cli.py +294 -0
  4. pretensor/benchmark/fixtures.py +84 -0
  5. pretensor/benchmark/l1/__init__.py +23 -0
  6. pretensor/benchmark/l1/metrics.py +141 -0
  7. pretensor/benchmark/l1/pipeline.py +188 -0
  8. pretensor/benchmark/l1/runner.py +245 -0
  9. pretensor/benchmark/l2/__init__.py +27 -0
  10. pretensor/benchmark/l2/gold.py +236 -0
  11. pretensor/benchmark/l2/metrics.py +146 -0
  12. pretensor/benchmark/l2/pipeline.py +124 -0
  13. pretensor/benchmark/l2/runner.py +530 -0
  14. pretensor/benchmark/l3/__init__.py +73 -0
  15. pretensor/benchmark/l3/agent.py +316 -0
  16. pretensor/benchmark/l3/db.py +188 -0
  17. pretensor/benchmark/l3/gold.py +85 -0
  18. pretensor/benchmark/l3/llm_client.py +395 -0
  19. pretensor/benchmark/l3/mcp_client.py +357 -0
  20. pretensor/benchmark/l3/pretensor_runner.py +456 -0
  21. pretensor/benchmark/l3/prompt.py +132 -0
  22. pretensor/benchmark/l3/runner.py +358 -0
  23. pretensor/benchmark/l3/sql_equivalence.py +176 -0
  24. pretensor/benchmark/release_gate.py +448 -0
  25. pretensor/benchmark/results.py +298 -0
  26. pretensor/benchmark/runner.py +109 -0
  27. pretensor/cli/__init__.py +1 -0
  28. pretensor/cli/commands/_source_runner.py +147 -0
  29. pretensor/cli/commands/analyze.py +201 -0
  30. pretensor/cli/commands/connections/__init__.py +7 -0
  31. pretensor/cli/commands/connections/add_remove.py +126 -0
  32. pretensor/cli/commands/connections/register.py +12 -0
  33. pretensor/cli/commands/export.py +131 -0
  34. pretensor/cli/commands/index.py +559 -0
  35. pretensor/cli/commands/list.py +76 -0
  36. pretensor/cli/commands/quickstart.py +207 -0
  37. pretensor/cli/commands/reindex.py +646 -0
  38. pretensor/cli/commands/semantic.py +190 -0
  39. pretensor/cli/commands/serve.py +144 -0
  40. pretensor/cli/commands/sync_grants.py +149 -0
  41. pretensor/cli/commands/validate.py +176 -0
  42. pretensor/cli/config_file.py +442 -0
  43. pretensor/cli/constants.py +10 -0
  44. pretensor/cli/dbt_enrichment.py +96 -0
  45. pretensor/cli/main.py +109 -0
  46. pretensor/cli/paths.py +43 -0
  47. pretensor/cli/plugin.py +52 -0
  48. pretensor/config.py +226 -0
  49. pretensor/connectors/__init__.py +29 -0
  50. pretensor/connectors/base.py +165 -0
  51. pretensor/connectors/bigquery.py +468 -0
  52. pretensor/connectors/inspect.py +321 -0
  53. pretensor/connectors/lineage_sqlglot.py +97 -0
  54. pretensor/connectors/models.py +130 -0
  55. pretensor/connectors/mysql.py +402 -0
  56. pretensor/connectors/pg_array_parse.py +53 -0
  57. pretensor/connectors/postgres.py +938 -0
  58. pretensor/connectors/registry.py +93 -0
  59. pretensor/connectors/snapshot.py +244 -0
  60. pretensor/connectors/snowflake.py +908 -0
  61. pretensor/core/__init__.py +1 -0
  62. pretensor/core/builder.py +307 -0
  63. pretensor/core/dsn_crypto.py +51 -0
  64. pretensor/core/graph_schema_manager.py +246 -0
  65. pretensor/core/graph_store.py +1226 -0
  66. pretensor/core/ids.py +101 -0
  67. pretensor/core/portable_export.py +276 -0
  68. pretensor/core/query_runner.py +67 -0
  69. pretensor/core/registry.py +209 -0
  70. pretensor/core/schema.py +473 -0
  71. pretensor/core/secure_io.py +93 -0
  72. pretensor/core/store.py +469 -0
  73. pretensor/enrichment/__init__.py +1 -0
  74. pretensor/enrichment/analyze/__init__.py +0 -0
  75. pretensor/enrichment/analyze/classify.py +49 -0
  76. pretensor/enrichment/analyze/extract_python.py +196 -0
  77. pretensor/enrichment/analyze/parse.py +141 -0
  78. pretensor/enrichment/analyze/pipeline.py +195 -0
  79. pretensor/enrichment/analyze/summary.py +38 -0
  80. pretensor/enrichment/analyze/walker.py +98 -0
  81. pretensor/enrichment/analyze/writers.py +214 -0
  82. pretensor/enrichment/dbt/__init__.py +30 -0
  83. pretensor/enrichment/dbt/lineage.py +100 -0
  84. pretensor/enrichment/dbt/manifest.py +300 -0
  85. pretensor/enrichment/dbt/metadata.py +263 -0
  86. pretensor/enrichment/dbt/pipeline.py +77 -0
  87. pretensor/enrichment/dbt/resolution.py +101 -0
  88. pretensor/enrichment/dbt/signals.py +305 -0
  89. pretensor/entities/__init__.py +27 -0
  90. pretensor/entities/builder.py +63 -0
  91. pretensor/entities/classifier.py +383 -0
  92. pretensor/entities/llm_extract.py +66 -0
  93. pretensor/errors.py +35 -0
  94. pretensor/graph_models/__init__.py +17 -0
  95. pretensor/graph_models/base.py +11 -0
  96. pretensor/graph_models/consumer.py +71 -0
  97. pretensor/graph_models/edge.py +35 -0
  98. pretensor/graph_models/entity.py +21 -0
  99. pretensor/graph_models/node.py +79 -0
  100. pretensor/graph_models/relationship.py +33 -0
  101. pretensor/integrations/__init__.py +42 -0
  102. pretensor/integrations/_base.py +138 -0
  103. pretensor/integrations/google_adk.py +49 -0
  104. pretensor/integrations/langchain.py +55 -0
  105. pretensor/integrations/llamaindex.py +53 -0
  106. pretensor/intelligence/__init__.py +33 -0
  107. pretensor/intelligence/cluster_labeler.py +425 -0
  108. pretensor/intelligence/clustering.py +168 -0
  109. pretensor/intelligence/combining.py +32 -0
  110. pretensor/intelligence/discovery.py +114 -0
  111. pretensor/intelligence/embeddings.py +317 -0
  112. pretensor/intelligence/graph_export.py +200 -0
  113. pretensor/intelligence/heuristic.py +544 -0
  114. pretensor/intelligence/join_paths/__init__.py +130 -0
  115. pretensor/intelligence/join_paths/on_demand.py +516 -0
  116. pretensor/intelligence/join_paths/storage.py +70 -0
  117. pretensor/intelligence/llm_infer.py +78 -0
  118. pretensor/intelligence/llm_runtime.py +62 -0
  119. pretensor/intelligence/metric_templates.py +193 -0
  120. pretensor/intelligence/pipeline.py +364 -0
  121. pretensor/intelligence/role_exemplars.py +263 -0
  122. pretensor/intelligence/schema_classification.py +360 -0
  123. pretensor/intelligence/scoring.py +76 -0
  124. pretensor/intelligence/semantic.py +240 -0
  125. pretensor/intelligence/shadow_alias.py +101 -0
  126. pretensor/intelligence/statistical.py +50 -0
  127. pretensor/intelligence/steps.py +191 -0
  128. pretensor/intelligence/steps_embedding.py +168 -0
  129. pretensor/introspection/__init__.py +6 -0
  130. pretensor/introspection/inspector.py +5 -0
  131. pretensor/introspection/models/__init__.py +0 -0
  132. pretensor/introspection/models/base.py +5 -0
  133. pretensor/introspection/models/config.py +237 -0
  134. pretensor/introspection/models/dsn.py +550 -0
  135. pretensor/introspection/models/plan.py +116 -0
  136. pretensor/introspection/models/schema.py +10 -0
  137. pretensor/introspection/models/semantic.py +121 -0
  138. pretensor/introspection/models/validation.py +116 -0
  139. pretensor/introspection/snapshot.py +46 -0
  140. pretensor/mcp/__init__.py +16 -0
  141. pretensor/mcp/config_json.py +24 -0
  142. pretensor/mcp/payload_types.py +274 -0
  143. pretensor/mcp/resources/__init__.py +17 -0
  144. pretensor/mcp/resources/markdown.py +314 -0
  145. pretensor/mcp/server.py +285 -0
  146. pretensor/mcp/service.py +49 -0
  147. pretensor/mcp/service_context.py +142 -0
  148. pretensor/mcp/service_registry.py +294 -0
  149. pretensor/mcp/store_cache.py +43 -0
  150. pretensor/mcp/tool_registry.py +136 -0
  151. pretensor/mcp/tools/__init__.py +1 -0
  152. pretensor/mcp/tools/_rank.py +244 -0
  153. pretensor/mcp/tools/_timed.py +26 -0
  154. pretensor/mcp/tools/compile_metric.py +144 -0
  155. pretensor/mcp/tools/consumers.py +161 -0
  156. pretensor/mcp/tools/context.py +1121 -0
  157. pretensor/mcp/tools/cypher.py +509 -0
  158. pretensor/mcp/tools/detect_changes.py +254 -0
  159. pretensor/mcp/tools/impact.py +271 -0
  160. pretensor/mcp/tools/list.py +131 -0
  161. pretensor/mcp/tools/schema.py +170 -0
  162. pretensor/mcp/tools/search.py +316 -0
  163. pretensor/mcp/tools/semantic_search.py +282 -0
  164. pretensor/mcp/tools/traverse.py +1027 -0
  165. pretensor/mcp/tools/validate_sql.py +150 -0
  166. pretensor/observability.py +203 -0
  167. pretensor/py.typed +0 -0
  168. pretensor/quickstart/README.md +29 -0
  169. pretensor/quickstart/__init__.py +6 -0
  170. pretensor/quickstart/docker-compose.yml +18 -0
  171. pretensor/quickstart/pagila_data.sql +63 -0
  172. pretensor/quickstart/pagila_ddl.sql +92 -0
  173. pretensor/search/__init__.py +6 -0
  174. pretensor/search/base.py +80 -0
  175. pretensor/search/index.py +435 -0
  176. pretensor/semantic/__init__.py +24 -0
  177. pretensor/semantic/base.py +123 -0
  178. pretensor/semantic/compiler.py +487 -0
  179. pretensor/semantic/yaml_layer.py +180 -0
  180. pretensor/skills/__init__.py +5 -0
  181. pretensor/skills/generator.py +235 -0
  182. pretensor/staleness/__init__.py +15 -0
  183. pretensor/staleness/graph_patcher.py +355 -0
  184. pretensor/staleness/impact_analyzer.py +162 -0
  185. pretensor/staleness/snapshot_store.py +38 -0
  186. pretensor/validation/__init__.py +9 -0
  187. pretensor/validation/query_validator.py +436 -0
  188. pretensor/visibility/__init__.py +23 -0
  189. pretensor/visibility/config.py +126 -0
  190. pretensor/visibility/filter.py +143 -0
  191. pretensor/visibility/kuzu_helpers.py +32 -0
  192. pretensor/visibility/runtime.py +36 -0
  193. pretensor/visibility/sync_grants.py +188 -0
  194. pretensor-0.1.0.dist-info/METADATA +251 -0
  195. pretensor-0.1.0.dist-info/RECORD +198 -0
  196. pretensor-0.1.0.dist-info/WHEEL +4 -0
  197. pretensor-0.1.0.dist-info/entry_points.txt +2 -0
  198. pretensor-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,298 @@
1
+ """Benchmark results aggregator — schema, I/O, and regression-gate comparator.
2
+
3
+ The JSON schema this module reads/writes is the canonical contract that
4
+ every benchmark level (L1, L2, L3) emits and that the release-gate script
5
+ consumes. See ``docs/specs/benchmark/spec.md`` §JSON output schema.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import csv
11
+ import json
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any, Literal
15
+
16
+ from pretensor.errors import PretensorError
17
+
18
+ __all__ = [
19
+ "BenchmarkResult",
20
+ "ComparisonError",
21
+ "ComparisonReport",
22
+ "Direction",
23
+ "Metric",
24
+ "MetricDiff",
25
+ "compare",
26
+ "read_json",
27
+ "write_csv",
28
+ "write_json",
29
+ ]
30
+
31
+ Direction = Literal["higher_is_better", "lower_is_better"]
32
+
33
+
34
+ def _fmt(value: float | None, *, signed: bool = False) -> str:
35
+ if value is None:
36
+ return "null"
37
+ return f"{value:+.6g}" if signed else f"{value:.6g}"
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class Metric:
42
+ """One metric value plus the direction that defines a regression.
43
+
44
+ ``value`` is ``None`` when the metric is unavailable on this run
45
+ (e.g. ``semantic_search`` Recall@K on a build without ``[embeddings]``).
46
+ ``direction`` encodes regression semantics — a drop on a
47
+ ``higher_is_better`` metric is a regression; a rise on a
48
+ ``lower_is_better`` metric is a regression.
49
+ """
50
+
51
+ value: float | None
52
+ direction: Direction
53
+
54
+ def to_dict(self) -> dict[str, Any]:
55
+ return {"value": self.value, "direction": self.direction}
56
+
57
+ @classmethod
58
+ def from_dict(cls, data: dict[str, Any]) -> Metric:
59
+ return cls(value=data["value"], direction=data["direction"])
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class BenchmarkResult:
64
+ """One benchmark run's full output — matches the spec's JSON schema.
65
+
66
+ ``extra`` is a free-form per-level envelope for fields that don't
67
+ belong in ``metrics`` or ``per_item`` but must be captured for
68
+ reproducibility — e.g. L3 records the LLM model, temperature, seed,
69
+ and prompt hash here. L1 / L2 leave it empty. The spec's JSON-schema
70
+ section ("downstream consumers must tolerate additional keys")
71
+ sanctions adding new top-level keys this way.
72
+ """
73
+
74
+ level: str
75
+ dataset: str
76
+ pretensor_version: str
77
+ embeddings_enabled: bool
78
+ ran_at: str
79
+ fixture_sha: str
80
+ metrics: dict[str, Metric]
81
+ per_item: list[dict[str, Any]]
82
+ notes: list[str] = field(default_factory=list)
83
+ extra: dict[str, Any] = field(default_factory=dict)
84
+
85
+ def to_dict(self) -> dict[str, Any]:
86
+ out: dict[str, Any] = {
87
+ "level": self.level,
88
+ "dataset": self.dataset,
89
+ "pretensor_version": self.pretensor_version,
90
+ "embeddings_enabled": self.embeddings_enabled,
91
+ "ran_at": self.ran_at,
92
+ "fixture_sha": self.fixture_sha,
93
+ "metrics": {name: m.to_dict() for name, m in self.metrics.items()},
94
+ "per_item": list(self.per_item),
95
+ "notes": list(self.notes),
96
+ }
97
+ if self.extra:
98
+ out["extra"] = dict(self.extra)
99
+ return out
100
+
101
+ @classmethod
102
+ def from_dict(cls, data: dict[str, Any]) -> BenchmarkResult:
103
+ return cls(
104
+ level=data["level"],
105
+ dataset=data["dataset"],
106
+ pretensor_version=data["pretensor_version"],
107
+ embeddings_enabled=data["embeddings_enabled"],
108
+ ran_at=data["ran_at"],
109
+ fixture_sha=data["fixture_sha"],
110
+ metrics={name: Metric.from_dict(m) for name, m in data["metrics"].items()},
111
+ per_item=list(data["per_item"]),
112
+ notes=list(data.get("notes", [])),
113
+ extra=dict(data.get("extra", {})),
114
+ )
115
+
116
+
117
+ def write_json(result: BenchmarkResult, path: Path) -> None:
118
+ """Serialize ``result`` to ``path`` as deterministic, pretty-printed JSON.
119
+
120
+ Output is byte-stable for identical inputs: keys are sorted, indent is 2
121
+ spaces, and the file ends with a single trailing newline.
122
+ """
123
+ text = json.dumps(result.to_dict(), sort_keys=True, indent=2) + "\n"
124
+ path.write_text(text, encoding="utf-8")
125
+
126
+
127
+ def read_json(path: Path) -> BenchmarkResult:
128
+ """Parse a JSON file into a ``BenchmarkResult``."""
129
+ data = json.loads(path.read_text(encoding="utf-8"))
130
+ return BenchmarkResult.from_dict(data)
131
+
132
+
133
+ class ComparisonError(PretensorError, ValueError):
134
+ """Raised when two ``BenchmarkResult`` inputs cannot be compared."""
135
+
136
+
137
+ @dataclass(frozen=True, slots=True)
138
+ class MetricDiff:
139
+ """One metric's before/after delta with its regression direction."""
140
+
141
+ name: str
142
+ baseline: float | None
143
+ current: float | None
144
+ direction: Direction
145
+ delta: float | None
146
+
147
+
148
+ @dataclass(frozen=True, slots=True)
149
+ class ComparisonReport:
150
+ """Result of comparing two ``BenchmarkResult`` instances."""
151
+
152
+ has_regression: bool = False
153
+ regressions: list[MetricDiff] = field(default_factory=list)
154
+ improvements: list[MetricDiff] = field(default_factory=list)
155
+ added: list[str] = field(default_factory=list)
156
+ removed: list[str] = field(default_factory=list)
157
+
158
+ def format_diff(self) -> str:
159
+ """Render a human-readable diff block suitable for stderr output."""
160
+ lines: list[str] = []
161
+ if self.has_regression:
162
+ lines.append("REGRESSION DETECTED")
163
+ else:
164
+ lines.append("no regression")
165
+ if self.regressions:
166
+ lines.append("")
167
+ lines.append("Regressions:")
168
+ for d in self.regressions:
169
+ arrow = "↓" if d.direction == "higher_is_better" else "↑"
170
+ lines.append(
171
+ f" {d.name} ({d.direction}): "
172
+ f"{_fmt(d.baseline)} → {_fmt(d.current)} "
173
+ f"(Δ{_fmt(d.delta, signed=True)}) {arrow}"
174
+ )
175
+ if self.removed:
176
+ lines.append("")
177
+ lines.append("Metrics removed (present in baseline, missing in current):")
178
+ for name in self.removed:
179
+ lines.append(f" {name}")
180
+ if self.added:
181
+ lines.append("")
182
+ lines.append("New metrics in current (not in baseline):")
183
+ for name in self.added:
184
+ lines.append(f" {name}")
185
+ if self.improvements:
186
+ lines.append("")
187
+ lines.append("Improvements:")
188
+ for d in self.improvements:
189
+ lines.append(
190
+ f" {d.name}: {_fmt(d.baseline)} → {_fmt(d.current)} "
191
+ f"(Δ{_fmt(d.delta, signed=True)})"
192
+ )
193
+ return "\n".join(lines) + "\n"
194
+
195
+
196
+ def compare(
197
+ baseline: BenchmarkResult,
198
+ current: BenchmarkResult,
199
+ *,
200
+ tolerance: float = 1e-6,
201
+ ) -> ComparisonReport:
202
+ """Compare two benchmark results and classify metric changes.
203
+
204
+ Raises ``ComparisonError`` if the two inputs disagree on ``dataset`` or
205
+ ``level`` — comparing across datasets or levels is meaningless. A metric
206
+ present in ``baseline`` but missing in ``current`` is a regression
207
+ (``removed``). A metric new in ``current`` is recorded under ``added``
208
+ but never counts as a regression. Metrics whose value is ``None`` on
209
+ either side are skipped (no diff emitted).
210
+ """
211
+ if baseline.dataset != current.dataset:
212
+ raise ComparisonError(
213
+ f"cannot compare across datasets: baseline={baseline.dataset!r}, "
214
+ f"current={current.dataset!r}"
215
+ )
216
+ if baseline.level != current.level:
217
+ raise ComparisonError(
218
+ f"cannot compare across levels: baseline={baseline.level!r}, "
219
+ f"current={current.level!r}"
220
+ )
221
+
222
+ regressions: list[MetricDiff] = []
223
+ improvements: list[MetricDiff] = []
224
+ removed: list[str] = []
225
+ added: list[str] = sorted(set(current.metrics) - set(baseline.metrics))
226
+
227
+ for name in sorted(baseline.metrics):
228
+ base_metric = baseline.metrics[name]
229
+ cur_metric = current.metrics.get(name)
230
+ if cur_metric is None:
231
+ removed.append(name)
232
+ continue
233
+ if base_metric.value is None or cur_metric.value is None:
234
+ continue
235
+ delta = cur_metric.value - base_metric.value
236
+ diff = MetricDiff(
237
+ name=name,
238
+ baseline=base_metric.value,
239
+ current=cur_metric.value,
240
+ direction=base_metric.direction,
241
+ delta=delta,
242
+ )
243
+ if base_metric.direction == "higher_is_better":
244
+ if delta < -tolerance:
245
+ regressions.append(diff)
246
+ elif delta > tolerance:
247
+ improvements.append(diff)
248
+ else: # lower_is_better
249
+ if delta > tolerance:
250
+ regressions.append(diff)
251
+ elif delta < -tolerance:
252
+ improvements.append(diff)
253
+
254
+ return ComparisonReport(
255
+ has_regression=bool(regressions or removed),
256
+ regressions=regressions,
257
+ improvements=improvements,
258
+ added=added,
259
+ removed=removed,
260
+ )
261
+
262
+
263
+ _CSV_META_COLUMNS = (
264
+ "level",
265
+ "dataset",
266
+ "pretensor_version",
267
+ "embeddings_enabled",
268
+ "ran_at",
269
+ )
270
+
271
+
272
+ def write_csv(results: list[BenchmarkResult], path: Path) -> None:
273
+ """Write one row per ``BenchmarkResult`` as a flat CSV for spreadsheet review.
274
+
275
+ Metric columns are the sorted union of metric names across ``results``.
276
+ A row missing a metric (or whose value is ``None``) writes an empty cell.
277
+ Metric ``direction`` is JSON-only metadata and is omitted from CSV;
278
+ reviewers consult the JSON for direction.
279
+ """
280
+ metric_names = sorted({name for r in results for name in r.metrics})
281
+ columns = list(_CSV_META_COLUMNS) + metric_names
282
+ with path.open("w", encoding="utf-8", newline="") as fh:
283
+ writer = csv.DictWriter(fh, fieldnames=columns)
284
+ writer.writeheader()
285
+ for r in results:
286
+ row: dict[str, Any] = {
287
+ "level": r.level,
288
+ "dataset": r.dataset,
289
+ "pretensor_version": r.pretensor_version,
290
+ "embeddings_enabled": r.embeddings_enabled,
291
+ "ran_at": r.ran_at,
292
+ }
293
+ for name in metric_names:
294
+ metric = r.metrics.get(name)
295
+ row[name] = (
296
+ "" if metric is None or metric.value is None else metric.value
297
+ )
298
+ writer.writerow(row)
@@ -0,0 +1,109 @@
1
+ """Dispatch stubs for the three benchmark levels.
2
+
3
+ These functions are intentionally minimal: the initial scaffolding lands the
4
+ CLI contract and a stable Python entry point, while the metric
5
+ implementations ship in follow-up changes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import enum
11
+ from pathlib import Path
12
+
13
+
14
+ class Dataset(str, enum.Enum):
15
+ """Closed set of benchmark fixture keys.
16
+
17
+ Matches ``docs/specs/benchmark/spec.md`` §CLI contract. Unknown values
18
+ are rejected by Typer with exit code 2 before reaching ``run_*``.
19
+ """
20
+
21
+ PAGILA = "pagila"
22
+ TPCH = "tpch"
23
+ ANALYTICS_DWH = "analytics_dwh"
24
+ ADVERSARIAL = "adversarial"
25
+ SAAS_MULTITENANT = "saas_multitenant"
26
+ ADVENTUREWORKS = "adventureworks"
27
+ MESSY_WAREHOUSE = "messy_warehouse"
28
+
29
+
30
+ class RunnerKind(str, enum.Enum):
31
+ """L3 runner selector — baseline (raw schema) vs pretensor (MCP)."""
32
+
33
+ BASELINE = "baseline"
34
+ PRETENSOR = "pretensor"
35
+
36
+
37
+ def run_l1(
38
+ dataset: Dataset,
39
+ out: Path | None,
40
+ graph_dir: Path,
41
+ *,
42
+ embeddings: bool,
43
+ ) -> None:
44
+ """Run L1 (graph-quality) metrics for ``dataset``.
45
+
46
+ Computes inferred-join precision / recall against declared FKs,
47
+ cluster-stability Jaccard across two reindex runs, and role-
48
+ classification F1 against ``<dataset>_roles.yaml`` when present.
49
+ ``embeddings=True`` enables the Layer-A embeddings path when the
50
+ optional extra is installed; missing extra logs an install hint
51
+ and falls back to the heuristic-only run.
52
+ """
53
+ # Imported lazily so the heavy intelligence-pipeline dependency
54
+ # graph (Kuzu, igraph, etc.) does not load when other run_*
55
+ # callers import this module.
56
+ from pretensor.benchmark.l1 import run_l1 as _run_l1
57
+
58
+ _run_l1(dataset, out, graph_dir, embeddings=embeddings)
59
+
60
+
61
+ def run_l2(
62
+ dataset: Dataset,
63
+ out: Path | None,
64
+ graph_dir: Path,
65
+ *,
66
+ embeddings: bool,
67
+ ) -> None:
68
+ """Run L2 (MCP-tool-quality) metrics for ``dataset``.
69
+
70
+ Computes ``query`` Recall@K, ``traverse`` path correctness, and
71
+ ``compile_metric`` SQL correctness against the fixture's gold data.
72
+ When ``embeddings=True`` and the ``semantic_search`` MCP tool is
73
+ available, also emits ``semantic_search`` Recall@K (Layer-B gate).
74
+ """
75
+ # Imported lazily so the heavy MCP / Kuzu / sqlglot dependency
76
+ # graph does not load when other run_* callers import this module.
77
+ from pretensor.benchmark.l2 import run_l2 as _run_l2
78
+
79
+ _run_l2(dataset, out, graph_dir, embeddings=embeddings)
80
+
81
+
82
+ def run_l3(
83
+ dataset: Dataset,
84
+ out: Path | None,
85
+ graph_dir: Path,
86
+ *,
87
+ runner: RunnerKind,
88
+ model: str,
89
+ seed: int | None,
90
+ ) -> None:
91
+ """Run L3 (agent-task-success) for ``dataset`` via the chosen runner.
92
+
93
+ ``runner=RunnerKind.BASELINE`` runs the agent against the raw schema.
94
+ ``runner=RunnerKind.PRETENSOR`` runs the agent through the MCP server.
95
+ The baseline ignores ``graph_dir``; the pretensor runner spawns
96
+ ``pretensor serve --graph-dir <graph_dir>``.
97
+ """
98
+ if runner is RunnerKind.BASELINE:
99
+ # Imported lazily so the LLM-client / httpx dependency graph does
100
+ # not load when other run_* callers import this module.
101
+ from pretensor.benchmark.l3.runner import run_l3_baseline
102
+
103
+ run_l3_baseline(dataset, out, model=model, seed=seed)
104
+ return
105
+ # Lazy import: the pretensor runner pulls in the MCP stdio client and
106
+ # asyncio plumbing; other run_* callers should not pay that cost.
107
+ from pretensor.benchmark.l3.pretensor_runner import run_l3_pretensor
108
+
109
+ run_l3_pretensor(dataset, out, graph_dir, model=model, seed=seed)
@@ -0,0 +1 @@
1
+ """Command-line interface for Pretensor Graph."""
@@ -0,0 +1,147 @@
1
+ """Shared CLI scaffolding for commands that operate over named sources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING
8
+
9
+ import typer
10
+ from rich.console import Console
11
+
12
+ from pretensor.introspection.models.dsn import validate_source_env_vars
13
+
14
+ if TYPE_CHECKING:
15
+ from pretensor.cli.config_file import PretensorCliConfig
16
+
17
+
18
+ def resolve_embeddings_mode(
19
+ embeddings: bool | None,
20
+ console: Console,
21
+ *,
22
+ auto_enable_message: str | None = None,
23
+ ) -> bool:
24
+ """Verify or auto-detect the embeddings mode.
25
+
26
+ Parameters
27
+ ----------
28
+ embeddings:
29
+ Raw CLI value: ``True`` = explicit opt-in, ``False`` = explicit
30
+ opt-out, ``None`` = auto-detect.
31
+ console:
32
+ Rich console for user-facing output.
33
+ auto_enable_message:
34
+ If given, printed when auto-detection enables embeddings.
35
+ Pass ``None`` (default) to suppress the message.
36
+
37
+ Returns
38
+ -------
39
+ bool
40
+ Resolved embeddings flag.
41
+ """
42
+ if embeddings:
43
+ from pretensor.intelligence.embeddings import verify_embeddings_extra_installed
44
+
45
+ try:
46
+ verify_embeddings_extra_installed()
47
+ except ImportError as e:
48
+ console.print(str(e), style="red", markup=False)
49
+ raise typer.Exit(1) from e
50
+ return True
51
+ if embeddings is None:
52
+ from pretensor.intelligence.embeddings import resolve_embeddings_auto
53
+
54
+ resolved = resolve_embeddings_auto(None)
55
+ if resolved and auto_enable_message:
56
+ console.print(auto_enable_message)
57
+ return resolved
58
+ return False
59
+
60
+
61
+ def check_source_exists(
62
+ source: str,
63
+ cli_config: PretensorCliConfig,
64
+ console: Console,
65
+ ) -> None:
66
+ """Exit 1 with an informative message if *source* is not in ``cli_config.sources``."""
67
+ if source not in cli_config.sources:
68
+ available = ", ".join(cli_config.sources) or "(none)"
69
+ console.print(f"[red]Unknown source {source!r}.[/red] Available: {available}")
70
+ raise typer.Exit(1)
71
+
72
+
73
+ def print_multi_source_summary(
74
+ console: Console,
75
+ results: list[tuple[str, bool, str]],
76
+ ) -> None:
77
+ """Print the tick/cross summary table and exit 1 if any source failed."""
78
+ console.print(f"\n[bold]{'─' * 40}[/bold]")
79
+ console.print("[bold]Summary:[/bold]")
80
+ failed = 0
81
+ for src_name, ok, msg in results:
82
+ status = "[green]✓[/green]" if ok else "[red]✗[/red]"
83
+ console.print(f" {status} {src_name}: {msg}")
84
+ if not ok:
85
+ failed += 1
86
+ if failed:
87
+ raise typer.Exit(1)
88
+
89
+
90
+ def run_for_all_sources(
91
+ *,
92
+ cli_config: PretensorCliConfig,
93
+ console: Console,
94
+ logger: logging.Logger,
95
+ source_banner: str,
96
+ action_verb: str,
97
+ run_one: Callable[[str], None],
98
+ ) -> None:
99
+ """Iterate all configured sources, skip ones with missing env vars, exit 1 on any failure.
100
+
101
+ Parameters
102
+ ----------
103
+ cli_config:
104
+ Loaded CLI config (must have a non-empty ``sources`` mapping).
105
+ console:
106
+ Rich console for user-facing output.
107
+ logger:
108
+ Module-level logger for structured warnings/errors.
109
+ source_banner:
110
+ Label shown in the per-source header, e.g. ``"Source:"`` or
111
+ ``"Reindexing source:"``.
112
+ action_verb:
113
+ Verb used in error messages, e.g. ``"indexing"`` or ``"reindexing"``.
114
+ run_one:
115
+ Callable ``(src_name) -> None`` that performs the per-source
116
+ action; should raise ``typer.Exit`` on failure.
117
+ """
118
+ if not cli_config.sources:
119
+ console.print(
120
+ "[red]No sources defined in config.[/red] "
121
+ "Add a `sources:` section to .pretensor/config.yaml."
122
+ )
123
+ raise typer.Exit(1)
124
+ results: list[tuple[str, bool, str]] = []
125
+ for src_name, src_cfg in cli_config.sources.items():
126
+ console.print(
127
+ f"\n[bold]{'─' * 40}[/bold]\n"
128
+ f"[bold blue]{source_banner}[/bold blue] {src_name}\n"
129
+ )
130
+ missing_vars = validate_source_env_vars(src_cfg)
131
+ if missing_vars:
132
+ msg = f"missing env vars: {', '.join(missing_vars)}"
133
+ console.print(f"[yellow]Skipping {src_name}:[/yellow] {msg}")
134
+ logger.warning("Skipping source %s: %s", src_name, msg)
135
+ results.append((src_name, False, f"skipped ({msg})"))
136
+ continue
137
+ try:
138
+ run_one(src_name)
139
+ results.append((src_name, True, "ok"))
140
+ except typer.Exit:
141
+ logger.warning("Source %s exited with failure", src_name)
142
+ results.append((src_name, False, "failed"))
143
+ except Exception as e:
144
+ logger.exception("Error %s source %s", action_verb, src_name)
145
+ console.print(f"[red]Error {action_verb} {src_name}:[/red] {e}")
146
+ results.append((src_name, False, str(e)))
147
+ print_multi_source_summary(console, results)