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
pretensor/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Pretensor Graph — schema knowledge graph backed by Kuzu.
|
|
2
|
+
|
|
3
|
+
Heavy imports (e.g. :class:`GraphBuilder`) are lazy so ``import pretensor``
|
|
4
|
+
works in tests that only touch the registry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# pyright: reportUnsupportedDunderAll=false
|
|
8
|
+
# Names in __all__ are provided via __getattr__; Pyright does not model that.
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"GraphBuilder",
|
|
16
|
+
"GraphEdge",
|
|
17
|
+
"GraphNode",
|
|
18
|
+
"GraphRegistry",
|
|
19
|
+
"KuzuStore",
|
|
20
|
+
"RelationshipCandidate",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def __getattr__(name: str) -> Any:
|
|
25
|
+
if name == "GraphBuilder":
|
|
26
|
+
from pretensor.core.builder import GraphBuilder
|
|
27
|
+
|
|
28
|
+
return GraphBuilder
|
|
29
|
+
if name == "GraphRegistry":
|
|
30
|
+
from pretensor.core.registry import GraphRegistry
|
|
31
|
+
|
|
32
|
+
return GraphRegistry
|
|
33
|
+
if name == "KuzuStore":
|
|
34
|
+
from pretensor.core.store import KuzuStore
|
|
35
|
+
|
|
36
|
+
return KuzuStore
|
|
37
|
+
if name == "GraphNode":
|
|
38
|
+
from pretensor.graph_models.node import GraphNode
|
|
39
|
+
|
|
40
|
+
return GraphNode
|
|
41
|
+
if name == "GraphEdge":
|
|
42
|
+
from pretensor.graph_models.edge import GraphEdge
|
|
43
|
+
|
|
44
|
+
return GraphEdge
|
|
45
|
+
if name == "RelationshipCandidate":
|
|
46
|
+
from pretensor.graph_models.relationship import RelationshipCandidate
|
|
47
|
+
|
|
48
|
+
return RelationshipCandidate
|
|
49
|
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
|
50
|
+
raise AttributeError(msg)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""``pretensor benchmark`` subsystem — 3-level benchmark harness.
|
|
2
|
+
|
|
3
|
+
The CLI contract and metric definitions are specified in
|
|
4
|
+
``pretensor-ai/pretensor-specs`` at ``docs/specs/benchmark/spec.md``. This
|
|
5
|
+
package contains the CLI dispatch / I/O wiring, the results aggregator
|
|
6
|
+
(``BenchmarkResult``, ``compare``, JSON / CSV I/O), and the ``compare``
|
|
7
|
+
regression-gate. L1 / L2 metric computation and the L3 agent-task runners
|
|
8
|
+
land in follow-up changes.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pretensor.benchmark.cli import register_benchmark_command
|
|
14
|
+
from pretensor.benchmark.fixtures import Fixture, load_dataset
|
|
15
|
+
from pretensor.benchmark.results import (
|
|
16
|
+
BenchmarkResult,
|
|
17
|
+
ComparisonError,
|
|
18
|
+
ComparisonReport,
|
|
19
|
+
Direction,
|
|
20
|
+
Metric,
|
|
21
|
+
MetricDiff,
|
|
22
|
+
compare,
|
|
23
|
+
read_json,
|
|
24
|
+
write_csv,
|
|
25
|
+
write_json,
|
|
26
|
+
)
|
|
27
|
+
from pretensor.benchmark.runner import (
|
|
28
|
+
Dataset,
|
|
29
|
+
RunnerKind,
|
|
30
|
+
run_l1,
|
|
31
|
+
run_l2,
|
|
32
|
+
run_l3,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"BenchmarkResult",
|
|
37
|
+
"ComparisonError",
|
|
38
|
+
"ComparisonReport",
|
|
39
|
+
"Dataset",
|
|
40
|
+
"Direction",
|
|
41
|
+
"Fixture",
|
|
42
|
+
"Metric",
|
|
43
|
+
"MetricDiff",
|
|
44
|
+
"RunnerKind",
|
|
45
|
+
"compare",
|
|
46
|
+
"load_dataset",
|
|
47
|
+
"read_json",
|
|
48
|
+
"register_benchmark_command",
|
|
49
|
+
"run_l1",
|
|
50
|
+
"run_l2",
|
|
51
|
+
"run_l3",
|
|
52
|
+
"write_csv",
|
|
53
|
+
"write_json",
|
|
54
|
+
]
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""``pretensor benchmark`` command group — L1 / L2 / L3 dispatch.
|
|
2
|
+
|
|
3
|
+
Contract: see ``docs/specs/benchmark/spec.md`` §CLI contract. stdout carries
|
|
4
|
+
only the JSON document when ``--out`` is absent; all human-facing text goes
|
|
5
|
+
to stderr.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
# Type-only import — runtime cost is paid lazily inside l3_command.
|
|
19
|
+
from pretensor.benchmark.l3.mcp_client import McpClientError
|
|
20
|
+
|
|
21
|
+
from pretensor.benchmark.results import (
|
|
22
|
+
ComparisonError,
|
|
23
|
+
compare,
|
|
24
|
+
read_json,
|
|
25
|
+
)
|
|
26
|
+
from pretensor.benchmark.runner import (
|
|
27
|
+
Dataset,
|
|
28
|
+
RunnerKind,
|
|
29
|
+
run_l1,
|
|
30
|
+
run_l2,
|
|
31
|
+
run_l3,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = ["register_benchmark_command"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Exit 1 covers both stub run failures and detected regressions — the spec
|
|
38
|
+
# §CLI contract collapses them under a single "release-gating assertion
|
|
39
|
+
# failed" code; downstream scripts inspect stderr to disambiguate.
|
|
40
|
+
_EXIT_FAILED = 1
|
|
41
|
+
_EXIT_BAD_INPUT = 2
|
|
42
|
+
_DEFAULT_GRAPH_DIR = Path(".pretensor")
|
|
43
|
+
_DATASET_HELP = (
|
|
44
|
+
"Fixture key (pagila, tpch, analytics_dwh, adversarial, "
|
|
45
|
+
"saas_multitenant, adventureworks)."
|
|
46
|
+
)
|
|
47
|
+
_OUT_HELP = "Write JSON output to this path; stdout when omitted."
|
|
48
|
+
_GRAPH_DIR_HELP = "Existing indexed graph directory. Defaults to .pretensor."
|
|
49
|
+
_BASELINE_HELP = "Path to the baseline benchmark result JSON."
|
|
50
|
+
_CURRENT_HELP = (
|
|
51
|
+
"Path to the current benchmark result JSON to compare against the baseline."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def register_benchmark_command(app: typer.Typer) -> None:
|
|
56
|
+
"""Register ``pretensor benchmark`` onto ``app``.
|
|
57
|
+
|
|
58
|
+
The benchmark commands route all human output to a dedicated stderr
|
|
59
|
+
console (the spec reserves stdout for the JSON document). Unlike most
|
|
60
|
+
``register_*`` callers, this function therefore does not accept the
|
|
61
|
+
shared stdout console from ``cli.main``.
|
|
62
|
+
"""
|
|
63
|
+
err_console = Console(stderr=True)
|
|
64
|
+
|
|
65
|
+
benchmark_app = typer.Typer(
|
|
66
|
+
name="benchmark",
|
|
67
|
+
help=(
|
|
68
|
+
"Run the 3-level benchmark harness (L1 graph quality, "
|
|
69
|
+
"L2 MCP tool quality, L3 agent task success)."
|
|
70
|
+
),
|
|
71
|
+
no_args_is_help=True,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _handle_not_implemented(exc: NotImplementedError) -> None:
|
|
75
|
+
err_console.print(f"[yellow]{exc}[/yellow]")
|
|
76
|
+
raise typer.Exit(_EXIT_FAILED) from exc
|
|
77
|
+
|
|
78
|
+
def _handle_input_error(
|
|
79
|
+
exc: LookupError | FileNotFoundError | McpClientError,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Friendly exit for the L3 runner's user-recoverable errors.
|
|
82
|
+
|
|
83
|
+
Three families land here:
|
|
84
|
+
|
|
85
|
+
* ``LookupError`` — missing per-dataset DB URL env var or the
|
|
86
|
+
LLM-provider key (the L3 runner converts the constructor's
|
|
87
|
+
``LlmCallError`` to ``LookupError`` so this handler picks it
|
|
88
|
+
up too).
|
|
89
|
+
* ``FileNotFoundError`` — missing DDL bundle, missing gold
|
|
90
|
+
file, or the pretensor runner's missing-indexed-graph guard.
|
|
91
|
+
* ``McpClientError`` — the pretensor runner failed to spawn or
|
|
92
|
+
handshake with ``pretensor serve`` (broken binary, version
|
|
93
|
+
mismatch, transport fault). Distinct from missing-graph-dir
|
|
94
|
+
so an operator can tell apart "I forgot to index" from "the
|
|
95
|
+
serve binary is broken" in the output.
|
|
96
|
+
|
|
97
|
+
All three are user-recoverable misconfigurations — print the
|
|
98
|
+
message as-is (the exception text is already actionable) and
|
|
99
|
+
exit with the same release-gate code as a stub failure so
|
|
100
|
+
wrappers don't need to special-case L3 vs L1/L2.
|
|
101
|
+
"""
|
|
102
|
+
err_console.print(f"[red]{exc}[/red]")
|
|
103
|
+
raise typer.Exit(_EXIT_FAILED) from exc
|
|
104
|
+
|
|
105
|
+
@benchmark_app.command("l1")
|
|
106
|
+
def l1_command(
|
|
107
|
+
dataset: Dataset = typer.Option(
|
|
108
|
+
...,
|
|
109
|
+
"--dataset",
|
|
110
|
+
help=_DATASET_HELP,
|
|
111
|
+
case_sensitive=False,
|
|
112
|
+
),
|
|
113
|
+
out: Path | None = typer.Option(
|
|
114
|
+
None,
|
|
115
|
+
"--out",
|
|
116
|
+
help=_OUT_HELP,
|
|
117
|
+
file_okay=True,
|
|
118
|
+
dir_okay=False,
|
|
119
|
+
writable=True,
|
|
120
|
+
resolve_path=True,
|
|
121
|
+
),
|
|
122
|
+
graph_dir: Path = typer.Option(
|
|
123
|
+
_DEFAULT_GRAPH_DIR,
|
|
124
|
+
"--graph-dir",
|
|
125
|
+
help=_GRAPH_DIR_HELP,
|
|
126
|
+
file_okay=False,
|
|
127
|
+
dir_okay=True,
|
|
128
|
+
resolve_path=True,
|
|
129
|
+
),
|
|
130
|
+
embeddings: bool = typer.Option(
|
|
131
|
+
False,
|
|
132
|
+
"--embeddings/--no-embeddings",
|
|
133
|
+
help="Run the Layer-A embeddings path (requires the "
|
|
134
|
+
"'embeddings' optional extra).",
|
|
135
|
+
),
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Run L1 graph-quality metrics against a fixture."""
|
|
138
|
+
try:
|
|
139
|
+
run_l1(dataset, out, graph_dir, embeddings=embeddings)
|
|
140
|
+
except NotImplementedError as exc:
|
|
141
|
+
_handle_not_implemented(exc)
|
|
142
|
+
|
|
143
|
+
@benchmark_app.command("l2")
|
|
144
|
+
def l2_command(
|
|
145
|
+
dataset: Dataset = typer.Option(
|
|
146
|
+
...,
|
|
147
|
+
"--dataset",
|
|
148
|
+
help=_DATASET_HELP,
|
|
149
|
+
case_sensitive=False,
|
|
150
|
+
),
|
|
151
|
+
out: Path | None = typer.Option(
|
|
152
|
+
None,
|
|
153
|
+
"--out",
|
|
154
|
+
help=_OUT_HELP,
|
|
155
|
+
file_okay=True,
|
|
156
|
+
dir_okay=False,
|
|
157
|
+
writable=True,
|
|
158
|
+
resolve_path=True,
|
|
159
|
+
),
|
|
160
|
+
graph_dir: Path = typer.Option(
|
|
161
|
+
_DEFAULT_GRAPH_DIR,
|
|
162
|
+
"--graph-dir",
|
|
163
|
+
help=_GRAPH_DIR_HELP,
|
|
164
|
+
file_okay=False,
|
|
165
|
+
dir_okay=True,
|
|
166
|
+
resolve_path=True,
|
|
167
|
+
),
|
|
168
|
+
embeddings: bool = typer.Option(
|
|
169
|
+
False,
|
|
170
|
+
"--embeddings/--no-embeddings",
|
|
171
|
+
help="Include the semantic_search Recall@K metric "
|
|
172
|
+
"(requires the 'embeddings' optional extra).",
|
|
173
|
+
),
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Run L2 MCP-tool-quality metrics against a fixture."""
|
|
176
|
+
try:
|
|
177
|
+
run_l2(dataset, out, graph_dir, embeddings=embeddings)
|
|
178
|
+
except NotImplementedError as exc:
|
|
179
|
+
_handle_not_implemented(exc)
|
|
180
|
+
|
|
181
|
+
@benchmark_app.command("l3")
|
|
182
|
+
def l3_command(
|
|
183
|
+
dataset: Dataset = typer.Option(
|
|
184
|
+
...,
|
|
185
|
+
"--dataset",
|
|
186
|
+
help=_DATASET_HELP,
|
|
187
|
+
case_sensitive=False,
|
|
188
|
+
),
|
|
189
|
+
out: Path | None = typer.Option(
|
|
190
|
+
None,
|
|
191
|
+
"--out",
|
|
192
|
+
help=_OUT_HELP,
|
|
193
|
+
file_okay=True,
|
|
194
|
+
dir_okay=False,
|
|
195
|
+
writable=True,
|
|
196
|
+
resolve_path=True,
|
|
197
|
+
),
|
|
198
|
+
graph_dir: Path = typer.Option(
|
|
199
|
+
_DEFAULT_GRAPH_DIR,
|
|
200
|
+
"--graph-dir",
|
|
201
|
+
help=_GRAPH_DIR_HELP,
|
|
202
|
+
file_okay=False,
|
|
203
|
+
dir_okay=True,
|
|
204
|
+
resolve_path=True,
|
|
205
|
+
),
|
|
206
|
+
runner: RunnerKind = typer.Option(
|
|
207
|
+
...,
|
|
208
|
+
"--runner",
|
|
209
|
+
help="Which L3 runner to execute: baseline (agent + raw "
|
|
210
|
+
"schema) or pretensor (agent + MCP).",
|
|
211
|
+
case_sensitive=False,
|
|
212
|
+
),
|
|
213
|
+
model: str = typer.Option(
|
|
214
|
+
...,
|
|
215
|
+
"--model",
|
|
216
|
+
help="LLM model identifier (e.g. claude-haiku-4-5).",
|
|
217
|
+
),
|
|
218
|
+
seed: int | None = typer.Option(
|
|
219
|
+
None,
|
|
220
|
+
"--seed",
|
|
221
|
+
help="Optional integer seed for reproducible agent runs.",
|
|
222
|
+
),
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Run L3 agent-task-success evaluation against a fixture."""
|
|
225
|
+
# Lazy import: ``McpClientError`` lives in the L3 MCP-client
|
|
226
|
+
# module, which transitively pulls in the MCP SDK. Importing it
|
|
227
|
+
# at CLI module load would defeat the lazy-import strategy used
|
|
228
|
+
# by the run_l3 dispatcher (and slow `pretensor benchmark
|
|
229
|
+
# --help` for everyone).
|
|
230
|
+
from pretensor.benchmark.l3.mcp_client import McpClientError
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
run_l3(
|
|
234
|
+
dataset,
|
|
235
|
+
out,
|
|
236
|
+
graph_dir,
|
|
237
|
+
runner=runner,
|
|
238
|
+
model=model,
|
|
239
|
+
seed=seed,
|
|
240
|
+
)
|
|
241
|
+
except NotImplementedError as exc:
|
|
242
|
+
_handle_not_implemented(exc)
|
|
243
|
+
except (LookupError, FileNotFoundError, McpClientError) as exc:
|
|
244
|
+
_handle_input_error(exc)
|
|
245
|
+
|
|
246
|
+
@benchmark_app.command("compare")
|
|
247
|
+
def compare_command(
|
|
248
|
+
baseline: Path = typer.Option(
|
|
249
|
+
...,
|
|
250
|
+
"--baseline",
|
|
251
|
+
help=_BASELINE_HELP,
|
|
252
|
+
exists=True,
|
|
253
|
+
file_okay=True,
|
|
254
|
+
dir_okay=False,
|
|
255
|
+
readable=True,
|
|
256
|
+
resolve_path=True,
|
|
257
|
+
),
|
|
258
|
+
current: Path = typer.Option(
|
|
259
|
+
...,
|
|
260
|
+
"--current",
|
|
261
|
+
help=_CURRENT_HELP,
|
|
262
|
+
exists=True,
|
|
263
|
+
file_okay=True,
|
|
264
|
+
dir_okay=False,
|
|
265
|
+
readable=True,
|
|
266
|
+
resolve_path=True,
|
|
267
|
+
),
|
|
268
|
+
tolerance: float = typer.Option(
|
|
269
|
+
1e-6,
|
|
270
|
+
"--tolerance",
|
|
271
|
+
help="Numeric tolerance below which metric drift is treated as noise.",
|
|
272
|
+
),
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Compare two benchmark result JSONs and exit non-zero on regression."""
|
|
275
|
+
try:
|
|
276
|
+
baseline_result = read_json(baseline)
|
|
277
|
+
except (json.JSONDecodeError, KeyError) as exc:
|
|
278
|
+
err_console.print(f"[red]Failed to read baseline: {exc}[/red]")
|
|
279
|
+
raise typer.Exit(_EXIT_BAD_INPUT) from exc
|
|
280
|
+
try:
|
|
281
|
+
current_result = read_json(current)
|
|
282
|
+
except (json.JSONDecodeError, KeyError) as exc:
|
|
283
|
+
err_console.print(f"[red]Failed to read current: {exc}[/red]")
|
|
284
|
+
raise typer.Exit(_EXIT_BAD_INPUT) from exc
|
|
285
|
+
try:
|
|
286
|
+
report = compare(baseline_result, current_result, tolerance=tolerance)
|
|
287
|
+
except ComparisonError as exc:
|
|
288
|
+
err_console.print(f"[red]{exc}[/red]")
|
|
289
|
+
raise typer.Exit(_EXIT_BAD_INPUT) from exc
|
|
290
|
+
err_console.print(report.format_diff())
|
|
291
|
+
if report.has_regression:
|
|
292
|
+
raise typer.Exit(_EXIT_FAILED)
|
|
293
|
+
|
|
294
|
+
app.add_typer(benchmark_app)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Benchmark-fixture loader.
|
|
2
|
+
|
|
3
|
+
Every L1 / L2 / L3 runner resolves datasets through :func:`load_dataset`, so
|
|
4
|
+
the set of supported keys and the physical file layout lives in one place.
|
|
5
|
+
Fixtures are resolved relative to the repo root (this module's grandparent of
|
|
6
|
+
``src``) — callers never need to know where the files live.
|
|
7
|
+
|
|
8
|
+
The returned :class:`Fixture` names the dataset, points to its schema-snapshot
|
|
9
|
+
YAML (always present), and optionally to a DDL-only ``schema.sql`` and a gold
|
|
10
|
+
NL-to-SQL question set. Datasets without DDL or a question set (``adversarial``,
|
|
11
|
+
``analytics_dwh``, ``saas_multitenant``, ``messy_warehouse``) return ``None`` for
|
|
12
|
+
those fields.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from pretensor.benchmark.runner import Dataset
|
|
21
|
+
|
|
22
|
+
__all__ = ["Fixture", "load_dataset"]
|
|
23
|
+
|
|
24
|
+
# Walk up from src/pretensor/benchmark/fixtures.py to the repo root.
|
|
25
|
+
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
26
|
+
_SCHEMAS_DIR = _REPO_ROOT / "tests" / "fixtures" / "schemas"
|
|
27
|
+
_DATA_DIR = _REPO_ROOT / "scripts" / "data"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class Fixture:
|
|
32
|
+
"""One benchmark dataset's on-disk layout.
|
|
33
|
+
|
|
34
|
+
``name`` is the canonical :class:`Dataset` enum key. ``schema_yaml_path``
|
|
35
|
+
is always set and points to a file that :meth:`SchemaSnapshot.from_yaml`
|
|
36
|
+
can parse. The remaining fields are ``None`` for datasets that have no
|
|
37
|
+
DDL dump, no gold question set, or no metric-templates file checked in.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name: Dataset
|
|
41
|
+
schema_yaml_path: Path
|
|
42
|
+
ddl_sql_path: Path | None
|
|
43
|
+
questions_path: Path | None
|
|
44
|
+
metric_templates_path: Path | None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_dataset(name: str | Dataset) -> Fixture:
|
|
48
|
+
"""Resolve a benchmark dataset's fixture paths.
|
|
49
|
+
|
|
50
|
+
Accepts a :class:`Dataset` enum value or its string form. Unknown keys
|
|
51
|
+
raise :class:`ValueError`. The schema YAML must exist — if missing,
|
|
52
|
+
:class:`FileNotFoundError` is raised. DDL and question-set paths are
|
|
53
|
+
returned as ``None`` when the dataset has no such file on disk.
|
|
54
|
+
"""
|
|
55
|
+
if isinstance(name, Dataset):
|
|
56
|
+
key = name
|
|
57
|
+
else:
|
|
58
|
+
try:
|
|
59
|
+
key = Dataset(name)
|
|
60
|
+
except ValueError as exc:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Unknown benchmark dataset: {name!r}. "
|
|
63
|
+
f"Known: {[d.value for d in Dataset]}"
|
|
64
|
+
) from exc
|
|
65
|
+
|
|
66
|
+
schema_path = _SCHEMAS_DIR / f"{key.value}.yaml"
|
|
67
|
+
if not schema_path.exists():
|
|
68
|
+
raise FileNotFoundError(
|
|
69
|
+
f"Schema YAML missing for dataset {key.value!r}: {schema_path}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
ddl_path = _DATA_DIR / key.value / "schema.sql"
|
|
73
|
+
questions_path = _DATA_DIR / f"{key.value}_nl2sql_bench.json"
|
|
74
|
+
metric_templates_path = _DATA_DIR / f"{key.value}_metric_templates.yaml"
|
|
75
|
+
|
|
76
|
+
return Fixture(
|
|
77
|
+
name=key,
|
|
78
|
+
schema_yaml_path=schema_path,
|
|
79
|
+
ddl_sql_path=ddl_path if ddl_path.exists() else None,
|
|
80
|
+
questions_path=questions_path if questions_path.exists() else None,
|
|
81
|
+
metric_templates_path=(
|
|
82
|
+
metric_templates_path if metric_templates_path.exists() else None
|
|
83
|
+
),
|
|
84
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""L1 graph-quality benchmark — metric implementations and runner.
|
|
2
|
+
|
|
3
|
+
See ``docs/specs/benchmark/spec.md`` §L1 metric definitions for the
|
|
4
|
+
contract this module fulfils. Metric values are wrapped in the
|
|
5
|
+
``{value, direction}`` envelope from
|
|
6
|
+
:mod:`pretensor.benchmark.results`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pretensor.benchmark.l1.metrics import (
|
|
12
|
+
cluster_stability_jaccard,
|
|
13
|
+
inferred_join_pr,
|
|
14
|
+
role_f1,
|
|
15
|
+
)
|
|
16
|
+
from pretensor.benchmark.l1.runner import run_l1
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"cluster_stability_jaccard",
|
|
20
|
+
"inferred_join_pr",
|
|
21
|
+
"role_f1",
|
|
22
|
+
"run_l1",
|
|
23
|
+
]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Pure L1 metric functions.
|
|
2
|
+
|
|
3
|
+
Each function takes only data — no graph store, no I/O. The runner
|
|
4
|
+
collects predictions from the intelligence pipeline, then hands them
|
|
5
|
+
to these helpers. Boundary cases (empty ground truth, identical
|
|
6
|
+
inputs, etc.) are handled here so the runner never divides by zero.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterable, Mapping
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"canonicalise_join_key",
|
|
15
|
+
"cluster_stability_jaccard",
|
|
16
|
+
"inferred_join_pr",
|
|
17
|
+
"role_f1",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
JoinKey = tuple[str, str, str, str]
|
|
22
|
+
"""``(src_table, src_column, dst_table, dst_column)`` — bare names.
|
|
23
|
+
|
|
24
|
+
Schema is folded into the table name (``schema.table``) so callers can
|
|
25
|
+
key on either form consistently.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def inferred_join_pr(
|
|
30
|
+
inferred: Iterable[JoinKey],
|
|
31
|
+
declared_fks: Iterable[JoinKey],
|
|
32
|
+
) -> tuple[float, float]:
|
|
33
|
+
"""Precision and recall of inferred joins against declared FK ground truth.
|
|
34
|
+
|
|
35
|
+
Both inputs are sets of ``(src_table, src_column, dst_table, dst_column)``
|
|
36
|
+
tuples. Keys are canonicalised to unordered pairs before comparison —
|
|
37
|
+
declared FKs are directional, but the heuristic discovery layer may
|
|
38
|
+
emit either orientation for the same physical relationship, and an
|
|
39
|
+
inferred edge ``A.x ↔ B.y`` should match the declared FK ``A.x → B.y``
|
|
40
|
+
regardless of orientation.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
``(precision, recall)``. Both are ``0.0`` when the corresponding
|
|
44
|
+
denominator is zero (no candidates ⇒ precision 0; no ground truth
|
|
45
|
+
⇒ recall 0). Identical sets ⇒ ``(1.0, 1.0)``.
|
|
46
|
+
"""
|
|
47
|
+
inferred_set = {canonicalise_join_key(k) for k in inferred}
|
|
48
|
+
declared_set = {canonicalise_join_key(k) for k in declared_fks}
|
|
49
|
+
|
|
50
|
+
true_positives = inferred_set & declared_set
|
|
51
|
+
precision = len(true_positives) / len(inferred_set) if inferred_set else 0.0
|
|
52
|
+
recall = len(true_positives) / len(declared_set) if declared_set else 0.0
|
|
53
|
+
return precision, recall
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cluster_stability_jaccard(
|
|
57
|
+
run_a: Iterable[Iterable[str]],
|
|
58
|
+
run_b: Iterable[Iterable[str]],
|
|
59
|
+
) -> float:
|
|
60
|
+
"""Mean pairwise Jaccard between two cluster partitions of the same input.
|
|
61
|
+
|
|
62
|
+
Each cluster is a collection of table node IDs. A score of ``1.0``
|
|
63
|
+
means the two partitions are identical (modulo cluster ordering); a
|
|
64
|
+
score of ``0.0`` means no cluster from ``run_a`` matches any cluster
|
|
65
|
+
in ``run_b``.
|
|
66
|
+
|
|
67
|
+
For each cluster in ``run_a``, this picks the best-matching cluster
|
|
68
|
+
in ``run_b`` (highest Jaccard) and averages those scores. Empty
|
|
69
|
+
inputs on both sides ⇒ ``1.0`` (vacuously stable). Empty input on
|
|
70
|
+
one side only ⇒ ``0.0``.
|
|
71
|
+
"""
|
|
72
|
+
a_sets = [frozenset(c) for c in run_a if c]
|
|
73
|
+
b_sets = [frozenset(c) for c in run_b if c]
|
|
74
|
+
|
|
75
|
+
if not a_sets and not b_sets:
|
|
76
|
+
return 1.0
|
|
77
|
+
if not a_sets or not b_sets:
|
|
78
|
+
return 0.0
|
|
79
|
+
|
|
80
|
+
total = 0.0
|
|
81
|
+
for cluster_a in a_sets:
|
|
82
|
+
best = 0.0
|
|
83
|
+
for cluster_b in b_sets:
|
|
84
|
+
# Both sides are filtered to non-empty frozensets above, so
|
|
85
|
+
# the union is always non-empty — no zero-divide guard needed.
|
|
86
|
+
jacc = len(cluster_a & cluster_b) / len(cluster_a | cluster_b)
|
|
87
|
+
if jacc > best:
|
|
88
|
+
best = jacc
|
|
89
|
+
total += best
|
|
90
|
+
return total / len(a_sets)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def role_f1(
|
|
94
|
+
predicted: Mapping[str, str],
|
|
95
|
+
gold: Mapping[str, str],
|
|
96
|
+
) -> float:
|
|
97
|
+
"""Macro-averaged F1 across the gold role labels.
|
|
98
|
+
|
|
99
|
+
For each role appearing in ``gold``, computes precision and recall
|
|
100
|
+
against ``predicted`` and averages the per-class F1 scores. Tables
|
|
101
|
+
in ``gold`` that the predictor did not classify count as misses;
|
|
102
|
+
tables in ``predicted`` whose gold role is absent are ignored
|
|
103
|
+
(they're outside the evaluation set).
|
|
104
|
+
|
|
105
|
+
Returns ``1.0`` when ``gold`` and ``predicted`` are both empty
|
|
106
|
+
(vacuously correct). Returns ``0.0`` when ``gold`` is non-empty but
|
|
107
|
+
``predicted`` is empty, and vice versa.
|
|
108
|
+
"""
|
|
109
|
+
if not gold and not predicted:
|
|
110
|
+
return 1.0
|
|
111
|
+
if not gold or not predicted:
|
|
112
|
+
return 0.0
|
|
113
|
+
|
|
114
|
+
gold_roles = sorted(set(gold.values()))
|
|
115
|
+
f1_sum = 0.0
|
|
116
|
+
for role in gold_roles:
|
|
117
|
+
gold_for_role = {t for t, r in gold.items() if r == role}
|
|
118
|
+
pred_for_role = {t for t, r in predicted.items() if r == role}
|
|
119
|
+
true_positives = gold_for_role & pred_for_role
|
|
120
|
+
precision = len(true_positives) / len(pred_for_role) if pred_for_role else 0.0
|
|
121
|
+
recall = len(true_positives) / len(gold_for_role) if gold_for_role else 0.0
|
|
122
|
+
if precision + recall == 0.0:
|
|
123
|
+
f1 = 0.0
|
|
124
|
+
else:
|
|
125
|
+
f1 = 2 * precision * recall / (precision + recall)
|
|
126
|
+
f1_sum += f1
|
|
127
|
+
return f1_sum / len(gold_roles)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def canonicalise_join_key(key: JoinKey) -> tuple[tuple[str, str], tuple[str, str]]:
|
|
131
|
+
"""Collapse a directional ``JoinKey`` to an unordered pair.
|
|
132
|
+
|
|
133
|
+
Treats ``(A.x → B.y)`` and ``(B.y → A.x)`` as the same edge by
|
|
134
|
+
sorting the two endpoint ``(table, column)`` tuples. Exposed so
|
|
135
|
+
other modules (e.g. the runner's per-item collector) can canonicalise
|
|
136
|
+
keys against the same convention without duplicating the logic.
|
|
137
|
+
"""
|
|
138
|
+
src_t, src_c, dst_t, dst_c = key
|
|
139
|
+
a = (src_t, src_c)
|
|
140
|
+
b = (dst_t, dst_c)
|
|
141
|
+
return (a, b) if a <= b else (b, a)
|