Framework integrations
Wire Ratel into Pydantic AI: one adapter to the framework's tool type, one loop that assembles the capability tools plus a top-K pre-filter.
Ratel ships no framework plugins because it needs none. Everything the SDK hands you — your registered tools and the capability tools (search_capabilities / invoke_tool; get_skill_content joins the set when a SkillCatalog is registered) — is a plain executable tool: metadata plus an execute handler. Wiring a framework is two small pieces:
- An adapter from that shape to the framework's tool type.
- A turn assembler that builds each turn's tool list: the always-present capability tools plus the top-K catalog hits for the user's message (ADR 0004 replace-mode pre-filtering).
This page follows the shipped examples/pydantic-ai. Using the Vercel AI SDK? Its wiring lives on the TypeScript framework integrations page.
Pydantic AI
The example pins pydantic-ai>=1.0 alongside ratel-ai, on Python 3.10+. Every tool is built from the catalog's JSON schemas via Pydantic AI's Tool.from_schema, no Python signature introspection, so the schema the model sees is the one Ratel ranks.
The adapter: two dispatch paths
Catalog tools (the top-K hits) dispatch through catalog.invoke; the capability tools own async def handlers and are awaited directly:
from typing import Any
from pydantic_ai import Tool
from ratel_ai import ToolCatalog
def _tool_from_fn(fn: Any, name: str, description: str, schema: dict[str, Any]) -> Tool:
fn.__name__ = name
return Tool.from_schema(fn, name=name, description=description, json_schema=schema)
def _catalog_tool(catalog: ToolCatalog, tool_id: str, description: str, schema: dict[str, Any]) -> Tool:
# `invoke` is the SDK's tested entry point: it handles sync *and* async
# executors and emits the `invoke_*` trace events.
async def fn(**kwargs: Any) -> Any:
return await catalog.invoke(tool_id, kwargs)
return _tool_from_fn(fn, tool_id, description, schema)
def _capability_tool(execute: Any, name: str, description: str, schema: dict[str, Any]) -> Tool:
# search_capabilities / invoke_tool are not catalog entries; their execute
# handlers are async, so await them directly.
async def fn(**kwargs: Any) -> Any:
return await execute(kwargs)
return _tool_from_fn(fn, name, description, schema)Never await a catalog tool's raw execute in Python: executors may be sync, and re-wrapping them is how a sync executor ends up wrongly awaited (TypeError: object dict can't be used in 'await' expression). Route catalog tools through catalog.invoke, which awaits only when needed.
Assemble the turn and run the agent
from pydantic_ai import Agent
from ratel_ai import invoke_tool_tool, search_capabilities_tool
def build_tools(catalog: ToolCatalog, prompt: str, initial_top_k: int = 3) -> list[Tool]:
search = search_capabilities_tool(catalog)
invoke = invoke_tool_tool(catalog)
tools: dict[str, Tool] = {
search.id: _capability_tool(search.execute, search.id, search.description, search.input_schema),
invoke.id: _capability_tool(invoke.execute, invoke.id, invoke.description, invoke.input_schema),
}
for hit in catalog.search(prompt, initial_top_k):
executable = catalog.get_executable(hit.tool_id)
if executable is not None:
tools[executable.id] = _catalog_tool(
catalog, executable.id, executable.description, executable.input_schema
)
return list(tools.values())
async def run_agent(*, prompt: str, model: str, catalog: ToolCatalog) -> str:
agent = Agent(model, tools=build_tools(catalog, prompt))
result = await agent.run(prompt)
return result.outputPydantic AI runs its own loop: await agent.run(prompt) auto-executes tools and threads results back until the final answer. model is a provider-prefixed id string rather than a model object, and the example sets no explicit step cap.
Run it
import asyncio
import os
from agent import run_agent
from tools import build_catalog # your ToolCatalog assembly
model = os.environ.get("RATEL_EXAMPLE_MODEL", "openai:gpt-5-mini")
text = asyncio.run(run_agent(prompt="find every TODO comment under src/", model=model, catalog=build_catalog()))To run the shipped example from a checkout (uv run builds the native extension on first run):
export OPENAI_API_KEY=sk-... # or ANTHROPIC_API_KEY, GEMINI_API_KEY, GROQ_API_KEY
uv run main.py "send an email to alice@example.com saying ship it"Swapping providers is an env var, not an import: RATEL_EXAMPLE_MODEL=anthropic:claude-sonnet-4-6. With no supported API key set, the example prints the BM25 top-3 in diagnostic mode and never calls a model.
The same pattern over an MCP server
examples/mcp-chat applies the identical wiring to tools it does not own: it ingests an upstream MCP server's tools into the catalog (server-namespaced ids like ev__echo), then rebuilds the tool list every turn. With 13 upstream tools registered and a top-K of 3, the model sees five tool definitions per turn; the rest stay reachable through search_capabilities / invoke_tool without occupying context. In Python, ingestion is register_mcp_server.
Any other framework
LangChain, OpenAI Agents, a hand-rolled loop: the recipe does not change, because ToolCatalog entries and the capability-tool factories all return the same plain executable shape (id, name, description, input/output schemas, execute). Write one adapter from that shape to your framework's tool type, and one assembler that puts the capability tools plus the top-K hits into each turn. Progressive disclosure covers the contract those tools expose to the model.
Next steps
Use the discovery tools
Hand the agent search_capabilities, invoke_tool, and get_skill_content, then assemble each turn's toolset with a top-K pre-filter.
Telemetry & OTel
Capture Ratel's search and invoke funnel as local trace events, or export it as OpenTelemetry spans — greenfield or alongside an existing provider.