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 |
required |
exclude_names
|
frozenset[str]
|
Names a higher-precedence source (the |
frozenset()
|
resolve_spec_mapping¶
resolve_spec_mapping ¶
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.
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
-32602and tool-levelvalidation_errorresults raisepydantic_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
RuntimeErrorand abort the run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
server
|
Any
|
The drf-mcp |
required |
request
|
HttpRequest
|
The request carried into every call; its |
required |
exclude_names
|
frozenset[str]
|
Names the |
frozenset()
|
max_retries
|
int
|
Per-tool retry budget: how many times a |
1
|
get_tools
async
¶
Load tool defs from drf-mcp's tools/list once, then wrap them.