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,168 @@
|
|
|
1
|
+
"""Index-time embedding computation for SchemaTable nodes.
|
|
2
|
+
|
|
3
|
+
Opt-in via ``PretensorConfig.embeddings.index_tables``. When the toggle is
|
|
4
|
+
off (default) nothing runs — zero reads, zero writes — so the null path
|
|
5
|
+
stays byte-identical to the pre-embedding pipeline output.
|
|
6
|
+
|
|
7
|
+
:func:`compute_table_embeddings` is the single computation entry point.
|
|
8
|
+
Index and reindex call it right after schema rows are written and *before*
|
|
9
|
+
relationship discovery, so the embedding relationship scorer, the
|
|
10
|
+
clustering blend, and the classify step's role vote all consume vectors
|
|
11
|
+
from the same run — no one-cycle staging lag. :class:`EmbeddingIndexStep`
|
|
12
|
+
remains in the pipeline for callers that invoke
|
|
13
|
+
``run_intelligence_layer`` directly; it delegates to the same function and
|
|
14
|
+
skips when the context marks vectors as already computed this run.
|
|
15
|
+
|
|
16
|
+
Every entry point honors the ``PRETENSOR_EMBEDDINGS_DISABLED`` environment
|
|
17
|
+
variable: when set to a truthy value (``1``, ``true``, ``yes`` —
|
|
18
|
+
case-insensitive), the embedding pass is forced to the null path even when
|
|
19
|
+
the ``[embeddings]`` extra is installed and ``index_tables`` is True. CI
|
|
20
|
+
exercises this to assert that "extra installed but user opted out"
|
|
21
|
+
produces output byte-identical to "extra absent" — the determinism
|
|
22
|
+
contract behind the release gate.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from typing import TYPE_CHECKING
|
|
29
|
+
|
|
30
|
+
from pretensor.intelligence.embeddings import (
|
|
31
|
+
embeddings_disabled_via_env,
|
|
32
|
+
format_entity_text,
|
|
33
|
+
get_default_embedding_client,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from pretensor.config import EmbeddingsConfig
|
|
38
|
+
from pretensor.core.store import KuzuStore
|
|
39
|
+
from pretensor.intelligence.steps import PipelineContext
|
|
40
|
+
|
|
41
|
+
# ``embeddings_disabled_via_env`` is canonically defined in
|
|
42
|
+
# ``embeddings.py``; it is imported above for this module's own use but is
|
|
43
|
+
# deliberately NOT re-exported here, so there is one authoritative import
|
|
44
|
+
# path for every caller.
|
|
45
|
+
__all__ = [
|
|
46
|
+
"EmbeddingIndexStep",
|
|
47
|
+
"compute_table_embeddings",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Canonical context keys live in ``steps.py`` (also home to PipelineStep
|
|
54
|
+
# / PipelineRunner / PipelineContext). Importing them keeps writer and
|
|
55
|
+
# reader on one source of truth — drift between sibling string literals
|
|
56
|
+
# would silently turn this step into a no-op.
|
|
57
|
+
from pretensor.intelligence.steps import ( # noqa: E402 (post-imports to keep the section clear)
|
|
58
|
+
_CTX_DATABASE_KEY,
|
|
59
|
+
_CTX_EMBEDDINGS_CONFIG,
|
|
60
|
+
_CTX_EMBEDDINGS_PRECOMPUTED,
|
|
61
|
+
_CTX_STORE,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compute_table_embeddings(store: KuzuStore, database_key: str) -> None:
|
|
66
|
+
"""Compute one embedding per ``SchemaTable`` and persist it to Kuzu.
|
|
67
|
+
|
|
68
|
+
Callers gate on ``EmbeddingsConfig.index_tables`` and
|
|
69
|
+
:func:`embeddings_disabled_via_env` — this function assumes the user
|
|
70
|
+
opted in. When the ``[embeddings]`` extra is absent it logs once at
|
|
71
|
+
``WARNING`` and returns without raising. Per-table write failures are
|
|
72
|
+
tolerated: the offending table is left with ``embedding=None`` and the
|
|
73
|
+
rest of the pass continues.
|
|
74
|
+
"""
|
|
75
|
+
rows = store.query_all_rows(
|
|
76
|
+
"""
|
|
77
|
+
MATCH (t:SchemaTable {database: $db})
|
|
78
|
+
OPTIONAL MATCH (t)-[:HAS_COLUMN]->(c:SchemaColumn)
|
|
79
|
+
RETURN t.node_id, t.schema_name, t.table_name, collect(c.column_name)
|
|
80
|
+
""",
|
|
81
|
+
{"db": database_key},
|
|
82
|
+
)
|
|
83
|
+
if not rows:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
client = get_default_embedding_client()
|
|
87
|
+
|
|
88
|
+
# Build the per-table input texts and a parallel ``node_id`` list so
|
|
89
|
+
# we can issue ONE batched ``embed()`` call instead of N. ONNX
|
|
90
|
+
# inference is dominated by per-call overhead (model warm-up,
|
|
91
|
+
# tensor allocation), so a single 50-row batch beats 50 single-row
|
|
92
|
+
# calls by a wide margin on every realistic schema size.
|
|
93
|
+
nids: list[str] = []
|
|
94
|
+
texts: list[str] = []
|
|
95
|
+
for row in rows:
|
|
96
|
+
nids.append(str(row[0]))
|
|
97
|
+
qualified_name = f"{row[1]}.{row[2]}"
|
|
98
|
+
column_names = [str(c) for c in (row[3] or []) if c is not None]
|
|
99
|
+
texts.append(format_entity_text(qualified_name, column_names))
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
vectors = client.embed(texts)
|
|
103
|
+
except ImportError as exc:
|
|
104
|
+
logger.warning(
|
|
105
|
+
"embedding_index: skipping, [embeddings] extra not installed: %s",
|
|
106
|
+
exc,
|
|
107
|
+
)
|
|
108
|
+
return
|
|
109
|
+
except Exception as exc: # noqa: BLE001 — batch failure is tolerated
|
|
110
|
+
logger.warning(
|
|
111
|
+
"embedding_index: batched embed failed (%s); leaving every "
|
|
112
|
+
"table's embedding=None",
|
|
113
|
+
exc,
|
|
114
|
+
)
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if not vectors:
|
|
118
|
+
return
|
|
119
|
+
if len(vectors) != len(texts):
|
|
120
|
+
logger.warning(
|
|
121
|
+
"embedding_index: client returned %d vectors for %d texts; "
|
|
122
|
+
"skipping write to avoid mis-aligned assignment",
|
|
123
|
+
len(vectors),
|
|
124
|
+
len(texts),
|
|
125
|
+
)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
for nid, vec in zip(nids, vectors, strict=True):
|
|
129
|
+
try:
|
|
130
|
+
store.set_table_embedding(nid, vec)
|
|
131
|
+
except ValueError as exc:
|
|
132
|
+
logger.warning("embedding_index: invalid vector for %s: %s", nid, exc)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class EmbeddingIndexStep:
|
|
136
|
+
"""Pipeline wrapper around :func:`compute_table_embeddings`.
|
|
137
|
+
|
|
138
|
+
Kept for callers that drive ``run_intelligence_layer`` directly (and
|
|
139
|
+
for plugins that anchor on the ``embedding_index`` step name). The
|
|
140
|
+
index/reindex CLI paths compute embeddings *before* relationship
|
|
141
|
+
discovery and mark the context with ``embeddings_precomputed=True``,
|
|
142
|
+
in which case this step is a no-op.
|
|
143
|
+
|
|
144
|
+
Setting ``PRETENSOR_EMBEDDINGS_DISABLED=1`` forces the null path even
|
|
145
|
+
when ``index_tables`` is True; used by CI to enforce null-path parity.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
name = "embedding_index"
|
|
149
|
+
# No dependencies: the computation reads only SchemaTable + HAS_COLUMN
|
|
150
|
+
# rows, which exist before the intelligence pipeline starts. Running
|
|
151
|
+
# first lets the classify step's role vote consume same-run vectors
|
|
152
|
+
# instead of being one index cycle behind.
|
|
153
|
+
dependencies: list[str] = []
|
|
154
|
+
|
|
155
|
+
async def execute(self, ctx: PipelineContext) -> None:
|
|
156
|
+
emb_cfg: EmbeddingsConfig | None = ctx.get(_CTX_EMBEDDINGS_CONFIG)
|
|
157
|
+
if emb_cfg is None or not emb_cfg.index_tables:
|
|
158
|
+
return
|
|
159
|
+
if embeddings_disabled_via_env():
|
|
160
|
+
logger.info("embedding_index: skipping (PRETENSOR_EMBEDDINGS_DISABLED set)")
|
|
161
|
+
return
|
|
162
|
+
if ctx.get(_CTX_EMBEDDINGS_PRECOMPUTED, False):
|
|
163
|
+
logger.debug("embedding_index: vectors already computed this run")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
store: KuzuStore = ctx.get(_CTX_STORE)
|
|
167
|
+
database_key: str = ctx.get(_CTX_DATABASE_KEY)
|
|
168
|
+
compute_table_embeddings(store, database_key)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Schema introspection: project config, DSN parsing, and compatibility shims.
|
|
2
|
+
|
|
3
|
+
Schema snapshot DTOs, PostgreSQL introspection, and ``diff_snapshots`` live in
|
|
4
|
+
``pretensor.connectors``. This package re-exports ``inspector.inspect``,
|
|
5
|
+
``snapshot`` helpers, and ``models.schema`` for backward compatibility.
|
|
6
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import ConfigDict, Field, model_validator
|
|
10
|
+
from ruamel.yaml import YAML
|
|
11
|
+
|
|
12
|
+
from .base import PretensorModel
|
|
13
|
+
|
|
14
|
+
ENV_VAR_PATTERN = re.compile(r"\$\{([^}]+)\}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _resolve_env_vars(value: str) -> str:
|
|
18
|
+
"""Replace ${VAR_NAME} references with their environment variable values."""
|
|
19
|
+
|
|
20
|
+
def _replace(match: re.Match[str]) -> str:
|
|
21
|
+
var_name = match.group(1)
|
|
22
|
+
env_value = os.environ.get(var_name)
|
|
23
|
+
if env_value is None:
|
|
24
|
+
raise ValueError(f"Environment variable '{var_name}' is not set")
|
|
25
|
+
return env_value
|
|
26
|
+
|
|
27
|
+
return ENV_VAR_PATTERN.sub(_replace, value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DatabaseType(StrEnum):
|
|
31
|
+
POSTGRES = "postgres"
|
|
32
|
+
BIGQUERY = "bigquery"
|
|
33
|
+
SNOWFLAKE = "snowflake"
|
|
34
|
+
DUCKDB = "duckdb"
|
|
35
|
+
MYSQL = "mysql"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PrivacyMode(StrEnum):
|
|
39
|
+
SCHEMA_ONLY = "schema_only"
|
|
40
|
+
SYNTHETIC_SAMPLES = "synthetic_samples"
|
|
41
|
+
REAL_SAMPLES = "real_samples"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SchemaFilter(PretensorModel):
|
|
45
|
+
include: list[str] = Field(default_factory=list)
|
|
46
|
+
exclude: list[str] = Field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ConnectionConfig(PretensorModel):
|
|
50
|
+
# Mutable config (env resolution, optional connector-specific fields).
|
|
51
|
+
model_config = ConfigDict(frozen=False)
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
type: DatabaseType
|
|
55
|
+
host: str | None = None
|
|
56
|
+
port: int | str | None = None
|
|
57
|
+
database: str | None = None
|
|
58
|
+
user: str | None = None
|
|
59
|
+
password: str | None = None
|
|
60
|
+
schema_filter: SchemaFilter = Field(default_factory=SchemaFilter)
|
|
61
|
+
metadata_extra: dict[str, Any] = Field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
@model_validator(mode="after")
|
|
64
|
+
def resolve_env_vars(self) -> ConnectionConfig:
|
|
65
|
+
for field_name in self.__class__.model_fields:
|
|
66
|
+
raw = getattr(self, field_name)
|
|
67
|
+
if isinstance(raw, str) and ENV_VAR_PATTERN.search(raw):
|
|
68
|
+
object.__setattr__(self, field_name, _resolve_env_vars(raw))
|
|
69
|
+
# Also resolve env vars inside metadata_extra string values.
|
|
70
|
+
for key, val in self.metadata_extra.items():
|
|
71
|
+
if isinstance(val, str) and ENV_VAR_PATTERN.search(val):
|
|
72
|
+
self.metadata_extra[key] = _resolve_env_vars(val)
|
|
73
|
+
# Coerce port back to int after env var substitution (${PORT} → "5432").
|
|
74
|
+
raw_port = getattr(self, "port")
|
|
75
|
+
if isinstance(raw_port, str):
|
|
76
|
+
object.__setattr__(self, "port", int(raw_port))
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class LLMProvider(StrEnum):
|
|
81
|
+
"""Registered LLM backends (see `LLMConfig.provider`)."""
|
|
82
|
+
|
|
83
|
+
ANTHROPIC = "anthropic"
|
|
84
|
+
OPENROUTER = "openrouter"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LLMStepKey(StrEnum):
|
|
88
|
+
"""Pipeline step identifiers for per-step LLM overrides (`llm_steps`)."""
|
|
89
|
+
|
|
90
|
+
SEMANTIC = "semantic"
|
|
91
|
+
PLANNER = "planner"
|
|
92
|
+
CODEGEN_QUERIES = "codegen_queries"
|
|
93
|
+
CODEGEN_PAGES = "codegen_pages"
|
|
94
|
+
VALIDATION = "validation"
|
|
95
|
+
REPAIR = "repair"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class StepLLMOverride(PretensorModel):
|
|
99
|
+
"""Partial LLM settings merged over the global `llm` block for one step.
|
|
100
|
+
|
|
101
|
+
Omitted fields keep the global default. ``http_headers`` replaces the
|
|
102
|
+
global header map when set (non-``None``).
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
provider: LLMProvider | None = None
|
|
106
|
+
model: str | None = None
|
|
107
|
+
temperature: float | None = None
|
|
108
|
+
max_tokens: int | None = None
|
|
109
|
+
base_url: str | None = None
|
|
110
|
+
http_headers: dict[str, str] | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class LLMConfig(PretensorModel):
|
|
114
|
+
provider: LLMProvider = LLMProvider.ANTHROPIC
|
|
115
|
+
model: str = "claude-sonnet-4-20250514"
|
|
116
|
+
temperature: float = 0.0
|
|
117
|
+
max_tokens: int = 4096
|
|
118
|
+
base_url: str | None = None
|
|
119
|
+
http_headers: dict[str, str] = Field(default_factory=dict)
|
|
120
|
+
|
|
121
|
+
def merged_with(self, override: StepLLMOverride | None) -> LLMConfig:
|
|
122
|
+
"""Return a copy with non-``None`` fields from ``override`` applied."""
|
|
123
|
+
if override is None:
|
|
124
|
+
return self.model_copy(deep=True)
|
|
125
|
+
data = self.model_dump()
|
|
126
|
+
if override.provider is not None:
|
|
127
|
+
data["provider"] = override.provider
|
|
128
|
+
if override.model is not None:
|
|
129
|
+
data["model"] = override.model
|
|
130
|
+
if override.temperature is not None:
|
|
131
|
+
data["temperature"] = override.temperature
|
|
132
|
+
if override.max_tokens is not None:
|
|
133
|
+
data["max_tokens"] = override.max_tokens
|
|
134
|
+
if override.base_url is not None:
|
|
135
|
+
data["base_url"] = override.base_url
|
|
136
|
+
if override.http_headers is not None:
|
|
137
|
+
data["http_headers"] = dict(override.http_headers)
|
|
138
|
+
return LLMConfig.model_validate(data)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class SyntheticOptions(PretensorModel):
|
|
142
|
+
hash_strings: bool = True
|
|
143
|
+
randomize_numbers: bool = True
|
|
144
|
+
preserve_magnitude: bool = True
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class PrivacyConfig(PretensorModel):
|
|
148
|
+
mode: PrivacyMode = PrivacyMode.SCHEMA_ONLY
|
|
149
|
+
synthetic: SyntheticOptions = Field(default_factory=SyntheticOptions)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class MCPConfig(PretensorModel):
|
|
153
|
+
enabled: bool = False
|
|
154
|
+
url: str = "https://evidence.studio/mcp"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class EvidenceConfig(PretensorModel):
|
|
158
|
+
mcp: MCPConfig = Field(default_factory=MCPConfig)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class RepairConfig(PretensorModel):
|
|
162
|
+
max_code_retries: int = 3
|
|
163
|
+
max_plan_retries: int = 2
|
|
164
|
+
max_total_iterations: int = 10
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class StagingConfig(PretensorModel):
|
|
168
|
+
annotations_enabled: bool = True
|
|
169
|
+
staging_env_var: str = "PRETENSOR_STAGING"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ValidationConfig(PretensorModel):
|
|
173
|
+
threshold: int = 7
|
|
174
|
+
top_components_check: int = 2
|
|
175
|
+
repair: RepairConfig = Field(default_factory=RepairConfig)
|
|
176
|
+
staging: StagingConfig = Field(default_factory=StagingConfig)
|
|
177
|
+
# Layer 1 — skip LLM self-review when SQL lint, page markdown, and Evidence
|
|
178
|
+
# build (if run) produced no ERROR issues; still run when any deterministic
|
|
179
|
+
# step reports ERROR.
|
|
180
|
+
l1_skip_llm_self_review_on_clean_deterministic: bool = True
|
|
181
|
+
# Layer 3 — deterministic structure vs plan + optional presentation hints.
|
|
182
|
+
l3_structural_checks_enabled: bool = True
|
|
183
|
+
l3_orphan_chart_severity: Literal["error", "warning"] = "error"
|
|
184
|
+
l3_structural_presentation_warnings: bool = True
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ProjectConfig(PretensorModel):
|
|
188
|
+
project_name: str | None = None
|
|
189
|
+
connections: list[ConnectionConfig] | dict[str, ConnectionConfig] = Field(
|
|
190
|
+
default_factory=list
|
|
191
|
+
)
|
|
192
|
+
llm: LLMConfig = Field(default_factory=LLMConfig)
|
|
193
|
+
llm_steps: dict[LLMStepKey, StepLLMOverride] = Field(default_factory=dict)
|
|
194
|
+
privacy: PrivacyConfig = Field(default_factory=PrivacyConfig)
|
|
195
|
+
evidence: EvidenceConfig = Field(default_factory=EvidenceConfig)
|
|
196
|
+
validation: ValidationConfig = Field(default_factory=ValidationConfig)
|
|
197
|
+
|
|
198
|
+
@model_validator(mode="before")
|
|
199
|
+
@classmethod
|
|
200
|
+
def _preprocess_config(cls, data: dict) -> dict:
|
|
201
|
+
"""Handle legacy and shorthand config formats."""
|
|
202
|
+
if not isinstance(data, dict):
|
|
203
|
+
return data
|
|
204
|
+
|
|
205
|
+
# Infer project_name if missing
|
|
206
|
+
if not data.get("project_name"):
|
|
207
|
+
data["project_name"] = Path.cwd().name
|
|
208
|
+
|
|
209
|
+
# Handle connections as dict
|
|
210
|
+
connections = data.get("connections")
|
|
211
|
+
if isinstance(connections, dict):
|
|
212
|
+
connection_list = []
|
|
213
|
+
for name, config in connections.items():
|
|
214
|
+
if isinstance(config, dict):
|
|
215
|
+
config["name"] = name
|
|
216
|
+
connection_list.append(config)
|
|
217
|
+
data["connections"] = connection_list
|
|
218
|
+
|
|
219
|
+
return data
|
|
220
|
+
|
|
221
|
+
def resolved_llm_for_step(self, step: LLMStepKey) -> LLMConfig:
|
|
222
|
+
"""Global ``llm`` merged with ``llm_steps[step]`` when present."""
|
|
223
|
+
override = self.llm_steps.get(step)
|
|
224
|
+
return self.llm.merged_with(override)
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def from_yaml(cls, yaml_str: str) -> ProjectConfig:
|
|
228
|
+
yaml = YAML()
|
|
229
|
+
data = yaml.load(yaml_str)
|
|
230
|
+
return cls.model_validate(data)
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def load(cls, path: Path) -> ProjectConfig:
|
|
234
|
+
"""Load project config from a YAML file on disk."""
|
|
235
|
+
if not path.exists():
|
|
236
|
+
raise FileNotFoundError(f"Config file not found: {path}")
|
|
237
|
+
return cls.from_yaml(path.read_text())
|