API reference¶
Autodoc of the public surface re-exported from django_ag_ui. Everything below
is importable directly, e.g. from django_ag_ui import ToolRegistry.
Registry¶
ToolRegistry ¶
An ordered, named collection of server-side tools.
State lives on the instance: a DjangoAGUIView holds one registry;
tests build a fresh registry per scenario. The registry derives a
JSON Schema for each tool at registration (including the
x-destructive / x-category extensions) and can dispatch sync
or async callables.
register ¶
Register spec and return its binding.
Raises:
| Type | Description |
|---|---|
ValueError
|
when |
call ¶
Dispatch a sync call to the registered tool.
Refuses coroutine functions to avoid silently returning an
un-awaited coroutine. Use :meth:acall for async tools.
acall
async
¶
Dispatch an async call; transparently awaits sync callables.
tool ¶
tool(
registry: ToolRegistry,
*,
name: str | None = None,
description: str | None = None,
destructive: bool = False,
category: ToolCategory = ToolCategory.OTHER,
confirm: str | None = None,
summary: str | None = None,
) -> Callable[[F], F]
Register the decorated callable on registry as a tool.
The tool's name defaults to the function name; its description
defaults to the first paragraph of the function's docstring. Both
can be overridden via the keyword arguments. confirm supplies a
human-readable confirmation prompt for a destructive tool (surfaced as
x-confirm); summary a short display label (surfaced as
x-summary).
build_input_schema ¶
build_input_schema(
fn: Callable[..., Any],
*,
destructive: bool = False,
category: ToolCategory = ToolCategory.OTHER,
confirm: str | None = None,
summary: str | None = None,
) -> dict[str, Any]
Derive a JSON Schema object from fn's parameters.
Supports the primitive types used by the built-in tool surface:
str, int, float, bool, list[T], dict[str, Any],
and X | None unions. Anything richer falls back to an empty
schema fragment (no type constraint), which is still wire-valid JSON
Schema.
The destructive flag is stamped at the schema root as
x-destructive; category as x-category; confirm (when
given) as x-confirm. AG-UI passes these extensions through
verbatim, and frontends read x-destructive / x-confirm to gate
execution behind a confirmation step.
ToolSpec
dataclass
¶
Canonical declaration of a server-side tool.
A ToolSpec bundles the callable with the metadata the registry
needs to expose it to a Pydantic-AI agent and to a frontend: a
stable name, a human-facing description, a destructive risk
flag, and a coarse category.
name
instance-attribute
¶
Stable identifier exposed to the agent. Must be unique within a
:class:~django_pydantic_agent.registry.tool_registry.ToolRegistry.
fn
instance-attribute
¶
The Python callable that implements the tool. Must have typed parameters; the registry derives a JSON Schema from the signature.
description
instance-attribute
¶
User-facing summary shown to the agent. The first line is what most clients display.
destructive
class-attribute
instance-attribute
¶
If True, calling this tool may mutate state. The registry
stamps x-destructive: true into the tool's JSON Schema so
frontends can gate it behind a confirmation step.
category
class-attribute
instance-attribute
¶
Coarse capability grouping. Surfaced as x-category in the
JSON Schema.
confirm
class-attribute
instance-attribute
¶
Optional human-readable confirmation prompt for a destructive tool
(e.g. "Activate this project?"). Stamped as x-confirm so the frontend
can show it instead of a generic "Run
summary
class-attribute
instance-attribute
¶
Optional short label for the tool (e.g. "Query orders"). Stamped as
x-summary so the frontend shows it on the tool-call card instead of the
raw tool name.
ToolBinding
dataclass
¶
A registered tool plus the JSON Schema derived from its signature.
The schema is computed once at registration (including the
x-destructive / x-category extensions) and carried alongside
the spec so tool listings don't re-introspect on every request.
ToolCategory ¶
Bases: str, Enum
Coarse grouping for a tool, surfaced to the agent and the UI.
Categories are advisory metadata: they let a frontend group tools,
let a system prompt reason about capability classes, and let a
project apply category-wide policy. They do not by themselves
gate execution — that is the destructive flag's job.
Skills¶
SkillRegistry ¶
An ordered, named collection of :class:SkillSpecs.
State lives on the instance (like :class:~django_pydantic_agent.registry.tool_registry.ToolRegistry).
:meth:payload produces the JSON-serialisable catalog the frontend consumes
(camelCase keys, optional fields omitted when default).
register ¶
Register spec; raise ValueError if the name is taken.
add ¶
add(
name: str,
title: str,
prompt: str,
*,
description: str | None = None,
send_immediately: bool = False,
chip: bool = False,
) -> SkillSpec
Construct a :class:SkillSpec and register it (convenience).
payload ¶
The client catalog: a list of skill dicts with camelCase keys.
SkillSpec
dataclass
¶
A pre-defined prompt offered to the user (a "skill").
Serialised into the client catalog the frontend surfaces as chips and/or
the /-command palette. Skills are data, not callables — the prompt
is a static string (it may contain {placeholder}s the client fills from
its skill context before sending).
description
class-attribute
instance-attribute
¶
Optional secondary line shown in the palette.
send_immediately
class-attribute
instance-attribute
¶
Send on pick instead of pre-filling the input. Surfaced as
sendImmediately.
chip
class-attribute
instance-attribute
¶
Also surface as a chip (the palette shows all skills regardless).
SkillsView ¶
A read-only endpoint returning a :class:SkillRegistry's client catalog.
A callable instance (like :class:~django_ag_ui.agent.agui_view.DjangoAGUIView)
so it can hold the registry and a project can mount several. GET returns
the JSON skill list the frontend fetches via data-skills-url.
Skill prompts can encode internal workflows — worth gating. The view
carries the same authentication seam as DjangoAGUIView
(require_authenticated / get_user, sync or async hooks), so one
policy can cover the agent endpoint and its catalogs. Defaults stay open
for backwards compatibility — lock the catalog down whenever the agent
endpoint is locked down.
Agent and view¶
AGUIServer ¶
One config object that mounts an AG-UI endpoint and its sub-views.
The Django-idiomatic front door for the package — the admin.site idiom
and the mirror of drf-mcp's MCPServer. Construct it once with the tool
registry (plus optional stores / auth), then mount its namespaced
:attr:urls with include()::
from django_ag_ui import AGUIServer
agent = AGUIServer(registry, require_authenticated=True)
urlpatterns = [
path("agent/", agent.urls),
]
# reverse("ag_ui:endpoint") · "ag_ui:tools" · "ag_ui:threads" · ...
The registry is passed once: the object builds the agent view
(:class:~django_ag_ui.agent.agui_view.DjangoAGUIView) and the read-only
tool catalog (:class:~django_ag_ui.agent.tools_view.ToolsView) from it — no
tools=registry echo. The mount point is the consumer's to choose the
Django way (path("<prefix>", agent.urls)); there is no prefix=.
What gets mounted. The agent endpoint (endpoint) and its tool catalog
(tools) always mount. The persistence sub-views mount only when their
backend is active:
skills— when a :class:~django_ag_ui.skills.skill_registry.SkillRegistryis passed (skills/, GET JSON fordata-skills-url).threads/thread— when the conversation store is not a :class:~django_pydantic_agent.persistence.null_conversation_store.NullConversationStore(threads/+threads/<id>/for the history drawer'sdata-threads-url).attachments/attachment— when the attachment store is not a :class:~django_pydantic_agent.persistence.null_attachment_store.NullAttachmentStore(attachments/+attachments/<id>/for the composer'sdata-attachments-url).transcribe— when the transcription backend is not a :class:~django_ag_ui.persistence.null_transcription_backend.NullTranscriptionBackend(transcribe/for the mic'sdata-transcribe-url).resume/fork/runs— when astep_storeis configured (resume/<run_id>/+fork/<run_id>/seed a new run from a prior run's last continuable snapshot;runs/indexes what may be resumed).
conversation_store / attachment_store / transcription_backend
default to the DJANGO_AG_UI settings-resolved backend (the same one the
agent view persists to), so configuring a store in settings mounts its
sub-view automatically; pass an instance to override. Since the defaults
resolve to the Null* backends, a bare AGUIServer(registry) mounts only
the agent endpoint and its tool catalog — the same surface the old
get_urls(view) produced.
Authentication seam. require_authenticated / get_user /
authorize are forwarded to every view this object builds — the agent
endpoint and all sub-views — so one policy locks down the whole mount
(401 for anonymous when require_authenticated, 403 from an
authorize predicate, get_user establishing the acting user). The
agent view's model / instructions / audit_logger / csrf_exempt
fall back to settings when not passed. Everything defaults open for
backwards compatibility; the endpoints are unauthenticated until you set
these.
Anonymous scoping caveat. With the endpoints left open (the default) and
a model-backed store, an anonymous request has no owner id. The reference
contrib stores refuse anonymous thread / attachment operations unless
ALLOW_ANONYMOUS is set (in which case they bucket per browser session) —
so pass require_authenticated=True (or a get_user hook) whenever the
store persists, rather than relying on owner scoping to isolate anonymous
visitors from one another.
Spec tools. service_specs takes a name -> spec mapping or a
spec registry — drf-services 0.27's SpecRegistry, the single declaration
site for a project exposing the same specs over more than one transport, so
this endpoint reads the same source an MCP server and the HTTP views do::
AGUIServer(registry, service_specs=spec_registry.by_tag("public"))
Either shape is normalised once, here, into a plain dict; a filtered view
(by_tag / subset) is itself a registry, so two endpoints can be given
different projections with no shared state. Requires the
django-ag-ui[spec-tools] extra.
Per-run dependencies. deps_factory is a request -> AgentDeps
callable replacing the default, which binds only the acting user. Use it to
carry project-specific per-run context (a tenant, a feature-flag snapshot)
on an AgentDeps subclass, or to seed AgentDeps.state with a Pydantic
model — the only way to have AG-UI's inbound shared state validated, since
pydantic-ai validates it against type(deps.state). Whatever it returns
reaches every tool, toolset and capability as ctx.deps.
Durable step persistence. step_store is a factory — a
request -> StepStore callable, not a shared store instance, because the
pydantic-ai-harness step-store protocol carries no request, so the store
binds one and is built per run. When set, every run attaches a
StepPersistence capability that records an owner-scoped run / event /
snapshot / tool-effect ledger through that store. Pass
:class:~django_pydantic_agent.contrib.store.default_step_store.DefaultStepStore (its
constructor is the request -> StepStore factory) for the reference
model-backed store, or any such callable. Requires the
django-ag-ui[harness] extra. Configuring it also mounts three owner-scoped
endpoints: resume/<run_id>/ and fork/<run_id>/, which seed a new run
with a prior run's last continuable snapshot, and runs/, which indexes the
user's runs so a client can discover what it may resume. Without that index
a client can only continue a run whose id it still holds — ruling out
resuming after a page reload or from another device, which is most of what
durable persistence is for.
Namespacing. :attr:urls returns the (patterns, app_name, namespace)
triple path() mounts directly (like admin.site.urls — no
include()), so endpoint names are namespaced (namespace, default
"ag_ui") and multiple mounts don't collide — reverse("ag_ui:endpoint").
urls
property
¶
The namespaced (patterns, app_name, namespace) triple path() mounts.
Mounts directly at any prefix — path("agent/", server.urls), no
include() — exactly like admin.site.urls. Reverse the endpoints
within the namespace: reverse("<namespace>:endpoint"),
"<namespace>:tools", "<namespace>:skills", "<namespace>:threads",
"<namespace>:thread", "<namespace>:attachments",
"<namespace>:attachment", "<namespace>:transcribe",
"<namespace>:resume", "<namespace>:fork".
DjangoAGUIView ¶
An async Django view that serves an AG-UI endpoint.
Bridges a Django HttpRequest to Pydantic-AI's AGUIAdapter without
Starlette: it parses the posted RunAgentInput, builds a Pydantic-AI
Agent from the server-side tool registry, and returns a
StreamingHttpResponse of AG-UI events (Server-Sent Events). Frontend
tools declared in the request are merged by the adapter automatically.
The view is a callable instance, so configuration lives on self and a
project can mount several with independent registries. model,
instructions, and audit_logger fall back to the DJANGO_AG_UI
settings when not passed explicitly (tests inject a TestModel).
Authentication is the host's responsibility. Tools (and the drf-mcp
bridge) act as request.user; if your middleware hasn't authenticated the
request, that is AnonymousUser — a data-exposure footgun. Pass
require_authenticated=True to fail closed (401 for unauthenticated
requests), and/or a get_user(request) hook to establish the user (e.g.
from a token) before tools run. get_user may be sync or async; a
sync hook runs off the event loop, so a plain ORM token → User lookup
(Token.objects.select_related("user").get(key=...).user) is fully
supported. A hook that raises propagates as an unhandled error (500) —
return AnonymousUser (or None) for a clean 401 instead.
CSRF: the view defaults to csrf_exempt=True because AG-UI clients
typically authenticate via headers (Bearer / API key), where CSRF does not
apply. If your deployment authenticates with session cookies, pass
csrf_exempt=False and send the CSRF token from the client — tools act
as request.user, so a cookie-auth endpoint without CSRF protection
lets any third-party page drive the agent as the logged-in user
(mitigated, not eliminated, by Django's default SameSite=Lax cookie).
AgentSession ¶
Per-run orchestration between the HTTP transport and the agent.
Owns everything one AG-UI run needs after the transport has authenticated
the request, parsed the RunAgentInput, and built the agent — and before
the response object exists: the AGUIAdapter, the composed event stream
(native → transformed → reasoning-filtered → encoded → disconnect-guarded),
completed-run persistence, and the cancelled-run persist + audit path.
Splitting it from :class:~django_ag_ui.agent.agui_view.DjangoAGUIView
makes the streaming pipeline testable without a StreamingHttpResponse
(drive :meth:stream directly) and keeps the SSE transport swappable —
a future WebSocket transport reuses the session unchanged.
stream ¶
The encoded AG-UI event stream for this run, disconnect-guarded.
Composed by hand (rather than adapter.run_stream) so the session
keeps a reference to the native event stream — the innermost
generator, whose context manager owns the provider's streaming
request. On client disconnect the guard closes it explicitly, then
persists the partial exchange and audits the cancellation.
ToolsView ¶
A read-only endpoint returning the agent's server-tool catalog (GET, JSON).
A callable instance (like :class:~django_ag_ui.agent.agui_view.DjangoAGUIView)
holding the same :class:~django_pydantic_agent.registry.tool_registry.ToolRegistry
the view uses. GET returns the :func:build_tool_catalog list the web
component fetches via data-tools-url to label tool-call cards for
server-side tools (whose schema never reaches the browser).
The catalog names every server tool the agent can wield — an inventory
worth gating. The view carries the same authentication seam as
DjangoAGUIView (require_authenticated / get_user, sync or
async hooks), so one policy can cover the agent endpoint and its
catalogs. Defaults stay open for backwards compatibility — lock the
catalog down whenever the agent endpoint is locked down.
build_agent ¶
Build a Pydantic-AI Agent from a registry and an :class:AgentConfig.
Each registry tool is registered as a plain Pydantic-AI tool. When
config.audit_logger is set, an :class:AuditCapability on the
wrap_tool_execute lifecycle hook times and records every tool the
agent runs — registry tools and composed toolsets (drf-mcp / spec /
attachment / skill tools) alike. Frontend tools declared in the AG-UI
RunAgentInput are merged automatically by the adapter and are not
registered here.
model_settings / retries tune the model; toolsets and
capabilities compose external Pydantic-AI toolsets/capabilities (e.g. an
MCP-client toolset) alongside the registry tools, so the agent can reach
beyond the registered set. tool_guard, when enabled, adds a
:class:ToolGuard that flips destructive tools to require approval.
The agent is typed Agent[AgentDeps, ...], so a run must be given
:class:AgentDeps (the transport builds one per run and passes deps=).
Capabilities are composed order-independently: each declares its
position via get_ordering (audit is outermost, the guard is orthogonal),
and pydantic-ai's CombinedCapability topologically sorts them — so the
list built here needn't be pre-ordered.
AgentConfig
dataclass
¶
Resolved construction parameters for a Pydantic-AI Agent.
Bundles everything :func:~django_pydantic_agent.agent.agent_factory.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 + success/failure
records. None means no auditing (a no-op logger).
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, :func:~django_pydantic_agent.agent.agent_factory.build_agent composes
a :class:~django_pydantic_agent.policy.guard.tool_guard.ToolGuard capability built
from the registry's destructive tools. None (or disabled) leaves the
agent ungated.
AgentFactoryFn ¶
Bases: Protocol
The escape-hatch signature for a transport's agent_factory= argument.
A callable matching this shape fully replaces the built-in
:func:~django_pydantic_agent.agent.agent_factory.build_agent, giving a project
complete control over Pydantic-AI Agent construction (custom model
providers, output types, toolsets, instrumentation, …). It receives the
server-side tool registry and that transport's resolved config object.
config is deliberately untyped here: this substrate reads no settings and
owns no settings namespace, so the second argument is whatever configuration
record the calling transport resolved — AGUIConfig for the AG-UI
transport, its own equivalent for another. Each transport documents the
concrete type its users receive.
The returned agent must be typed Agent[AgentDeps, ...] — build it
with deps_type=AgentDeps. Transports hand every run an
:class:~django_pydantic_agent.agent.types.agent_deps.AgentDeps, and that
is how the acting user reaches spec tools (ctx.deps.user) and how AG-UI
state reaches deps.state. A factory that omits deps_type produces an
agent whose tools see no user.
DEFAULT_SYSTEM_PROMPT
module-attribute
¶
DEFAULT_SYSTEM_PROMPT = "You are an assistant embedded in a web application. You can call tools to read data and to drive the user interface on the user's behalf. Prefer the most specific tool available. For a destructive action, call the tool directly with the right arguments — the interface shows the user an explicit confirmation before it runs, so do NOT ask for confirmation in text or wait for the user to say yes. Briefly state what you are doing. When the user refers to something by name, use a listing or search tool's arguments to find it and then act on the result — don't stop after the lookup. Treat 'open', 'go to', or 'show me' as a request to navigate. Always finish your turn with a short reply or a completed action — never an empty turn. Keep replies concise."
Configuration¶
AGUIConfig
dataclass
¶
An endpoint's resolved scalar configuration.
Every field is already resolved — there is no "unset" state and no
settings lookup left to do. :class:AGUIServer builds one in __init__
(via :func:~django_ag_ui.config.build_ag_ui_config.build_ag_ui_config,
which reads DJANGO_AG_UI) and threads it to the agent view and every
sub-view.
That indirection is the point. Read at request time, these values could only
ever be global, so two AG-UI endpoints in one project could not differ on any
of them — an /internal/agent and a /public/agent were forced to share
one tool-guard policy, one retry budget, one upload cap.
Replaces the old AppSettings, which mixed these scalars with dotted
paths to collaborators. Those are now constructor arguments taking real
objects (toolsets=, capabilities=, conversation_store=, …), so
what remains here is only values.
Do not construct this directly to override a field: a
partially-specified config would silently discard the project's own
DJANGO_AG_UI values. Use build_ag_ui_config(retries=3), which layers
overrides over the settings.
model
instance-attribute
¶
The Pydantic-AI model: a "provider:name" string (e.g.
"anthropic:claude-sonnet-4.6") or a pre-built Model instance. May be
None here; the agent factory raises a clear error if it is still unset
when an agent is actually built.
api_key
instance-attribute
¶
API key handed to the provider when model is a "provider:name"
string, so the key comes from settings rather than the environment. Ignored
when a provider is passed or model is already a Model.
system_prompt
instance-attribute
¶
Override for the agent's default system prompt. None uses
:data:DEFAULT_SYSTEM_PROMPT.
model_settings
instance-attribute
¶
Pydantic-AI ModelSettings (e.g. {"temperature": 0.2}) passed
straight to the Agent. None leaves the model defaults untouched.
retries
instance-attribute
¶
Default tool/output retry budget passed to the Agent. None uses
Pydantic-AI's default.
attachment_max_bytes
instance-attribute
¶
Maximum accepted upload size in bytes (server-authoritative). 0
disables the cap.
attachment_allowed_types
instance-attribute
¶
Allowed (client-declared) content types for uploads. Empty accepts any.
manage_system_prompt
instance-attribute
¶
Who owns the system prompt on the wire: "server" (the agent's prompt
is authoritative and a client-posted system message is ignored) or
"client". instructions are always server-side regardless.
allow_uploaded_files
instance-attribute
¶
Whether UploadedFile references in client-submitted messages are
honoured. False drops them with a warning before they reach the agent.
forward_reasoning
instance-attribute
¶
Whether to forward a reasoning model's chain-of-thought to the client as
AG-UI reasoning events — a pure adapter pass-through, only emitted if a
thinking budget is enabled via model_settings.
transcription_max_bytes
instance-attribute
¶
Maximum accepted audio-clip size in bytes (server-authoritative). 0
disables the cap.
transcription_allowed_types
instance-attribute
¶
Allowed (client-declared) content types for voice clips. Empty accepts any.
thread_list_limit
instance-attribute
¶
Maximum threads the index endpoint returns in one call. A larger
?limit is clamped to this ceiling.
tool_guard
instance-attribute
¶
Server-side destructive-tool approval policy. When enabled, a
ToolGuard capability flips destructive tools to require the AG-UI
approval interrupt.
build_ag_ui_config ¶
build_ag_ui_config(
*,
model: Any = None,
api_key: str | None = None,
system_prompt: str | None = None,
model_settings: dict[str, Any] | None = None,
retries: int | None = None,
attachment_max_bytes: int | None = None,
attachment_allowed_types: tuple[str, ...] | list[str] | None = None,
manage_system_prompt: str | None = None,
allow_uploaded_files: bool | None = None,
forward_reasoning: bool | None = None,
transcription_max_bytes: int | None = None,
transcription_allowed_types: tuple[str, ...] | list[str] | None = None,
thread_list_limit: int | None = None,
tool_guard: ToolGuardConfig | None = None,
) -> AGUIConfig
Resolve an :class:AGUIConfig from DJANGO_AG_UI, applying overrides.
The single place the scalar settings are read. :class:AGUIServer calls this
once in __init__; nothing reads these settings per request, which is what
lets two endpoints in one project hold different values.
Every argument is None by default, meaning "take it from settings". Pass
one to override just that field for this endpoint::
AGUIServer(registry, config=build_ag_ui_config(retries=3))
Use this rather than constructing :class:AGUIConfig directly — it is what
layers your overrides over the project's settings instead of discarding
them.
Policy and audit¶
AuditLogger ¶
Bases: Protocol
Sink for tool-invocation records.
Implementations are free to drop, sample, or forward events. The
package ships a no-op default (NullAuditLogger) and a
logging-backed implementation (LoggingAuditLogger); projects
supply their own by passing it to their transport's audit_logger=
argument.
AuditCapability ¶
Bases: AbstractCapability[Any]
Records every tool execution to an :class:AuditLogger sink.
A Pydantic-AI capability on the wrap_tool_execute lifecycle hook, so it
times and records every tool the agent runs — registry tools, drf-mcp /
spec-toolset bridges, attachment and skill tools alike — where the old
per-tool wrapper saw only the registry.
Recording is non-raising: a sink that throws is caught and logged to the
django_pydantic_agent.audit Python logger, so a broken audit backend degrades to
lost audit records, never a broken agent run.
ip_address / organization_id pre-fill the matching
:class:AuditEvent fields for every event this capability records — the
view passes the client IP; a multi-tenant host can pass its org scope.
get_ordering ¶
Pin audit as the outermost capability in the chain.
Audit is the observability layer: its wrap_tool_execute should
surround every other capability's execution hooks so it records the tool
regardless of what else composes the run. Declaring the position here
(rather than relying on list order at the build_agent call site)
makes composition deterministic once a second capability — e.g.
:class:~django_pydantic_agent.policy.guard.tool_guard.ToolGuard — joins the
chain: pydantic-ai's CombinedCapability topologically sorts by these
constraints, so audit stays outermost no matter the insertion order.
AuditEvent
dataclass
¶
A single tool invocation as seen by the audit logger.
Arguments are stored as a string (typically JSON-encoded) to keep audit records cheap to serialize and to discourage retention of sensitive raw values.
One run-level record rides this shape: when a client disconnects
mid-run (cancel/stop), the view records tool_name="agent.run" with
success=False and an error starting with "cancelled:", so
cancelled runs are distinguishable in audit sinks without widening the
AuditLogger protocol.
organization_id
class-attribute
instance-attribute
¶
Multi-tenant scope of the acting user, when the sink can derive one.
None at this layer — a custom :class:AuditLogger fills it from its
own tenancy model.
target_type
class-attribute
instance-attribute
¶
Kind of domain object the call acted on ("order", "user", …).
None at this layer — tool args are domain-opaque here; a custom sink
that knows its tools can classify them.
target_id
class-attribute
instance-attribute
¶
Identifier of the acted-on object, paired with target_type.
ip_address
class-attribute
instance-attribute
¶
Client IP of the request that drove the run, when the view knows it.
NullAuditLogger ¶
Discards every event. The default when no audit logger is configured.
LoggingAuditLogger ¶
Writes audit events to the Python logging framework.
Successful invocations log at INFO; failures log at WARNING
with the error message included.
Conversation persistence¶
ConversationStore ¶
Bases: Protocol
Pluggable server-side persistence for AG-UI conversations.
Passed to AGUIServer(conversation_store=...). The package ships a no-op
default (NullConversationStore — the server stays stateless) and a
session-backed implementation; projects supply their own (a DB model, Redis,
…). All methods are async so an implementation can use the async ORM or a
network backend.
Threads key by (owner_id, thread_id), so two endpoints sharing a store
share one user's thread list. Wrap with
:class:~django_pydantic_agent.persistence.scoped_conversation_store.ScopedConversationStore
to partition them.
list returns owner-scoped metadata only (no message bodies) for the
thread drawer, capped at limit rows (None = the store's own default);
a store that can't enumerate (the stateless default) returns an empty list.
exists is a cheap owner-scoped presence check — no message body loaded —
so a rename / probe doesn't deserialize a whole thread just to 404. rename
sets a thread's display title (a store that can't persist one is a no-op).
Conversation
dataclass
¶
A persisted conversation, keyed by thread_id.
messages are JSON-serialisable message records whose shape the calling
transport owns — this substrate persists and returns them verbatim and
never interprets them. The AG-UI transport stores its own wire Message
shape (so client message ids survive a round trip untouched); another
transport stores its own. That is what keeps the storage contract neutral:
the core supplies owner scoping, async plumbing and the models, while the
message vocabulary stays with whoever speaks it.
owner_id scopes the conversation to a user for authorization.
ConversationMeta
dataclass
¶
Lightweight metadata for one conversation — the thread-drawer row shape.
Returned by :meth:ConversationStore.list <django_pydantic_agent.ConversationStore>
so a thread list stays cheap: it carries no message bodies. title
defaults to a truncation of the first user message (unless a store records an
explicit rename); preview is a one-line excerpt of the latest message;
updated_at is when the conversation last changed, or None when the
store doesn't track it. owner_id scopes the conversation to a user and is
not surfaced on the wire.
NullConversationStore ¶
The default store: no-op, keeping the server stateless.
load always returns None and save / delete do nothing, so
the conversation lives entirely in the client's posted history (today's
behaviour). The view treats this store as "persistence off".
ScopedConversationStore ¶
Partition another :class:ConversationStore by a scope name.
Stores key threads by (owner_id, thread_id). Two AG-UI endpoints sharing
one store therefore share one user's thread list: a conversation started at
/internal/agent appears in /public/agent's history drawer and can be
resumed there — under the public agent's model, tools and guard policy.
Wrapping fixes that without a migration::
internal = AGUIServer(
registry,
conversation_store=ScopedConversationStore(store, scope="internal"),
)
public = AGUIServer(
registry,
conversation_store=ScopedConversationStore(store, scope="public"),
)
Prefixing the thread id is deliberate. A scope column would mean a
migration and a breaking change to the
:class:~django_pydantic_agent.persistence.types.conversation_store.ConversationStore
protocol — which every custom store implements — for a partition the id space
already expresses. This composes with any implementation, third-party ones
included.
Opt in explicitly. :class:AGUIServer deliberately does not wrap by
itself: doing so from its namespace would silently orphan the whole
thread history of an existing single-endpoint project the moment it set one.
The scope is invisible on the wire — thread ids are echoed back to the client unchanged; only the storage key carries the prefix.
list
async
¶
This scope's threads only, with storage keys translated back.
limit is applied by the inner store before this filter, so a busy
sibling scope can crowd out rows. Acceptable for a drawer that is capped
anyway; a store that needs exact per-scope paging should partition at the
query, not by wrapping.
DjangoSessionConversationStore ¶
Conversation persistence in the Django session (no migration).
Conversations are namespaced by thread_id within the logged-in user's
session, so scoping to the user is implicit and durability spans that
user's browser session. The batteries-included server-side store; for
cross-device or audited persistence, supply a model-backed store instead.
ModelConversationStore ¶
Bases: ABC
Abstract base for a model-backed (or any sync) ConversationStore.
Provides the async wrapping and per-request owner scoping; a subclass implements the three synchronous row operations against its own Django model (cross-device, auditable persistence). Kept model-agnostic on purpose — the package ships no concrete model so it forces no migration; consumers define the model, its fields, and the owner relationship.
Example::
class MyStore(ModelConversationStore):
def _fetch(self, thread_id, owner_id):
row = MyConversation.objects.filter(
thread_id=thread_id, owner_id=owner_id,
).first()
return None if row is None else Conversation(...)
def _store(self, conversation, owner_id): ...
def _remove(self, thread_id, owner_id): ...
ThreadsView ¶
Owner-scoped thread index endpoint for the chat-history drawer (async, JSON).
Mounted by :func:~django_ag_ui.get_urls with threads=<store> over the
same :class:~django_ag_ui.ConversationStore the agent view uses:
GET <prefix>threads/→ the user's threads, metadata only ({"threads": [...]});GET <prefix>threads/<id>/→ that thread's messages ({"thread_id", "messages"});PATCH <prefix>threads/<id>/→ rename (body{"title": "..."});DELETE <prefix>threads/<id>/→ delete the thread (204).
Every operation is scoped to the acting user: the store filters by owner, so
a thread owned by another user simply isn't found (404) — never another
user's history. The view carries the same authentication seam as
:class:~django_ag_ui.DjangoAGUIView (require_authenticated /
get_user, sync or async); defaults stay open for parity with the catalog
views, so lock it down whenever the agent endpoint is locked down.
Reference store (opt-in)¶
The django_pydantic_agent.contrib.store app ships a ready-to-use durable store. Add it
to INSTALLED_APPS, run migrate, then set
conversation_store= to
django_pydantic_agent.contrib.store.default_conversation_store.DefaultConversationStore.
Projects that don't opt in get no model and no migration.
DefaultConversationStore ¶
Bases: ModelConversationStore
A ready-to-use model-backed store over :class:StoredConversation.
The batteries-included durable store: cross-device, per-user history with a
cheap thread list. Enable it by adding "django_pydantic_agent.contrib.store" to
INSTALLED_APPS, running migrate, and passing an instance to your
transport's conversation_store= argument. For a bespoke schema, subclass
:class:ModelConversationStore instead.
Owner scoping: every query filters by the owner_id the
ModelConversationStore base resolves — the authenticated user's pk, or a
per-browser anon:<session_key> bucket when
the store is built with allow_anonymous=True (otherwise anonymous requests are
refused rather than sharing one "" bucket). The unique
(owner_id, thread_id) constraint holds regardless. Titles are derived
from the first user message at first save and then left alone except by
:meth:_rename; preview re-derives on every save.
StoredConversation ¶
Bases: Model
The reference durable conversation row for a model-backed store.
One row per (owner_id, thread_id). messages holds the AG-UI message
list as JSON (the same shape every store round-trips); title and
preview are denormalised so the thread drawer's list query never loads
message bodies, and updated_at orders it. owner_id is the resolved
owner (the user's pk, or an anon:<session_key> bucket under
ALLOW_ANONYMOUS) — the store always filters by it, the security boundary.
Used by
:class:~django_pydantic_agent.contrib.store.default_conversation_store.DefaultConversationStore.
Run lineage (run_id / step ledger) is intentionally out of scope here;
a future durability layer can extend the schema.
File uploads¶
AttachmentStore ¶
Bases: Protocol
Pluggable server-side storage for files a user attaches to a conversation.
Passed to AGUIServer(attachment_store=...). The package ships a
no-op default (:class:~django_pydantic_agent.NullAttachmentStore — uploads off) and
an abstract model-backed base
(:class:~django_pydantic_agent.ModelAttachmentStore); the opt-in
django_pydantic_agent.contrib.store app provides a ready
:class:~django_pydantic_agent.contrib.store.default_attachment_store.DefaultAttachmentStore
that keeps bytes in Django Storage (so S3 etc. come free via
STORAGES/DEFAULT_FILE_STORAGE) and metadata in a row.
Every method is async and owner-scoped: a store filters by the acting
user so one user can never read or delete another's files — the security
boundary for the whole feature. save validates nothing about size/type
itself (the view does, from its config); it just persists the bytes and
returns a durable :class:AttachmentRef. open returns None for a
missing or cross-owner id rather than raising, so callers map it to a 404.
Unlike conversations there is no scoped wrapper for attachments, and none is needed: they are id-referenced with no enumeration, and already owner-scoped, so two endpoints sharing a store expose nothing across the user boundary. Thread lists are the case that leaks.
AttachmentRef
dataclass
¶
A durable, lightweight reference to one uploaded file.
What an upload returns and what travels on the wire — never the bytes.
A user drops a file into the composer, it is uploaded out-of-band to the
:class:~django_pydantic_agent.persistence.attachments_view.AttachmentsView, and the
server hands back this ref; the client holds it on the message and the agent
reads the actual bytes server-side via the read_attachment tool. Keeping
the AG-UI message stream free of base64 mirrors how the tool catalog keeps
schemas off the wire.
id is the opaque, owner-scoped handle the store resolves back to bytes;
mime is the declared content type (client-supplied, so treat it as a
hint); size is the byte count; url is an optional direct fetch URL
(e.g. the owner-checked download endpoint) and stays None unless a store
fills it in.
OpenedAttachment
dataclass
¶
An attachment's metadata paired with a readable byte stream.
Returned by :meth:AttachmentStore.open
<django_pydantic_agent.persistence.types.attachment_store.AttachmentStore> so the
download view and the read_attachment tool both get the content and the
:class:AttachmentRef (name / mime / size) in a single owner-scoped call.
content is an open, readable binary stream (a file handle), not the
whole bytes — so a large attachment streams out via FileResponse rather
than being buffered in memory. The consumer owns it: the download view
hands it to FileResponse (which closes it) and the tool reads it under a
with block. Read it exactly once.
NullAttachmentStore ¶
The default attachment store: uploads disabled, server stays stateless.
save is never reached — the
A transport's attachments view detects this store and returns 410 Gone so a
misconfigured client gets a clear "uploads are off" signal instead of a
silent success. open returns None (every fetch is a 404) and
delete is a no-op, so the endpoint is inert until a real store is
configured. save still raises if called directly, to fail loudly rather
than fabricate a ref.
ModelAttachmentStore ¶
Bases: ABC
Abstract base for a model-backed (or any sync) AttachmentStore.
Provides the async wrapping and per-request owner scoping — the same shape as
:class:~django_pydantic_agent.ModelConversationStore — so a subclass implements
three synchronous operations against its own storage (a Django Storage
for the bytes, a model row for the metadata). Kept model-agnostic on purpose:
the package ships no concrete model so it forces no migration; the opt-in
django_pydantic_agent.contrib.store app supplies one.
Each _save / _open / _remove receives the resolved owner_id
(None for anonymous) and must filter by it so files never cross users.
Example::
class MyStore(ModelAttachmentStore):
def _save(self, upload, owner_id):
row = MyAttachment.objects.create(owner_id=owner_id or "", ...)
row.file.save(row.attachment_id, upload, save=True)
return AttachmentRef(id=row.attachment_id, name=..., mime=..., size=...)
def _open(self, attachment_id, owner_id): ...
def _remove(self, attachment_id, owner_id): ...
AttachmentsView ¶
Owner-scoped file-upload + download endpoint (async, multipart/JSON).
Mounted by :func:~django_ag_ui.get_urls with attachments=<store> over
an :class:~django_pydantic_agent.persistence.types.attachment_store.AttachmentStore:
POST <prefix>attachments/→ multipart upload under thefilefield; validates size/type fromDJANGO_AG_UIsettings, persists the bytes, and returns201with the :class:AttachmentRefJSON ({"id", "name", "mime", "size", "url"?}) — a durable ref, not bytes.GET <prefix>attachments/<id>/→ stream the bytes back (owner-checked) for preview/download; missing or cross-owner →404.DELETE <prefix>attachments/<id>/→ drop the attachment (204).
Every operation is scoped to the acting user: the store filters by owner, so
one user's id can never resolve another's file. Downloads are served as an
attachment with X-Content-Type-Options: nosniff so an uploaded
text/html can't execute as a same-origin page. The view carries the same
authentication seam as :class:~django_ag_ui.DjangoAGUIView
(require_authenticated / get_user); defaults stay open for parity
with the catalog views, so lock it down whenever the agent endpoint is.
With the default :class:NullAttachmentStore an upload returns 410 (off):
mount the view with a real store to enable it.
Reference attachment store (opt-in)¶
The same django_pydantic_agent.contrib.store app ships a ready-to-use durable file
store. With the app installed and migrated, set
attachment_store= to
django_pydantic_agent.contrib.store.default_attachment_store.DefaultAttachmentStore. The
bytes go to Django Storage (S3/GCS via STORAGES / DEFAULT_FILE_STORAGE);
projects that don't opt in get no model and no migration.
DefaultAttachmentStore ¶
Bases: ModelAttachmentStore
A ready-to-use model-backed store over :class:StoredAttachment.
The batteries-included durable file store: bytes live in Django Storage
(filesystem by default, S3/GCS via STORAGES), metadata in a row. Enable
it by adding "django_pydantic_agent.contrib.store" to INSTALLED_APPS, running
migrate, and passing an instance to your transport's
attachment_store= argument. For a bespoke schema, subclass
:class:ModelAttachmentStore.
Owner scoping: every query filters by the owner_id the
ModelAttachmentStore base resolves — the authenticated user's pk, or a
per-browser anon:<session_key> bucket when
the store is built with allow_anonymous=True (otherwise anonymous requests are
refused rather than sharing one "" bucket) — so one user's id never
resolves another's file. The unique (owner_id, attachment_id) constraint
holds regardless. The public attachment_id is an opaque UUID, kept
separate from the storage filename.
StoredAttachment ¶
Bases: Model
The reference durable attachment row for a model-backed store.
The opaque attachment_id is the handle the wire ref carries; file
holds the bytes via Django Storage (so S3 etc. come free through
STORAGES/DEFAULT_FILE_STORAGE), while name / mime / size
are the denormalised metadata returned without reading the file back.
owner_id is the resolved owner (the user's pk, or an anon:<session_key>
bucket under ALLOW_ANONYMOUS) — the store always filters by it, the
security boundary. thread_id optionally ties an
attachment to one conversation; it is left blank when a file is uploaded
before a thread exists, so attachments never depend on a conversation row.
Used by
:class:~django_pydantic_agent.contrib.store.default_attachment_store.DefaultAttachmentStore.
Voice input¶
TranscriptionBackend ¶
Bases: Protocol
Pluggable server-side speech-to-text for the composer's voice input.
Resolved from DJANGO_AG_UI["TRANSCRIPTION_BACKEND"]. The package ships a
no-op default (:class:~django_ag_ui.NullTranscriptionBackend — voice off)
and an opt-in reference implementation over an OpenAI-compatible
/audio/transcriptions endpoint
(:class:~django_ag_ui.contrib.transcription.openai_transcription_backend.OpenAITranscriptionBackend).
The single method is async and receives the acting request so a backend
can scope by user / rate-limit / bill per principal. Unlike
:class:~django_pydantic_agent.persistence.types.attachment_store.AttachmentStore,
transcription keeps no durable artifact: the recorded audio is transcribed
and the text returned in one shot (the composer drops it into the textarea),
so there is nothing to open or delete. transcribe validates
nothing about size/type itself (the view does, from settings); it just turns
audio bytes into text.
NullTranscriptionBackend ¶
The default transcription backend: voice input disabled.
transcribe is never reached through the endpoint — the
:class:~django_ag_ui.persistence.transcribe_view.TranscribeView detects
this backend and returns 410 Gone so a misconfigured client gets a clear
"voice is off" signal instead of a silent failure. transcribe still
raises if called directly, to fail loudly rather than fabricate a transcript.
The endpoint is inert until a real backend is configured via
DJANGO_AG_UI["TRANSCRIPTION_BACKEND"].
TranscribeView ¶
Owner-scoped speech-to-text endpoint (async, multipart in / JSON out).
Mounted by :func:~django_ag_ui.get_urls with transcribe=<backend> over
a :class:~django_ag_ui.persistence.types.transcription_backend.TranscriptionBackend:
POST <prefix>transcribe/→ multipart audio under theaudiofield; validates size/type fromDJANGO_AG_UIsettings, runs the backend, and returns200with{"text": "<transcript>"}.
The audio is transcribed and discarded — nothing is stored — so unlike
:class:~django_ag_ui.persistence.attachments_view.AttachmentsView there is
no download/delete route. The view carries the same authentication seam as
:class:~django_ag_ui.DjangoAGUIView (require_authenticated /
get_user); defaults stay open for parity with the other endpoints, so
lock it down whenever the agent endpoint is.
With the default :class:NullTranscriptionBackend a request returns 410
(off): mount the view with a real backend to enable it.
Reference transcription backend (opt-in)¶
A ready-to-use backend over any OpenAI-compatible /audio/transcriptions
endpoint. Install the [openai] extra and set
transcription_backend= to
django_ag_ui.contrib.transcription.openai_transcription_backend.OpenAITranscriptionBackend;
subclass it to change the model or point at another OpenAI-compatible server.
OpenAITranscriptionBackend ¶
A ready-to-use transcription backend over an OpenAI-compatible API.
The batteries-included voice backend: it forwards the recorded clip to an
OpenAI /audio/transcriptions endpoint and returns the text. Enable it by
installing the [openai] extra and pointing
DJANGO_AG_UI["TRANSCRIPTION_BACKEND"] at this class's dotted path
(django_ag_ui.contrib.transcription.openai_transcription_backend.OpenAITranscriptionBackend).
Self-configuring so :func:~django_ag_ui.resolve_transcription_backend can
instantiate it with no arguments: the API key comes from the OPENAI_API_KEY
environment variable (the SDK default). Override the model or point at an
OpenAI-compatible server (Azure OpenAI, a local Whisper server, Groq, …) by
subclassing and setting the class attributes::
class GroqTranscription(OpenAITranscriptionBackend):
model = "whisper-large-v3"
base_url = "https://api.groq.com/openai/v1"
The openai SDK is imported lazily inside :meth:transcribe so the base
package keeps it an optional dependency (the [openai] extra).
Internal helpers¶
These are not part of the public re-export surface but are referenced from the guides.
build_model ¶
Build a Pydantic-AI model from a "provider:name" string + explicit key.
Delegates the provider: prefix → Model-class resolution to Pydantic-AI's
own :func:infer_model, supplying a provider_factory that injects the
credentials instead of letting Pydantic-AI read them from the environment:
provider(aProviderinstance) takes precedence — used as-is, so it can carry a custombase_url/ client.- otherwise
api_keyis passed to the prefix's defaultProviderclass (resolved via :func:infer_provider_class).
Because the prefix map lives in Pydantic-AI, every provider it knows works
automatically (anthropic, openai, openai-responses, google,
groq, bedrock, …) — there is no hand-maintained table to drift out
of date.
A bare model name Pydantic-AI can map to a provider (e.g. claude-… →
anthropic) is accepted too; only when the provider can't be resolved at all
is an error raised.
Raises:
| Type | Description |
|---|---|
ImproperlyConfigured
|
when the model's provider can't be resolved — an
unknown / uninferable prefix, or the matching provider extra not
installed. Pass |
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 (the @tool registry, and drf-mcp tools when a
drf_mcp_server is passed) execute server-side, so their
JSON Schema never reaches the browser — the web component can't read an
x-summary off it. This catalog is the channel for those labels: the
component fetches it via data-tools-url and maps tool name → label.
Each entry is {"name", "summary", "description"?}. summary is always
present, resolved from the single source of truth with a fallback chain:
- registry tools →
@tool(summary=…)→ a prettified name; - drf-mcp tools →
display_name→title→ a prettified name.
description (a longer blurb for tooltips) is included when available
(ToolSpec.description / drf-mcp display_description → description).
Registry tools win on name collisions.
DRFMCPToolset ¶
Bases: AbstractToolset[Any]
Exposes a drf-mcp MCPServer's tools as a Pydantic-AI toolset.
Built per request so the acting user is the current AG-UI user. Tool
schemas and execution both route through drf-mcp's public in-process
surface (MCPServer.list_tools / acall_tool), so the advertised
parameters, serializer validation, and permissions match the HTTP transport
exactly. Tool definitions carry the default kind="function" — the
in-process kind the run loop routes into call_tool, which then emits a
TOOL_CALL_RESULT and lets the model continue (an external tool
would instead be deferred to the client and never run).
exclude_names carries the @tool registry's names: on a collision
the registry tool wins (the same rule build_tool_catalog applies) and
the drf-mcp twin is skipped — otherwise pydantic-ai raises UserError
for the duplicate name at run time.
max_retries is each tool's retry budget: how many times a
:class:pydantic_ai.ModelRetry (malformed arguments, a service-raised
validation error) is fed back to the model before the run aborts. Defaults
to 1, matching pydantic-ai's own function-tool default.
get_tools
async
¶
Load tool defs from drf-mcp's tools/list once, then wrap them.
Loading runs in a thread (sync_to_async) because the sync
list_tools may evaluate per-user listing permissions against the DB,
which Django forbids on the async event loop.
build_attachment_toolset ¶
Build a per-request toolset exposing read_attachment over store.
Mirrors the per-request drf-mcp bridge: the request is captured in a
closure so store.open is owner-scoped to the acting user — the model can
only read files that user uploaded, never another's by id. Wired in by a
transport's agent build when an attachment store is configured, so the wire
stays protocol-vanilla: attachments travel as lightweight refs and the bytes
are reached here, server-side, only when the model asks.