Skip to content

Integrations

Optional bridges exposing an existing DRF surface as agent tools. Both are lazily imported and live under django_pydantic_agent.integrations rather than being re-exported at the package root, so the base install pulls neither dependency. See Integrations for which to choose.

Spec tools ([spec-tools])

build_spec_capability

build_spec_capability

build_spec_capability(
    specs: Mapping[str, Any] | SpecSource,
    *,
    exclude_names: frozenset[str] = frozenset(),
) -> Any

A SpecCapability over specs, acting as the run's deps.user.

Each spec is called in process through drf-services' transport-neutral surface, which enforces its permission_classes. Nothing here closes over a request: SpecToolset's default extractor already reads ctx.deps.user, so a run given AgentDeps binds the acting user natively and the capability stays request-independent, which is what makes an agent built once reusable across runs.

Every spec here needs its own permission_classes. A spec with permission_classes=None makes SpecCapability raise ImproperlyConfigured rather than expose an ungated tool: over HTTP None means inherit — the view's classes, then DEFAULT_PERMISSION_CLASSES — and off HTTP there is nothing to inherit from, so a spec properly guarded behind a viewset would become callable by whatever the model decides to call.

PAI's require_permissions=False migration flag is not exposed here; a project migrating a large registry constructs SpecCapability itself and passes it to AgentConfig(capabilities=...). That path skips the tool-catalog registration this function participates in, so tool-call cards render unlabelled — a migration step, not a destination.

A registry is passed through as a registry, narrowed with its own subset when exclude_names bites. Flattening it to a mapping first would drop everything the entry carries beyond the spec -- including the OfflineContract that says what a caller with no HTTP request has to be told -- and drop it silently, since the resulting toolset is well-formed and merely missing declarations nobody asked it for.

Parameters:

Name Type Description Default
specs Mapping[str, Any] | SpecSource

A name -> ServiceSpec/SelectorSpec mapping, or a spec registry to read one from. Prefer the registry: it is the same specs plus the per-entry declarations an agent transport reads.

required
exclude_names frozenset[str]

Names a higher-precedence source (the @tool registry, the drf-mcp bridge) already claimed. They are dropped so that source wins the collision, since pydantic-ai raises UserError for a duplicate name at run time.

frozenset()

resolve_spec_mapping

resolve_spec_mapping

resolve_spec_mapping(specs: Mapping[str, Any] | SpecSource) -> Mapping[str, Any]

Return the name -> spec mapping, whether given one or a registry.

A SpecSource is recognised structurally, by having a specs() method.

Public because a transport needs the same normalisation before build_spec_capability runs, when it reserves each tool name for collision detection: iterating a registry yields RegisteredSpec records rather than names, so a transport that iterated the raw argument would fill its set with dataclasses and silently stop detecting collisions.

SpecSource

SpecSource

Bases: Protocol

Anything that can hand back a name -> spec mapping.

djangorestframework-services' SpecRegistry (0.27+) is the intended implementation: a project exposing the same specs over more than one transport declares them there once, and each transport reads that source.

It is matched structurally rather than imported, because this substrate depends on pydantic-ai-slim alone and drf-services arrives only with the optional [spec-tools] extra. Naming SpecRegistry in a signature would force that dependency on every install or bury the type behind a lazy import where a signature cannot reach it.

A plain dict is not a SpecSource — it has no specs() — which is what lets a caller accept either and tell them apart.

specs

specs() -> dict[str, Any]

The name -> spec mapping this source declares.

MCP tools ([drf-mcp])

DRFMCPToolset

DRFMCPToolset

Bases: AbstractToolset[Any]

Exposes a drf-mcp MCPServer's tools as a Pydantic-AI toolset.

Built per request, so the agent acts as the request's logged-in user. Both schemas and execution route through drf-mcp's public in-process surface (MCPServer.list_tools / acall_tool, drf-mcp 0.9+), so the advertised parameters, serializer validation and permissions match the HTTP transport exactly — without the network hop. Tool definitions carry the default kind="function", the in-process kind the run loop calls itself; an external tool would instead be deferred to the client and never run.

Failures split three ways, along MCP's protocol-vs-tool boundary:

  • JSON-RPC -32602 and tool-level validation_error results raise pydantic_ai.ModelRetry, so the model retries with the field errors instead of the run dying;
  • other tool-level failures (service_error / not_found) are returned as the tool's content, for the model to read;
  • protocol faults (auth, rate limits, an internal error) raise RuntimeError and abort the run.

Parameters:

Name Type Description Default
server Any

The drf-mcp MCPServer whose registry is bridged.

required
request HttpRequest

The request carried into every call; its user is the acting user.

required
exclude_names frozenset[str]

Names the @tool registry has already claimed. A colliding drf-mcp tool is skipped, so the registry wins — the rule build_tool_catalog applies — because pydantic-ai raises UserError for a duplicate name at run time.

frozenset()
max_retries int

Per-tool retry budget: how many times a ModelRetry is fed back to the model before the run aborts. The default matches pydantic-ai's own function-tool default.

1

get_tools async

get_tools(ctx: Any) -> dict[str, ToolsetTool[Any]]

Load tool defs from drf-mcp's tools/list once, then wrap them.