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.
Files changed (83) hide show
  1. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/CHANGELOG.md +14 -0
  2. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/PKG-INFO +51 -1
  3. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/README.md +50 -0
  4. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/pyproject.toml +1 -1
  5. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/__init__.py +4 -1
  6. agentic_data_contracts-0.3.0/src/agentic_data_contracts/core/contract.py +148 -0
  7. agentic_data_contracts-0.3.0/src/agentic_data_contracts/core/prompt.py +249 -0
  8. agentic_data_contracts-0.3.0/tests/test_core/test_prompt_renderers.py +332 -0
  9. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_scalability.py +7 -7
  10. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_system_prompt_metrics.py +4 -4
  11. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_public_api.py +4 -0
  12. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/test_relationships.py +1 -1
  13. agentic_data_contracts-0.2.6/src/agentic_data_contracts/core/contract.py +0 -279
  14. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/.github/dependabot.yml +0 -0
  15. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/.github/workflows/ci.yml +0 -0
  16. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/.gitignore +0 -0
  17. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/.pre-commit-config.yaml +0 -0
  18. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/.python-version +0 -0
  19. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/CLAUDE.md +0 -0
  20. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/LICENSE +0 -0
  21. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/docs/architecture.md +0 -0
  22. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/examples/revenue_agent/agent.py +0 -0
  23. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/examples/revenue_agent/contract.yml +0 -0
  24. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/examples/revenue_agent/semantic.yml +0 -0
  25. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/examples/revenue_agent/setup_db.py +0 -0
  26. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/adapters/__init__.py +0 -0
  27. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/adapters/base.py +0 -0
  28. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/adapters/duckdb.py +0 -0
  29. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/bridge/__init__.py +0 -0
  30. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/bridge/compiler.py +0 -0
  31. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/core/__init__.py +0 -0
  32. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/core/schema.py +0 -0
  33. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/core/session.py +0 -0
  34. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/py.typed +0 -0
  35. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/semantic/__init__.py +0 -0
  36. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/semantic/base.py +0 -0
  37. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/semantic/cube.py +0 -0
  38. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/semantic/dbt.py +0 -0
  39. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/semantic/yaml_source.py +0 -0
  40. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/tools/__init__.py +0 -0
  41. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/tools/factory.py +0 -0
  42. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/tools/middleware.py +0 -0
  43. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/tools/sdk.py +0 -0
  44. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/validation/__init__.py +0 -0
  45. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/validation/checkers.py +0 -0
  46. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/validation/explain.py +0 -0
  47. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/src/agentic_data_contracts/validation/validator.py +0 -0
  48. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/__init__.py +0 -0
  49. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/conftest.py +0 -0
  50. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/fixtures/minimal_contract.yml +0 -0
  51. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/fixtures/sample_cube_schema.yml +0 -0
  52. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/fixtures/sample_dbt_manifest.json +0 -0
  53. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/fixtures/semantic_source.yml +0 -0
  54. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/fixtures/valid_contract.yml +0 -0
  55. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_adapters/__init__.py +0 -0
  56. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_adapters/test_duckdb.py +0 -0
  57. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_bridge/__init__.py +0 -0
  58. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_bridge/test_compiler.py +0 -0
  59. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/__init__.py +0 -0
  60. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_contract.py +0 -0
  61. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_load_semantic_source.py +0 -0
  62. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_schema.py +0 -0
  63. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_sdk_config.py +0 -0
  64. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_session.py +0 -0
  65. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_core/test_wildcard_tables.py +0 -0
  66. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/__init__.py +0 -0
  67. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/test_cube.py +0 -0
  68. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/test_dbt.py +0 -0
  69. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/test_search.py +0 -0
  70. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_semantic/test_yaml_source.py +0 -0
  71. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/__init__.py +0 -0
  72. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_auto_load.py +0 -0
  73. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_factory.py +0 -0
  74. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_middleware.py +0 -0
  75. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_pagination.py +0 -0
  76. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_sdk.py +0 -0
  77. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_semantic_tools.py +0 -0
  78. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_tools/test_wildcard_tools.py +0 -0
  79. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_validation/__init__.py +0 -0
  80. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_validation/test_checkers.py +0 -0
  81. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_validation/test_explain.py +0 -0
  82. {agentic_data_contracts-0.2.6 → agentic_data_contracts-0.3.0}/tests/test_validation/test_validator.py +0 -0
  83. {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.2.6
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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentic-data-contracts"
3
- version = "0.2.6"
3
+ version = "0.3.0"
4
4
  description = "YAML-first data contract governance for AI agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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
- "create_tools",
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