Skip to content

Releases: flyersworder/agentic-data-contracts

v0.30.0 — verified-examples contract validation

Choose a tag to compare

@flyersworder flyersworder released this 19 Jul 20:17
30e6564

Verified-examples contract validation

validate_examples(...) re-validates an external corpus of question → SQL examples against a DataContract using the same two-layer Validator that gates live agent queries. The corpus stays entirely yours (repo, YAML, human-reviewed MR flow); the framework contributes exactly one verb — validate.

Highlights

  • Two uses of one call: an authoring-time MR gate (if not report.ok: sys.exit(1)) and a contract-change drift sweep (report.violations are what a schema/contract change just broke).
  • Each example gets one status — valid / violation / unverified / unchecked — plus contract_checked / engine_checked flags. report.ok is a safe gate: True only when every example is valid.
  • Decision B: for SQL an engine parses but sqlglot cannot (e.g. Denodo/VDP), the engine's own planner renders the verdict — unverified (plannable, policy not statically checked), never counted ok.
  • Never executes the SQL; confirms allowed, well-formed, plannable against the current schema, not result-correctness.
  • Runnable demo: examples/revenue_agent/verify_examples.py (live DuckDB EXPLAIN, incl. a schema-drift catch).

Compatibility — backward compatible: one new defaulted field (ValidationResult.parse_error), a net-new module, four new exports, no new dependencies.

See the CHANGELOG and the "Validating a verified-examples corpus" README section for details.

v0.29.0 — metric decomposition reconciliation check

Choose a tag to compare

@flyersworder flyersworder released this 19 Jul 14:52
b50a03a

Added

Metric decomposition reconciliation check. New reconcile_decomposition(...) (in agentic_data_contracts.validation) executes a metric's declared decompositions against a live database and asserts the arithmetic identity holds within tolerance — the reconciliation half of Spec B. It catches an identity that has gone false in the data (ETL drift, a child metric SQL that diverged, a join that skews a population) which the per-query validators (sqlglot, EXPLAIN) never see because the SQL is still authorized.

The check is keyed off the declared decomposition — the contract owns what the identity is (operator + operand names), while the caller supplies scalar SQL for the parent and each declared operand, owning how to measure each over its chosen slice; no metric executor is assumed. Malformed input raises ValueError; data conditions are findings (reconciles=False with a mechanical reason) — a NULL / empty / non-finite measurement, or a ratio zero denominator. ReconciliationResult.reason reports only the mechanical condition and never infers the cause — diagnosis stays agent-owned. Default rel_tol=1e-4 is tight because decompositions are exact identities. Intended primary home is CI: a hermetic per-PR regression guard plus a live-warehouse nightly drift detector.

Compatibility

Fully backward compatible — a net-new module plus two new exports (reconcile_decomposition, ReconciliationResult); nothing existing changes behavior, no new dependencies.

Full changelog: https://github.com/flyersworder/agentic-data-contracts/blob/main/CHANGELOG.md

v0.28.1 — dump omit-empty fix + example/doc coverage

Choose a tag to compare

@flyersworder flyersworder released this 19 Jul 12:37
b867af1

Patch release following v0.28.0 (metric identity decomposition).

Fixed

  • dump_semantic_source now omits empty decompositions / drill_by. 0.28.0 emitted both keys on every metric even when empty, which diverged from the tools layer's omit-when-empty convention and changed a frozen contract's contract_digest for a source that declares no decompositions — re-freezing a pre-0.28 contract produced a different content address than under 0.27.x. A leaf metric now dumps byte-identically to the pre-0.28 format, so contract_digest is stable across the upgrade for contracts that don't use the new fields. Metrics that do declare decompositions/drill_by are unaffected and still round-trip.

Docs

  • The revenue_agent and growth_agent examples now demonstrate the feature — revenue_agent a product identity (total_revenue = active_customers × revenue_per_customer) plus a region drill, growth_agent a ratio identity on conversion_rate that exercises a mixed identity + influence graph for trace_metric_impacts's kinds filter.
  • Completed decomposition coverage in docs/architecture.md (tool signature, lookup_metric fields, MetricDefinition field list).

Full suite green (724 tests); ruff / ruff format / ty clean.

v0.28.0 — metric identity decomposition + drill dimensions

Choose a tag to compare

@flyersworder flyersworder released this 19 Jul 12:19
34574b5

Added

  • Metric identity decomposition. A metric can declare decompositions — arithmetic identities describing how its value is exactly reconstructed from other metrics via sum, product, ratio, or difference (e.g. total_revenue = product(paying_customers, arpu)). Unlike the causal metric_impacts graph (evidential, non-exhaustive), an identity decomposition is exact and exhaustive, so an agent doing root-cause analysis can walk the arithmetic skeleton deterministically before reaching for speculative drivers. Validated loudly at load (on both file load and frozen-contract from_raw rehydration): unknown operator, wrong operand arity (ratio/difference binary; sum/product ≥2), unresolved operand, and any cycle — identity edges must form a DAG.
  • Dimensional drill hints via drill_by. A priority-ordered list of dimensional slice hints (dimension + schema.table.column) naming the exhaustive cuts (revenue GROUP BY region) that dominate weekly-review diagnosis. Columns are soft-validated (malformed shape raises; undeclared table is skipped).
  • trace_metric_impacts walks both edge kinds. Decomposition operands become IdentityEdges sharing the metric graph with MetricImpact (influence) edges; the tool tags each edge with its kind and takes a new kinds argument (all | identity | influence, default all) so an agent can walk the deterministic identity skeleton first, then the causal drivers. lookup_metric surfaces decompositions and drill_by directly.

Compatibility

Fully backward compatible — both fields are new, optional, and default empty; existing contracts and dbt/Cube-sourced metrics behave identically. Extraction from dbt/Cube, an execution-based reconciliation check, and a variance-diagnosis tool are deferred; today the fields are YamlSource-only.

Full suite green (722 tests); ruff / ruff format / ty clean.

v0.27.0 — portable contracts + ARD publish path

Choose a tag to compare

@flyersworder flyersworder released this 28 Jun 19:22

Portable, self-contained contracts + Agentic Resource Discovery (ARD) publish path

Added

  • DataContract.freeze_semantic_source() — snapshots a contract's semantic source inline (metrics, relationships, metric-impacts, table column-schemas) so a serialized contract enforces identically on any machine with no filesystem access to the original dbt/Cube/YAML source. Freezing clears the machine-specific path and normalizes type, so the content address is reproducible across machines and leaks no local paths. New YamlSource.from_raw()dump_semantic_source() inverse pair — frozen snapshots are source-type-agnostic (dbt/Cube normalize to the canonical YAML-source shape).
  • New agentic_data_contracts.ard modulebuild_catalog_entry() / build_ai_catalog() emit a spec-valid Agentic Resource Discovery ai-catalog.json entry for a contract-governed MCP server, with the frozen contract pinned as a digest-addressed data-contract attestation in the trust manifest. contract_canonical_bytes() / contract_digest() produce the content-addressable artifact a consumer independently recomputes — so the publish→verify loop closes with no trust in the publisher's assertion.

Breaking (fail-loud)

  • A declared-but-unavailable semantic source now raises SemanticSourceUnavailableError (a new top-level export, deliberately not a FileNotFoundError subclass) instead of a bare FileNotFoundError — covering missing file, a directory path, a permission error, malformed YAML, and malformed dbt JSON — so enforcement never silently degrades off the authoring box. Migration: code that caught FileNotFoundError around contract/tool construction should catch SemanticSourceUnavailableError.
  • The SemanticSource protocol gains get_table_schemas() (affects only third-party custom sources that are frozen). SemanticSource.path is now optional, gated by a validator requiring path-or-inline.
  • Existing contracts and the common path (source present, or letting errors propagate) are unaffected.

Built TDD red-first and hardened by two independent code-review passes. 688 tests green; ruff + ty clean. Full details in CHANGELOG.md.

v0.26.0 — metric-first domain membership

Choose a tag to compare

@flyersworder flyersworder released this 27 Jun 15:53

Metric-first domain membership

Domain↔metric membership is now declared in one place: each metric self-declares its domains (domains: [...], read from meta.domains by the yaml/dbt/cube adapters). The contract's Domain carries catalog metadata only (summary, description, owners, review cadence) and no longer lists its metrics — the old Domain.metrics field and the union shim that reconciled it are gone. The catalog is authoritative for which domains exist; list_metrics(domain=), lookup_domain, and the system-prompt domain index all agree on that universe.

⚠️ Breaking changes

  • Domain.metrics removed; Domain now sets extra="forbid". A pre-0.26 contract that still lists metrics: under a domain raises a ValidationError at load time (rather than silently dropping it and leaving the domain empty). Migration is mechanical: delete those metrics: lines — each metric already declares its domain in the semantic source.
  • lookup_domain membership is reverse-looked-up from metric.domains, so it returns every metric that declares the domain. With no semantic source configured it returns an empty member list.
  • The [pydantic-ai] extra now requires pydantic-ai-slim[anthropic]>=2.0.0 (was >=1.107.0); the adapter is verified against 2.x.

Other

  • Restored a metric-first membership-validation warning (a metric declaring an uncataloged domain now warns at startup).
  • Per-domain metric_count tallied via a dedupe-safe domain_metric_counts helper (O(metrics), not O(domains×metrics)).
  • Dependencies refreshed (uv lock --upgrade); pre-commit hooks bumped (prek autoupdate).
  • Hardened across two code-review passes. 663 tests; ruff/format/ty clean.

See CHANGELOG.md for full detail.

v0.25.0 — dual-role ownership + per-metric freshness

Choose a tag to compare

@flyersworder flyersworder released this 27 Jun 13:36

Governance metadata mined from Lyft's Metric Semantic Layer article (lessons #1 dual-role ownership, #3 freshness surfacing).

Added

  • Dual-role ownership — optional business_owner / operational_owner (teams, not individuals) on MetricDefinition and Domain. Business owner owns the definition + review cadence; operational owner owns data health.
  • Per-metric last_reviewed + metric stalenessfind_stale_reviews() / DataContract.find_stale() now audit metrics as a third artefact kind (domain / metric / metric_impact), with owners carried in finding context so the report says who to nag.
  • Owners + freshness in the agent-facing toolslookup_metric / lookup_domain surface owners + last_reviewed + a stale flag; list_metrics carries a lean stale flag. create_tools(..., staleness_threshold_days=90).
  • Two audiences, two policiesfind_stale() is the strict governance/CI audit (missing last_reviewed = stale); the agent-facing tools are lenient (emit stale/last_reviewed only when set).

Compatibility

  • Additive API (all new fields optional; new params keyword-only). Behavior change: DataContract.find_stale() now also flags un-reviewed metrics — grandfather with f.age_days is not None or back-fill last_reviewed. dbt/Cube sources leave the new fields unset for now.

Full notes in CHANGELOG.md.

v0.24.0 — deps-aware Pydantic AI toolset

Choose a tag to compare

@flyersworder flyersworder released this 20 Jun 13:55

Deps-aware Pydantic AI toolset — one shared Agent for many users

create_pydantic_ai_toolset(contract, ...) returns a Pydantic AI ToolsetFunc you register on a single shared agent via agent.toolset(per_run_step=False)(...). On each run it reads a per-user ContractDeps (session + caller_principal) from RunContext.deps and rebuilds the contract's tools bound to that user — so you build the Agent once and each user is just a message_history + a small ContractDeps, instead of a separate per-user tools list.

  • caller_principal passthrough added to create_pydantic_ai_tools, so per-principal table/rule gating applies in the baked-in path too.
  • Enforcement unchanged: validation block → ModelRetry; session-budget breach → terminal ContractSessionLimitError.
  • Built on Pydantic AI's dynamic-toolset mechanism (not a hand-rolled AbstractToolset). Purely additive — base installs and existing adapters are unaffected.
  • End-to-end isolation proven through agent.run() on one shared agent (user A exhausting their budget doesn't affect user B).

CI / security

uv lock --upgrade clears 3 transitive advisories (langchain → 1.3.10, langsmith → 0.8.18, pydantic-settings → 2.14.2); uv-secure reports no vulnerabilities. Full suite 635 green; ruff + ty clean.

See CHANGELOG.md for full details.

v0.21.1 (data-plugin skills overlay + dependency refresh)

Choose a tag to compare

@flyersworder flyersworder released this 30 May 15:24

Added

  • Reference template for layering Anthropic's data knowledge-work plugin on top of contract-governed tools. All three example agents (revenue_agent, growth_agent, ops_agent) carry an opt-in overlay (off by default, enabled via DATA_PLUGIN_PATH). The agent gains the plugin's analyst skills (validate-data, statistical-analysis, explore-data, sql-queries) while every query stays contract-enforced. growth_agent/agent.py is the canonical template; README documents the pattern.
  • Security guard: strict_mcp_config=True restricts the session to the governed in-process server only, so the plugin's bundled .mcp.json warehouse servers stay inert and the agent has no ungoverned path around the contract. Skill list deliberately omits data-context-extractor and viz/dashboard skills.

Compatibility

  • No library API change — touches only examples/, docs, lockfile, and tooling. Patch bump.
  • Opt-in and degrades gracefully — feature-detects plugins/skills/strict_mcp_config on ClaudeAgentOptions, so examples run unchanged on older SDKs and with zero setup.

Internal

  • uv lock --upgrade: claude-agent-sdk 0.1.81 → 0.2.87 (overlay API verified intact across the jump), plus langchain 1.3.0→1.3.2, langgraph 1.2.0→1.2.2, duckdb 1.5.2→1.5.3, snowflake-connector-python 4.5.0→4.6.0, mcp 1.27.1→1.27.2, ruff 0.15.13→0.15.15, and others. Full 602-test suite + ruff + ty green.
  • prek autoupdate: ruff-pre-commit v0.15.13 → v0.15.15.

v0.21.0 — describe_table emits column descriptions

Choose a tag to compare

@flyersworder flyersworder released this 17 May 18:06

Fixed

describe_table now emits column descriptions to the agent. Since the tool factory's first commit, the tool serialised columns as {name, type, nullable} only — Column.description was silently dropped on the way out, even when populated by the adapter (e.g., a Denodo deployment carrying authored catalog comments) or available in the contract's semantic source. This is the single largest context improvement a data-contract library can make: per the Datacult "boring work" benchmark, adding column descriptions moved an agent's SQL accuracy from 0% to 15% and SQL generation from 38.5% to 100% — the largest jump in their six-layer experiment.

The fix overlays descriptions onto the tool response with this precedence:

  1. Semantic source via SemanticSource.get_table_schema(schema, table) — the canonical agent-facing authority
  2. Column.description from the adapter — captures warehouse catalog comments
  3. Field omitted entirely when both are empty, keeping responses tight

The SemanticSource.get_table_schema protocol method is no longer dead code from the tool layer's perspective. All three built-in semantic sources (YamlSource, DbtSource, CubeSource) already populated TableSchema.columns[*].description from their respective inputs; the tool just never consulted them. Now it does.

Added

  • 3 new tests in tests/test_tools/test_factory.py pinning the merge behaviour: semantic-source descriptions reach the agent, adapter-supplied descriptions surface when the semantic source has no entry (with the field omitted when both are empty), and semantic source wins when both have descriptions for the same column.

Compatibility

  • Backward-compatible response shape. The new `description` field is additive only — consumers that ignore unknown keys see no behaviour change. The field is omitted (not set to `""`) when no description exists, so JSON payload size is unchanged for description-less columns.
  • No new failure modes. The merge guards `semantic_source is None`, `get_table_schema(...)` returning `None`, columns appearing in one source but not the other, and empty-string descriptions. A column described in the semantic source but absent from the warehouse is silently dropped — the adapter's column list is the source of truth for which columns exist; the semantic source only adorns them.
  • No new dependencies. The fix uses interfaces that already existed in the codebase.

Internal

  • `uv lock --upgrade` refreshed transitive dependencies (notable bumps: `sqlglot 30.6.0 → 30.8.0`, `langchain 1.2.17 → 1.3.0`, `langgraph 1.1.10 → 1.2.0`, `pydantic 2.13.3 → 2.13.4`, `cryptography 47.0.0 → 48.0.0`). Full 602-test suite + ruff + ty all green against the new versions.
  • `.pre-commit-config.yaml`: `ruff-pre-commit` rev bumped to `v0.15.13` to match the lockfile-pinned `ruff` binary.