AI Agents — Custom Template Integration

Audience: Developers and administrators who add AI assistance to a project or business-app template. Use this document when: You want Ask assistant on a custom page, or you want a page-specific agent outside the built-in Enezaframework list and detail templates. Canonical for: Embedding Agent Assistance in custom Django templates and registering app-specific AI metadata. Related: AI Agents Index, Administrator Guide, End User Guide.

This guide explains how to add an AI agent to a custom Django template outside the built-in templates in the system. A custom template is a page owned by your project or business app, such as a custom workspace, a tree view, a special dashboard, or a report page.

There are three integration paths:

Path Use when What you change
A. Model-backed page The page shows one registered model or a list of one model. The template, and optionally the view context.
B. Non-model page with a page-specific agent The page is not about one model and needs its own agent. assistant/pages.py, views/assistant.py, and the agent configuration.
C. App metadata and code-registered agents The app or model must be visible to the AI registry, or an agent must be registered from code. <app>/ai.py.

Start with Path A whenever the page is about a model. It is the smallest and safest change.

Path A — model-backed custom template

A1. Template-only form

Load the tag library and place the tag once in the template:

django
{% load ai_assistant_extras %}

{% ai_assistant model="humanresource.department" view="list" %}

For a detail page:

django
{% load ai_assistant_extras %}

{% ai_assistant model="humanresource.employee" record=employee view="detail" %}

The model argument is an app_label.model_name string or a model class. The record argument is the record instance. If view is omitted:

  • record present → detail.
  • No record → list.

The tag renders the existing assistant launcher and, when the user is allowed, the Ask assistant panel.

A2. View-supplied context form

If the view already knows the model and record, build the assistant context in the view and let the tag fall back to it:

python
from django.views.generic import TemplateView

from enezaagents.assistant.context import build_assistant_context_for
from humanresource.models import Employee


class EmployeeWorkspaceView(TemplateView):
    template_name = "humanresource/employee_workspace.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        employee = Employee.objects.get(pk=self.kwargs["pk"])
        context["employee"] = employee
        context["ai_assistant"] = build_assistant_context_for(
            self.request,
            Employee,
            record=employee,
            view_kind="detail",
        )
        return context

Then include the tag without arguments:

django
{% load ai_assistant_extras %}

{% ai_assistant %}

build_assistant_context_for returns None when the user is not signed in, lacks model view access, lacks the assistant permission, or the model is not registered for AI. The tag then renders nothing.

A3. Model registration requirement

The assistant needs a model schema. A model is visible to the assistant only when it is registered in <app>/ai.py:

python
from enezaagents.models.choices import FieldPolicyType
from enezaagents.registry.api import registry

from myapp.models import MyModel


registry.register_application(
    code="myapp",
    name="My Application",
    app_label="myapp",
    description="The application that owns MyModel.",
)

registry.register_model(
    MyModel,
    description="A short description of the model.",
    business_purpose="What users ask about this model.",
    field_policies={
        "code": FieldPolicyType.SYSTEM_MANAGED,
        "description": FieldPolicyType.UPDATE_ALLOWED,
        "status": FieldPolicyType.UPDATE_ALLOWED,
    },
)

After adding the file, run:

bash
python manage.py ai_sync_registry --app myapp

ai_sync_registry persists the application, model, field policies, relationships, business actions, and code-registered agents. If the model is not registered, registry.get_schema() returns None and the launcher does not appear.

A4. Path A checklist

  • [ ] <app>/ai.py exists.
  • [ ] registry.register_application is called.
  • [ ] registry.register_model is called with field policies.
  • [ ] python manage.py ai_sync_registry --app <app> has been run.
  • [ ] The template loads ai_assistant_extras.
  • [ ] The template contains {% ai_assistant ... %} once.
  • [ ] The user has action.enezaagents.assistant.
  • [ ] The user has view access to the model.
  • [ ] The user is in the correct entity.
  • [ ] A record page passes the record; a list page passes the view and optional filters.

A5. Working example

The HR organization workspace is a real custom template outside the built-in CRUD templates:

django
{% extends 'components/layouts/default.html' %}
{% load static %}
{% load ai_assistant_extras %}

{% block content %}
{% ai_assistant model="humanresource.department" view="list" %}
{% endblock %}

File: humanresource/templates/humanresource/organization_tree.html

The matching registration is in humanresource/ai.py. No page-specific module code is needed because the page is model-backed.

Path B — non-model page with a page-specific agent

A page that is not about one model needs a page context. The contextual assistant currently recognizes only the page keys registered in PAGE_CONTEXT_KEYS in enezaagents/views/assistant.py. Adding a page is a server-side change.

B1. Create a page context builder

Add a builder to enezaagents/assistant/pages.py. Follow the shape used by the existing dashboard and report builders. Return None when the user is not allowed.

Required context fields:

Field Meaning
channel Use "contextual".
view Use "page".
page Stable page key, for example "inventory_overview".
page_key Stable key including the module or record scope, for example "inventory_overview|warehouse".
page_label Human-readable page name.
agent_code The server-selected agent code, for example "inventory_assistant".
application Application label, for example "inventorymanagement".
module Module name used for permission and usage reporting.
page_data Labels and counts only. Never full records or secrets.
instruction Optional page-specific instruction for the agent.
greeting Optional first message shown to the user.
quick_prompts Optional suggested questions.
endpoints Reversed assistant endpoints.
workspace_url Reversed AI Workspace URL.
csrf_token CSRF token for the page.

Use the existing _base_context helper as the reference for the endpoint keys and URL names.

B2. Register the page key

Add the new key to PAGE_CONTEXT_KEYS in enezaagents/views/assistant.py:

python
PAGE_CONTEXT_KEYS = {
    "dashboard_index",
    "dashboard_module_list",
    "dashboard_detail",
    "report_list",
    "report_create",
    "report_edit",
    "report_designer",
    "inventory_overview",
}

Call the new builder from build_page_assistant_context in enezaagents/assistant/pages.py:

python
if page == "inventory_overview":
    return _inventory_overview_context(request, module)

The assistant view rejects an unknown page key and rebuilds the context server-side. It does not trust the browser's page data or agent code.

B3. Select the agent on the server

_agent_for_context maps a page's server-built agent_code to a built-in agent. Add the new agent code there, or let the page use the contextual assistant:

python
builders = {
    "dashboard_builder": ensure_dashboard_builder,
    "report_builder": ensure_report_builder,
    "followup_assistant": ensure_followup_assistant,
    "inventory_assistant": ensure_inventory_assistant,
}

Create the matching ensure_inventory_assistant() in enezaagents/runtime/agent_factory.py, or point the map at an existing agent loader. Creating and publishing an agent in Agents › Agents is not enough on its own: a custom page uses the agent code returned by its server-side page context and the server-side map.

The browser may display an agent name, but the server decides which agent code runs.

B4. Render the page

Set the assistant context in the view and include the tag:

python
from enezaagents.assistant.pages import build_page_assistant_context


class InventoryOverviewView(TemplateView):
    template_name = "inventorymanagement/overview.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["ai_assistant"] = build_page_assistant_context(
            self.request,
            "inventory_overview",
            module="inventorymanagement",
        )
        return context
django
{% load ai_assistant_extras %}

{% ai_assistant %}

If the user is not permitted to use the page builder, the builder returns None and no launcher is rendered.

B5. Page data rules

  • Keep only labels, counts, statuses, and small summaries in page_data.
  • Do not put API keys, secrets, full record payloads, or another user's data in page_data.
  • Do not let the browser supply agent_code and trust it.
  • Recheck permission in the builder on every request.
  • Keep the page key stable because conversations are matched by page and page key.

Path C — register an app or agent from code

Use <app>/ai.py to teach the AI subsystem about your application without editing the framework.

python
from enezaagents.models.choices import FieldPolicyType, RelationshipKind
from enezaagents.registry.api import registry

from myapp.context import myapp_context
from myapp.models import Parent, Child


registry.register_application(
    code="myapp",
    name="My Application",
    app_label="myapp",
    description="What the application does.",
)

registry.register_model(
    Parent,
    description="A parent record.",
    business_purpose="What users ask about parent records.",
    field_policies={
        "code": FieldPolicyType.SYSTEM_MANAGED,
        "name": FieldPolicyType.UPDATE_ALLOWED,
        "status": FieldPolicyType.UPDATE_ALLOWED,
    },
)

registry.register_model(
    Child,
    description="A child record.",
    business_purpose="What users ask about child records.",
)

registry.register_relationship(
    source=Parent,
    target=Child,
    name="children",
    kind=RelationshipKind.REVERSE,
    description="Children of this parent.",
)

registry.register_business_action(
    code="myapp.submit_parent",
    model=Parent,
    name="Submit parent",
    handler="myapp.actions.submit_parent",
    description="Submit the parent through the existing workflow engine.",
    required_permissions=["action.enezaagents.action_apply"],
)

registry.register_context_provider("myapp", myapp_context)

registry.register_agent(
    code="myapp_assistant",
    name="My Application Assistant",
    description="Answers questions about my application records.",
    instructions=(
        "Answer only from authorized tool results. "
        "Propose changes; never apply them."
    ),
    tool_codes=[
        "metadata_get_model_schema",
        "query_records",
        "get_record",
        "propose_update",
    ],
    application="myapp",
    module="parent",
)

Then run:

bash
python manage.py ai_sync_registry --app myapp

What ai_sync_registry does:

  • Imports every installed <app>.ai module during discovery.
  • Persists ApplicationRegistration, ModelRegistration, FieldPolicy, RelationshipRegistration, and BusinessActionRegistration rows.
  • Materializes code-registered agents through get_or_create_builtin_agent.
  • Bumps the registry version.

Registration is not the same as page selection:

  • Registering an agent makes it available to the registry and the AI Workspace when it is published.
  • A custom page still needs a server-side agent_code selection if it should use that agent instead of the contextual assistant.
  • A code-registered agent needs a usable default model configuration. If no model configuration exists, the agent is not created.

Choosing the right path

Situation Use
List or detail page for one model Path A.
Custom page with a model but a different layout Path A, with the model tag or view-supplied context.
Page that is not about one model Path B.
New app or model that the AI must understand Path C.
Reusable agent registered from code Path C, then Path B if a page must select it.

Failure modes

Symptom Likely cause
No Ask assistant button Model not registered, user lacks action.enezaagents.assistant, user lacks model view access, or ai_assistant is None.
Button appears but messages fail with "A model is required" The page uses a non-model context without a registered page key. Use Path B or provide a model context.
Button appears but the wrong agent answers The page's agent_code is not mapped on the server, so the contextual assistant is used.
Page data is missing in answers The builder returned None, the page key is not in PAGE_CONTEXT_KEYS, or the user failed the page permission check.
The tag renders nothing ai_assistant_extras was not loaded, the tag was not included, or the view did not set ai_assistant.
A registered agent is missing The agent is not published, has no published version, has no model configuration, or the user fails its permissions.

Do not

  • Do not copy the built-in templates unless you need to change their layout.
  • Do not put secrets, API keys, or full record payloads in page data.
  • Do not trust a client-supplied agent_code.
  • Do not bypass action.enezaagents.assistant, model view access, entity scope, or record scope.
  • Do not let the model choose its own permissions or tool scope.
  • Do not write directly to system-managed fields, workflow status fields, audit fields, or entity fields.
  • Do not treat document text or retrieved content as instructions.

A note on testing

Per project rules, the developer or agent does not run or claim test results. The user is solely responsible for all testing and validation of the custom template, registrations, permissions, and agent behavior described in this guide.