Configuration¶
Configuration comes in two halves, split on a simple line: objects are passed, values are settings.
Collaborators are constructor arguments. Toolsets, capabilities, stores, an
agent factory, a drf-mcp server — you build them and hand them to AGUIServer:
# urls.py
agent = AGUIServer(
registry,
toolsets=[weather_toolset],
conversation_store=DjangoSessionConversationStore(),
drf_mcp_server=internal_mcp,
)
urlpatterns = [path("agent/", agent.urls)]
Scalars live in a DJANGO_AG_UI dict, read once, when the server is
built — never per request. Every key is optional.
Override a scalar for one endpoint with
build_ag_ui_config, which layers your
values over the settings:
Why the split
Read on every request, these could only ever be global — so an
/internal/agent and a /public/agent were forced to share one tool-guard
policy, one retry budget, one toolset list. And a dotted path to a
collaborator only ever existed because settings.py cannot hold a live
object; urls.py can. That is also why drf_mcp_server=internal_mcp is
expressible at all — with one global path there was no way to say which
agent bridges to which MCP server. See
Multiple endpoints.
| Key | Type | Default | Purpose |
|---|---|---|---|
MODEL |
str |
None |
Pydantic-AI model string (or pre-built Model). |
API_KEY |
str |
None |
Explicit provider key (builds the model via build_model). |
SYSTEM_PROMPT |
str |
None |
Override the agent's instructions. |
MODEL_SETTINGS |
dict |
None |
Pydantic-AI ModelSettings. |
RETRIES |
int |
None |
Tool/output retry budget. |
THREAD_LIST_LIMIT |
int |
200 |
Max threads the index endpoint returns per call; ?limit=N requests fewer, a larger value is clamped down. |
ATTACHMENT_MAX_BYTES |
int |
10485760 |
Max accepted upload size in bytes (0 disables the cap). |
ATTACHMENT_ALLOWED_TYPES |
tuple[str, ...] |
() |
Allowed upload content types (empty = any). |
MANAGE_SYSTEM_PROMPT |
str |
"server" |
Who owns the system prompt on the wire. |
ALLOW_UPLOADED_FILES |
bool |
False |
Honour UploadedFile refs in client messages. |
FORWARD_REASONING |
bool |
True |
Forward a reasoning model's thoughts to the client (False strips them). |
TRANSCRIPTION_MAX_BYTES |
int |
26214400 |
Max accepted audio-clip size in bytes (0 disables the cap). |
TRANSCRIPTION_ALLOWED_TYPES |
tuple[str, ...] |
() |
Allowed audio content types (empty = any). |
ALLOW_ANONYMOUS |
bool |
False |
Default for ModelConversationStore(allow_anonymous=) / ModelAttachmentStore(allow_anonymous=) — a store policy (see Authentication & anonymous scoping). |
TOOL_GUARD |
dict |
{} |
Server-side destructive-tool approval gate (off by default). See TOOL_GUARD. |
Every one of these is also a build_ag_ui_config(...) keyword.
Collaborators (constructor arguments)¶
| Argument | Purpose |
|---|---|
toolsets=[...] |
Extra Pydantic-AI toolsets. |
capabilities=[...] |
Pydantic-AI capabilities. |
agent_factory=fn |
Escape hatch replacing build_agent. |
audit_logger=... |
AuditLogger implementation. |
provider=... |
Explicit Pydantic-AI Provider; takes precedence over API_KEY. |
conversation_store=... |
Server-side conversation persistence. |
attachment_store=... |
Server-side file-upload storage (uploads off when unset). |
transcription_backend=... |
Speech-to-text backend for voice input (voice off when unset). |
drf_mcp_server=... |
drf-mcp server whose tools the agent gets. |
service_specs={...} |
drf-services specs mapping exposed as tools, no MCP hop ([spec-tools] extra). |
Multiple endpoints¶
Two AG-UI endpoints in one project, each with its own agent, tools and policy:
# urls.py
internal = AGUIServer(
internal_registry,
namespace="internal-agent",
drf_mcp_server=internal_mcp,
conversation_store=ScopedConversationStore(store, scope="internal"),
config=build_ag_ui_config(tool_guard=ToolGuardConfig(enabled=True)),
)
public = AGUIServer(
public_registry,
namespace="public-agent",
conversation_store=ScopedConversationStore(store, scope="public"),
)
urlpatterns = [
path("internal/agent/", internal.urls),
path("public/agent/", public.urls),
]
ScopedConversationStore is what keeps
their thread histories apart: stores key by (owner_id, thread_id), so without
it a conversation started at /internal/agent shows up in /public/agent's
drawer — and resumes there under the public agent's model and tools. It is
opt-in on purpose: wrapping automatically would silently orphan an existing
project's history.
MODEL¶
A Pydantic-AI model string, e.g. "anthropic:claude-sonnet-4.6", or a
pre-built Pydantic-AI Model instance (which passes through untouched).
Optional in settings, but an agent cannot be built without a model: if MODEL
is unset and no model= is passed to
DjangoAGUIView, the view raises
django.core.exceptions.ImproperlyConfigured. A model= argument to the view
always wins over this setting.
When API_KEY or PROVIDER is set, the MODEL string
is routed through build_model,
which delegates the provider: prefix → model resolution to Pydantic-AI itself —
so any provider it knows works (anthropic, openai, openai-responses,
google, google-gla, groq, bedrock, …), as does a bare model name it can
map to a provider (e.g. claude-…). When the provider can't be resolved the view
raises ImproperlyConfigured (set PROVIDER instead). A pre-built Model
instance ignores API_KEY / PROVIDER and is used as-is.
API_KEY¶
An explicit provider API key. When set (and MODEL is a provider:name
string), the view builds the model via
build_model — passing the
key to the prefix's default Provider — instead of letting Pydantic-AI
infer the key from environment variables. Requires the matching provider extra
installed (e.g. django-ag-ui[anthropic]). PROVIDER, if also
set, takes precedence over API_KEY.
DJANGO_AG_UI = {
"MODEL": "anthropic:claude-sonnet-4.6",
"API_KEY": os.environ["ANTHROPIC_API_KEY"],
}
provider=¶
A Pydantic-AI Provider instance, passed straight to the constructed model. Use it when you need a custom
base_url or HTTP client (a proxy, a gateway, a self-hosted endpoint). It
takes precedence over API_KEY. As with API_KEY, the matching
provider extra must be installed.
DJANGO_AG_UI = {"MODEL": "openai:gpt-4o"}
# urls.py
AGUIServer(registry, provider=OpenAIProvider(base_url="https://gateway.example"))
audit_logger=¶
An AuditLogger instance. Omitted (the default)
means NullAuditLogger — no auditing. Pass
LoggingAuditLogger or your own:
Because you construct it, a logger that needs constructor arguments just works — the old dotted-path form required one that was importable with no arguments at all.
SYSTEM_PROMPT¶
Overrides the agent's instructions. When unset, the view uses
DEFAULT_SYSTEM_PROMPT. An instructions=
argument to the view takes precedence over both.
MODEL_SETTINGS¶
A Pydantic-AI ModelSettings dict (e.g. {"temperature": 0.2, "max_tokens":
1024}) passed straight to the Agent. None leaves model defaults untouched.
DJANGO_AG_UI = {
"MODEL": "anthropic:claude-sonnet-4.6",
"MODEL_SETTINGS": {"temperature": 0.2, "max_tokens": 1024},
}
RETRIES¶
The default tool/output retry budget passed to the Agent. None uses
Pydantic-AI's own default.
agent_factory=¶
The escape hatch. A callable matching
AgentFactoryFn —
(registry: ToolRegistry, config: AGUIConfig) -> Agent — that fully
replaces the built-in build_agent. Use it for
custom model providers, output types, instrumentation, or toolset wiring the
sugar arguments do not cover. When passed, the view hands construction entirely
to your factory and does not apply MODEL_SETTINGS, RETRIES, toolsets,
capabilities, or the drf-mcp bridge itself — your factory owns all of it.
# myproject/agent.py
from pydantic_ai import Agent
def build_my_agent(registry, config):
return Agent(model="anthropic:claude-sonnet-4.6") # …plus your own wiring
# urls.py
AGUIServer(registry, agent_factory=build_my_agent)
toolsets=¶
Extra Pydantic-AI toolsets composed alongside the registry tools — e.g. an
MCP-client toolset. Empty by default. (Ignored when agent_factory= is passed.)
Two endpoints can now hold different toolsets — the point of the change. With a
single global TOOLSETS setting, a public agent necessarily carried the internal
one's tools.
capabilities=¶
Pydantic-AI capabilities passed to the Agent. Empty by default. (Ignored when
agent_factory= is passed.)
MANAGE_SYSTEM_PROMPT¶
Who owns the system prompt on the AG-UI wire: "server" (the default — the
agent's configured prompt is authoritative and a client-posted system message
is stripped before it reaches the model) or "client" (the client-supplied
system message is honoured). instructions are always injected server-side
regardless. Client-submitted history is additionally passed through
Pydantic-AI's sanitize_messages hardening on every run.
ALLOW_UPLOADED_FILES¶
Whether UploadedFile references in client-submitted messages are honoured.
False (the default) drops them with a warning before the messages reach the
model — an UploadedFile is fetched by the model provider using the server's
credentials, so it should only be accepted from trusted clients. The
file-upload flow is unaffected either way: it
travels server-issued refs in message text, not AG-UI file parts.
conversation_store=¶
A ConversationStore instance. Omitted (the
default) keeps the server stateless using
NullConversationStore — the conversation
lives entirely in the client's posted history.
The package ships
DjangoSessionConversationStore
(session-backed, no migration) and an abstract
ModelConversationStore base you can
subclass with your own model. See
Conversation persistence.
For a ready-made durable, cross-device store, opt into the
django_pydantic_agent.contrib.store app instead of writing your own model — add it to
INSTALLED_APPS, run migrate, and point the setting at its store:
from django_pydantic_agent.contrib.store.default_conversation_store import (
DefaultConversationStore,
)
AGUIServer(registry, conversation_store=DefaultConversationStore())
The base package ships no model, so projects that don't opt in get no migration.
When an active (non-Null) store is passed, AGUIServer mounts the
thread-history drawer endpoints automatically — see
Thread history.
attachment_store=¶
A dotted path to an AttachmentStore class,
An AttachmentStore instance. Omitted (the
default) keeps uploads disabled using
NullAttachmentStore — the upload endpoint
answers 410 Gone.
When a store is set, the view wires a per-request read_attachment(attachment_id)
tool onto the agent, scoped to the acting user, so the model can read the bytes a
user attached — the AG-UI message stream stays free of file payloads (uploads go
out-of-band and travel as lightweight refs). A consumer that registers its own
read_attachment tool keeps it (registry tools win).
The package ships an abstract
ModelAttachmentStore base you can subclass
with your own model + storage. For a ready-made store, opt into the
django_pydantic_agent.contrib.store app — add it to INSTALLED_APPS, run migrate, and
point the setting at its store (bytes go to Django Storage, so S3/GCS come free
via STORAGES / DEFAULT_FILE_STORAGE):
from django_pydantic_agent.contrib.store.default_attachment_store import (
DefaultAttachmentStore,
)
AGUIServer(registry, attachment_store=DefaultAttachmentStore())
When an active (non-Null) store is passed, AGUIServer mounts the upload
endpoints over HTTP automatically — see
File uploads.
ATTACHMENT_MAX_BYTES¶
The maximum accepted upload size in bytes, enforced server-side by
AttachmentsView (an oversize upload → 413).
Defaults to 10485760 (10 MiB); set 0 to disable the cap. Client-side checks
in the web component are a UX nicety — this is the authoritative limit.
ATTACHMENT_ALLOWED_TYPES¶
A tuple of allowed (client-declared) content types for uploads, e.g.
("image/png", "image/jpeg", "application/pdf", "text/plain"). Empty (the
default) accepts any type; otherwise an upload whose Content-Type is not listed
is rejected with 415. The content type is client-declared, so treat this as a
coarse filter — the store decides what to do with the bytes.
FORWARD_REASONING¶
Whether a reasoning model's chain-of-thought is forwarded to the client. When
True (the default), the AG-UI reasoning events Pydantic-AI emits for a
ThinkingPart pass straight through to the browser (where the web component can
render a collapsible "thinking" region). Set False to let the model reason
privately — the events are stripped from the stream before encoding, so the
chain-of-thought never leaves the server.
Reasoning is only emitted when the model is actually configured to think, which
is a MODEL_SETTINGS concern, not a separate switch. For an Anthropic model:
DJANGO_AG_UI = {
"MODEL": "anthropic:claude-sonnet-4.6",
"MODEL_SETTINGS": {"anthropic_thinking": {"type": "enabled", "budget_tokens": 2048}},
# "FORWARD_REASONING": False, # think privately; don't stream the thoughts
}
It is a pure pass-through (no protocol extension): the events ride the standard AG-UI reasoning event family, and the server-side transcript ignores them (they are ephemeral and never persisted).
transcription_backend=¶
A dotted path to a TranscriptionBackend
A TranscriptionBackend instance. Omitted
(the default) keeps voice input disabled using
NullTranscriptionBackend — the
transcribe endpoint answers 410 Gone.
The package ships a ready reference backend over any OpenAI-compatible
/audio/transcriptions endpoint —
django_ag_ui.contrib.transcription.openai_transcription_backend.OpenAITranscriptionBackend
— self-configuring from the OPENAI_API_KEY environment variable (requires the
[openai] extra). Subclass it to change the model or point at another
OpenAI-compatible server (Azure OpenAI, Groq, a local Whisper server):
from django_ag_ui.contrib.transcription.openai_transcription_backend import (
OpenAITranscriptionBackend,
)
AGUIServer(registry, transcription_backend=OpenAITranscriptionBackend())
When this setting resolves to an active (non-Null) backend, AGUIServer mounts
the voice endpoint automatically: POST <prefix>transcribe/ accepts a multipart
audio clip and returns {"text": "<transcript>"} for the web component's
data-transcribe-url.
TRANSCRIPTION_MAX_BYTES¶
The maximum accepted audio-clip size in bytes, enforced server-side by
TranscribeView (an oversize clip → 413).
Defaults to 26214400 (25 MiB, the OpenAI transcription limit); set 0 to
disable the cap.
TRANSCRIPTION_ALLOWED_TYPES¶
A tuple of allowed (client-declared) content types for voice clips, e.g.
("audio/webm", "audio/mp4", "audio/mpeg"). Empty (the default) accepts any
type; otherwise a clip whose Content-Type is not listed is rejected with 415.
drf_mcp_server=¶
A dotted path to a djangorestframework-mcp-server MCPServer instance whose
tools are exposed to the agent in-process (requires the [drf-mcp] extra).
None (the default) disables the bridge. When set, the view builds a per-request
DRFMCPToolset carrying the current
request, so the agent acts as the logged-in user and drf-mcp's own validation
and permission checks apply. See
Installation → the [drf-mcp] extra.
The drf-mcp tools also appear in the
tool metadata catalog (mounted automatically
by AGUIServer), which reads each tool's display_name / display_description
as the web component's card label.
service_specs=¶
A name -> spec mapping (drf-services ServiceSpec / SelectorSpec objects),
or a SpecRegistry, exposed to the agent as tools without an MCP server via
djangorestframework-pydantic-ai's SpecCapability. None (the default)
disables it. Requires the [spec-tools] extra
(pip install "django-ag-ui[spec-tools]"), imported lazily.
This is the no-MCP-hop sibling of drf_mcp_server=: the specs
are dispatched in-process through drf-services' transport-neutral surface
(dispatch_spec + its off-HTTP helpers), enforcing each spec's
permission_classes. The agent acts as the logged-in AG-UI user (bound from
request), and a registry @tool wins a name collision. The model is also
taught the spec conventions — that list tools accept page / limit / order,
and how errors come back (an {"error": …} result is a final answer, a retry
message means fix the argument, a permission error is final) — so it doesn't
rediscover them by failing a call. Those instructions come from the underlying
SpecToolset, which the capability delegates to, so they reach the system prompt
exactly once. Use this when you have drf-services specs but no reason to stand up
an MCP server; use drf_mcp_server= when you already run one (or want MCP
clients to share the tools).
# myproject/specs.py
from rest_framework_services import SelectorKind, SelectorSpec, ServiceSpec
SPECS = {
"list_orders": SelectorSpec(
kind=SelectorKind.LIST,
selector=list_orders,
output_serializer=OrderSerializer,
),
"create_order": ServiceSpec(service=create_order, input_serializer=CreateOrderInput),
}
Declaring specs once, across transports¶
If the same specs are also exposed over MCP or as HTTP views, keep them in a
SpecRegistry
(drf-services 0.27+) and pass that instead — each transport then reads one
source rather than repeating the list:
from myproject.registry import registry
AGUIServer(registry_of_tools, service_specs=registry)
A filtered view is itself a registry, so two endpoints can be given different projections with no shared state:
internal = AGUIServer(tools, service_specs=spec_registry)
public = AGUIServer(tools, service_specs=spec_registry.by_tag("public"))
Either shape is normalised into a plain mapping once, at construction.
The spec tools' card labels are surfaced to the web component through the same
AGUIServer-mounted tool catalog (data-tools-url).
Authentication & anonymous scoping¶
The agent endpoint and every mounted sub-view (tools, skills, threads,
attachments, transcribe) share one authentication seam, and it defaults
open — an unauthenticated visitor can drive the agent and reach the stores.
Lock a mount down by passing the seam to AGUIServer; it forwards to every view
it builds, including the agent endpoint:
from django.urls import path
from django_ag_ui import AGUIServer
agent = AGUIServer(
registry,
require_authenticated=True, # 401 for anonymous requests
# authorize=lambda r: r.user.is_staff, # 403 for a non-staff user
# get_user=lambda r: token_user(r), # establish the acting user
)
urlpatterns = [
path("agent/", agent.urls),
]
require_authenticated=True→ an anonymous request gets 401 (JSON).authorize=<predicate>runs after the user is established; a falsy return gives 403 (JSON, never an HTML login redirect). Use it for a staff gate.get_user=<hook>establishesrequest.user(sync or async — a sync ORM token lookup runs off the event loop).
ALLOW_ANONYMOUS¶
Governs how the model-backed stores (ModelConversationStore /
ModelAttachmentStore and the contrib.store reference implementations) treat
anonymous requests. It exists because owner scoping alone can't isolate
anonymous visitors from one another — they have no user id.
False(default) — anonymous store operations are refused (AnonymousOperationError, surfaced as 403 by the persistence views; the agent endpoint's save path skips persistence so the run still streams). This prevents every anonymous visitor from sharing one owner bucket and reading or deleting each other's threads and attachments.True— anonymous requests are bucketed per browser byrequest.session.session_key(anon:<key>; requires session middleware).
Whenever a store persists, prefer authenticating the endpoints
(require_authenticated=True / get_user) over relying on ALLOW_ANONYMOUS.
TOOL_GUARD¶
An opt-in server-side approval gate for destructive tools. By default a
server-side tool (a @tool registry tool or a drf-mcp-bridged tool) runs
mid-stream with no confirmation — the destructive flag reaches only the model
as a schema hint, not a gate. TOOL_GUARD changes that: when enabled, a
ToolGuard capability flips destructive tools to require approval, so the run
defers and finishes on a RUN_FINISHED interrupt the client approves or
denies via the AG-UI tool-approval loop (RunAgentInput.resume[]) — the same
mechanism the web component already applies to client-registered destructive
tools, now for server-side ones. The wire stays vanilla AG-UI. For the full
end-to-end flow (what the user sees, custom clients, ask_user), see
Tool approval.
DJANGO_AG_UI = {
# …
"TOOL_GUARD": {
"ENABLED": True,
"EXEMPT": ["refresh_cache"], # never gate these, even if destructive
"REQUIRE_APPROVAL": ["export_pii"], # always gate these, even if not destructive
},
}
| Key | Type | Default | Meaning |
|---|---|---|---|
ENABLED |
bool |
False |
Compose the ToolGuard capability. |
EXEMPT |
list[str] |
[] |
Tool names never gated (wins over REQUIRE_APPROVAL). |
REQUIRE_APPROVAL |
list[str] |
[] |
Tool names always gated, even if not flagged destructive. |
What counts as destructive: a registry @tool(destructive=True), or a
drf-mcp tool whose MCP readOnlyHint annotation is False (selectors are
read-only, services mutate; a project can override per registration). A tool is
gated when it is destructive or named in REQUIRE_APPROVAL, unless it is
named in EXEMPT.
The gate is only useful with a client that renders the interrupt and resumes — the web component's approval card is the front-end half of this feature; a bespoke AG-UI client handles the interrupt itself.