agentic-data-contracts 0.2.3__tar.gz → 0.2.5__tar.gz
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.
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/CHANGELOG.md +17 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/PKG-INFO +21 -2
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/README.md +20 -1
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/pyproject.toml +1 -1
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/adapters/base.py +1 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/adapters/duckdb.py +12 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/core/contract.py +29 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/semantic/base.py +8 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/semantic/cube.py +8 -1
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/semantic/dbt.py +8 -1
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/semantic/yaml_source.py +16 -1
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/tools/factory.py +9 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/semantic_source.yml +21 -0
- agentic_data_contracts-0.2.5/tests/test_core/test_wildcard_tables.py +106 -0
- agentic_data_contracts-0.2.5/tests/test_semantic/test_relationships.py +83 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/test_yaml_source.py +1 -1
- agentic_data_contracts-0.2.5/tests/test_tools/test_wildcard_tools.py +76 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/uv.lock +1 -1
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/.github/dependabot.yml +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/.github/workflows/ci.yml +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/.gitignore +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/.pre-commit-config.yaml +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/.python-version +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/CLAUDE.md +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/LICENSE +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/docs/architecture.md +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/agent.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/contract.yml +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/semantic.yml +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/setup_db.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/adapters/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/bridge/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/bridge/compiler.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/core/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/core/schema.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/core/session.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/py.typed +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/semantic/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/tools/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/tools/middleware.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/tools/sdk.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/validation/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/validation/checkers.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/validation/explain.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/validation/validator.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/conftest.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/minimal_contract.yml +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/sample_cube_schema.yml +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/sample_dbt_manifest.json +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/valid_contract.yml +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_adapters/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_adapters/test_duckdb.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_bridge/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_bridge/test_compiler.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_contract.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_load_semantic_source.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_schema.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_sdk_config.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_session.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_system_prompt_metrics.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_public_api.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/test_cube.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/test_dbt.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/test_search.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_auto_load.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_factory.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_middleware.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_sdk.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_semantic_tools.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_validation/__init__.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_validation/test_checkers.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_validation/test_explain.py +0 -0
- {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_validation/test_validator.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.5] - 2026-03-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Table relationship metadata**: `Relationship` dataclass and `get_relationships()` on `SemanticSource` protocol for declaring join paths between tables (from/to column + relationship type)
|
|
10
|
+
- **Relationships in system prompt**: `to_system_prompt()` includes join paths so the agent knows how to combine tables correctly
|
|
11
|
+
- **YamlSource relationships**: Parsed from `relationships` section in semantic YAML files
|
|
12
|
+
- DbtSource and CubeSource return empty relationships (ready for future parsing of native join metadata)
|
|
13
|
+
|
|
14
|
+
## [0.2.4] - 2026-03-29
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Wildcard table support**: Use `tables: ["*"]` in `allowed_tables` to allow all tables in a schema, discovered from the database at runtime via `adapter.list_tables()`
|
|
19
|
+
- **`DataContract.resolve_tables(adapter)`**: Expands wildcard entries using the database adapter; called automatically by `create_tools()` when an adapter is provided
|
|
20
|
+
- **`DatabaseAdapter.list_tables(schema)`**: New protocol method for listing tables in a schema; implemented in `DuckDBAdapter` via `information_schema.tables`
|
|
21
|
+
|
|
5
22
|
## [0.2.3] - 2026-03-29
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-data-contracts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: YAML-first data contract governance for AI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/flyersworder/agentic-data-contracts
|
|
6
6
|
Project-URL: Repository, https://github.com/flyersworder/agentic-data-contracts
|
|
@@ -117,7 +117,9 @@ semantic:
|
|
|
117
117
|
path: "./semantic.yml"
|
|
118
118
|
allowed_tables:
|
|
119
119
|
- schema: analytics
|
|
120
|
-
tables: [
|
|
120
|
+
tables: ["*"] # all tables in schema (discovered from database)
|
|
121
|
+
- schema: marketing
|
|
122
|
+
tables: [campaigns] # or list specific tables
|
|
121
123
|
forbidden_operations: [DELETE, DROP, TRUNCATE, UPDATE, INSERT]
|
|
122
124
|
rules:
|
|
123
125
|
- name: tenant_isolation
|
|
@@ -275,6 +277,23 @@ semantic:
|
|
|
275
277
|
path: "./cube/schema.yml"
|
|
276
278
|
```
|
|
277
279
|
|
|
280
|
+
## Table Relationships
|
|
281
|
+
|
|
282
|
+
Define join paths so the agent knows how to combine tables correctly:
|
|
283
|
+
|
|
284
|
+
```yaml
|
|
285
|
+
# semantic.yml
|
|
286
|
+
relationships:
|
|
287
|
+
- from: analytics.orders.customer_id
|
|
288
|
+
to: analytics.customers.id
|
|
289
|
+
type: many_to_one
|
|
290
|
+
- from: analytics.orders.product_id
|
|
291
|
+
to: analytics.products.id
|
|
292
|
+
type: many_to_one
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The agent sees these in its system prompt and uses them to write correct JOINs instead of guessing from column names.
|
|
296
|
+
|
|
278
297
|
## Scalable Metric Discovery
|
|
279
298
|
|
|
280
299
|
For large data lakes with hundreds of KPIs, group metrics by domain and let the agent discover them efficiently:
|
|
@@ -64,7 +64,9 @@ semantic:
|
|
|
64
64
|
path: "./semantic.yml"
|
|
65
65
|
allowed_tables:
|
|
66
66
|
- schema: analytics
|
|
67
|
-
tables: [
|
|
67
|
+
tables: ["*"] # all tables in schema (discovered from database)
|
|
68
|
+
- schema: marketing
|
|
69
|
+
tables: [campaigns] # or list specific tables
|
|
68
70
|
forbidden_operations: [DELETE, DROP, TRUNCATE, UPDATE, INSERT]
|
|
69
71
|
rules:
|
|
70
72
|
- name: tenant_isolation
|
|
@@ -222,6 +224,23 @@ semantic:
|
|
|
222
224
|
path: "./cube/schema.yml"
|
|
223
225
|
```
|
|
224
226
|
|
|
227
|
+
## Table Relationships
|
|
228
|
+
|
|
229
|
+
Define join paths so the agent knows how to combine tables correctly:
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
# semantic.yml
|
|
233
|
+
relationships:
|
|
234
|
+
- from: analytics.orders.customer_id
|
|
235
|
+
to: analytics.customers.id
|
|
236
|
+
type: many_to_one
|
|
237
|
+
- from: analytics.orders.product_id
|
|
238
|
+
to: analytics.products.id
|
|
239
|
+
type: many_to_one
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The agent sees these in its system prompt and uses them to write correct JOINs instead of guessing from column names.
|
|
243
|
+
|
|
225
244
|
## Scalable Metric Discovery
|
|
226
245
|
|
|
227
246
|
For large data lakes with hundreds of KPIs, group metrics by domain and let the agent discover them efficiently:
|
|
@@ -37,6 +37,7 @@ class DatabaseAdapter(Protocol):
|
|
|
37
37
|
def execute(self, sql: str) -> QueryResult: ...
|
|
38
38
|
def explain(self, sql: str) -> ExplainResult: ...
|
|
39
39
|
def describe_table(self, schema: str, table: str) -> TableSchema: ...
|
|
40
|
+
def list_tables(self, schema: str) -> list[str]: ...
|
|
40
41
|
|
|
41
42
|
@property
|
|
42
43
|
def dialect(self) -> str: ...
|
|
@@ -59,6 +59,18 @@ class DuckDBAdapter:
|
|
|
59
59
|
last_estimate = int(match.group(1))
|
|
60
60
|
return last_estimate
|
|
61
61
|
|
|
62
|
+
def list_tables(self, schema: str) -> list[str]:
|
|
63
|
+
rows = self.connection.execute(
|
|
64
|
+
"""
|
|
65
|
+
SELECT table_name
|
|
66
|
+
FROM information_schema.tables
|
|
67
|
+
WHERE table_schema = ?
|
|
68
|
+
ORDER BY table_name
|
|
69
|
+
""",
|
|
70
|
+
[schema],
|
|
71
|
+
).fetchall()
|
|
72
|
+
return [row[0] for row in rows]
|
|
73
|
+
|
|
62
74
|
def describe_table(self, schema: str, table: str) -> TableSchema:
|
|
63
75
|
rows = self.connection.execute(
|
|
64
76
|
"""
|
|
@@ -14,6 +14,7 @@ from agentic_data_contracts.core.schema import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
|
+
from agentic_data_contracts.adapters.base import DatabaseAdapter
|
|
17
18
|
from agentic_data_contracts.semantic.base import SemanticSource
|
|
18
19
|
|
|
19
20
|
|
|
@@ -38,10 +39,27 @@ class DataContract:
|
|
|
38
39
|
schema = DataContractSchema.model_validate(raw)
|
|
39
40
|
return cls(schema=schema)
|
|
40
41
|
|
|
42
|
+
def has_wildcard_tables(self) -> bool:
|
|
43
|
+
"""Check if any schema uses wildcard ('*') for tables."""
|
|
44
|
+
return any("*" in entry.tables for entry in self.schema.semantic.allowed_tables)
|
|
45
|
+
|
|
46
|
+
def resolve_tables(self, adapter: DatabaseAdapter) -> None:
|
|
47
|
+
"""Expand wildcard tables using the database adapter.
|
|
48
|
+
|
|
49
|
+
Replaces ["*"] entries with actual table names from the database.
|
|
50
|
+
Call this once after creating the adapter. Results are cached
|
|
51
|
+
on the schema object.
|
|
52
|
+
"""
|
|
53
|
+
for entry in self.schema.semantic.allowed_tables:
|
|
54
|
+
if "*" in entry.tables:
|
|
55
|
+
entry.tables = adapter.list_tables(entry.schema_)
|
|
56
|
+
|
|
41
57
|
def allowed_table_names(self) -> list[str]:
|
|
42
58
|
names: list[str] = []
|
|
43
59
|
for entry in self.schema.semantic.allowed_tables:
|
|
44
60
|
for table in entry.tables:
|
|
61
|
+
if table == "*":
|
|
62
|
+
continue # unresolved wildcard — skip
|
|
45
63
|
names.append(f"{entry.schema_}.{table}")
|
|
46
64
|
return names
|
|
47
65
|
|
|
@@ -153,6 +171,17 @@ class DataContract:
|
|
|
153
171
|
)
|
|
154
172
|
sections.append(line)
|
|
155
173
|
|
|
174
|
+
# Table relationships
|
|
175
|
+
if semantic_source is not None:
|
|
176
|
+
rels = semantic_source.get_relationships()
|
|
177
|
+
if rels:
|
|
178
|
+
sections.append(
|
|
179
|
+
"\n### Table Relationships\n"
|
|
180
|
+
"Use these join paths when combining tables:"
|
|
181
|
+
)
|
|
182
|
+
for r in rels:
|
|
183
|
+
sections.append(f"- {r.from_} \u2192 {r.to} ({r.type})")
|
|
184
|
+
|
|
156
185
|
# Resource limits
|
|
157
186
|
res = self.schema.resources
|
|
158
187
|
if res:
|
|
@@ -20,12 +20,20 @@ class MetricDefinition:
|
|
|
20
20
|
filters: list[str] = field(default_factory=list)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
@dataclass
|
|
24
|
+
class Relationship:
|
|
25
|
+
from_: str # "schema.table.column"
|
|
26
|
+
to: str # "schema.table.column"
|
|
27
|
+
type: str = "many_to_one" # many_to_one | one_to_one | many_to_many
|
|
28
|
+
|
|
29
|
+
|
|
23
30
|
@runtime_checkable
|
|
24
31
|
class SemanticSource(Protocol):
|
|
25
32
|
def get_metrics(self) -> list[MetricDefinition]: ...
|
|
26
33
|
def get_metric(self, name: str) -> MetricDefinition | None: ...
|
|
27
34
|
def get_table_schema(self, schema: str, table: str) -> TableSchema | None: ...
|
|
28
35
|
def search_metrics(self, query: str) -> list[MetricDefinition]: ...
|
|
36
|
+
def get_relationships(self) -> list[Relationship]: ...
|
|
29
37
|
|
|
30
38
|
|
|
31
39
|
def fuzzy_search_metrics(
|
|
@@ -7,7 +7,11 @@ from pathlib import Path
|
|
|
7
7
|
import yaml
|
|
8
8
|
|
|
9
9
|
from agentic_data_contracts.adapters.base import Column, TableSchema
|
|
10
|
-
from agentic_data_contracts.semantic.base import
|
|
10
|
+
from agentic_data_contracts.semantic.base import (
|
|
11
|
+
MetricDefinition,
|
|
12
|
+
Relationship,
|
|
13
|
+
fuzzy_search_metrics,
|
|
14
|
+
)
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
class CubeSource:
|
|
@@ -54,5 +58,8 @@ class CubeSource:
|
|
|
54
58
|
def search_metrics(self, query: str) -> list[MetricDefinition]:
|
|
55
59
|
return fuzzy_search_metrics(self._metrics, self.get_metric, query)
|
|
56
60
|
|
|
61
|
+
def get_relationships(self) -> list[Relationship]:
|
|
62
|
+
return [] # TODO: parse from Cube joins config
|
|
63
|
+
|
|
57
64
|
def get_table_schema(self, schema: str, table: str) -> TableSchema | None:
|
|
58
65
|
return self._tables.get(f"{schema}.{table}")
|
|
@@ -7,7 +7,11 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from agentic_data_contracts.adapters.base import Column, TableSchema
|
|
10
|
-
from agentic_data_contracts.semantic.base import
|
|
10
|
+
from agentic_data_contracts.semantic.base import (
|
|
11
|
+
MetricDefinition,
|
|
12
|
+
Relationship,
|
|
13
|
+
fuzzy_search_metrics,
|
|
14
|
+
)
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
class DbtSource:
|
|
@@ -77,5 +81,8 @@ class DbtSource:
|
|
|
77
81
|
def search_metrics(self, query: str) -> list[MetricDefinition]:
|
|
78
82
|
return fuzzy_search_metrics(self._metrics, self.get_metric, query)
|
|
79
83
|
|
|
84
|
+
def get_relationships(self) -> list[Relationship]:
|
|
85
|
+
return [] # TODO: parse from dbt manifest relationships/refs
|
|
86
|
+
|
|
80
87
|
def get_table_schema(self, schema: str, table: str) -> TableSchema | None:
|
|
81
88
|
return self._tables.get(f"{schema}.{table}")
|
|
@@ -7,7 +7,11 @@ from pathlib import Path
|
|
|
7
7
|
import yaml
|
|
8
8
|
|
|
9
9
|
from agentic_data_contracts.adapters.base import Column, TableSchema
|
|
10
|
-
from agentic_data_contracts.semantic.base import
|
|
10
|
+
from agentic_data_contracts.semantic.base import (
|
|
11
|
+
MetricDefinition,
|
|
12
|
+
Relationship,
|
|
13
|
+
fuzzy_search_metrics,
|
|
14
|
+
)
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
class YamlSource:
|
|
@@ -38,6 +42,14 @@ class YamlSource:
|
|
|
38
42
|
for c in t.get("columns", [])
|
|
39
43
|
]
|
|
40
44
|
)
|
|
45
|
+
self._relationships = [
|
|
46
|
+
Relationship(
|
|
47
|
+
from_=r["from"],
|
|
48
|
+
to=r["to"],
|
|
49
|
+
type=r.get("type", "many_to_one"),
|
|
50
|
+
)
|
|
51
|
+
for r in raw.get("relationships", [])
|
|
52
|
+
]
|
|
41
53
|
|
|
42
54
|
def get_metrics(self) -> list[MetricDefinition]:
|
|
43
55
|
return list(self._metrics)
|
|
@@ -51,5 +63,8 @@ class YamlSource:
|
|
|
51
63
|
def search_metrics(self, query: str) -> list[MetricDefinition]:
|
|
52
64
|
return fuzzy_search_metrics(self._metrics, self.get_metric, query)
|
|
53
65
|
|
|
66
|
+
def get_relationships(self) -> list[Relationship]:
|
|
67
|
+
return list(self._relationships)
|
|
68
|
+
|
|
54
69
|
def get_table_schema(self, schema: str, table: str) -> TableSchema | None:
|
|
55
70
|
return self._tables.get(f"{schema}.{table}")
|
|
@@ -41,6 +41,10 @@ def create_tools(
|
|
|
41
41
|
if semantic_source is None:
|
|
42
42
|
semantic_source = contract.load_semantic_source()
|
|
43
43
|
|
|
44
|
+
# Resolve wildcard tables if adapter is available
|
|
45
|
+
if adapter is not None and contract.has_wildcard_tables():
|
|
46
|
+
contract.resolve_tables(adapter)
|
|
47
|
+
|
|
44
48
|
dialect = adapter.dialect if adapter else None
|
|
45
49
|
validator = Validator(contract, dialect=dialect, explain_adapter=adapter)
|
|
46
50
|
|
|
@@ -60,6 +64,11 @@ def create_tools(
|
|
|
60
64
|
for entry in contract.schema.semantic.allowed_tables:
|
|
61
65
|
if schema_filter and entry.schema_ != schema_filter:
|
|
62
66
|
continue
|
|
67
|
+
if "*" in entry.tables:
|
|
68
|
+
return _text_response(
|
|
69
|
+
f"Schema '{entry.schema_}' uses wildcard tables"
|
|
70
|
+
" but no database adapter is available to resolve them."
|
|
71
|
+
)
|
|
63
72
|
for table in entry.tables:
|
|
64
73
|
info: dict[str, Any] = {
|
|
65
74
|
"schema": entry.schema_,
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/semantic_source.yml
RENAMED
|
@@ -29,3 +29,24 @@ tables:
|
|
|
29
29
|
- name: status
|
|
30
30
|
type: VARCHAR
|
|
31
31
|
description: "Order status: pending, completed, cancelled"
|
|
32
|
+
- name: customer_id
|
|
33
|
+
type: INTEGER
|
|
34
|
+
description: "FK to customers"
|
|
35
|
+
|
|
36
|
+
- schema: analytics
|
|
37
|
+
table: customers
|
|
38
|
+
columns:
|
|
39
|
+
- name: id
|
|
40
|
+
type: INTEGER
|
|
41
|
+
description: "Primary key"
|
|
42
|
+
- name: name
|
|
43
|
+
type: VARCHAR
|
|
44
|
+
description: "Customer name"
|
|
45
|
+
- name: region
|
|
46
|
+
type: VARCHAR
|
|
47
|
+
description: "Geographic region"
|
|
48
|
+
|
|
49
|
+
relationships:
|
|
50
|
+
- from: analytics.orders.customer_id
|
|
51
|
+
to: analytics.customers.id
|
|
52
|
+
type: many_to_one
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Tests for wildcard table support in allowed_tables."""
|
|
2
|
+
|
|
3
|
+
from agentic_data_contracts.adapters.duckdb import DuckDBAdapter
|
|
4
|
+
from agentic_data_contracts.core.contract import DataContract
|
|
5
|
+
from agentic_data_contracts.core.schema import (
|
|
6
|
+
AllowedTable,
|
|
7
|
+
DataContractSchema,
|
|
8
|
+
SemanticConfig,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _make_contract(tables_config: list[dict]) -> DataContract:
|
|
13
|
+
allowed = [AllowedTable.model_validate(t) for t in tables_config]
|
|
14
|
+
schema = DataContractSchema(
|
|
15
|
+
name="test",
|
|
16
|
+
semantic=SemanticConfig(allowed_tables=allowed),
|
|
17
|
+
)
|
|
18
|
+
return DataContract(schema)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _make_adapter() -> DuckDBAdapter:
|
|
22
|
+
db = DuckDBAdapter(":memory:")
|
|
23
|
+
db.connection.execute("""
|
|
24
|
+
CREATE SCHEMA IF NOT EXISTS analytics;
|
|
25
|
+
CREATE TABLE analytics.orders (id INTEGER);
|
|
26
|
+
CREATE TABLE analytics.customers (id INTEGER);
|
|
27
|
+
CREATE TABLE analytics.products (id INTEGER);
|
|
28
|
+
CREATE SCHEMA IF NOT EXISTS raw;
|
|
29
|
+
CREATE TABLE raw.events (id INTEGER);
|
|
30
|
+
""")
|
|
31
|
+
return db
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_has_wildcard_tables_true() -> None:
|
|
35
|
+
dc = _make_contract([{"schema": "analytics", "tables": ["*"]}])
|
|
36
|
+
assert dc.has_wildcard_tables()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_has_wildcard_tables_false() -> None:
|
|
40
|
+
dc = _make_contract(
|
|
41
|
+
[
|
|
42
|
+
{"schema": "analytics", "tables": ["orders"]},
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
assert not dc.has_wildcard_tables()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_resolve_tables_expands_wildcard() -> None:
|
|
49
|
+
dc = _make_contract([{"schema": "analytics", "tables": ["*"]}])
|
|
50
|
+
adapter = _make_adapter()
|
|
51
|
+
dc.resolve_tables(adapter)
|
|
52
|
+
|
|
53
|
+
names = dc.allowed_table_names()
|
|
54
|
+
assert "analytics.orders" in names
|
|
55
|
+
assert "analytics.customers" in names
|
|
56
|
+
assert "analytics.products" in names
|
|
57
|
+
assert not any(n.startswith("raw.") for n in names)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_resolve_tables_mixed() -> None:
|
|
61
|
+
dc = _make_contract(
|
|
62
|
+
[
|
|
63
|
+
{"schema": "analytics", "tables": ["*"]},
|
|
64
|
+
{"schema": "raw", "tables": []},
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
adapter = _make_adapter()
|
|
68
|
+
dc.resolve_tables(adapter)
|
|
69
|
+
|
|
70
|
+
names = dc.allowed_table_names()
|
|
71
|
+
assert "analytics.orders" in names
|
|
72
|
+
assert not any(n.startswith("raw.") for n in names)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_resolve_tables_preserves_explicit() -> None:
|
|
76
|
+
dc = _make_contract(
|
|
77
|
+
[
|
|
78
|
+
{"schema": "analytics", "tables": ["orders"]},
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
adapter = _make_adapter()
|
|
82
|
+
dc.resolve_tables(adapter)
|
|
83
|
+
|
|
84
|
+
names = dc.allowed_table_names()
|
|
85
|
+
assert names == ["analytics.orders"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_unresolved_wildcard_skipped() -> None:
|
|
89
|
+
dc = _make_contract([{"schema": "analytics", "tables": ["*"]}])
|
|
90
|
+
# Without calling resolve_tables, wildcard is skipped
|
|
91
|
+
names = dc.allowed_table_names()
|
|
92
|
+
assert names == []
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_adapter_list_tables() -> None:
|
|
96
|
+
adapter = _make_adapter()
|
|
97
|
+
tables = adapter.list_tables("analytics")
|
|
98
|
+
assert "orders" in tables
|
|
99
|
+
assert "customers" in tables
|
|
100
|
+
assert "products" in tables
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_adapter_list_tables_empty_schema() -> None:
|
|
104
|
+
adapter = _make_adapter()
|
|
105
|
+
tables = adapter.list_tables("nonexistent")
|
|
106
|
+
assert tables == []
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Tests for table relationship metadata."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from agentic_data_contracts.core.contract import DataContract
|
|
6
|
+
from agentic_data_contracts.core.schema import (
|
|
7
|
+
AllowedTable,
|
|
8
|
+
DataContractSchema,
|
|
9
|
+
SemanticConfig,
|
|
10
|
+
)
|
|
11
|
+
from agentic_data_contracts.semantic.cube import CubeSource
|
|
12
|
+
from agentic_data_contracts.semantic.dbt import DbtSource
|
|
13
|
+
from agentic_data_contracts.semantic.yaml_source import YamlSource
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_yaml_source_loads_relationships(fixtures_dir: Path) -> None:
|
|
17
|
+
source = YamlSource(fixtures_dir / "semantic_source.yml")
|
|
18
|
+
rels = source.get_relationships()
|
|
19
|
+
assert len(rels) == 1
|
|
20
|
+
assert rels[0].from_ == "analytics.orders.customer_id"
|
|
21
|
+
assert rels[0].to == "analytics.customers.id"
|
|
22
|
+
assert rels[0].type == "many_to_one"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_yaml_source_no_relationships(tmp_path: Path) -> None:
|
|
26
|
+
(tmp_path / "empty.yml").write_text("metrics: []")
|
|
27
|
+
source = YamlSource(tmp_path / "empty.yml")
|
|
28
|
+
assert source.get_relationships() == []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_dbt_source_returns_empty_relationships(
|
|
32
|
+
fixtures_dir: Path,
|
|
33
|
+
) -> None:
|
|
34
|
+
source = DbtSource(fixtures_dir / "sample_dbt_manifest.json")
|
|
35
|
+
assert source.get_relationships() == []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_cube_source_returns_empty_relationships(
|
|
39
|
+
fixtures_dir: Path,
|
|
40
|
+
) -> None:
|
|
41
|
+
source = CubeSource(fixtures_dir / "sample_cube_schema.yml")
|
|
42
|
+
assert source.get_relationships() == []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_system_prompt_includes_relationships(
|
|
46
|
+
fixtures_dir: Path,
|
|
47
|
+
) -> None:
|
|
48
|
+
source = YamlSource(fixtures_dir / "semantic_source.yml")
|
|
49
|
+
schema = DataContractSchema(
|
|
50
|
+
name="test",
|
|
51
|
+
semantic=SemanticConfig(
|
|
52
|
+
allowed_tables=[
|
|
53
|
+
AllowedTable.model_validate(
|
|
54
|
+
{"schema": "analytics", "tables": ["orders", "customers"]}
|
|
55
|
+
),
|
|
56
|
+
],
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
dc = DataContract(schema)
|
|
60
|
+
prompt = dc.to_system_prompt(semantic_source=source)
|
|
61
|
+
assert "Table Relationships" in prompt
|
|
62
|
+
assert "analytics.orders.customer_id" in prompt
|
|
63
|
+
assert "analytics.customers.id" in prompt
|
|
64
|
+
assert "many_to_one" in prompt
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_system_prompt_no_relationships_when_empty(
|
|
68
|
+
fixtures_dir: Path,
|
|
69
|
+
) -> None:
|
|
70
|
+
source = DbtSource(fixtures_dir / "sample_dbt_manifest.json")
|
|
71
|
+
schema = DataContractSchema(
|
|
72
|
+
name="test",
|
|
73
|
+
semantic=SemanticConfig(
|
|
74
|
+
allowed_tables=[
|
|
75
|
+
AllowedTable.model_validate(
|
|
76
|
+
{"schema": "analytics", "tables": ["orders"]}
|
|
77
|
+
),
|
|
78
|
+
],
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
dc = DataContract(schema)
|
|
82
|
+
prompt = dc.to_system_prompt(semantic_source=source)
|
|
83
|
+
assert "Table Relationships" not in prompt
|
|
@@ -39,7 +39,7 @@ def test_get_metric_not_found(source: YamlSource) -> None:
|
|
|
39
39
|
def test_get_table_schema(source: YamlSource) -> None:
|
|
40
40
|
schema = source.get_table_schema("analytics", "orders")
|
|
41
41
|
assert schema is not None
|
|
42
|
-
assert len(schema.columns) ==
|
|
42
|
+
assert len(schema.columns) == 5
|
|
43
43
|
col_names = [c.name for c in schema.columns]
|
|
44
44
|
assert "id" in col_names
|
|
45
45
|
assert "amount" in col_names
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Tests for tools with wildcard table resolution."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from agentic_data_contracts.adapters.duckdb import DuckDBAdapter
|
|
8
|
+
from agentic_data_contracts.core.contract import DataContract
|
|
9
|
+
from agentic_data_contracts.core.schema import (
|
|
10
|
+
AllowedTable,
|
|
11
|
+
DataContractSchema,
|
|
12
|
+
SemanticConfig,
|
|
13
|
+
)
|
|
14
|
+
from agentic_data_contracts.tools.factory import create_tools
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def adapter() -> DuckDBAdapter:
|
|
19
|
+
db = DuckDBAdapter(":memory:")
|
|
20
|
+
db.connection.execute("""
|
|
21
|
+
CREATE SCHEMA IF NOT EXISTS analytics;
|
|
22
|
+
CREATE TABLE analytics.orders (id INTEGER, amount DECIMAL);
|
|
23
|
+
CREATE TABLE analytics.customers (id INTEGER, name VARCHAR);
|
|
24
|
+
INSERT INTO analytics.orders VALUES (1, 100.00);
|
|
25
|
+
""")
|
|
26
|
+
return db
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def wildcard_contract() -> DataContract:
|
|
31
|
+
schema = DataContractSchema(
|
|
32
|
+
name="test",
|
|
33
|
+
semantic=SemanticConfig(
|
|
34
|
+
allowed_tables=[
|
|
35
|
+
AllowedTable.model_validate({"schema": "analytics", "tables": ["*"]}),
|
|
36
|
+
],
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
return DataContract(schema)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.asyncio
|
|
43
|
+
async def test_list_tables_after_wildcard_resolve(
|
|
44
|
+
wildcard_contract: DataContract, adapter: DuckDBAdapter
|
|
45
|
+
) -> None:
|
|
46
|
+
tools = create_tools(wildcard_contract, adapter=adapter)
|
|
47
|
+
tool = next(t for t in tools if t.name == "list_tables")
|
|
48
|
+
result = await tool.callable({})
|
|
49
|
+
text = result["content"][0]["text"]
|
|
50
|
+
data = json.loads(text)
|
|
51
|
+
table_names = [t["table"] for t in data["tables"]]
|
|
52
|
+
assert "orders" in table_names
|
|
53
|
+
assert "customers" in table_names
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
async def test_run_query_with_wildcard_tables(
|
|
58
|
+
wildcard_contract: DataContract, adapter: DuckDBAdapter
|
|
59
|
+
) -> None:
|
|
60
|
+
tools = create_tools(wildcard_contract, adapter=adapter)
|
|
61
|
+
tool = next(t for t in tools if t.name == "run_query")
|
|
62
|
+
result = await tool.callable({"sql": "SELECT id, amount FROM analytics.orders"})
|
|
63
|
+
text = result["content"][0]["text"]
|
|
64
|
+
assert "100" in text
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_validate_query_with_wildcard_tables(
|
|
69
|
+
wildcard_contract: DataContract, adapter: DuckDBAdapter
|
|
70
|
+
) -> None:
|
|
71
|
+
tools = create_tools(wildcard_contract, adapter=adapter)
|
|
72
|
+
tool = next(t for t in tools if t.name == "validate_query")
|
|
73
|
+
# analytics.orders should be allowed after wildcard resolution
|
|
74
|
+
result = await tool.callable({"sql": "SELECT id FROM analytics.orders"})
|
|
75
|
+
text = result["content"][0]["text"]
|
|
76
|
+
assert "valid" in text.lower()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/agent.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/contract.yml
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/semantic.yml
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/setup_db.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/minimal_contract.yml
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/sample_cube_schema.yml
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/valid_contract.yml
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_adapters/__init__.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_adapters/test_duckdb.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_bridge/test_compiler.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_schema.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_sdk_config.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/__init__.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/test_cube.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/test_dbt.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/test_search.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_auto_load.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_factory.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_validation/__init__.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_validation/test_checkers.py
RENAMED
|
File without changes
|
{agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_validation/test_explain.py
RENAMED
|
File without changes
|
|
File without changes
|