Installation & setup¶
Install¶
This pulls in django-ag-ui and ships the vendored <ag-ui-chat> Web
Component bundle as static files — there is no separate npm install for the
admin sidebar.
To also expose the server-side admin tools as an MCP server over HTTP (via the
djangorestframework-mcp-server stack), install the extra:
Compatibility floor
Python 3.10+ (tested 3.10–3.14), Django 4.2 LTS+ (tested 4.2, 5.0, 5.1,
5.2, 6.0, 6.1), django-ag-ui>=0.59, and — with the [mcp] extra above —
djangorestframework-mcp-server>=0.37. Django Unfold 0.40+ is optional.
Neither sibling window has an upper bound, so this package installs
alongside the current django-ag-ui on the day that ships rather than
waiting for a release here. The floors are what is tested; nothing above
them is excluded.
1. Add to INSTALLED_APPS¶
This makes the vendored static bundle, the django_admin_agent template-tag
library, and the sidebar template discoverable.
2. Mount the agent server¶
AdminAgentServer is a
django_ag_ui.AGUIServer pre-configured
for the admin (the default admin tool registry, a staff gate, and the acting
user's own admin permissions on every model the tools read). Mount
its namespaced .urls the admin.site.urls way. The endpoints live under the
admin_agent namespace — the sidebar reverses admin_agent:endpoint to find the
agent endpoint.
from django.contrib import admin
from django.urls import path
from django_admin_agent import AdminAgentServer
urlpatterns = [
path("admin/", admin.site.urls),
path("admin-agent/", AdminAgentServer(model="anthropic:claude-sonnet-4.6").urls),
]
AdminAgentServer(...) accepts:
registry(first positional) — a customdjango_ag_ui.ToolRegistry. Omit it to usebuild_default_registry()(the fullshell.*+introspect.*tool set).namespace(default"admin_agent") — the URL namespace.urlsmounts under. Pass the same value to the template tag so the sidebar reverses against this server:{% django_admin_agent_sidebar namespace="internal-agent" %}.- Any extra keyword arguments (
model,instructions,audit_logger,conversation_store,attachment_store,transcription_backend, …) pass straight through to the underlyingAGUIServer.
Where the model is configured
The agent model can be supplied per-mount as AdminAgentServer(model=...)
or globally via DJANGO_AG_UI["MODEL"]. See
Configuration.
3. Inject the sidebar¶
The sidebar is rendered into the admin chrome. There are two ways to do it; the template tag is the more common path and needs no admin-site swap.
Option A — the template tag (recommended)¶
Override admin/base_site.html in a template directory that wins over the admin
app's copy, and drop the inclusion tag into the branding block:
{% extends "admin/base.html" %}
{% load django_admin_agent %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">My admin</a></h1>
{% django_admin_agent_sidebar %}
{% endblock %}
The tag is self-contained: it computes its own context (endpoint URL, title, auto-confirm flag, presentation settings, skill catalog, bootstrap module URL, admin base URL, and route map), so the admin site does not need swapping. See Configuration for the theming, tool-display, and skills settings.
Option B — SidebarAdminSite¶
If you prefer to swap the admin site, subclass (or instantiate)
SidebarAdminSite. It adds the sidebar context to each_context under the
django_admin_agent key, so a base template can render the chat from that
context without the template tag.
from django_admin_agent.admin.sidebar_admin_site import SidebarAdminSite
admin_site = SidebarAdminSite(name="myadmin")
Both paths render the same sidebar from the same
build_sidebar_context() helper, and neither offers
anything to a signed-out visitor — the admin's login page goes through
base_site.html like every other page, and the agent endpoint refuses anyone who
is not active staff.
They differ only in how they say no. The tag renders nothing at all. Option B
hands your template an empty django_admin_agent, because the markup is yours
and the render decision has to stay with it; guard on {% if django_admin_agent %}
if you want the surrounding chrome to disappear too.
4. Serve over ASGI¶
ASGI is required. The agent endpoint streams AG-UI events over Server-Sent Events, which needs an event loop the synchronous WSGI worker will not provide. Deploy the admin under an ASGI server such as Daphne or Uvicorn:
In development, ASGI is not enough on its own¶
runserver serves your static files and cannot stream — the SSE response
buffers, so the sidebar sits there waiting. A bare uvicorn streams and serves
no static files at all. The sidebar's bootstrap module is a static file, so
following only the paragraph above gives you GET
/static/django_admin_agent/admin_agent.js → 404, an unstyled admin, and no
agent — with nothing on screen to say why.
Pick one of these for local work:
Add the static-files URLs under DEBUG. No new dependency, and it is what
this project's own example uses:
# urls.py
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
if settings.DEBUG:
urlpatterns += staticfiles_urlpatterns()
Or use WhiteNoise, which serves them under any server and is closest to how production will behave.
Or put daphne first in INSTALLED_APPS, which replaces runserver with an
ASGI one that also serves static files:
In production, collectstatic behind your web server (or WhiteNoise) covers it,
which is why this only bites in development — the environment where the first
person to install this package meets it.
The vendored web-component bundle¶
django_admin_agent/static/django_admin_agent/ag-ui-web-component.bundle.js is
a build artefact — esbuild's minified output from
@artooi/ag-ui-web-component, with @ag-ui/* inlined. It is not hand-written
and should not be edited.
The bundle version is pinned in the Makefile (WEB_COMPONENT_VERSION), and
the committed file is the artefact: CI checks it byte-for-byte against the
pinned published component on every pull request, and a release refuses to
publish if the two disagree. So the bundle in a released wheel is the same one
that was reviewed in a diff, exercised by the browser suite, and installed by
git clone + runserver. The bootstrap module (admin_agent.js) imports it by
relative path and registers the <ag-ui-chat> custom element.
With INSTALLED_APPS set and collectstatic serving django_admin_agent/,
production needs no further static wiring. Development does — see
In development, ASGI is not enough on its own,
because the server that streams and the server that serves static files are not
the same one.
Charts¶
The sidebar draws charts your own server-side code pushes. A tool returning a
ToolReturn whose metadata carries django_ag_ui.chart_activity(spec) renders
in the transcript as SVG the component builds itself — the numbers never enter
the model's context, and nothing chart-shaped is ever parsed as markup, which is
why a visual is safe on a surface that keeps images off. See django-ag-ui's
charts guide for the spec shape.
The other route — a render_chart tool the agent may call, so it can discuss
the numbers it drew — is left to you, because it widens what the agent can do:
Run it any time after the page loads; it redraws charts already in the restored transcript.
The agent moving its own panel¶
The sidebar sits over the changelist or changeform it is discussing, which is the one problem a chat pinned to its own tab never has. The agent has four tools for it -- read where the panel is, send it to a corner, minimise it to the launcher, restore it -- and each move it makes is written into the transcript with an undo beside it, so the panel never rearranges itself silently.
On by default, which is the opposite of the call made for render_chart
just above, and the difference is what the widened surface can reach. A chart
tool draws whatever the agent decides to draw; these move this sidebar and
nothing else. They read no model, change no row, and touch no data. The cost is
four tool definitions in each request, and that is what the setting is for:
Worth turning off for a PLACEMENT that owns its own position and has no
collapsed state, since there the tools can only ever answer that they did
nothing. They decline rather than lie: a docked placement, or a panel filling a
phone screen, answers moved: false with the reason and what would work
instead.
Pointing at something on the admin page¶
showHighlightOverlay rings an element from an overlay drawn outside it, so a
target is not restyled and nothing in your admin templates has to cooperate. It
can dim everything else with a scrim, and run a gradient around the ring:
import {
showHighlightOverlay,
} from "/static/django_admin_agent/ag-ui-web-component.bundle.js";
const dismiss = showHighlightOverlay(document.querySelector("#result_list"), {
scrim: true,
gradient: true,
});
// Call dismiss() to take it down -- it does not retire itself.
It reads its colours from --ag-ui-accent and the --ag-ui-highlight-* tokens
on the target, not on the chat element, because the overlay lives in your
page rather than in the component's shadow tree.
Wiring it to a tool the agent can call is a step further again, and the same judgement applies as above: nothing here registers one for you.