agentic-data-contracts 0.2.6__tar.gz → 0.3.0__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.6 → agentic_data_contracts-0.3.0}/CHANGELOG.md +14 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/PKG-INFO +51 -1
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/README.md +50 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/pyproject.toml +1 -1
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/__init__.py +4 -1
- agentic_data_contracts-0.3.0/src/agentic_data_contracts/core/contract.py +148 -0
- agentic_data_contracts-0.3.0/src/agentic_data_contracts/core/prompt.py +249 -0
- agentic_data_contracts-0.3.0/tests/test_core/test_prompt_renderers.py +332 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_scalability.py +7 -7
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_system_prompt_metrics.py +4 -4
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_public_api.py +4 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/test_relationships.py +1 -1
- agentic_data_contracts-0.2.6/src/agentic_data_contracts/core/contract.py +0 -279
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/.github/dependabot.yml +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/.github/workflows/ci.yml +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/.gitignore +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/.pre-commit-config.yaml +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/.python-version +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/CLAUDE.md +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/LICENSE +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/docs/architecture.md +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/examples/revenue_agent/agent.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/examples/revenue_agent/contract.yml +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/examples/revenue_agent/semantic.yml +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/examples/revenue_agent/setup_db.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/adapters/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/adapters/base.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/adapters/duckdb.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/bridge/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/bridge/compiler.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/core/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/core/schema.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/core/session.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/py.typed +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/semantic/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/semantic/base.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/semantic/cube.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/semantic/dbt.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/semantic/yaml_source.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/tools/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/tools/factory.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/tools/middleware.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/tools/sdk.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/validation/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/validation/checkers.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/validation/explain.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/validation/validator.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/conftest.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/fixtures/minimal_contract.yml +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/fixtures/sample_cube_schema.yml +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/fixtures/sample_dbt_manifest.json +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/fixtures/semantic_source.yml +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/fixtures/valid_contract.yml +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_adapters/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_adapters/test_duckdb.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_bridge/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_bridge/test_compiler.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_contract.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_load_semantic_source.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_schema.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_sdk_config.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_session.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_wildcard_tables.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/test_cube.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/test_dbt.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/test_search.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/test_yaml_source.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_auto_load.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_factory.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_middleware.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_pagination.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_sdk.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_semantic_tools.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_wildcard_tools.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_validation/__init__.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_validation/test_checkers.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_validation/test_explain.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_validation/test_validator.py +0 -0
- {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/uv.lock +0 -0
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-03-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`PromptRenderer` protocol**: New `@runtime_checkable` protocol for custom system prompt formatting. Users can implement `render(contract, semantic_source) -> str` to control how contracts are presented to their model of choice.
|
|
10
|
+
- **`ClaudePromptRenderer`**: Built-in XML-structured renderer optimized for Claude models (Sonnet 4.6+). Uses XML tags for structural boundaries, places constraints at the end for better instruction-following, and merges resource/temporal limits into a single section.
|
|
11
|
+
- **Custom renderer support**: `to_system_prompt(renderer=MyRenderer())` delegates entirely to a user-provided renderer.
|
|
12
|
+
- **Top-level exports**: `from agentic_data_contracts import PromptRenderer, ClaudePromptRenderer`
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **Default system prompt format**: `to_system_prompt()` now generates XML output (was Markdown). Pass a custom renderer if you need a different format.
|
|
17
|
+
- **`contract.py` simplified**: `to_system_prompt()` is now a thin delegate (~7 lines). All prompt-building logic moved to `core/prompt.py`.
|
|
18
|
+
|
|
5
19
|
## [0.2.6] - 2026-03-29
|
|
6
20
|
|
|
7
21
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-data-contracts
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -294,6 +294,29 @@ relationships:
|
|
|
294
294
|
|
|
295
295
|
The agent sees these in its system prompt and uses them to write correct JOINs instead of guessing from column names.
|
|
296
296
|
|
|
297
|
+
## Custom Prompt Rendering
|
|
298
|
+
|
|
299
|
+
The system prompt is generated by a `PromptRenderer`. The default `ClaudePromptRenderer` produces XML-structured output optimized for Claude models:
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
dc = DataContract.from_yaml("contract.yml")
|
|
303
|
+
print(dc.to_system_prompt()) # XML output, optimized for Claude
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
For other models (GPT-4, Gemini, Llama), implement the `PromptRenderer` protocol:
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
from agentic_data_contracts import PromptRenderer, DataContract
|
|
310
|
+
|
|
311
|
+
class MarkdownRenderer:
|
|
312
|
+
def render(self, contract, semantic_source=None):
|
|
313
|
+
tables = "\n".join(f"- {t}" for t in contract.allowed_table_names())
|
|
314
|
+
return f"## {contract.name}\n\nAllowed tables:\n{tables}"
|
|
315
|
+
|
|
316
|
+
dc = DataContract.from_yaml("contract.yml")
|
|
317
|
+
print(dc.to_system_prompt(renderer=MarkdownRenderer()))
|
|
318
|
+
```
|
|
319
|
+
|
|
297
320
|
## Scalable Metric Discovery
|
|
298
321
|
|
|
299
322
|
For large data lakes with hundreds of KPIs, group metrics by domain and let the agent discover them efficiently:
|
|
@@ -348,6 +371,33 @@ resources:
|
|
|
348
371
|
| `agent-sdk` | `claude-agent-sdk` | Claude Agent SDK integration |
|
|
349
372
|
| `agent-contracts` | `ai-agent-contracts>=0.2.0` | ai-agent-contracts bridge |
|
|
350
373
|
|
|
374
|
+
## Optional: Formal Governance with ai-agent-contracts
|
|
375
|
+
|
|
376
|
+
The library works standalone with lightweight enforcement. Install [`ai-agent-contracts`](https://pypi.org/project/ai-agent-contracts/) to upgrade to the formal governance framework:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
pip install "agentic-data-contracts[agent-contracts]"
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
from agentic_data_contracts.bridge.compiler import compile_to_contract
|
|
384
|
+
|
|
385
|
+
contract = compile_to_contract(dc) # YAML → formal 7-tuple Contract
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**What you get with the bridge:**
|
|
389
|
+
|
|
390
|
+
| Concern | Standalone | With ai-agent-contracts |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| Resource tracking | Manual counters | Formal `ResourceConstraints` with auto-enforcement |
|
|
393
|
+
| Rule violations | Exception + retry | `TerminationCondition` with contract state machine |
|
|
394
|
+
| Success evaluation | Log-based | Weighted `SuccessCriterion` scoring, LLM judge support |
|
|
395
|
+
| Contract lifecycle | None | `DRAFTED → ACTIVE → FULFILLED / VIOLATED / TERMINATED` |
|
|
396
|
+
| Framework support | Claude Agent SDK | + LiteLLM, LangChain, LangGraph, Google ADK |
|
|
397
|
+
| Multi-agent | Single agent | Coordination patterns (sequential, parallel, hierarchical) |
|
|
398
|
+
|
|
399
|
+
**When to use it:** formal audit trails, success scoring, multi-agent coordination, or integration with non-Claude agent frameworks.
|
|
400
|
+
|
|
351
401
|
## Example
|
|
352
402
|
|
|
353
403
|
See [`examples/revenue_agent/`](examples/revenue_agent/) for a complete working example with a DuckDB database, YAML semantic source, and Claude Agent SDK integration.
|
|
@@ -241,6 +241,29 @@ relationships:
|
|
|
241
241
|
|
|
242
242
|
The agent sees these in its system prompt and uses them to write correct JOINs instead of guessing from column names.
|
|
243
243
|
|
|
244
|
+
## Custom Prompt Rendering
|
|
245
|
+
|
|
246
|
+
The system prompt is generated by a `PromptRenderer`. The default `ClaudePromptRenderer` produces XML-structured output optimized for Claude models:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
dc = DataContract.from_yaml("contract.yml")
|
|
250
|
+
print(dc.to_system_prompt()) # XML output, optimized for Claude
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
For other models (GPT-4, Gemini, Llama), implement the `PromptRenderer` protocol:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from agentic_data_contracts import PromptRenderer, DataContract
|
|
257
|
+
|
|
258
|
+
class MarkdownRenderer:
|
|
259
|
+
def render(self, contract, semantic_source=None):
|
|
260
|
+
tables = "\n".join(f"- {t}" for t in contract.allowed_table_names())
|
|
261
|
+
return f"## {contract.name}\n\nAllowed tables:\n{tables}"
|
|
262
|
+
|
|
263
|
+
dc = DataContract.from_yaml("contract.yml")
|
|
264
|
+
print(dc.to_system_prompt(renderer=MarkdownRenderer()))
|
|
265
|
+
```
|
|
266
|
+
|
|
244
267
|
## Scalable Metric Discovery
|
|
245
268
|
|
|
246
269
|
For large data lakes with hundreds of KPIs, group metrics by domain and let the agent discover them efficiently:
|
|
@@ -295,6 +318,33 @@ resources:
|
|
|
295
318
|
| `agent-sdk` | `claude-agent-sdk` | Claude Agent SDK integration |
|
|
296
319
|
| `agent-contracts` | `ai-agent-contracts>=0.2.0` | ai-agent-contracts bridge |
|
|
297
320
|
|
|
321
|
+
## Optional: Formal Governance with ai-agent-contracts
|
|
322
|
+
|
|
323
|
+
The library works standalone with lightweight enforcement. Install [`ai-agent-contracts`](https://pypi.org/project/ai-agent-contracts/) to upgrade to the formal governance framework:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
pip install "agentic-data-contracts[agent-contracts]"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
from agentic_data_contracts.bridge.compiler import compile_to_contract
|
|
331
|
+
|
|
332
|
+
contract = compile_to_contract(dc) # YAML → formal 7-tuple Contract
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**What you get with the bridge:**
|
|
336
|
+
|
|
337
|
+
| Concern | Standalone | With ai-agent-contracts |
|
|
338
|
+
|---|---|---|
|
|
339
|
+
| Resource tracking | Manual counters | Formal `ResourceConstraints` with auto-enforcement |
|
|
340
|
+
| Rule violations | Exception + retry | `TerminationCondition` with contract state machine |
|
|
341
|
+
| Success evaluation | Log-based | Weighted `SuccessCriterion` scoring, LLM judge support |
|
|
342
|
+
| Contract lifecycle | None | `DRAFTED → ACTIVE → FULFILLED / VIOLATED / TERMINATED` |
|
|
343
|
+
| Framework support | Claude Agent SDK | + LiteLLM, LangChain, LangGraph, Google ADK |
|
|
344
|
+
| Multi-agent | Single agent | Coordination patterns (sequential, parallel, hierarchical) |
|
|
345
|
+
|
|
346
|
+
**When to use it:** formal audit trails, success scoring, multi-agent coordination, or integration with non-Claude agent frameworks.
|
|
347
|
+
|
|
298
348
|
## Example
|
|
299
349
|
|
|
300
350
|
See [`examples/revenue_agent/`](examples/revenue_agent/) for a complete working example with a DuckDB database, YAML semantic source, and Claude Agent SDK integration.
|
{agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/__init__.py
RENAMED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"""Agentic Data Contracts — YAML-first data contract governance for AI agents."""
|
|
2
2
|
|
|
3
3
|
from agentic_data_contracts.core.contract import DataContract
|
|
4
|
+
from agentic_data_contracts.core.prompt import ClaudePromptRenderer, PromptRenderer
|
|
4
5
|
from agentic_data_contracts.tools.factory import create_tools
|
|
5
6
|
from agentic_data_contracts.tools.middleware import contract_middleware
|
|
6
7
|
from agentic_data_contracts.tools.sdk import create_sdk_mcp_server
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
10
|
+
"ClaudePromptRenderer",
|
|
9
11
|
"DataContract",
|
|
10
|
-
"
|
|
12
|
+
"PromptRenderer",
|
|
11
13
|
"contract_middleware",
|
|
12
14
|
"create_sdk_mcp_server",
|
|
15
|
+
"create_tools",
|
|
13
16
|
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""DataContract — loads YAML, provides accessors and system prompt generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from agentic_data_contracts.core.schema import (
|
|
11
|
+
DataContractSchema,
|
|
12
|
+
Enforcement,
|
|
13
|
+
SemanticRule,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from agentic_data_contracts.adapters.base import DatabaseAdapter
|
|
18
|
+
from agentic_data_contracts.core.prompt import PromptRenderer
|
|
19
|
+
from agentic_data_contracts.semantic.base import SemanticSource
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataContract:
|
|
23
|
+
"""Main entry point: load a YAML data contract and interact with it."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, schema: DataContractSchema) -> None:
|
|
26
|
+
self.schema = schema
|
|
27
|
+
self._tables_resolved: bool = False
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
return self.schema.name
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_yaml(cls, path: str | Path) -> DataContract:
|
|
35
|
+
text = Path(path).read_text()
|
|
36
|
+
return cls.from_yaml_string(text)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_yaml_string(cls, text: str) -> DataContract:
|
|
40
|
+
raw = yaml.safe_load(text)
|
|
41
|
+
schema = DataContractSchema.model_validate(raw)
|
|
42
|
+
return cls(schema=schema)
|
|
43
|
+
|
|
44
|
+
def has_wildcard_tables(self) -> bool:
|
|
45
|
+
"""Check if any schema uses wildcard ('*') for tables."""
|
|
46
|
+
return any("*" in entry.tables for entry in self.schema.semantic.allowed_tables)
|
|
47
|
+
|
|
48
|
+
def resolve_tables(self, adapter: DatabaseAdapter, *, force: bool = False) -> None:
|
|
49
|
+
"""Expand wildcard tables using the database adapter.
|
|
50
|
+
|
|
51
|
+
Replaces ["*"] entries with actual table names from the database.
|
|
52
|
+
Results are cached — subsequent calls are no-ops unless force=True.
|
|
53
|
+
"""
|
|
54
|
+
if self._tables_resolved and not force:
|
|
55
|
+
return
|
|
56
|
+
for entry in self.schema.semantic.allowed_tables:
|
|
57
|
+
if "*" in entry.tables:
|
|
58
|
+
entry.tables = adapter.list_tables(entry.schema_)
|
|
59
|
+
self._tables_resolved = True
|
|
60
|
+
|
|
61
|
+
def allowed_table_names(self) -> list[str]:
|
|
62
|
+
names: list[str] = []
|
|
63
|
+
for entry in self.schema.semantic.allowed_tables:
|
|
64
|
+
for table in entry.tables:
|
|
65
|
+
if table == "*":
|
|
66
|
+
continue # unresolved wildcard — skip
|
|
67
|
+
names.append(f"{entry.schema_}.{table}")
|
|
68
|
+
return names
|
|
69
|
+
|
|
70
|
+
def block_rules(self) -> list[SemanticRule]:
|
|
71
|
+
return [
|
|
72
|
+
r for r in self.schema.semantic.rules if r.enforcement == Enforcement.BLOCK
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
def warn_rules(self) -> list[SemanticRule]:
|
|
76
|
+
return [
|
|
77
|
+
r for r in self.schema.semantic.rules if r.enforcement == Enforcement.WARN
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def log_rules(self) -> list[SemanticRule]:
|
|
81
|
+
return [
|
|
82
|
+
r for r in self.schema.semantic.rules if r.enforcement == Enforcement.LOG
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
def to_sdk_config(self) -> dict[str, object]:
|
|
86
|
+
"""Generate Claude Agent SDK configuration from contract limits.
|
|
87
|
+
|
|
88
|
+
Returns a dict of SDK options derived from contract resource/temporal
|
|
89
|
+
constraints, suitable for passing to ClaudeAgentOptions.
|
|
90
|
+
"""
|
|
91
|
+
config: dict[str, object] = {}
|
|
92
|
+
res = self.schema.resources
|
|
93
|
+
if res:
|
|
94
|
+
if res.token_budget is not None:
|
|
95
|
+
config["task_budget"] = res.token_budget
|
|
96
|
+
if res.max_retries is not None:
|
|
97
|
+
config["max_turns"] = res.max_retries
|
|
98
|
+
return config
|
|
99
|
+
|
|
100
|
+
def load_semantic_source(self) -> SemanticSource | None:
|
|
101
|
+
"""Auto-load the semantic source from the contract's source config.
|
|
102
|
+
|
|
103
|
+
Returns None if no source is configured.
|
|
104
|
+
"""
|
|
105
|
+
source_config = self.schema.semantic.source
|
|
106
|
+
if source_config is None:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
from agentic_data_contracts.semantic.cube import CubeSource
|
|
110
|
+
from agentic_data_contracts.semantic.dbt import DbtSource
|
|
111
|
+
from agentic_data_contracts.semantic.yaml_source import YamlSource
|
|
112
|
+
|
|
113
|
+
source_type = source_config.type.lower()
|
|
114
|
+
path = source_config.path
|
|
115
|
+
|
|
116
|
+
loaders: dict[str, type] = {
|
|
117
|
+
"yaml": YamlSource,
|
|
118
|
+
"dbt": DbtSource,
|
|
119
|
+
"cube": CubeSource,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
loader_cls = loaders.get(source_type)
|
|
123
|
+
if loader_cls is None:
|
|
124
|
+
msg = (
|
|
125
|
+
f"Unknown semantic source type: '{source_type}'."
|
|
126
|
+
f" Supported: {list(loaders.keys())}"
|
|
127
|
+
)
|
|
128
|
+
raise ValueError(msg)
|
|
129
|
+
|
|
130
|
+
return loader_cls(path)
|
|
131
|
+
|
|
132
|
+
def to_system_prompt(
|
|
133
|
+
self,
|
|
134
|
+
semantic_source: SemanticSource | None = None,
|
|
135
|
+
*,
|
|
136
|
+
renderer: PromptRenderer | None = None,
|
|
137
|
+
) -> str:
|
|
138
|
+
"""Generate a formatted system prompt section for an AI agent.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
semantic_source: Optional semantic source for metric/relationship data.
|
|
142
|
+
renderer: Optional custom prompt renderer. Defaults to ClaudePromptRenderer.
|
|
143
|
+
"""
|
|
144
|
+
if renderer is None:
|
|
145
|
+
from agentic_data_contracts.core.prompt import ClaudePromptRenderer
|
|
146
|
+
|
|
147
|
+
renderer = ClaudePromptRenderer()
|
|
148
|
+
return renderer.render(self, semantic_source)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""PromptRenderer protocol and ClaudePromptRenderer implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from agentic_data_contracts.core.contract import DataContract
|
|
9
|
+
from agentic_data_contracts.semantic.base import SemanticSource
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class PromptRenderer(Protocol):
|
|
14
|
+
"""Renders a DataContract into a string prompt."""
|
|
15
|
+
|
|
16
|
+
def render(
|
|
17
|
+
self,
|
|
18
|
+
contract: DataContract,
|
|
19
|
+
semantic_source: SemanticSource | None = None,
|
|
20
|
+
) -> str: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ClaudePromptRenderer:
|
|
24
|
+
"""Renders a DataContract as XML-structured output for Claude agents."""
|
|
25
|
+
|
|
26
|
+
# Max metrics to list individually before switching to compact summaries.
|
|
27
|
+
METRIC_DETAIL_THRESHOLD = 20
|
|
28
|
+
|
|
29
|
+
def render(
|
|
30
|
+
self,
|
|
31
|
+
contract: DataContract,
|
|
32
|
+
semantic_source: SemanticSource | None = None,
|
|
33
|
+
) -> str:
|
|
34
|
+
lines: list[str] = []
|
|
35
|
+
|
|
36
|
+
# Opening wrapper
|
|
37
|
+
lines.append(f'<data_contract name="{contract.name}">')
|
|
38
|
+
|
|
39
|
+
# 1. Allowed tables
|
|
40
|
+
lines.extend(self._render_allowed_tables(contract))
|
|
41
|
+
|
|
42
|
+
# 2. Available metrics or semantic_source fallback
|
|
43
|
+
metrics_lines = self._render_metrics(contract, semantic_source)
|
|
44
|
+
if metrics_lines:
|
|
45
|
+
lines.extend(metrics_lines)
|
|
46
|
+
elif contract.schema.semantic.source:
|
|
47
|
+
lines.extend(self._render_semantic_source_fallback(contract))
|
|
48
|
+
|
|
49
|
+
# 3. Table relationships
|
|
50
|
+
rel_lines = self._render_relationships(semantic_source)
|
|
51
|
+
if rel_lines:
|
|
52
|
+
lines.extend(rel_lines)
|
|
53
|
+
|
|
54
|
+
# 4. Resource limits (resources + temporal merged)
|
|
55
|
+
resource_lines = self._render_resource_limits(contract)
|
|
56
|
+
if resource_lines:
|
|
57
|
+
lines.extend(resource_lines)
|
|
58
|
+
|
|
59
|
+
# 5. Constraints (forbidden ops + rules)
|
|
60
|
+
lines.extend(self._render_constraints(contract))
|
|
61
|
+
|
|
62
|
+
# Closing wrapper
|
|
63
|
+
lines.append("</data_contract>")
|
|
64
|
+
|
|
65
|
+
return "\n".join(lines)
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# Section renderers
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def _render_allowed_tables(self, contract: DataContract) -> list[str]:
|
|
72
|
+
lines = ["<allowed_tables>", "Only query these tables:"]
|
|
73
|
+
for name in contract.allowed_table_names():
|
|
74
|
+
lines.append(f"- {name}")
|
|
75
|
+
lines.append("</allowed_tables>")
|
|
76
|
+
return lines
|
|
77
|
+
|
|
78
|
+
def _render_metrics(
|
|
79
|
+
self,
|
|
80
|
+
contract: DataContract,
|
|
81
|
+
semantic_source: SemanticSource | None,
|
|
82
|
+
) -> list[str]:
|
|
83
|
+
if semantic_source is None:
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
metrics = semantic_source.get_metrics()
|
|
87
|
+
if not metrics:
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
lines: list[str] = ["<available_metrics>"]
|
|
91
|
+
domains = contract.schema.semantic.domains
|
|
92
|
+
compact = len(metrics) > self.METRIC_DETAIL_THRESHOLD
|
|
93
|
+
|
|
94
|
+
if compact and domains:
|
|
95
|
+
# Large metric set with domains — show counts only
|
|
96
|
+
metric_names = {m.name for m in metrics}
|
|
97
|
+
for domain, names in domains.items():
|
|
98
|
+
count = sum(1 for n in names if n in metric_names)
|
|
99
|
+
if count:
|
|
100
|
+
lines.append(f' <domain name="{domain}" count="{count}" />')
|
|
101
|
+
lines.append(
|
|
102
|
+
' <hint>Use list_metrics(domain="...") to browse,'
|
|
103
|
+
' lookup_metric("...") to get SQL definitions.</hint>'
|
|
104
|
+
)
|
|
105
|
+
elif domains:
|
|
106
|
+
# Small metric set with domains — list with descriptions
|
|
107
|
+
metric_map = {m.name: m for m in metrics}
|
|
108
|
+
for domain, names in domains.items():
|
|
109
|
+
entries = [metric_map[n] for n in names if n in metric_map]
|
|
110
|
+
if entries:
|
|
111
|
+
lines.append(f' <domain name="{domain}">')
|
|
112
|
+
for m in entries:
|
|
113
|
+
lines.append(
|
|
114
|
+
f' <metric name="{m.name}">{m.description}</metric>'
|
|
115
|
+
)
|
|
116
|
+
lines.append(" </domain>")
|
|
117
|
+
lines.append(
|
|
118
|
+
" <hint>Use lookup_metric tool to get the SQL definition"
|
|
119
|
+
" before computing any KPI.</hint>"
|
|
120
|
+
)
|
|
121
|
+
elif compact:
|
|
122
|
+
# Large metric set without domains — just count
|
|
123
|
+
lines.append(f" <count>{len(metrics)} metrics available.</count>")
|
|
124
|
+
lines.append(
|
|
125
|
+
" <hint>Use list_metrics() to browse,"
|
|
126
|
+
' lookup_metric("...") to get SQL definitions.</hint>'
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
# Small metric set without domains — list all
|
|
130
|
+
for m in metrics:
|
|
131
|
+
lines.append(f' <metric name="{m.name}">{m.description}</metric>')
|
|
132
|
+
lines.append(
|
|
133
|
+
" <hint>Use lookup_metric tool to get the SQL definition"
|
|
134
|
+
" before computing any KPI.</hint>"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
lines.append("</available_metrics>")
|
|
138
|
+
return lines
|
|
139
|
+
|
|
140
|
+
def _render_semantic_source_fallback(self, contract: DataContract) -> list[str]:
|
|
141
|
+
src = contract.schema.semantic.source
|
|
142
|
+
assert src is not None
|
|
143
|
+
lines = [
|
|
144
|
+
"<semantic_source>",
|
|
145
|
+
f" <type>{src.type}</type>",
|
|
146
|
+
f" <path>{src.path}</path>",
|
|
147
|
+
" <hint>Consult this source for metric definitions"
|
|
148
|
+
" before computing metrics.</hint>",
|
|
149
|
+
"</semantic_source>",
|
|
150
|
+
]
|
|
151
|
+
return lines
|
|
152
|
+
|
|
153
|
+
def _render_relationships(
|
|
154
|
+
self, semantic_source: SemanticSource | None
|
|
155
|
+
) -> list[str]:
|
|
156
|
+
if semantic_source is None:
|
|
157
|
+
return []
|
|
158
|
+
rels = semantic_source.get_relationships()
|
|
159
|
+
if not rels:
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
lines = ["<table_relationships>"]
|
|
163
|
+
for r in rels:
|
|
164
|
+
lines.append(
|
|
165
|
+
f' <relationship type="{r.type}">'
|
|
166
|
+
f"<from>{r.from_}</from>"
|
|
167
|
+
f"<to>{r.to}</to>"
|
|
168
|
+
"</relationship>"
|
|
169
|
+
)
|
|
170
|
+
lines.append("</table_relationships>")
|
|
171
|
+
return lines
|
|
172
|
+
|
|
173
|
+
def _render_resource_limits(self, contract: DataContract) -> list[str]:
|
|
174
|
+
res = contract.schema.resources
|
|
175
|
+
temporal = contract.schema.temporal
|
|
176
|
+
|
|
177
|
+
has_resources = res is not None and any(
|
|
178
|
+
v is not None
|
|
179
|
+
for v in [
|
|
180
|
+
res.cost_limit_usd,
|
|
181
|
+
res.max_query_time_seconds,
|
|
182
|
+
res.max_retries,
|
|
183
|
+
res.max_rows_scanned,
|
|
184
|
+
res.token_budget,
|
|
185
|
+
]
|
|
186
|
+
)
|
|
187
|
+
has_temporal = (
|
|
188
|
+
temporal is not None and temporal.max_duration_seconds is not None
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if not has_resources and not has_temporal:
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
lines = ["<resource_limits>"]
|
|
195
|
+
if res is not None:
|
|
196
|
+
if res.cost_limit_usd is not None:
|
|
197
|
+
lines.append(
|
|
198
|
+
f" <cost_limit_usd>{res.cost_limit_usd:.2f}</cost_limit_usd>"
|
|
199
|
+
)
|
|
200
|
+
if res.max_query_time_seconds is not None:
|
|
201
|
+
val = res.max_query_time_seconds
|
|
202
|
+
lines.append(
|
|
203
|
+
f" <max_query_time_seconds>{val}</max_query_time_seconds>"
|
|
204
|
+
)
|
|
205
|
+
if res.max_retries is not None:
|
|
206
|
+
lines.append(f" <max_retries>{res.max_retries}</max_retries>")
|
|
207
|
+
if res.max_rows_scanned is not None:
|
|
208
|
+
lines.append(
|
|
209
|
+
f" <max_rows_scanned>{res.max_rows_scanned}</max_rows_scanned>"
|
|
210
|
+
)
|
|
211
|
+
if res.token_budget is not None:
|
|
212
|
+
lines.append(f" <token_budget>{res.token_budget}</token_budget>")
|
|
213
|
+
if has_temporal:
|
|
214
|
+
assert temporal is not None
|
|
215
|
+
dur = temporal.max_duration_seconds
|
|
216
|
+
lines.append(f" <max_duration_seconds>{dur}</max_duration_seconds>")
|
|
217
|
+
lines.append("</resource_limits>")
|
|
218
|
+
return lines
|
|
219
|
+
|
|
220
|
+
def _render_constraints(self, contract: DataContract) -> list[str]:
|
|
221
|
+
forbidden = contract.schema.semantic.forbidden_operations
|
|
222
|
+
block_rules = contract.block_rules()
|
|
223
|
+
warn_rules = contract.warn_rules()
|
|
224
|
+
if not forbidden and not block_rules and not warn_rules:
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
lines = ["<constraints>"]
|
|
228
|
+
|
|
229
|
+
# Forbidden operations
|
|
230
|
+
if forbidden:
|
|
231
|
+
ops = ", ".join(forbidden)
|
|
232
|
+
lines.append(f"Forbidden operations: {ops}")
|
|
233
|
+
|
|
234
|
+
# Block rules
|
|
235
|
+
if block_rules:
|
|
236
|
+
lines.append("")
|
|
237
|
+
lines.append("Rules (violations block execution):")
|
|
238
|
+
for rule in block_rules:
|
|
239
|
+
lines.append(f"- [{rule.name}] {rule.description}")
|
|
240
|
+
|
|
241
|
+
# Warn rules
|
|
242
|
+
if warn_rules:
|
|
243
|
+
lines.append("")
|
|
244
|
+
lines.append("Rules (violations produce warnings):")
|
|
245
|
+
for rule in warn_rules:
|
|
246
|
+
lines.append(f"- [{rule.name}] {rule.description}")
|
|
247
|
+
|
|
248
|
+
lines.append("</constraints>")
|
|
249
|
+
return lines
|