Skip to content

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:

# settings.py
DJANGO_AG_UI = {
    "MODEL": "anthropic:claude-sonnet-4.6",
}

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):

pip install "django-ag-ui[anthropic]"

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:

from pydantic_ai.models.test import TestModel

agent = AGUIServer(registry, model=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.