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,146 @@
1
+ """Pure L2 metric functions.
2
+
3
+ Each function takes already-collected observations and returns a scalar.
4
+ The runner owns all I/O — these helpers stay testable without spinning
5
+ up Kuzu, the keyword index, or any MCP machinery.
6
+
7
+ Boundary cases (empty input, missing gold per item) are handled here
8
+ so the runner never divides by zero.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Iterable, Sequence
14
+
15
+ __all__ = [
16
+ "JoinPair",
17
+ "RankedHit",
18
+ "compile_metric_correctness",
19
+ "query_recall_at_k",
20
+ "semantic_search_recall_at_k",
21
+ "top_k_with_ties",
22
+ "traverse_correctness",
23
+ ]
24
+
25
+
26
+ JoinPair = tuple[str, str]
27
+ """``(from_table, to_table)`` — schema-qualified bare names."""
28
+
29
+ RankedHit = tuple[float, str]
30
+ """``(score, table_name)`` — one search hit; higher score = better."""
31
+
32
+
33
+ _SCORE_ROUNDING_DECIMALS = 4
34
+ """Bucket precision applied to scores before sorting/cutoff.
35
+
36
+ Upstream BM25 (SQLite FTS5) has been observed to emit micro-noise — e.g.
37
+ ``-1e-06`` versus ``-0.0`` — for hits that are functionally tied in
38
+ ranking. Rounding to 4 decimals collapses that noise into clean zero
39
+ buckets while preserving meaningful score differences (anything ≥ 1e-4).
40
+ """
41
+
42
+
43
+ def top_k_with_ties(
44
+ ranked: Sequence[RankedHit],
45
+ k: int,
46
+ ) -> list[RankedHit]:
47
+ """Return the top-K hits plus any extra items tied at the K-th score.
48
+
49
+ Sorts defensively by ``(-score, name)`` before slicing so the caller
50
+ needn't pre-sort. AC #5: a tied top-K includes ALL tied items so a
51
+ near-cutoff hit is not missed when comparing against gold.
52
+
53
+ Scores are rounded to :data:`_SCORE_ROUNDING_DECIMALS` decimals before
54
+ comparison so that floating-point noise (e.g. SQLite FTS5's ``-1e-06``
55
+ vs ``-0.0`` for functionally-tied BM25 hits) doesn't make the cutoff
56
+ flip between runs. The returned tuples carry the rounded score; the
57
+ metric only consumes the table-name field, so this loss of precision
58
+ is invisible downstream.
59
+ """
60
+ if k <= 0 or not ranked:
61
+ return []
62
+ rounded: list[RankedHit] = [
63
+ (round(score, _SCORE_ROUNDING_DECIMALS), name) for score, name in ranked
64
+ ]
65
+ ordered = sorted(rounded, key=lambda h: (-h[0], h[1]))
66
+ if len(ordered) <= k:
67
+ return ordered
68
+ cutoff_score = ordered[k - 1][0]
69
+ return [h for h in ordered if h[0] >= cutoff_score]
70
+
71
+
72
+ def query_recall_at_k(
73
+ observations: Iterable[tuple[Iterable[str], Sequence[RankedHit]]],
74
+ k: int = 5,
75
+ ) -> float:
76
+ """Mean Recall@K across observations.
77
+
78
+ Each observation is ``(gold_tables, ranked_results)``. ``ranked_results``
79
+ may arrive in any order — :func:`top_k_with_ties` re-sorts it. Recall
80
+ per observation is ``|gold ∩ retrieved_top_k| / |gold|``. Observations
81
+ with empty ``gold_tables`` are skipped (recall undefined). Returns
82
+ ``0.0`` when no observation has gold data.
83
+ """
84
+ total = 0.0
85
+ n = 0
86
+ for gold, ranked in observations:
87
+ gold_set = set(gold)
88
+ if not gold_set:
89
+ continue
90
+ top_k = top_k_with_ties(list(ranked), k)
91
+ retrieved_set = {name for _, name in top_k}
92
+ recall = len(gold_set & retrieved_set) / len(gold_set)
93
+ total += recall
94
+ n += 1
95
+ return total / n if n > 0 else 0.0
96
+
97
+
98
+ def semantic_search_recall_at_k(
99
+ observations: Iterable[tuple[Iterable[str], Sequence[RankedHit]]],
100
+ k: int = 5,
101
+ ) -> float:
102
+ """Mean Recall@K against ``semantic_search`` retrievals.
103
+
104
+ Same shape and semantics as :func:`query_recall_at_k` — kept distinct
105
+ so the two retrieval systems can diverge later (different ranking
106
+ semantics, different post-processing) without forcing one to fit the
107
+ other's API.
108
+ """
109
+ return query_recall_at_k(observations, k=k)
110
+
111
+
112
+ def traverse_correctness(
113
+ observations: Iterable[tuple[Sequence[JoinPair], Sequence[Sequence[JoinPair]]]],
114
+ ) -> float:
115
+ """Fraction of items whose gold path matches at least one returned path.
116
+
117
+ The ``traverse`` tool emits all top-ranked paths when ties exist;
118
+ this metric counts the item as correct when ANY returned path
119
+ equals the gold sequence (tuple-wise comparison). Returns ``0.0``
120
+ when ``observations`` is empty.
121
+ """
122
+ obs_list = list(observations)
123
+ if not obs_list:
124
+ return 0.0
125
+
126
+ correct = 0
127
+ for gold_path, returned_paths in obs_list:
128
+ gold_t = tuple(tuple(p) for p in gold_path)
129
+ for path in returned_paths:
130
+ if tuple(tuple(p) for p in path) == gold_t:
131
+ correct += 1
132
+ break
133
+ return correct / len(obs_list)
134
+
135
+
136
+ def compile_metric_correctness(observations: Iterable[bool]) -> float:
137
+ """Fraction of metric templates that compiled to valid SQL.
138
+
139
+ Each observation is the boolean ``valid`` flag returned by
140
+ ``compile_metric_payload``. Returns ``0.0`` when ``observations`` is
141
+ empty.
142
+ """
143
+ obs_list = list(observations)
144
+ if not obs_list:
145
+ return 0.0
146
+ return sum(1 for ok in obs_list if ok) / len(obs_list)
@@ -0,0 +1,124 @@
1
+ """Per-call graph construction for the L2 benchmark.
2
+
3
+ Each invocation of :func:`run_l2` indexes the fixture's schema YAML into
4
+ a fresh, throwaway Kuzu store + ``registry.json`` under a temp dir,
5
+ then hands that directory to the MCP tool payload functions
6
+ (``query_payload``, ``traverse_payload``, ``compile_metric_payload``).
7
+ This guarantees determinism — no leftover state from previous runs.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+
15
+ from pretensor.config import EmbeddingsConfig, PretensorConfig
16
+ from pretensor.connectors.models import SchemaSnapshot
17
+ from pretensor.core.builder import GraphBuilder
18
+ from pretensor.core.registry import GraphRegistry
19
+ from pretensor.core.store import KuzuStore
20
+
21
+ __all__ = ["build_l2_graph_dir"]
22
+
23
+
24
+ # Pinned so the persisted ``last_indexed_at`` doesn't churn the registry
25
+ # bytes between runs. The benchmark only consults graph contents — the
26
+ # timestamp is metadata that has no semantic effect on tool output, but
27
+ # we still want byte-identical state across invocations for any future
28
+ # code that hashes the registry.
29
+ _DETERMINISTIC_INDEXED_AT = datetime(1970, 1, 1, tzinfo=timezone.utc)
30
+
31
+
32
+ def build_l2_graph_dir(
33
+ snapshot: SchemaSnapshot, *, work_dir: Path, embeddings: bool = False
34
+ ) -> Path:
35
+ """Build a Kuzu graph + registry under ``work_dir`` and return the dir.
36
+
37
+ When ``embeddings`` is True, tables are indexed with
38
+ ``EmbeddingsConfig(index_tables=True)`` so the ``semantic_search`` /
39
+ hybrid-rerank legs measure real cosine retrieval instead of the
40
+ no-vectors fallback (which would peg semantic recall at 0.0 and make
41
+ the embeddings CI lane indistinguishable from the plain one). The
42
+ embedding model revision is pinned, so vectors are deterministic
43
+ across runs.
44
+
45
+ The returned path is suitable as the ``graph_dir`` argument to every
46
+ MCP tool payload function. ``work_dir`` is created if it doesn't
47
+ exist; the caller (typically ``tempfile.TemporaryDirectory``) is
48
+ responsible for cleanup.
49
+
50
+ Notes:
51
+ ``run_relationship_discovery=False`` matches the determinism
52
+ policy — L2 metrics measure the tools' behaviour over the
53
+ declared FK / structural shape, not over heuristic discovery
54
+ output. (L1 covers the discovery quality leg via its own
55
+ :func:`pretensor.benchmark.l1.pipeline.discover_inferred_joins_blind`.)
56
+
57
+ After build, this helper strips :class:`Cluster` nodes (and the
58
+ ``IN_CLUSTER`` edges that link them to tables). Reason: the
59
+ intelligence layer that ``GraphBuilder.build`` runs unconditionally
60
+ produces cluster labels via Louvain/Leiden community detection,
61
+ and even with a seeded RNG the labels can differ across
62
+ environments (different igraph builds, different platform RNG
63
+ wiring). Those labels feed into the FTS5 keyword index's
64
+ ``cluster_context`` column and shift BM25 scores across machines.
65
+ L2 measures ``query`` / ``semantic_search`` / ``traverse`` /
66
+ ``compile_metric`` quality — none of those tools depend on
67
+ clustering being present, so removing it from the graph keeps
68
+ the benchmark cross-environment-deterministic without changing
69
+ what's being measured.
70
+ """
71
+ work_dir.mkdir(parents=True, exist_ok=True)
72
+ graph_path = work_dir / "graphs" / f"{snapshot.connection_name}.kuzu"
73
+ graph_path.parent.mkdir(parents=True, exist_ok=True)
74
+
75
+ store = KuzuStore(graph_path)
76
+ try:
77
+ cfg = PretensorConfig(
78
+ embeddings=EmbeddingsConfig(index_tables=embeddings),
79
+ )
80
+ GraphBuilder().build(
81
+ snapshot, store, run_relationship_discovery=False, config=cfg
82
+ )
83
+ if embeddings and not store.has_any_table_embeddings():
84
+ # The production embed path tolerates per-run failures (a
85
+ # degraded index beats an aborted one), but a benchmark lane
86
+ # that silently measures the no-vectors fallback would report
87
+ # a bogus regression against the embeddings baseline. Fail
88
+ # loudly instead — the usual culprit is a failed model
89
+ # download (e.g. HF Hub rate-limiting in CI).
90
+ msg = (
91
+ "L2 was invoked with --embeddings but no table vectors were "
92
+ "computed; the embedding model is likely unavailable "
93
+ "(download failure / rate limit). Fix the model fetch or "
94
+ "re-run without --embeddings."
95
+ )
96
+ raise RuntimeError(msg)
97
+ # See docstring: clusters introduce cross-environment nondeterminism
98
+ # via FTS5's cluster_context column. L2 doesn't measure clustering
99
+ # quality (L1 does), so strip them before the runner queries the graph.
100
+ _strip_clusters(store)
101
+ finally:
102
+ store.close()
103
+
104
+ reg = GraphRegistry(work_dir / "registry.json").load()
105
+ reg.upsert(
106
+ connection_name=snapshot.connection_name,
107
+ database=snapshot.database,
108
+ dsn="",
109
+ graph_path=graph_path,
110
+ indexed_at=_DETERMINISTIC_INDEXED_AT,
111
+ )
112
+ reg.save()
113
+
114
+ return work_dir
115
+
116
+
117
+ def _strip_clusters(store: KuzuStore) -> None:
118
+ """Remove all ``Cluster`` nodes (and ``IN_CLUSTER`` edges) from the graph.
119
+
120
+ Kuzu doesn't expose ``DETACH DELETE`` semantics on every type, so
121
+ we wipe the relationship rows first and then the cluster nodes.
122
+ """
123
+ store.execute_write("MATCH ()-[r:IN_CLUSTER]->() DELETE r")
124
+ store.execute_write("MATCH (c:Cluster) DELETE c")