feat(sdk): add wren-pydantic package for Pydantic AI integration#2255
Conversation
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughThis PR adds the complete ChangesRelease and CI Infrastructure
SDK Documentation and Configuration
Core SDK Data Models and Exceptions
Project Configuration Providers
Memory Operations
WrenToolkit Main Facade
LLM-Facing Tools and Prompts
Comprehensive Test Suite
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (6)
sdk/wren-pydantic/examples/pydantic_ai_demo.py (1)
25-25: 💤 Low valueConsider aligning default PROJECT_PATH with the docstring.
The default path
./analytics_dbsuggests a subdirectory, but the docstring (lines 4-7) implies running from the project root (where you'd runwren 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 winVerify 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 + 1024Or 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 winSpecify explicit encoding when reading user markdown files.
Line 240 calls
read_text()without an explicit encoding parameter. User-providedinstructions.mdfiles may contain non-ASCII characters. For consistency with the explicitencoding="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 winUse context managers for file operations.
Lines 78 and 83 open files without context managers (
withstatements). 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 winConsider documenting the optional parameters.
The docstring briefly describes the return value but doesn't explain
item_type,model, orthreshold. Since this is a direct API exposed astoolkit.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 winException hierarchy mismatch:
MemoryNotEnabledErroris not aWrenErrorsubclass.
MemoryNotEnabledErrorinherits directly fromException(perexceptions.py:12), not fromWrenError. The helper functions in lines 181–211 only catchWrenError, so ifMemoryNotEnabledErrorwere raised, it would not be caught.However, this is mitigated in practice:
build_memory_toolset()is only called whenself._memory.enabledisTrue(see_toolkit.py:92), and_MemoryAPI._store()(line 92) raisesMemoryNotEnabledErroronly 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
MemoryNotEnabledErrorexplicitly 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
📒 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.jsondocs/core/sdk/pydantic.mdrelease-please-config.jsonsdk/wren-pydantic/.gitignoresdk/wren-pydantic/CHANGELOG.mdsdk/wren-pydantic/LICENSEsdk/wren-pydantic/README.mdsdk/wren-pydantic/examples/pydantic_ai_demo.pysdk/wren-pydantic/examples/pydantic_ai_structured_demo.pysdk/wren-pydantic/pyproject.tomlsdk/wren-pydantic/src/wren_pydantic/__init__.pysdk/wren-pydantic/src/wren_pydantic/_errors.pysdk/wren-pydantic/src/wren_pydantic/_instructions.pysdk/wren-pydantic/src/wren_pydantic/_memory_api.pysdk/wren-pydantic/src/wren_pydantic/_models.pysdk/wren-pydantic/src/wren_pydantic/_providers/__init__.pysdk/wren-pydantic/src/wren_pydantic/_providers/connection.pysdk/wren-pydantic/src/wren_pydantic/_providers/mdl_source.pysdk/wren-pydantic/src/wren_pydantic/_providers/memory.pysdk/wren-pydantic/src/wren_pydantic/_toolkit.pysdk/wren-pydantic/src/wren_pydantic/_tools.pysdk/wren-pydantic/src/wren_pydantic/_tools_memory.pysdk/wren-pydantic/src/wren_pydantic/exceptions.pysdk/wren-pydantic/tests/__init__.pysdk/wren-pydantic/tests/conformance/__init__.pysdk/wren-pydantic/tests/conformance/test_pydantic_ai_contract.pysdk/wren-pydantic/tests/conftest.pysdk/wren-pydantic/tests/test_smoke.pysdk/wren-pydantic/tests/unit/__init__.pysdk/wren-pydantic/tests/unit/test_errors.pysdk/wren-pydantic/tests/unit/test_exceptions.pysdk/wren-pydantic/tests/unit/test_instructions.pysdk/wren-pydantic/tests/unit/test_memory_api.pysdk/wren-pydantic/tests/unit/test_models.pysdk/wren-pydantic/tests/unit/test_providers_connection.pysdk/wren-pydantic/tests/unit/test_providers_mdl.pysdk/wren-pydantic/tests/unit/test_providers_memory.pysdk/wren-pydantic/tests/unit/test_toolkit_init.pysdk/wren-pydantic/tests/unit/test_toolkit_runtime.pysdk/wren-pydantic/tests/unit/test_tools_memory.pysdk/wren-pydantic/tests/unit/test_tools_runtime.pysdk/wren-pydantic/tests/unit/test_toolset_facade.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).
Summary
sdk/wren-pydanticpackage — Pydantic AI integration for Wren AI Core, sibling tosdk/wren-langchainFunctionToolsetWrenError → ModelRetrymapping with phase-aware messages, recursive secret redaction, 4KB capMemoryStoreshapes (no envelope dicts)What's included
WrenToolkit.from_project(path, profile=)wren_project.yml+target/mdl.json, loads.env, resolves memory provider via.wren/memory/, 3-tier profile fallbacktoolkit.toolset(*, include_memory_write=True, takes_ctx=False)FunctionToolset; auto-filters memory tools when disabled;takes_ctx=Truefor mixing withdeps_type=toolstoolkit.instructions(*, toolset=None)system_prompt()toolkit.memory.fetch / recall / storetoolkit.query / dry_plan / dry_runDesign decisions (locked during planning)
asyncio.to_threadwould be fake-async with no concurrency benefit. Aligns with wren-langchain.ModelRetryover envelope — framework-idiomatic; phase-aware messages give LLM enough context to self-correct (SQL_PARSING,METADATA_FETCHING,SQL_EXECUTIONwith dialect-SQL excerpt, etc.)retries=0onwren_store_query— write failures don't loop; LLM has already done the analytical workretries=2on read tools — gives the LLM two chances to fix SQL or model-name errorsFetchContextResultuses{strategy: full|search, schema/results},RecalledPairuses Core'snl_query/sql_query/tags-as-comma-string/_distance. Drift in Core surfaces here as test failures.See scoping doc and implementation plan (untracked — local only).
Commit structure
21 commits in chronological order:
feat(sdk): scaffold wren-pydantic package skeletonfeat(pydantic): add exceptions modulefeat(pydantic): copy provider modules from wren-langchainfeat(pydantic): add Pydantic return models for tool outputsfix(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)feat(pydantic): WrenError → ModelRetry mappingfeat(pydantic): WrenToolkit class — from_project + sync direct APIfeat(pydantic): runtime tools (query, dry_plan, list_models)feat(pydantic): WrenToolkit.toolset() facadefeat(pydantic): _MemoryAPI subscope (sync only)feat(pydantic): memory tools (fetch_context, recall_queries, store_query)feat(pydantic): wire memory tools into WrenToolkit.toolset()feat(pydantic): instructions() prompt builderstyle(pydantic): clean up _instructions.py — unused import + docstringtest(pydantic): Pydantic AI conformance suitefeat(pydantic): example scriptsdocs(pydantic): README + docs/core/sdk/pydantic.mdchore(ci): add wren-pydantic CI workflowfeat(ci): publish workflow for wren-pydanticchore(release): wire wren-pydantic into release-please + rc-releasestyle(pydantic): silence PLC0415 for legitimate local imports in testsTest plan
ruff check .+ruff format --check .cleanpip install -e .works againstwren-engine>=0.5.0from local + PyPIpydantic_ai_demo.py,pydantic_ai_structured_demo.py) tested with real OpenAI agent against DuckDB + Postgres projects.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_loansandproj_eventsdocs/core/sdk/pydantic.mdagainst an agent dev's mental modelOut of scope (deliberate, deferred to v0.2)
aquery/afetchetc.) — gated on Core supporting async-native enginewren-sdk-corepackage extraction between langchain + pydantic — premature; revisit when drift becomes painfulCompatibility
wren-pydanticwren-enginepydantic-ai🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
Chores