Agent¶
Construction: a ToolRegistry plus an AgentConfig in, a
pydantic_ai.Agent out. See Concepts for what gets composed.
AgentConfig¶
AgentConfig
dataclass
¶
Resolved construction parameters for a Pydantic-AI Agent.
Bundles everything build_agent
needs so the call site passes one record instead of a long keyword list.
A transport resolves these from its own configuration and hands the record
down; toolsets and capabilities arrive already resolved to instances
(never dotted paths — this substrate resolves nothing from settings).
instructions
class-attribute
instance-attribute
¶
System/instructions prompt for the agent.
audit_logger
class-attribute
instance-attribute
¶
Wraps every server-side tool call for timing and success/failure
records. None means no auditing.
audit_ip_address
class-attribute
instance-attribute
¶
Client IP stamped onto every audit event this agent records (the view
fills it from the driving request). None leaves the field unset.
model_settings
class-attribute
instance-attribute
¶
Pydantic-AI ModelSettings (temperature, max_tokens, …).
retries
class-attribute
instance-attribute
¶
Default tool/output retry budget.
toolsets
class-attribute
instance-attribute
¶
Extra Pydantic-AI toolsets composed alongside the registry tools.
capabilities
class-attribute
instance-attribute
¶
Pydantic-AI capabilities passed to the Agent.
tool_guard
class-attribute
instance-attribute
¶
Server-side destructive-tool approval policy. When set and enabled,
build_agent composes a
ToolGuard built from
the registry's destructive tools; None or disabled leaves the agent
ungated.
tool_failure
class-attribute
instance-attribute
¶
What an unhandled tool exception costs. On by default, so a raising tool
fails its own call and the run carries on, with the exception still reaching
the audit logger and the Python logger. A plain record rather than
... | None because there is no third state, and None would have to
mean "on" to keep that default — backwards next to tool_guard.
AttachmentInlineConfig¶
AttachmentInlineConfig
dataclass
¶
Which attachment types read_attachment hands back as file content.
Tunes build_attachment_toolset.
media_types governs the binary attachments, where the choice is
between attaching the bytes for the model to look at and returning a
one-line note about a file it will never see;
AttachmentInlineConfig(media_types=frozenset()) switches inlining off
entirely. A textual attachment is decoded and returned as text without
consulting that allowlist.
max_bytes governs both. A decoded text file reaches the provider and
stays in the run's history exactly as inlined bytes do -- decoding changes
the encoding, not the cost -- so a file over the limit is described rather
than returned whichever branch it takes.
media_types is an allowlist rather than "everything that is not text"
because bytes a provider cannot interpret make it reject the whole request:
a broad rule would trade a model that cannot read your PDF for a run that
does not start. max_bytes sits far below any sane upload cap because what
it bounds is one request, not a stored thread — the bytes ride in a
synthetic user message that stays in the run's history, so every further
model request ships the file again, base64 and all.
None of it reaches the client: the bytes never travel on the event stream,
and a follow-up question about the same file is answered by reading the
attachment again, server-side. The decision is made on
AttachmentRef.mime, which is client-declared, so a mislabelled file costs
a rejected request rather than a disclosure — the store is owner-scoped
either way, and the model only reaches files the acting user uploaded.
media_types
class-attribute
instance-attribute
¶
media_types: frozenset[str] = frozenset(
{"application/pdf", "image/png", "image/jpeg", "image/gif", "image/webp"}
)
Content types whose bytes are attached to the tool result; anything outside the set falls back to the one-line note.
max_bytes
class-attribute
instance-attribute
¶
Largest file, in bytes, returned rather than described -- text and binary
alike. Measured against the bytes the store returns, not the declared
AttachmentRef.size.
AgentDeps¶
AgentDeps
dataclass
¶
Per-run dependencies handed to the agent as ctx.deps.
Pydantic-AI's seam for threading request-scoped values into a run: tools,
toolsets and capabilities read them off RunContext.deps instead of
closing over a request, which is what lets one agent serve many requests.
djangorestframework-pydantic-ai's SpecToolset already reads
ctx.deps.user as its default user extractor, so a run given these deps
binds the acting user with nothing passed at the call site.
For more per-run context, subclass this and build it in your transport's
deps_factory (AGUIServer(deps_factory=...) for django-ag-ui); the
fields below are the ones the framework itself reads. The factory belongs to
the transport rather than to AgentConfig because the deps are per-request
and the config is not — an agent built once serves every run.
user
instance-attribute
¶
The acting Django user (request.user), or None for an
unauthenticated run. Any because it is a Django boundary: a User, an
AnonymousUser, or a project's own model.
Required, with no default. Pydantic-AI types deps as
AgentDepsT = None and never validates it, so nothing downstream would
catch a run built without one. A registry tool does not fail closed the way a
spec tool does: it runs with no user context, audit records it with the
fields it has, and the answer reads like any other. Saying user=None is
one word, and it is a decision rather than an omission.
ip_address
class-attribute
instance-attribute
¶
The client IP this run was driven from, stamped onto every audit event
AuditCapability
records. Per-run, so it belongs here rather than on the capability's
constructor — a transport that closes over an IP has an agent good for one
request. The constructor argument remains the right home for a value that is
genuinely fixed for the endpoint, and an unset run falls back to it.
state
class-attribute
instance-attribute
¶
AG-UI shared state for this run, inbound only.
Present so the deps satisfy StateHandler: the UI adapter validates the
client's RunAgentInput.state into this field, and drops the state with a
UserWarning when the deps type does not match. Nothing emits
STATE_SNAPSHOT / STATE_DELTA back — a tool has to return those itself
as ToolReturn metadata.
None keeps the default transport-agnostic: the adapter passes a raw
mapping through unvalidated. Seed it with a Pydantic model instance to get
validation, which the adapter runs against type(deps.state).
progress
class-attribute
instance-attribute
¶
Where a spec's progress(...) calls go for this run, or None.
A drf-services ProgressReporter: a callable
(progress, *, total, message, meta) -> None. SpecToolset reads this
field by name and forwards it into the dispatch pool, so a service that
declares a progress parameter reports to whatever the caller passed.
Typed structurally rather than as the Protocol, which lives in an optional
dependency.
Nothing here constructs one. Where a report should go is a transport's
decision — an SSE frame, a task record, a log line — and a substrate that
picked one would have chosen a transport it does not own. None is the
honest default and costs nothing: drf-services substitutes its no-op, so a
service declaring progress runs unchanged whether or not anyone is
listening. A transport that wants the reports on the wire supplies the sink
from its deps_factory and emits the events itself.
build_agent¶
build_agent ¶
Build a Pydantic-AI Agent from a registry and an
AgentConfig.
Each registry tool is registered as a plain Pydantic-AI tool, and config
supplies the model, the toolsets and capabilities composed alongside them,
and the policies below. Frontend tools declared in the AG-UI
RunAgentInput are merged by the adapter, not registered here.
Three capabilities are added from config on request: an
AuditCapability that times and
records every tool the agent runs — registry tools and composed toolsets
alike — a ToolGuard that flips
destructive tools to require approval, and a
ToolFailurePolicy, on unless
turned off, so a raising tool fails its own call rather than the whole run.
Each declares its position through
get_ordering and pydantic-ai sorts them, so the list needs no
pre-ordering.
A destructive tool is not confirmed unless a config asks for it. The
approval interrupt exists only when config.tool_guard is enabled, and
even then it reaches a tool only if that tool's source declares the mutation
— a registry @tool(destructive=True), an MCP readOnlyHint of
False, an x-destructive schema stamp, or an explicit
require_approval name. With no tool_guard, every server-side tool
here runs the moment the model calls it. The browser's own confirmation card
is no substitute: it is driven by a client-registered tool's schema and never
sees a tool that executes server-side. A transport describing this agent to
the model should say which of the two it configured.
The agent is typed Agent[AgentDeps, ...], so every run must be given
AgentDeps through deps=.
build_tool_catalog¶
build_tool_catalog ¶
build_tool_catalog(
registry: ToolRegistry,
*,
drf_mcp_server: Any = None,
service_specs: dict[str, Any] | None = None,
) -> list[dict[str, Any]]
The agent's server-tool catalog for the frontend to label tool-call cards.
Server-side tools execute server-side, so their JSON Schema never reaches the
browser and a web component cannot read an x-summary off it. This catalog
is that channel: the component fetches it and maps tool name to label.
Each entry is {"name", "summary", "description"?}. summary is always
present, resolved from @tool(summary=...) for registry tools and from
display_name then title for drf-mcp ones, falling back to a
prettified name. description is carried through when the source has one.
The catalog covers these sources and no others. A tool attached through
a transport's capabilities= / toolsets= is not listed and its card
falls back to a prettified name — a degraded label, not a broken call.
Enumerating those is not possible here: pydantic-ai exposes tool names only
through AbstractToolset.get_tools, which is async and needs a
RunContext, while this runs at configuration time with no run in sight.
Route spec tools through a transport's service_specs= to keep labels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
registry
|
ToolRegistry
|
The |
required |
drf_mcp_server
|
Any
|
A drf-mcp server whose registered tools are appended. |
None
|
service_specs
|
dict[str, Any] | None
|
A |
None
|
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
One entry per tool, in that source order. |
AgentFactoryFn¶
AgentFactoryFn ¶
Bases: Protocol
The escape-hatch signature for a transport's agent_factory= argument.
A callable of this shape fully replaces
build_agent, giving a
project complete control over Agent construction. It receives the
server-side tool registry and the calling transport's own resolved config
record, which is untyped here because this substrate owns no settings
namespace; each transport documents the concrete type its users receive.
The returned agent must be built with deps_type=AgentDeps. Transports
hand every run an
AgentDeps, and that is
how the acting user reaches spec tools and how AG-UI state reaches
deps.state. A factory that omits it produces an agent whose tools see no
user.