PydanticAI¶
mimir.integrations.pydantic_ai.as_toolset converts a collection of decision tools into a FunctionToolset with typed return values for use in a PydanticAI agent.
guard(toolset, check) wraps any toolset with a tool-call check: a certified denial fails the tool call with the check's reason, and an escalated call ends the run by raising DeferredToolRequests. Declare DeferredToolRequests in the agent's output_type to handle escalations cleanly.
Tested from pydantic-ai-slim 2.16.
Native tools¶
"""A PydanticAI agent that routes support tickets with MIMIR decision tools.
The tools return typed results, which PydanticAI keeps in the tool return parts. Install
`mimir-decisions[local,pydantic-ai]` and `pydantic-ai-slim[openai]`, set `OPENAI_API_KEY`, then run
`make example NAME=pydantic_ai/agent`.
"""
import asyncio
from typing import Final
from pydantic_ai import Agent
from pydantic_ai.models import Model
from mimir import Choice, Decider, Mimir
from mimir.integrations.pydantic_ai import as_toolset
MODEL: Final = "openai:gpt-5.5"
TEAMS: Final = {
"billing": "Billing: payments, refunds and invoices",
"security": "Security: account access, passwords and fraud",
"shipping": "Shipping: deliveries, tracking and returns",
}
INSTRUCTIONS: Final = (
"Route the customer's ticket with route_ticket, then tell the customer which team will "
"answer. If the decision is deferred, say that a person will review the ticket."
)
def build_agent(decider: Decider, model: str | Model = MODEL) -> Agent[None, str]:
"""The support agent, answering with `decider`."""
route_ticket = decider.tool(
"route_ticket",
Choice("Which team should handle this ticket?", TEAMS),
description="Route a customer support ticket to the team that owns it.",
)
return Agent(model, instructions=INSTRUCTIONS, toolsets=[as_toolset([route_ticket])])
async def main() -> None:
agent = build_agent(Mimir.from_pretrained("Mythologic/MIMIR-1"))
result = await agent.run("I was charged twice for order 4412.")
print(result.output)
if __name__ == "__main__":
asyncio.run(main())
Tool-call check¶
"""A PydanticAI agent whose refunds MIMIR checks against the refund rules first.
A certified yes runs the refund, a certified no fails it with the reason, and anything else
ends the run with deferred tool requests until a person at the console approves or denies
them. Install `mimir-decisions[local,pydantic-ai]` and `pydantic-ai-slim[openai]`, set
`OPENAI_API_KEY`, then run `make example NAME=pydantic_ai/guarded_agent`.
"""
import asyncio
from typing import Any, Final
from pydantic_ai import (
Agent,
DeferredToolRequests,
DeferredToolResults,
ToolApproved,
ToolDenied,
)
from pydantic_ai.models import Model
from pydantic_ai.run import AgentRunResult
from pydantic_ai.toolsets import FunctionToolset
from mimir import Decider, Mimir
from mimir.integrations.pydantic_ai import guard
MODEL: Final = "openai:gpt-5.5"
RULES: Final = (
"A refund is at most the amount the customer paid for the order.",
"Refunds above 500 dollars need a manager's approval.",
)
INSTRUCTIONS: Final = "Issue the refunds customers ask for with issue_refund."
Output = str | DeferredToolRequests
def issue_refund(order: str, amount: float) -> str:
"""Refund `amount` dollars on `order`."""
return f"Refunded {amount:.2f} dollars on order {order}."
def build_agent(
decider: Decider,
refunds: FunctionToolset[Any] | None = None,
model: str | Model = MODEL,
) -> Agent[None, Output]:
"""The refunds agent, its `refunds` toolset checked by `decider` against `RULES`."""
toolset = FunctionToolset([issue_refund]) if refunds is None else refunds
check = decider.tool_call_check(RULES)
return Agent(
model,
instructions=INSTRUCTIONS,
toolsets=[guard(toolset, check)],
output_type=[str, DeferredToolRequests],
)
async def run_with_approvals(agent: Agent[None, Output], request: str) -> AgentRunResult[Output]:
"""Run `agent`, asking at the console about each call the check escalates."""
result = await agent.run(request)
while isinstance(result.output, DeferredToolRequests):
approvals: dict[str, bool | ToolApproved | ToolDenied] = {}
for pending in result.output.approvals:
reason = result.output.metadata.get(pending.tool_call_id, {}).get("reason", "")
answer = input(f"{reason} Approve {pending.tool_name} {pending.args}? [y/N] ")
approved = answer.strip().lower() == "y"
approvals[pending.tool_call_id] = (
True if approved else ToolDenied("A person denied it.")
)
result = await agent.run(
message_history=result.all_messages(),
deferred_tool_results=DeferredToolResults(approvals=approvals),
)
return result
async def main() -> None:
agent = build_agent(Mimir.from_pretrained("Mythologic/MIMIR-1"))
result = await run_with_approvals(agent, "Refund 900 dollars on order 4412.")
print(result.output)
if __name__ == "__main__":
asyncio.run(main())
Over MCP¶
"""A PydanticAI agent using MIMIR's MCP server over stdio, through `MCPToolset`.
The server runs `examples/tools.yaml`, so the agent sees `route_ticket` and passes only the
ticket. Install `pydantic-ai-slim[mcp,openai]` and `uv`, set `OPENAI_API_KEY`, then run
`make example NAME=pydantic_ai/mcp_agent`.
"""
import asyncio
from collections.abc import Sequence
from pathlib import Path
from typing import Any, Final
from fastmcp.client.transports import StdioTransport
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPToolset
from pydantic_ai.models import Model
MODEL: Final = "openai:gpt-5.5"
TOOLS: Final = Path(__file__).parents[1] / "tools.yaml"
SERVER: Final = (
"uvx",
("--from", "mimir-decisions[local,mcp]", "mimir-decisions", "mcp", "--tools", str(TOOLS)),
)
INSTRUCTIONS: Final = (
"Route the customer's ticket with route_ticket, then tell the customer which team will "
"answer. If the decision is deferred, say that a person will review the ticket."
)
def mimir_server(command: str, arguments: Sequence[str]) -> MCPToolset[Any]:
"""MIMIR's MCP server, started as `command` with `arguments`."""
return MCPToolset(StdioTransport(command=command, args=list(arguments)))
def build_agent(server: MCPToolset[Any], model: str | Model = MODEL) -> Agent[None, str]:
"""The support agent, with the tools of `server`."""
return Agent(model, instructions=INSTRUCTIONS, toolsets=[server])
async def main() -> None:
agent = build_agent(mimir_server(*SERVER))
async with agent:
result = await agent.run("I was charged twice for order 4412.")
print(result.output)
if __name__ == "__main__":
asyncio.run(main())