Skip to content

feat(sdk): add wren-pydantic package for Pydantic AI integration#2255

Merged
goldmedal merged 23 commits into
mainfrom
feat/add-pydantic-sdk
May 13, 2026
Merged

feat(sdk): add wren-pydantic package for Pydantic AI integration#2255
goldmedal merged 23 commits into
mainfrom
feat/add-pydantic-sdk

Conversation

@PaulChen79
Copy link
Copy Markdown
Contributor

@PaulChen79 PaulChen79 commented May 11, 2026

Summary

  • New sdk/wren-pydantic package — Pydantic AI integration for Wren AI Core, sibling to sdk/wren-langchain
  • 6 LLM-facing tools (3 runtime + 3 memory) via FunctionToolset
  • WrenError → ModelRetry mapping with phase-aware messages, recursive secret redaction, 4KB cap
  • Typed Pydantic return models pinned to Core's MemoryStore shapes (no envelope dicts)
  • Full CI / publish / release-please / rc-release wiring mirroring wren-langchain

What's included

Surface Detail
WrenToolkit.from_project(path, profile=) Same signature as langchain — eager validates wren_project.yml + target/mdl.json, loads .env, resolves memory provider via .wren/memory/, 3-tier profile fallback
toolkit.toolset(*, include_memory_write=True, takes_ctx=False) Returns FunctionToolset; auto-filters memory tools when disabled; takes_ctx=True for mixing with deps_type= tools
toolkit.instructions(*, toolset=None) Builds an instructions string from same content as langchain system_prompt()
toolkit.memory.fetch / recall / store Direct Python API
toolkit.query / dry_plan / dry_run Sync direct API — no async wrappers (rationale below)

Design decisions (locked during planning)

  • Sync only — Pydantic AI auto-bridges sync tools to async; underlying engine is sync I/O so wrapping in asyncio.to_thread would be fake-async with no concurrency benefit. Aligns with wren-langchain.
  • ModelRetry over envelope — framework-idiomatic; phase-aware messages give LLM enough context to self-correct (SQL_PARSING, METADATA_FETCHING, SQL_EXECUTION with dialect-SQL excerpt, etc.)
  • retries=0 on wren_store_query — write failures don't loop; LLM has already done the analytical work
  • retries=2 on read tools — gives the LLM two chances to fix SQL or model-name errors
  • Pydantic return models pinned to Core's shapesFetchContextResult uses {strategy: full|search, schema/results}, RecalledPair uses Core's nl_query/sql_query/tags-as-comma-string/_distance. Drift in Core surfaces here as test failures.
  • No async direct API — defer until Core ships async-native engine

See scoping doc and implementation plan (untracked — local only).

Commit structure

21 commits in chronological order:

  1. feat(sdk): scaffold wren-pydantic package skeleton
  2. feat(pydantic): add exceptions module
  3. feat(pydantic): copy provider modules from wren-langchain
  4. feat(pydantic): add Pydantic return models for tool outputs
  5. fix(pydantic): align return models with Core's actual MemoryStore shapes (caught by code-review; lifted return-type schemas had wrong field names/literals — fixed before downstream code depended on them)
  6. feat(pydantic): WrenError → ModelRetry mapping
  7. feat(pydantic): WrenToolkit class — from_project + sync direct API
  8. feat(pydantic): runtime tools (query, dry_plan, list_models)
  9. feat(pydantic): WrenToolkit.toolset() facade
  10. feat(pydantic): _MemoryAPI subscope (sync only)
  11. feat(pydantic): memory tools (fetch_context, recall_queries, store_query)
  12. feat(pydantic): wire memory tools into WrenToolkit.toolset()
  13. feat(pydantic): instructions() prompt builder
  14. style(pydantic): clean up _instructions.py — unused import + docstring
  15. test(pydantic): Pydantic AI conformance suite
  16. feat(pydantic): example scripts
  17. docs(pydantic): README + docs/core/sdk/pydantic.md
  18. chore(ci): add wren-pydantic CI workflow
  19. feat(ci): publish workflow for wren-pydantic
  20. chore(release): wire wren-pydantic into release-please + rc-release
  21. style(pydantic): silence PLC0415 for legitimate local imports in tests

Test plan

  • 109 tests passing locally (84 unit + 9 conformance + 16 lifted from wren-langchain)
  • ruff check . + ruff format --check . clean
  • pip install -e . works against wren-engine>=0.5.0 from local + PyPI
  • Both example scripts (pydantic_ai_demo.py, pydantic_ai_structured_demo.py) tested with real OpenAI agent against DuckDB + Postgres projects
  • 7 additional scenario scripts in .tmp/pydantic-test-scenarios/ (untracked) exercise output_type, custom prompts, read-only memory, takes_ctx + deps_type, message history, and multi-project federation — all run successfully against /tmp/wren-federate-demo/proj_loans and proj_events
  • Reviewer: skim docs/core/sdk/pydantic.md against an agent dev's mental model
  • Reviewer: compare API parity matrix vs wren-langchain (covered in scoping doc §3)

Out of scope (deliberate, deferred to v0.2)

  • Async direct API (aquery / afetch etc.) — gated on Core supporting async-native engine
  • Async tool functions — Pydantic AI auto-bridges sync; no need
  • Multi-toolkit-per-agent helper — one toolkit per project is the right shape
  • MCPServer integration — separate workstream
  • Shared wren-sdk-core package extraction between langchain + pydantic — premature; revisit when drift becomes painful

Compatibility

wren-pydantic wren-engine pydantic-ai
0.1.x >= 0.5.0 >= 1.0, < 2.0

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added wren-pydantic SDK with WrenToolkit exposing LLM tools for query, dry-plan, and model discovery.
    • Optional project-backed memory: fetch, recall, and (opt-in) store capabilities.
    • Included runnable examples demonstrating quickstart and structured output.
  • Documentation

    • Comprehensive SDK docs, README, and changelog with quickstarts and troubleshooting.
  • Tests

    • Extensive unit and conformance tests covering toolkit, tools, memory, providers, models, and error mappings.
  • Chores

    • CI and publish workflows plus release configuration and initial package metadata/license.

Review Change Stack

PaulChen79 added 21 commits May 11, 2026 13:36
Empty package layout with pyproject.toml (deps: wren-engine, pydantic-ai
>=1.0,<2.0, pydantic>=2), LICENSE, CHANGELOG, README placeholder, and
a minimal __init__.py exporting __version__. Mirrors wren-langchain
folder structure and datasource extras.

Smoke test verifies `import wren_pydantic` works and version is 0.1.0.
Lifted verbatim from wren-langchain — WrenToolkitInitError and
MemoryNotEnabledError. Same semantics: init error for missing
prerequisites, memory-not-enabled for direct API access on a project
without .wren/memory/.

Tests cover both as distinct exception types with message
preservation. Ships before providers because providers import these.
Lift _providers/{__init__.py, mdl_source.py, connection.py, memory.py}
verbatim from sibling wren-langchain. Only delta: module-rename
import paths (wren_langchain → wren_pydantic).

Provider semantics are framework-agnostic — they wrap wren-engine's
profile resolution, MDL loader, and memory store. No async wrappers,
no Pydantic AI specifics here.

11 lifted tests pass (4 connection + 4 mdl + 3 memory).
Define WrenQueryResult, ModelSummary, FetchContextResult, RecalledPair
in _models.py. These are the strongly typed alternatives to wren-langchain's
hand-rolled envelope dict — Pydantic AI consumes their JSON schema to expose
typed tool outputs to the model.

Constraints:
- WrenQueryResult.row_count: ge=0, truncated flag distinguishes full vs
  capped results
- ModelSummary.description: optional (projects without MDL descriptions
  still load)
- FetchContextResult.strategy: Literal["search", "full_schema"] matches
  Core's behavior; items stays heterogeneous (tighten in v0.2)
- RecalledPair.score: optional (seeded pairs from queries.yml have no score)

6 unit tests cover round-trip, validation rejection paths, default
factories, and strategy enum enforcement.
Previous commit's schemas were guessed and didn't match what
core/wren/src/wren/memory/store.py actually returns. A later tool wiring
would have hit ValidationError on every real call. Fix now while only
unit tests are affected.

Concrete changes:
- FetchContextResult.strategy: Literal["full", "search"] (not "full_schema")
- FetchContextResult.schema_text aliased to "schema" (full path)
  + FetchContextResult.results for search path — discriminated by strategy
- RecalledPair: nl_query / sql_query (not nl / sql), tags is comma-joined
  str (not list), score aliased to LanceDB's _distance field. extra="ignore"
  so Core can add fields without breaking us.
- WrenQueryResult: add model_validator enforcing row_count == len(rows);
  truncated is the channel for "underlying set larger than payload".

Tests updated to pin Core's verbatim shapes via model_validate({...})
so future Core drift surfaces here as a test failure.
_errors.py:
- should_propagate(exc) classifies infra ErrorCodes (GET_CONNECTION_ERROR,
  INVALID_CONNECTION_INFO, DUCKDB_FILE_NOT_FOUND, ATTACH_DUCKDB_ERROR,
  GENERIC_INTERNAL_ERROR, NOT_IMPLEMENTED) as propagate-class. Tool
  wrappers use this to decide between `raise` and `raise to_model_retry(...)`.
- to_model_retry(exc) builds a ModelRetry with phase-aware framing:
  SQL_PARSING ("fix syntax"), SQL_EXECUTION (includes 200-char dialect
  SQL excerpt), METADATA_FETCHING ("verify model with wren_list_models"),
  etc. — eight phases get bespoke guidance, others fall back to generic.
- redact_secrets walks nested dicts/lists recursively, replacing values
  whose keys contain password / secret / token / credential.
- METADATA_CAP_BYTES = 4 * 1024. Hard byte-aware cap on the final
  message body; truncation marker appended on overflow.

15 unit tests cover propagate vs retry classification (parametrized
across 8 codes), per-phase message framing, dialect SQL truncation,
recursive secret redaction, and the 4KB cap.

Locks the retry/propagate decision matrix from plan §3 Commit 1.4 —
adjusted for Core's actual ErrorCode/ErrorPhase enums (plan listed
MEMORY_STORE_FAILURE which doesn't exist in Core; store retries are
handled by setting retries=0 on the tool, not by error classification).
_toolkit.py: WrenToolkit class lifted from wren-langchain with two
deltas:
- Drop langchain-specific imports (_memory_api, _prompt, _tools,
  _tools_memory). Memory subscope and toolset() facade land in later
  phases.
- No async direct API (aquery / adry_plan / adry_run). Plan §3 Commit
  2.1: Pydantic AI auto-bridges sync tools to its async run loop, so
  wrapping pure-sync engine I/O in asyncio.to_thread is fake-async with
  no real concurrency benefit. Add when Core ships an async-native
  engine.

Identical structure to langchain counterpart:
- from_project: eager validate wren_project.yml + target/mdl.json,
  load <project>/.env, resolve memory provider via .wren/memory/
  detection, 3-tier profile fallback
- Direct API: query, dry_plan, dry_run delegating to a per-call
  WrenEngine with read-through manifest
- Connector cache at toolkit level so DB auth happens once per
  toolkit lifetime

12 tests lifted from langchain (7 init + 5 runtime). Re-exports
WrenToolkit / WrenToolkitInitError / MemoryNotEnabledError from the
package root.
_tools.py: build_runtime_toolset() returns a FunctionToolset with the
3 runtime tools registered against a toolkit instance.

Design points:
- Sync tools (def, not async def) — Pydantic AI auto-bridges to its
  async run loop; wrapping sync engine I/O in asyncio.to_thread adds
  nothing.
- Typed returns: wren_query → WrenQueryResult, wren_dry_plan → str,
  wren_list_models → list[ModelSummary]. Pydantic AI serializes these
  for the LLM with field validation.
- retries=2 per tool — gives the LLM two chances to self-correct on
  SQL / metadata errors.
- takes_ctx switch (default False): inject ctx: RunContext as first
  arg for users mixing wren tools with their own deps-typed tools.
- WrenError handling: retry-class → to_model_retry(); propagate-class
  (infra errors) re-raises out of the agent loop.
- MAX_QUERY_ROWS = 1000 hard cap on the LLM-facing limit param; direct
  API stays unbounded.

13 tests cover registration, typed returns, limit clamping, WrenError
→ ModelRetry, infra-error propagation, truncated flag, and the
takes_ctx variant.
Adds toolset() method on WrenToolkit that returns a Pydantic AI
FunctionToolset with the 3 runtime tools registered. Memory tool
wiring lands in Phase 3.

`takes_ctx` kwarg controls whether tools expose `ctx: RunContext` as
their first parameter — opt-in for users who want to mix wren tools
with their own `deps_type=`-typed tools in the same agent. Default is
False (cleanest signatures).

Each call builds a fresh FunctionToolset so multiple toolsets can
coexist on the same toolkit with different takes_ctx settings.

4 unit tests cover tool count, signature shape, and instance freshness.
Lifted _memory_api.py from wren-langchain — sync fetch / recall / store
operations against the toolkit's cached MemoryStore. Comma-in-tags
rejection logic carries over (commas separate tags in Core's storage
format; a tag like "revenue, Q1" would silently corrupt the round-trip).

Wired toolkit.memory as a lazy property (the MemoryStore is heavy —
loads a sentence-transformer model — so we only construct it on first
access). Direct API raises MemoryNotEnabledError when memory is
disabled; tool wrappers will handle this case by filtering instead.

6 tests lifted from wren-langchain (no async parallels — see plan
§3 Commit 3.1 for the sync-only rationale).
…ery)

_tools_memory.py: build_memory_toolset() registers up to 3 memory
tools onto an existing FunctionToolset (so toolset() can compose
runtime + memory cleanly).

Tool returns are typed via the Pydantic models from _models.py:
- wren_fetch_context → FetchContextResult (full or search payload)
- wren_recall_queries → list[RecalledPair]
- wren_store_query → str (success message)

wren_store_query is registered with retries=0 — write failures usually
aren't fixable by retrying the same call, and the LLM has already done
the analytical work.

include_write=False drops wren_store_query from the toolset while
keeping the two read-only tools. takes_ctx switch mirrors the runtime
tools.

11 unit tests cover registration, typed returns for both fetch
strategies, WrenError mapping, retries=0 contract, None-tags
normalization, and takes_ctx variant.
toolset() now composes runtime + memory tools into one FunctionToolset:
- Memory tools are added when toolkit._memory.enabled (project has
  .wren/memory/ directory)
- include_memory_write=False drops wren_store_query, keeps read-only
  memory tools
- Memory disabled: include_memory_write has no effect (no memory tools
  registered regardless)

Test coverage expanded: now 7 toolset facade tests covering all four
states (memory-enabled, memory-disabled × include_memory_write True/False).

Total unit tests: 88 across the package, all green.
_instructions.py: lifted _prompt.py from wren-langchain with two
adaptations for Pydantic AI:
- Top-level fn renamed build_system_prompt → build_instructions to
  match Pydantic AI's Agent(instructions=...) parameter.
- tools= Iterable replaced with toolset= FunctionToolset; new
  _extract_tool_list() pulls tools from .tools (dict) or ._tools
  attributes. Same content composition as langchain (workflow blob /
  available tools / error recovery / things to avoid / project
  instructions.md).

WrenToolkit.instructions() method delegates to build_instructions().

The "strong defaults" content from wren-langchain is preserved
verbatim — "recall by default" / "store by default" framing learned
empirically that GPT-4o reads soft phrasing ("when useful") as "skip".

11 unit tests lifted from langchain's test_prompt.py, parametrized
on toolset= instead of tools= list.
Remove unused `from typing import Iterable` left over from the lift
(the function signature now takes `toolset: object`, not an iterable).
Update module docstring to say "Pydantic AI agents" instead of
"LangChain/LangGraph agents". Re-format per ruff.
tests/conformance/test_pydantic_ai_contract.py covers:
- Tool registration shape: every tool has a non-empty name + description,
  toolset shape adapts to memory enabled/disabled
- End-to-end via TestModel: agent.run_sync against the toolkit's toolset
  exercises the tool-call pipeline without real LLM cost
- ModelRetry flow: WrenError → ModelRetry → agent retry loop runs through
  without crashing
- 3-line quickstart shape: Agent(model, instructions=..., toolsets=...)
  constructs cleanly

9 conformance tests. Total package suite now 108 tests, all green.
Two runnable demos:
- pydantic_ai_demo.py — 3-line sync quickstart with openai:gpt-4o
- pydantic_ai_structured_demo.py — same skeleton but with
  output_type=TopCustomers showing Pydantic AI's structured-output
  feature (framework validates model response into typed instance).

Both pick up project path via PROJECT_PATH env var, default ./analytics_db.
Package README mirrors wren-langchain README structure: 3-line quickstart,
caution callout for CLI prereq, datasource extras matrix, "what you get"
listing 6 tools + direct API, configuration knobs, compatibility matrix,
known limitations.

docs/core/sdk/pydantic.md is the project-level guide: front-loaded caution
+ install-guide link, prerequisites, installation, quickstart, full API
reference, four integration patterns (structured output, read-only memory,
takes_ctx for deps mixing, multi-project federation), troubleshooting
table.

Key delta from langchain doc: pydantic.md features `output_type=`
prominently as a pattern (Pydantic AI's killer feature), and explicitly
documents the sync-only Direct API decision so readers don't go looking
for aquery / afetch.
Mirrors sdk-langchain-ci.yml: lint (ruff check + format --check), test
matrix (py3.11 / py3.12), build (sdist + wheel with LICENSE
verification). Triggers on PR or push to main touching the package
or this workflow file.
Mirrors publish-wren-langchain.yml: workflow_call only, validate-inputs
job (rejects pypi_target typos), build job with version regex check,
publish job with id-token: write for Trusted Publishing.
- release-please-config.json + manifest: add sdk/wren-pydantic package
  entry (component=wren-pydantic, release-type=python, bumps __init__.py)
- release-please.yml: 3 new outputs + publish-wren-pydantic job that
  fires when wren-pydantic--release_created is true
- rc-release.yml: add wren-pydantic to component choice, add
  publish-wren-pydantic job gated by inputs.component, extend
  create-release needs

Mirrors the wren-langchain rc-release wiring done in PR #2249.
Lifted tests used local imports for fixture scoping; add # noqa: PLC0415
to match wren-langchain test conventions. Pure cosmetic — same code path,
just lint-clean for the CI lane that runs ruff on the whole tree.
@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file ci labels May 11, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b3634813-7d37-4dc8-bb32-e670ff80b013

📥 Commits

Reviewing files that changed from the base of the PR and between cd87897 and b520f65.

📒 Files selected for processing (2)
  • .github/workflows/sdk-pydantic-ci.yml
  • sdk/wren-pydantic/src/wren_pydantic/_toolkit.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/sdk-pydantic-ci.yml
  • sdk/wren-pydantic/src/wren_pydantic/_toolkit.py

Walkthrough

This PR adds the complete wren-pydantic SDK package, enabling Pydantic AI agents to query Wren AI Core projects via a toolkit facade. The change spans release infrastructure (CI/workflows), comprehensive documentation, core SDK implementation (toolkit, providers, tools), memory integration, and a full test suite.

Changes

Release and CI Infrastructure

Layer / File(s) Summary
PyPI Publish Workflow
.github/workflows/publish-wren-pydantic.yml
Validates version format (X.Y.Z or X.Y.ZrcN), builds sdist/wheel, and publishes to PyPI or TestPyPI based on input parameter.
Release Orchestration
.github/workflows/rc-release.yml, .github/workflows/release-please.yml
Extended workflows to add wren-pydantic component option, gate pre-release on successful publish, and expose release outputs.
CI Pipeline and Versioning
.github/workflows/sdk-pydantic-ci.yml, .release-please-manifest.json, release-please-config.json
Dedicated SDK CI for lint/test/build; registered component in version manifest and configured for Release Please Python tagging.

SDK Documentation and Configuration

Layer / File(s) Summary
User-Facing Documentation
docs/core/sdk/pydantic.md, sdk/wren-pydantic/README.md
Comprehensive guides covering prerequisites, installation, quickstart, API reference, integration patterns, troubleshooting, and compatibility.
Example Scripts
sdk/wren-pydantic/examples/pydantic_ai_demo.py, sdk/wren-pydantic/examples/pydantic_ai_structured_demo.py
Demonstrations of basic agent usage and structured output integration.
Project Configuration
sdk/wren-pydantic/pyproject.toml, .gitignore, CHANGELOG.md, LICENSE
Build metadata, datasource/memory extras, linting/test config, and licensing.

Core SDK Data Models and Exceptions

Layer / File(s) Summary
Public API Contracts
sdk/wren-pydantic/src/wren_pydantic/__init__.py, sdk/wren-pydantic/src/wren_pydantic/exceptions.py, sdk/wren-pydantic/src/wren_pydantic/_models.py
Defined WrenToolkitInitError and MemoryNotEnabledError; created Pydantic models (WrenQueryResult, ModelSummary, FetchContextResult, RecalledPair) with validation rules; exposed __version__ and public exports.

Project Configuration Providers

Layer / File(s) Summary
Providers
sdk/wren-pydantic/src/wren_pydantic/_providers/connection.py, sdk/wren-pydantic/src/wren_pydantic/_providers/mdl_source.py, sdk/wren-pydantic/src/wren_pydantic/_providers/memory.py
Implemented profile resolution (explicit → project config → active profile), manifest loading from target/mdl.json, and memory provider abstraction (local LanceDB vs. noop).

Memory Operations

Layer / File(s) Summary
Direct Memory API
sdk/wren-pydantic/src/wren_pydantic/_memory_api.py
Toolkit-bound _MemoryAPI exposing fetch(), recall(), and store() with tag validation and lazy store initialization.

WrenToolkit Main Facade

Layer / File(s) Summary
Toolkit Facade
sdk/wren-pydantic/src/wren_pydantic/_toolkit.py
Coordinates project initialization, provider setup, and exposes toolset(), instructions(), direct query/plan/run API, and lazy memory property.

LLM-Facing Tools and Prompts

Layer / File(s) Summary
Error Mapping
sdk/wren-pydantic/src/wren_pydantic/_errors.py
Classifies WrenError into propagate vs. retry; generates phase-aware retry messages with secret redaction, dialect SQL excerpts, and byte capping.
Runtime Tools
sdk/wren-pydantic/src/wren_pydantic/_tools.py
Implements wren_query (row cap, limit validation), wren_dry_plan, and wren_list_models with typed results and error-to-retry conversion.
Memory Tools
sdk/wren-pydantic/src/wren_pydantic/_tools_memory.py
Registers wren_fetch_context, wren_recall_queries, and wren_store_query tools with typed results, error handling, and retries=0 for writes.
Instruction Generation
sdk/wren-pydantic/src/wren_pydantic/_instructions.py
Dynamic prompt builder generating Wren-aware workflows with conditional steps based on tool availability, error recovery, and optional project-specific instructions.

Comprehensive Test Suite

Layer / File(s) Summary
Test Infrastructure
sdk/wren-pydantic/tests/conftest.py, sdk/wren-pydantic/tests/test_smoke.py
Fixtures for temporary projects and profile mocking; smoke test for import and version verification.
Data Contract Tests
sdk/wren-pydantic/tests/unit/test_models.py, sdk/wren-pydantic/tests/unit/test_exceptions.py
Validation tests for Pydantic models, round-trip serialization, and exception types.
Provider Tests
sdk/wren-pydantic/tests/unit/test_providers_*.py
Tests for profile resolution precedence, manifest loading, and memory detection.
Toolkit Tests
sdk/wren-pydantic/tests/unit/test_toolkit_*.py
Tests for initialization, runtime query/plan/run API, manifest re-reading, and connector caching.
Tools and Error Tests
sdk/wren-pydantic/tests/unit/test_errors.py, sdk/wren-pydantic/tests/unit/test_tools_*.py
Tests for error classification, query/plan/list-models validation, and memory tool behavior.
Instructions and Toolset Tests
sdk/wren-pydantic/tests/unit/test_instructions.py, sdk/wren-pydantic/tests/unit/test_toolset_facade.py
Tests for instruction generation variations and toolset assembly with takes_ctx and memory options.
Conformance Tests
sdk/wren-pydantic/tests/conformance/test_pydantic_ai_contract.py
End-to-end Pydantic AI integration tests covering tool execution, error/retry flows, and instruction integration.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Canner/WrenAI#2247: Parallel SDK addition (wren-langchain) with similar structure and components.
  • Canner/WrenAI#2249: Similar RC release workflow modifications for another SDK component.
  • Canner/WrenAI#2232: Related release/publish workflow changes and permissions adjustments.

Suggested reviewers

  • goldmedal

🐰 Hop along the pydantic path so bright,
A toolkit born to make agents' queries light!
With tools for fetch, recall, and store,
The toolkit opens knowledge's door.
Memory and prompts dance in delight.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-pydantic-sdk

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
sdk/wren-pydantic/examples/pydantic_ai_demo.py (1)

25-25: 💤 Low value

Consider aligning default PROJECT_PATH with the docstring.

The default path ./analytics_db suggests a subdirectory, but the docstring (lines 4-7) implies running from the project root (where you'd run wren context init). Using "./" as the default would align better with the documented workflow and avoid confusion.

Optional alignment
-    project_path = Path(os.environ.get("PROJECT_PATH", "./analytics_db")).expanduser()
+    project_path = Path(os.environ.get("PROJECT_PATH", "./")).expanduser()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/wren-pydantic/examples/pydantic_ai_demo.py` at line 25, The default
PROJECT_PATH value is misaligned with the docstring; update how project_path is
derived (the project_path variable) to use "./" as the default instead of
"./analytics_db" so the example assumes running from the project root (where
wren context init is run); keep using Path(...).expanduser() and read from
os.environ.get("PROJECT_PATH", "./") so users can still override via env.
sdk/wren-pydantic/tests/unit/test_errors.py (1)

152-152: ⚡ Quick win

Verify the +1024 buffer is intentional and document the tolerance.

The test asserts the message size is under METADATA_CAP_BYTES + 1024, which adds a 25% buffer (1KB on top of 4KB). This could allow messages significantly larger than the declared cap. If the cap is meant to be strict, consider tightening the tolerance. If the buffer accounts for message scaffolding overhead, document why 1KB is the correct allowance.

💡 Suggested clarification
-    assert len(str(retry).encode("utf-8")) <= METADATA_CAP_BYTES + 1024
+    # Allow overhead for message scaffolding (phase label, error code, etc.)
+    # beyond the metadata cap itself.
+    assert len(str(retry).encode("utf-8")) <= METADATA_CAP_BYTES + 1024

Or if the cap should be strict:

-    assert len(str(retry).encode("utf-8")) <= METADATA_CAP_BYTES + 1024
+    # Strict cap: message body should not exceed the declared limit by more
+    # than a small overhead for formatting (e.g., 200 bytes).
+    assert len(str(retry).encode("utf-8")) <= METADATA_CAP_BYTES + 200
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/wren-pydantic/tests/unit/test_errors.py` at line 152, The assertion under
test uses a loose tolerance: change the test that currently checks assert
len(str(retry).encode("utf-8")) <= METADATA_CAP_BYTES + 1024 to either enforce a
strict cap or make the tolerance explicit and documented; specifically either
(A) tighten to assert len(str(retry).encode("utf-8")) <= METADATA_CAP_BYTES (or
a smaller explicit tolerance like +128) if the cap must be strict, or (B)
introduce a named constant METADATA_TOLERANCE_BYTES (set to 1024) and replace
the literal with METADATA_CAP_BYTES + METADATA_TOLERANCE_BYTES and add a
one-line comment above the test explaining why 1024 bytes of overhead is allowed
(e.g., scaffold/encoding overhead), updating the test docstring accordingly so
the intent is clear; locate the check by the symbols retry and
METADATA_CAP_BYTES in the test and update the assertion and surrounding comment.
sdk/wren-pydantic/src/wren_pydantic/_instructions.py (1)

236-243: ⚡ Quick win

Specify explicit encoding when reading user markdown files.

Line 240 calls read_text() without an explicit encoding parameter. User-provided instructions.md files may contain non-ASCII characters. For consistency with the explicit encoding="utf-8" used in the build workflow (lines 76-83 of .github/workflows/publish-wren-pydantic.yml), and to avoid platform-dependent behavior, specify the encoding.

📝 Proposed fix
 def _build_instructions_section(project_path: Path) -> str:
     instructions_file = project_path / "instructions.md"
     if not instructions_file.exists():
         return ""
-    body = instructions_file.read_text().strip()
+    body = instructions_file.read_text(encoding="utf-8").strip()
     if not body:
         return ""
     return f"## Project-specific instructions\n\n{body}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/wren-pydantic/src/wren_pydantic/_instructions.py` around lines 236 - 243,
The _build_instructions_section function reads user markdown with
instructions_file.read_text() without specifying encoding; update it to
explicitly pass encoding="utf-8" when reading the file (i.e., change the
read_text call used in _build_instructions_section for instructions_file) so
non-ASCII characters are handled consistently across platforms.
.github/workflows/publish-wren-pydantic.yml (1)

66-83: ⚡ Quick win

Use context managers for file operations.

Lines 78 and 83 open files without context managers (with statements). While this works in a CI script (the process exits anyway), using context managers is a Python best practice that ensures proper resource cleanup and makes the code more maintainable.

♻️ Proposed fix
           for path, pattern in [
               ("sdk/wren-pydantic/pyproject.toml", r'^(version\s*=\s*)".*?"'),
               ("sdk/wren-pydantic/src/wren_pydantic/__init__.py", r'^(__version__\s*=\s*)".*?"'),
           ]:
               # Explicit utf-8 — pyproject.toml description and __init__.py docstring
               # both contain non-ASCII; relying on platform default could corrupt them.
-              text = open(path, encoding="utf-8").read()
+              with open(path, encoding="utf-8") as f:
+                  text = f.read()
               text, n = re.subn(pattern, rf'\1"{version}"', text, count=1, flags=re.MULTILINE)
               if n != 1:
                   print(f"::error::Failed to update version in {path}")
                   sys.exit(1)
-              open(path, "w", encoding="utf-8").write(text)
+              with open(path, "w", encoding="utf-8") as f:
+                  f.write(text)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish-wren-pydantic.yml around lines 66 - 83, Replace
the bare file open calls with context managers: when reading the file use "with
open(path, encoding='utf-8') as f:" and call f.read() to get text, and when
writing use "with open(path, 'w', encoding='utf-8') as f:" and call
f.write(text); update the loop that uses the VERSION variable and re.subn to
operate on the text read/written via these with-blocks so no open(path, ...)
calls remain outside context managers.
sdk/wren-pydantic/src/wren_pydantic/_memory_api.py (1)

26-34: ⚡ Quick win

Consider documenting the optional parameters.

The docstring briefly describes the return value but doesn't explain item_type, model, or threshold. Since this is a direct API exposed as toolkit.memory.fetch(), developers would benefit from understanding these filtering options.

📝 Proposed docstring enhancement
-    def fetch(
-        self,
-        question: str,
-        *,
-        limit: int = 5,
-        item_type: str | None = None,
-        model: str | None = None,
-        threshold: int | None = None,
-    ) -> dict[str, Any]:
-        """Return schema/business context relevant to *question*."""
+    def fetch(
+        self,
+        question: str,
+        *,
+        limit: int = 5,
+        item_type: str | None = None,
+        model: str | None = None,
+        threshold: int | None = None,
+    ) -> dict[str, Any]:
+        """Return schema/business context relevant to *question*.
+
+        Args:
+            question: The analytical question to find context for.
+            limit: Maximum number of context items to return.
+            item_type: Filter by schema type (e.g., "model", "column").
+            model: Filter to a specific model name.
+            threshold: Similarity threshold for context matching.
+        """
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/wren-pydantic/src/wren_pydantic/_memory_api.py` around lines 26 - 34, The
fetch method's docstring is missing descriptions for the optional filtering
params (item_type, model, threshold) exposed via toolkit.memory.fetch; update
the docstring for wren_pydantic._memory_api.MemoryAPI.fetch to document each
optional parameter (item_type: filter by memory item type/string, model:
restrict results to embeddings/vector model name, threshold: numeric
similarity/relevance cutoff) and briefly note default behaviors (e.g., None
means no filtering) and how they affect the returned dict described currently in
the docstring. Ensure the parameter names match exactly (item_type, model,
threshold) and keep the style consistent with the existing return description.
sdk/wren-pydantic/src/wren_pydantic/_tools_memory.py (1)

30-47: ⚡ Quick win

Exception hierarchy mismatch: MemoryNotEnabledError is not a WrenError subclass.

MemoryNotEnabledError inherits directly from Exception (per exceptions.py:12), not from WrenError. The helper functions in lines 181–211 only catch WrenError, so if MemoryNotEnabledError were raised, it would not be caught.

However, this is mitigated in practice: build_memory_toolset() is only called when self._memory.enabled is True (see _toolkit.py:92), and _MemoryAPI._store() (line 92) raises MemoryNotEnabledError only when memory is disabled. So under normal operation, the exception won't be raised.

Optional: For robustness against state changes after tool registration, consider catching MemoryNotEnabledError explicitly in the helpers, or add a comment documenting the invariant that tools are only registered when memory is enabled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/wren-pydantic/src/wren_pydantic/_tools_memory.py` around lines 30 - 47,
MemoryNotEnabledError currently inherits Exception (not WrenError), so the
helper functions registered by _register_fetch_context, _register_recall_queries
and _register_store_query will not catch it when they only catch WrenError; to
fix, either make MemoryNotEnabledError inherit from WrenError, or update those
helpers to explicitly catch MemoryNotEnabledError in addition to WrenError (and
log/handle it the same way), and/or add a short comment in build_memory_toolset
noting the invariant that tools are only registered when memory is enabled to
justify relying on that invariant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/sdk-pydantic-ci.yml:
- Line 17: Replace the incorrect path filter string
'.github/workflows/sdk-langchain-ci.yml' with the correct
'.github/workflows/sdk-pydantic-ci.yml' in the workflow file so the CI triggers
on changes to itself; update both occurrences of the wrong string (the one shown
in the diff and the other occurrence referenced at "Also applies to: 23-23") to
the correct filename.

In `@sdk/wren-pydantic/src/wren_pydantic/_providers/mdl_source.py`:
- Around line 24-38: The code only wraps json.JSONDecodeError but lets
filesystem/encoding errors from self._mdl_path.read_text() leak; update the
logic around reading/parsing target/mdl.json so read_text is called with
explicit encoding='utf-8' and any OSError/UnicodeDecodeError (or any Exception
from read_text) is caught and re-raised as WrenToolkitInitError with a clear
message including the original exception details; keep the existing json.loads
call and its JSONDecodeError handling (raise WrenToolkitInitError from exc) but
add a prior try/except around read_text (or broaden the existing try to cover
read_text) referencing self._mdl_path, json.loads, and WrenToolkitInitError so
callers always receive a WrenToolkitInitError for manifest read/parse failures.

In `@sdk/wren-pydantic/src/wren_pydantic/_toolkit.py`:
- Around line 8-9: Remove the stale sentence in the module docstring that claims
toolset() and instructions() "aren't on this class yet"; instead update the
docstring to reflect that both methods are implemented (toolset and
instructions) or delete that specific line; locate the class in _toolkit.py and
remove or rewrite the inaccurate sentence so the docstring matches the actual
implementation of toolset() and instructions().
- Around line 160-161: The code assigns to the private attribute
engine._connector using self._connector_cache which relies on an unstable
internal API; update the assignment to first guard with a defensive existence
check (e.g., check hasattr(engine, '_connector') or catch AttributeError around
the assignment) so you only set engine._connector when that attribute exists,
and add a TODO comment to open an issue in the wren-engine project requesting a
public connector caching API and to document a wren-engine version constraint if
this behavior is required (refer to self._connector_cache and
engine._connector).

---

Nitpick comments:
In @.github/workflows/publish-wren-pydantic.yml:
- Around line 66-83: Replace the bare file open calls with context managers:
when reading the file use "with open(path, encoding='utf-8') as f:" and call
f.read() to get text, and when writing use "with open(path, 'w',
encoding='utf-8') as f:" and call f.write(text); update the loop that uses the
VERSION variable and re.subn to operate on the text read/written via these
with-blocks so no open(path, ...) calls remain outside context managers.

In `@sdk/wren-pydantic/examples/pydantic_ai_demo.py`:
- Line 25: The default PROJECT_PATH value is misaligned with the docstring;
update how project_path is derived (the project_path variable) to use "./" as
the default instead of "./analytics_db" so the example assumes running from the
project root (where wren context init is run); keep using Path(...).expanduser()
and read from os.environ.get("PROJECT_PATH", "./") so users can still override
via env.

In `@sdk/wren-pydantic/src/wren_pydantic/_instructions.py`:
- Around line 236-243: The _build_instructions_section function reads user
markdown with instructions_file.read_text() without specifying encoding; update
it to explicitly pass encoding="utf-8" when reading the file (i.e., change the
read_text call used in _build_instructions_section for instructions_file) so
non-ASCII characters are handled consistently across platforms.

In `@sdk/wren-pydantic/src/wren_pydantic/_memory_api.py`:
- Around line 26-34: The fetch method's docstring is missing descriptions for
the optional filtering params (item_type, model, threshold) exposed via
toolkit.memory.fetch; update the docstring for
wren_pydantic._memory_api.MemoryAPI.fetch to document each optional parameter
(item_type: filter by memory item type/string, model: restrict results to
embeddings/vector model name, threshold: numeric similarity/relevance cutoff)
and briefly note default behaviors (e.g., None means no filtering) and how they
affect the returned dict described currently in the docstring. Ensure the
parameter names match exactly (item_type, model, threshold) and keep the style
consistent with the existing return description.

In `@sdk/wren-pydantic/src/wren_pydantic/_tools_memory.py`:
- Around line 30-47: MemoryNotEnabledError currently inherits Exception (not
WrenError), so the helper functions registered by _register_fetch_context,
_register_recall_queries and _register_store_query will not catch it when they
only catch WrenError; to fix, either make MemoryNotEnabledError inherit from
WrenError, or update those helpers to explicitly catch MemoryNotEnabledError in
addition to WrenError (and log/handle it the same way), and/or add a short
comment in build_memory_toolset noting the invariant that tools are only
registered when memory is enabled to justify relying on that invariant.

In `@sdk/wren-pydantic/tests/unit/test_errors.py`:
- Line 152: The assertion under test uses a loose tolerance: change the test
that currently checks assert len(str(retry).encode("utf-8")) <=
METADATA_CAP_BYTES + 1024 to either enforce a strict cap or make the tolerance
explicit and documented; specifically either (A) tighten to assert
len(str(retry).encode("utf-8")) <= METADATA_CAP_BYTES (or a smaller explicit
tolerance like +128) if the cap must be strict, or (B) introduce a named
constant METADATA_TOLERANCE_BYTES (set to 1024) and replace the literal with
METADATA_CAP_BYTES + METADATA_TOLERANCE_BYTES and add a one-line comment above
the test explaining why 1024 bytes of overhead is allowed (e.g.,
scaffold/encoding overhead), updating the test docstring accordingly so the
intent is clear; locate the check by the symbols retry and METADATA_CAP_BYTES in
the test and update the assertion and surrounding comment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 25031332-ed72-4e0c-9dda-0c7c37993b5f

📥 Commits

Reviewing files that changed from the base of the PR and between ba187b7 and cd87897.

📒 Files selected for processing (46)
  • .github/workflows/publish-wren-pydantic.yml
  • .github/workflows/rc-release.yml
  • .github/workflows/release-please.yml
  • .github/workflows/sdk-pydantic-ci.yml
  • .release-please-manifest.json
  • docs/core/sdk/pydantic.md
  • release-please-config.json
  • sdk/wren-pydantic/.gitignore
  • sdk/wren-pydantic/CHANGELOG.md
  • sdk/wren-pydantic/LICENSE
  • sdk/wren-pydantic/README.md
  • sdk/wren-pydantic/examples/pydantic_ai_demo.py
  • sdk/wren-pydantic/examples/pydantic_ai_structured_demo.py
  • sdk/wren-pydantic/pyproject.toml
  • sdk/wren-pydantic/src/wren_pydantic/__init__.py
  • sdk/wren-pydantic/src/wren_pydantic/_errors.py
  • sdk/wren-pydantic/src/wren_pydantic/_instructions.py
  • sdk/wren-pydantic/src/wren_pydantic/_memory_api.py
  • sdk/wren-pydantic/src/wren_pydantic/_models.py
  • sdk/wren-pydantic/src/wren_pydantic/_providers/__init__.py
  • sdk/wren-pydantic/src/wren_pydantic/_providers/connection.py
  • sdk/wren-pydantic/src/wren_pydantic/_providers/mdl_source.py
  • sdk/wren-pydantic/src/wren_pydantic/_providers/memory.py
  • sdk/wren-pydantic/src/wren_pydantic/_toolkit.py
  • sdk/wren-pydantic/src/wren_pydantic/_tools.py
  • sdk/wren-pydantic/src/wren_pydantic/_tools_memory.py
  • sdk/wren-pydantic/src/wren_pydantic/exceptions.py
  • sdk/wren-pydantic/tests/__init__.py
  • sdk/wren-pydantic/tests/conformance/__init__.py
  • sdk/wren-pydantic/tests/conformance/test_pydantic_ai_contract.py
  • sdk/wren-pydantic/tests/conftest.py
  • sdk/wren-pydantic/tests/test_smoke.py
  • sdk/wren-pydantic/tests/unit/__init__.py
  • sdk/wren-pydantic/tests/unit/test_errors.py
  • sdk/wren-pydantic/tests/unit/test_exceptions.py
  • sdk/wren-pydantic/tests/unit/test_instructions.py
  • sdk/wren-pydantic/tests/unit/test_memory_api.py
  • sdk/wren-pydantic/tests/unit/test_models.py
  • sdk/wren-pydantic/tests/unit/test_providers_connection.py
  • sdk/wren-pydantic/tests/unit/test_providers_mdl.py
  • sdk/wren-pydantic/tests/unit/test_providers_memory.py
  • sdk/wren-pydantic/tests/unit/test_toolkit_init.py
  • sdk/wren-pydantic/tests/unit/test_toolkit_runtime.py
  • sdk/wren-pydantic/tests/unit/test_tools_memory.py
  • sdk/wren-pydantic/tests/unit/test_tools_runtime.py
  • sdk/wren-pydantic/tests/unit/test_toolset_facade.py

Comment thread .github/workflows/sdk-pydantic-ci.yml Outdated
Comment thread sdk/wren-pydantic/src/wren_pydantic/_providers/mdl_source.py
Comment thread sdk/wren-pydantic/src/wren_pydantic/_toolkit.py Outdated
Comment thread sdk/wren-pydantic/src/wren_pydantic/_toolkit.py
Two follow-ups to PR #2255 from CodeRabbit review:

- sdk-pydantic-ci.yml path filter referenced sdk-langchain-ci.yml
  (sed left-over from lifting wren-langchain CI). With the bad filter,
  changes to this workflow file itself never trigger CI, and changes
  to the langchain workflow file would (incorrectly) trigger this CI.
- _toolkit.py module docstring claimed toolset() and instructions()
  weren't implemented yet — they landed in Phase 2.3 and 4.1 of this
  PR. Drop the now-stale "coming soon" note.

109 unit + conformance tests still green.
release-please generates CHANGELOG.md on first release based on
conventional commits. The placeholder I committed in Phase 0.1 would
either conflict with release-please output or sit confusingly empty.

Aligns with sibling wren-langchain which never had a CHANGELOG.md
in tree either — release-please creates it the first time the
package ships. pyproject.toml's Changelog URL stays as-is (forward
reference, resolves once release-please runs).
@PaulChen79 PaulChen79 self-assigned this May 13, 2026
Copy link
Copy Markdown
Collaborator

@goldmedal goldmedal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @PaulChen79 👍

@goldmedal goldmedal merged commit ff6ae50 into main May 13, 2026
7 checks passed
@goldmedal goldmedal deleted the feat/add-pydantic-sdk branch May 13, 2026 05:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants