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.
- pretensor/__init__.py +50 -0
- pretensor/benchmark/__init__.py +54 -0
- pretensor/benchmark/cli.py +294 -0
- pretensor/benchmark/fixtures.py +84 -0
- pretensor/benchmark/l1/__init__.py +23 -0
- pretensor/benchmark/l1/metrics.py +141 -0
- pretensor/benchmark/l1/pipeline.py +188 -0
- pretensor/benchmark/l1/runner.py +245 -0
- pretensor/benchmark/l2/__init__.py +27 -0
- pretensor/benchmark/l2/gold.py +236 -0
- pretensor/benchmark/l2/metrics.py +146 -0
- pretensor/benchmark/l2/pipeline.py +124 -0
- pretensor/benchmark/l2/runner.py +530 -0
- pretensor/benchmark/l3/__init__.py +73 -0
- pretensor/benchmark/l3/agent.py +316 -0
- pretensor/benchmark/l3/db.py +188 -0
- pretensor/benchmark/l3/gold.py +85 -0
- pretensor/benchmark/l3/llm_client.py +395 -0
- pretensor/benchmark/l3/mcp_client.py +357 -0
- pretensor/benchmark/l3/pretensor_runner.py +456 -0
- pretensor/benchmark/l3/prompt.py +132 -0
- pretensor/benchmark/l3/runner.py +358 -0
- pretensor/benchmark/l3/sql_equivalence.py +176 -0
- pretensor/benchmark/release_gate.py +448 -0
- pretensor/benchmark/results.py +298 -0
- pretensor/benchmark/runner.py +109 -0
- pretensor/cli/__init__.py +1 -0
- pretensor/cli/commands/_source_runner.py +147 -0
- pretensor/cli/commands/analyze.py +201 -0
- pretensor/cli/commands/connections/__init__.py +7 -0
- pretensor/cli/commands/connections/add_remove.py +126 -0
- pretensor/cli/commands/connections/register.py +12 -0
- pretensor/cli/commands/export.py +131 -0
- pretensor/cli/commands/index.py +559 -0
- pretensor/cli/commands/list.py +76 -0
- pretensor/cli/commands/quickstart.py +207 -0
- pretensor/cli/commands/reindex.py +646 -0
- pretensor/cli/commands/semantic.py +190 -0
- pretensor/cli/commands/serve.py +144 -0
- pretensor/cli/commands/sync_grants.py +149 -0
- pretensor/cli/commands/validate.py +176 -0
- pretensor/cli/config_file.py +442 -0
- pretensor/cli/constants.py +10 -0
- pretensor/cli/dbt_enrichment.py +96 -0
- pretensor/cli/main.py +109 -0
- pretensor/cli/paths.py +43 -0
- pretensor/cli/plugin.py +52 -0
- pretensor/config.py +226 -0
- pretensor/connectors/__init__.py +29 -0
- pretensor/connectors/base.py +165 -0
- pretensor/connectors/bigquery.py +468 -0
- pretensor/connectors/inspect.py +321 -0
- pretensor/connectors/lineage_sqlglot.py +97 -0
- pretensor/connectors/models.py +130 -0
- pretensor/connectors/mysql.py +402 -0
- pretensor/connectors/pg_array_parse.py +53 -0
- pretensor/connectors/postgres.py +938 -0
- pretensor/connectors/registry.py +93 -0
- pretensor/connectors/snapshot.py +244 -0
- pretensor/connectors/snowflake.py +908 -0
- pretensor/core/__init__.py +1 -0
- pretensor/core/builder.py +307 -0
- pretensor/core/dsn_crypto.py +51 -0
- pretensor/core/graph_schema_manager.py +246 -0
- pretensor/core/graph_store.py +1226 -0
- pretensor/core/ids.py +101 -0
- pretensor/core/portable_export.py +276 -0
- pretensor/core/query_runner.py +67 -0
- pretensor/core/registry.py +209 -0
- pretensor/core/schema.py +473 -0
- pretensor/core/secure_io.py +93 -0
- pretensor/core/store.py +469 -0
- pretensor/enrichment/__init__.py +1 -0
- pretensor/enrichment/analyze/__init__.py +0 -0
- pretensor/enrichment/analyze/classify.py +49 -0
- pretensor/enrichment/analyze/extract_python.py +196 -0
- pretensor/enrichment/analyze/parse.py +141 -0
- pretensor/enrichment/analyze/pipeline.py +195 -0
- pretensor/enrichment/analyze/summary.py +38 -0
- pretensor/enrichment/analyze/walker.py +98 -0
- pretensor/enrichment/analyze/writers.py +214 -0
- pretensor/enrichment/dbt/__init__.py +30 -0
- pretensor/enrichment/dbt/lineage.py +100 -0
- pretensor/enrichment/dbt/manifest.py +300 -0
- pretensor/enrichment/dbt/metadata.py +263 -0
- pretensor/enrichment/dbt/pipeline.py +77 -0
- pretensor/enrichment/dbt/resolution.py +101 -0
- pretensor/enrichment/dbt/signals.py +305 -0
- pretensor/entities/__init__.py +27 -0
- pretensor/entities/builder.py +63 -0
- pretensor/entities/classifier.py +383 -0
- pretensor/entities/llm_extract.py +66 -0
- pretensor/errors.py +35 -0
- pretensor/graph_models/__init__.py +17 -0
- pretensor/graph_models/base.py +11 -0
- pretensor/graph_models/consumer.py +71 -0
- pretensor/graph_models/edge.py +35 -0
- pretensor/graph_models/entity.py +21 -0
- pretensor/graph_models/node.py +79 -0
- pretensor/graph_models/relationship.py +33 -0
- pretensor/integrations/__init__.py +42 -0
- pretensor/integrations/_base.py +138 -0
- pretensor/integrations/google_adk.py +49 -0
- pretensor/integrations/langchain.py +55 -0
- pretensor/integrations/llamaindex.py +53 -0
- pretensor/intelligence/__init__.py +33 -0
- pretensor/intelligence/cluster_labeler.py +425 -0
- pretensor/intelligence/clustering.py +168 -0
- pretensor/intelligence/combining.py +32 -0
- pretensor/intelligence/discovery.py +114 -0
- pretensor/intelligence/embeddings.py +317 -0
- pretensor/intelligence/graph_export.py +200 -0
- pretensor/intelligence/heuristic.py +544 -0
- pretensor/intelligence/join_paths/__init__.py +130 -0
- pretensor/intelligence/join_paths/on_demand.py +516 -0
- pretensor/intelligence/join_paths/storage.py +70 -0
- pretensor/intelligence/llm_infer.py +78 -0
- pretensor/intelligence/llm_runtime.py +62 -0
- pretensor/intelligence/metric_templates.py +193 -0
- pretensor/intelligence/pipeline.py +364 -0
- pretensor/intelligence/role_exemplars.py +263 -0
- pretensor/intelligence/schema_classification.py +360 -0
- pretensor/intelligence/scoring.py +76 -0
- pretensor/intelligence/semantic.py +240 -0
- pretensor/intelligence/shadow_alias.py +101 -0
- pretensor/intelligence/statistical.py +50 -0
- pretensor/intelligence/steps.py +191 -0
- pretensor/intelligence/steps_embedding.py +168 -0
- pretensor/introspection/__init__.py +6 -0
- pretensor/introspection/inspector.py +5 -0
- pretensor/introspection/models/__init__.py +0 -0
- pretensor/introspection/models/base.py +5 -0
- pretensor/introspection/models/config.py +237 -0
- pretensor/introspection/models/dsn.py +550 -0
- pretensor/introspection/models/plan.py +116 -0
- pretensor/introspection/models/schema.py +10 -0
- pretensor/introspection/models/semantic.py +121 -0
- pretensor/introspection/models/validation.py +116 -0
- pretensor/introspection/snapshot.py +46 -0
- pretensor/mcp/__init__.py +16 -0
- pretensor/mcp/config_json.py +24 -0
- pretensor/mcp/payload_types.py +274 -0
- pretensor/mcp/resources/__init__.py +17 -0
- pretensor/mcp/resources/markdown.py +314 -0
- pretensor/mcp/server.py +285 -0
- pretensor/mcp/service.py +49 -0
- pretensor/mcp/service_context.py +142 -0
- pretensor/mcp/service_registry.py +294 -0
- pretensor/mcp/store_cache.py +43 -0
- pretensor/mcp/tool_registry.py +136 -0
- pretensor/mcp/tools/__init__.py +1 -0
- pretensor/mcp/tools/_rank.py +244 -0
- pretensor/mcp/tools/_timed.py +26 -0
- pretensor/mcp/tools/compile_metric.py +144 -0
- pretensor/mcp/tools/consumers.py +161 -0
- pretensor/mcp/tools/context.py +1121 -0
- pretensor/mcp/tools/cypher.py +509 -0
- pretensor/mcp/tools/detect_changes.py +254 -0
- pretensor/mcp/tools/impact.py +271 -0
- pretensor/mcp/tools/list.py +131 -0
- pretensor/mcp/tools/schema.py +170 -0
- pretensor/mcp/tools/search.py +316 -0
- pretensor/mcp/tools/semantic_search.py +282 -0
- pretensor/mcp/tools/traverse.py +1027 -0
- pretensor/mcp/tools/validate_sql.py +150 -0
- pretensor/observability.py +203 -0
- pretensor/py.typed +0 -0
- pretensor/quickstart/README.md +29 -0
- pretensor/quickstart/__init__.py +6 -0
- pretensor/quickstart/docker-compose.yml +18 -0
- pretensor/quickstart/pagila_data.sql +63 -0
- pretensor/quickstart/pagila_ddl.sql +92 -0
- pretensor/search/__init__.py +6 -0
- pretensor/search/base.py +80 -0
- pretensor/search/index.py +435 -0
- pretensor/semantic/__init__.py +24 -0
- pretensor/semantic/base.py +123 -0
- pretensor/semantic/compiler.py +487 -0
- pretensor/semantic/yaml_layer.py +180 -0
- pretensor/skills/__init__.py +5 -0
- pretensor/skills/generator.py +235 -0
- pretensor/staleness/__init__.py +15 -0
- pretensor/staleness/graph_patcher.py +355 -0
- pretensor/staleness/impact_analyzer.py +162 -0
- pretensor/staleness/snapshot_store.py +38 -0
- pretensor/validation/__init__.py +9 -0
- pretensor/validation/query_validator.py +436 -0
- pretensor/visibility/__init__.py +23 -0
- pretensor/visibility/config.py +126 -0
- pretensor/visibility/filter.py +143 -0
- pretensor/visibility/kuzu_helpers.py +32 -0
- pretensor/visibility/runtime.py +36 -0
- pretensor/visibility/sync_grants.py +188 -0
- pretensor-0.1.0.dist-info/METADATA +251 -0
- pretensor-0.1.0.dist-info/RECORD +198 -0
- pretensor-0.1.0.dist-info/WHEEL +4 -0
- pretensor-0.1.0.dist-info/entry_points.txt +2 -0
- 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)
|