Quickstart¶
This walks through registering server-side tools, building the view, and mounting the AG-UI endpoint. It assumes you are deploying under ASGI (see Installation).
1. Configure the model¶
Set the Pydantic-AI model in your Django settings:
Any Pydantic-AI model string (or a Model instance passed to the view) works.
If you neither set MODEL nor pass model= to the view, building an agent
raises ImproperlyConfigured with a clear message.
Under pydantic-ai-slim, the matching provider extra must be installed for your
model (see Installation → Model provider extras):
By default Pydantic-AI infers the provider key from the environment. To pass it
explicitly instead, set API_KEY (or
provider= for a custom base_url / client):
DJANGO_AG_UI = {
"MODEL": "anthropic:claude-sonnet-4.6",
"API_KEY": os.environ["ANTHROPIC_API_KEY"],
}
No key yet? Walk the rest of this page anyway
"MODEL": "test" resolves to Pydantic-AI's TestModel, which talks to no
provider — so everything below (the mount, the auth gate, the tool registry,
the SSE stream) stands up and runs end to end with no API key, no provider
extra and no provider account. It exercises your wiring, not your
credential; see
Rehearsing the wiring before you have a key
for what a green run does and does not prove, and for the one caveat that
bites — TestModel calls every registered tool, destructive ones included.
2. Register tools¶
A ToolRegistry is an instance — build one and
attach tools with the @tool decorator. Tools declare
typed parameters and a typed return; the registry derives the JSON Schema
for AG-UI from the signature.
# agent_tools.py
from django_ag_ui import ToolCategory, ToolRegistry, tool
registry = ToolRegistry()
@tool(registry, category=ToolCategory.INTROSPECT)
def count_active_users() -> int:
"""Return how many users are currently active."""
from django.contrib.auth import get_user_model
return get_user_model().objects.filter(is_active=True).count()
@tool(
registry,
destructive=True,
category=ToolCategory.UI_WRITE,
confirm="Deactivate this user?",
summary="Deactivate user",
)
def deactivate_user(user_id: int) -> str:
"""Deactivate the user with the given id."""
from django.contrib.auth import get_user_model
user = get_user_model().objects.get(pk=user_id)
user.is_active = False
user.save(update_fields=["is_active"])
return f"deactivated {user_id}"
destructive=True is stamped into the tool's JSON Schema as x-destructive, so
an AG-UI client can gate it behind an inline confirmation card. The optional
confirm= prompt is stamped as x-confirm (shown in the card), and summary=
as x-summary (the card's label). The first paragraph of the docstring becomes
the tool description unless you pass description=.
3. Mount the server¶
AGUIServer is the package's front door: construct it
once with the tool registry, then mount its namespaced
.urls with include() — the
django.contrib.admin site.urls idiom.
# urls.py
from django.urls import path
from django_ag_ui import AGUIServer
from agent_tools import registry
agent = AGUIServer(registry)
urlpatterns = [
path("agent/", agent.urls),
]
This mounts a POST endpoint at agent/ (choose any prefix the Django way —
path("chat/", agent.urls)) plus a read-only tool catalog at agent/tools/. The
agent endpoint accepts a RunAgentInput JSON body and streams AG-UI events back
as text/event-stream.
.urls is the (patterns, app_name, namespace) triple path() mounts directly
(like admin.site.urls — no include()), so the endpoint names are
namespaced ("ag_ui" by default) and reversible —
reverse("ag_ui:endpoint"), reverse("ag_ui:tools"). Two mounts don't collide;
pass namespace="…" to distinguish them. Because the server holds its own
registry and config, you can mount several with independent registries — one per
surface, each with its own tools.
4. (Optional) override per mount¶
model, instructions, and audit_logger fall back to DJANGO_AG_UI but can
be passed explicitly — handy in tests, where you inject a Pydantic-AI
TestModel:
That is for your test suite, and it bypasses settings on purpose. To rehearse
a deployment — the settings read included — set "MODEL": "test" instead
(see step 1).
CSRF and cookie-authenticated deployments
CSRF is exempt unless you say otherwise — right for header-token auth
(Bearer / API key), where CSRF doesn't apply. If your deployment
authenticates with session cookies, pass csrf_exempt=False and send
the 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 (Django's default SameSite=Lax cookie mitigates,
but does not eliminate, the risk).
Saying nothing about it and passing no get_user hook warns at
construction — see CSRF.
The endpoint fails closed: require_authenticated defaults to True, so an
anonymous request gets a 401 before any agent runs. Establishing who is
acting is still the host's job, and a get_user= callable does it — sync or
async; a sync ORM lookup is fully supported (it runs off the event loop). Its
return value is assigned onto request.user, so tools and conversation
ownership act as that user:
def get_user(request):
token = request.headers.get("Authorization", "").removeprefix("Bearer ").strip()
return Token.objects.select_related("user").get(key=token).user
agent = AGUIServer(registry, get_user=get_user)
Serving anonymous runs deliberately is require_authenticated=False.
AGUIServer forwards require_authenticated / get_user / authorize to
every view it builds — the agent endpoint and the tool / skill / thread /
attachment catalogs — so one policy covers the whole mount (the catalogs
enumerate every server tool and skill prompt, so they are gated too).
5. (Optional) offer skills¶
Register pre-defined prompts in a SkillRegistry
and pass it as AGUIServer(..., skills=...) to mount a <prefix>skills/ catalog
the web component fetches via data-skills-url:
from django_ag_ui import AGUIServer, SkillRegistry
skills = SkillRegistry()
# No prompt: the catalog advertises the name and label only, and picking the
# skill sends the bare "/triage" token for the agent to resolve.
skills.add("triage", title="Triage this request", chip=True)
# With a prompt: the client holds the text and fills {placeholder}s from the
# page before sending.
skills.add(
"summarise",
title="Summarise",
prompt="Summarise the {selection} for me.",
chip=True,
)
urlpatterns = [
path("agent/", AGUIServer(registry, skills=skills).urls),
]
Prefer leaving prompt unset when the wording is internal. This catalog is
a plain GET, so anything in prompt is readable by anyone who can reach the
endpoint and is sitting in the page for anyone who opens the source. Without it
the client sends /triage and the agent decides what that means — from the
harness Skills capability, or from your own instructions. Set prompt when
it is genuinely a user-facing convenience, or when it carries {placeholder}s
only the page can fill.
6. The tool catalog¶
The read-only tool catalog is mounted automatically at <prefix>tools/ (GET,
JSON) — the server builds it from the same registry you pass, so there's
nothing extra to wire. The web component fetches it via data-tools-url to
label tool-call cards for server-side tools, whose JSON Schema never reaches the
browser.
Each entry is {"name", "summary", "description"?}; summary falls back from
@tool(summary=…) to a prettified tool name. With
drf_mcp_server= set, the catalog also surfaces
the drf-mcp tools, using their display_name as the label. See
Tool metadata catalog.
What next¶
- Configuration for every settings key.
- Key concepts for how the registry, audit logger, streaming, and persistence fit together.