Ratel Docs
Python SDK

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:

  1. An adapter from that shape to the framework's tool type.
  2. 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.output

Pydantic 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

On this page