Usage
For AI-enabled admin, use AIModelAdmin instead of ModelAdmin:
from habarico_enterprise.ai.admin import AIModelAdmin
from .models import Product
class ProductAdmin(AIModelAdmin):
model = Product
search_fields = ["name", "description"]
fields = ["name", "sku", "description", "short_description", "status", "category"]
AI fulltext filter and PredictiveTextField will be automatically used by default.
Who may call the AI actions
Both AI endpoints — prediction (behind every predictive field) and ai-assistant — reach a
paid provider on every call, so they are gated more tightly than the resource's own endpoints:
- the caller must be authenticated. This holds whatever
DEFAULT_PERMISSION_CLASSESis set to, so a project that has not tightened that setting is not exposing an anonymous, billable proxy to the provider. - the resource must be writable —
can_createorcan_update. Both actions exist to fill in a form, so on a read-only resource there is nothing for them to fill; they answer405 Method Not Allowed, and the assistant action is left out of the schema so the frontend does not render a button that would be refused.
class ProductAdmin(AIModelAdmin):
model = Product
can_update = True # without can_create or can_update the AI actions are refused
This is a resource-level gate, not authorization. It stops a read-only resource from being a free
route to the provider; deciding which users may write is still the job of permission_classes
(per admin or project-wide), exactly as for the CRUD endpoints.
Override is_ai_action_allowed() to narrow the gate further — for example to a group check:
class ProductAdmin(AIModelAdmin):
model = Product
can_update = True
def is_ai_action_allowed(self, request):
return super().is_ai_action_allowed(request) and request.user.groups.filter(name="ai").exists()
Payload limits
Everything a request sends to the AI actions ends up in the prompt, so each part of it is capped.
Raise a limit only as far as the resource needs it — every increase is a larger request the
provider bills for. A non-positive limit is rejected at startup with ImproperlyConfigured.
Most limits trim rather than refuse. What they bound is context nobody chose to send: data
and current_values are whatever the record happens to hold, schema.fields is however many
fields the form declares, and the prediction prefix is whatever has been typed into the field. A
product with a 3000-character description would otherwise get 422 on every AI call, and no user
could do anything about it. So the prompt is bounded by dropping context instead.
Two limits still answer 422 Unprocessable Entity, and both cover something the caller chose
rather than something the record contains.
| Option | Type | Default | Over the limit | Description |
|---|---|---|---|---|
ai_max_text_length | int | 2000 | trims | Length of the text prefix sent to prediction. Cut from the front: the completion continues where the text ends, so the tail is what matters. |
ai_max_value_length | int | 1000 | trims | Length of one value in prediction's data and in the assistant's current_values. |
ai_max_fields | int | 100 | trims | Number of entries in data, in current_values and in the assistant's schema.fields; the first N are kept. |
ai_assistant_max_messages | int | 30 | trims | Size of the assistant's context window — see below. |
ai_max_length | int | 500 | refuses | Largest completion length a request may ask for. Omitting length asks for 200, or this limit where it is lower. The frontend sends 100 (single line) or 200 (multiline). |
ai_assistant_max_message_length | int | 4000 | refuses | Length of a single chat message. Trimming it would corrupt what the user wrote. |
Trimming is silent — the request succeeds and the answer is simply built from less context. A
resource where the assistant seems to ignore half the form is a sign that ai_max_fields is too
low for it, not that the model is misbehaving.
class ProductAdmin(AIModelAdmin):
model = Product
can_update = True
fields = ["name", "sku", "description", "short_description", "status", "category"]
# A long-form description field needs a longer prefix than the default allows.
ai_max_text_length = 8000
The assistant's context window
The assistant endpoint keeps no state. The frontend holds the whole conversation in component state and re-sends it on every turn, so the provider is billed for the entire history each time — the cost of a chat grows with the square of its length, not linearly.
ai_assistant_max_messages is what bounds that: a conversation longer than the window is trimmed
to its newest messages before the prompt is built, dropping one more if that would leave the
window opening on an assistant reply whose question is no longer visible. The chat itself carries
on — nothing is refused, and the user is not asked to start over.
The window counts both roles, so the default of 30 is roughly fifteen exchanges of context. Lower it on a resource where the assistant only needs the last few turns; every message inside the window is re-sent, and paid for, on every turn.
ai_assistant_max_message_length is the one exception in the assistant: a single over-long
message is refused with 422, because trimming it would silently corrupt what the user wrote.
The frontend caps its chat input at the same 4000 characters, so reaching it takes a resource that
lowered the limit.
Which fields the AI search may filter on
The AI search turns the user's query into a Django Q expression. Every lookup path in that
expression is validated against a whitelist before it is applied; a query that references any
other field or relation falls back to the plain fulltext search instead.
By default the whitelist is the union of the admin's filter_fields and search_fields — the
AI can filter exactly on what the admin already exposes.
To let the AI use more (or different) fields than the standard filters — including relation
paths — define ai_search_fields. When set, it fully replaces the default whitelist:
class ProductAdmin(AIModelAdmin):
model = Product
search_fields = ["name", "description"]
filter_fields = ["name", "status"]
fields = ["name", "sku", "description", "short_description", "status", "category"]
# The AI search may also filter by the related category's name,
# but no longer by "status" - the list replaces the default whitelist.
ai_search_fields = ["name", "description", "category__name"]
Lookup operators (__icontains, __gte, __startswith, …) on a whitelisted field are always
allowed — the whitelist constrains field paths, not operators.
See also
- Custom AI fields — choosing which fields are predictive
- SchemaPredictiveTextInput — what the frontend renders