Skip to content

Installation & setup

Install

pip install django-admin-agent

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:

pip install django-admin-agent[mcp]

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

INSTALLED_APPS = [
    # ...
    "django.contrib.admin",
    "django_admin_agent",
]

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.

urls.py
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 custom django_ag_ui.ToolRegistry. Omit it to use build_default_registry() (the full shell.* + introspect.* tool set).
  • namespace (default "admin_agent") — the URL namespace .urls mounts 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 underlying AGUIServer.

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.

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:

templates/admin/base_site.html
{% 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:

uvicorn myproject.asgi:application

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:

INSTALLED_APPS = ["daphne", ..., "django_admin_agent"]

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:

document.querySelector("ag-ui-chat#django-admin-agent").enableCharts(["tool"]);

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:

DJANGO_ADMIN_AGENT = {"CHAT_SURFACE_TOOLS": False}

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.