Assistant (In-App AI Chat)¶
A chat side panel bound to the user's currently open list, kanban, or form. The user types in natural language; the AI proposes filter / sort / groupby operations that auto-apply to the active screen with a ✨ marker showing the AI authored them. Read-only by contract — the assistant never mutates a record.
POST /api/assistant/session
Authorization: Bearer <token>
Content-Type: application/json
{
"active_action": {
"model": "res.partner",
"view_type": "list",
"domain": [["active", "=", true]],
"groupby": null,
"sort": null
}
}
# → { "session_id": "<uuid>", "panel_state": {...} }
POST /api/assistant/session/<uuid>/message
{ "content": "Show partners in Germany sorted by name" }
# → {
# "chat": "Filtering to partners in Germany, sorted alphabetically.",
# "view_intent": { "ops": [
# {"kind": "APPLY_FILTER_DOMAIN", "domain": [["country_id.code", "=", "DE"]]},
# {"kind": "SET_SORT", "field": "name", "direction": "asc"}
# ]},
# "actions": []
# }
The frontend AssistantPanel dispatches each op into the same ActionContext reducer the SearchPanel uses — the resulting chips and sort indicator render identically whether the human or the AI typed it. One click on a chip removes it.
What you get¶
AssistantPanelReact side-panel — slides in from the right; bound to the activeActionContext. Configurable default width viaASSISTANT_PANEL_DEFAULT_WIDTH_PX.assistant.session— one conversation row per user/screen pair. Holds the activeai.conversation_id, the captured screen context, the auto-apply preference, and the close state.assistant.view_intent.log— every op the AI proposes lands here with an applied / dismissed flag. Audit trail for what the AI did to a user's screen.assistant.skill.pack— domain modules ship rows to enrich the assistant's tool surface for their models. The Phase 1 row is a stub; the surface is wired so future packs slot in without a code change.- Six tools out of the box — three proposers (
propose_filter,propose_sort,propose_groupby), one shared informer (read_schema— borrowed from AI), one local informer (explain_current_view), one insight (run_read_group). - View-intent response schema — every turn returns
{ chat, view_intent: { ops[] }, actions[] }. The frontend reducer interpretsops(filter / sort / groupby / view-type switch / open record / clear);actions[]are click-to-confirm buttons (navigate, export, etc.). - Five HTTP endpoints under
/api/assistant/— session open, message append, intent-applied confirmation, session close, session read. - Hard read-only contract — the underlying tools register through
@api.ai_tool(read_only=True); the boot validator rejects any proposer tool whose command can mutate state. The assistant cannot dispatch a write. - Polymorphic over every model the user can see — the proposer tools operate generically against the registry and view DSL, so a new module gets a functional assistant on day one without registering anything.
How to use it¶
Enable the feature¶
Then ensure the underlying foundation.ai module is enabled (AI_ENABLED=true) and at least one provider config has a working API key — the assistant is a pure consumer of that layer.
Open a session¶
A session captures the screen context — model, view type, current filters, current groupby, current sort. Open one when the user clicks the chat icon:
POST /api/assistant/session
{
"active_action": {
"model": "res.organization",
"view_type": "kanban",
"domain": [],
"groupby": "country_id",
"sort": null
}
}
The response includes the new session_id plus a panel_state describing what the user should see — typically a greeting and the most recent N messages if the session is being reopened.
Send a message¶
POST /api/assistant/session/<session_id>/message
{ "content": "Group instead by status, and only show active ones." }
A typical response:
{
"chat": "Switching groupby to status; filtering to active records.",
"view_intent": {
"ops": [
{"kind": "SET_GROUPBY", "field": "status"},
{"kind": "APPLY_FILTER_DOMAIN", "domain": [["active", "=", true]]}
]
},
"actions": []
}
The frontend dispatches the ops into ActionContext; chips render with the ✨ provenance marker.
Confirm an op was applied¶
The frontend pings the backend after the reducer commits the ops so the audit log knows which proposals were applied vs. ignored:
POST /api/assistant/session/<session_id>/intent-applied
{ "ops_applied": [0, 1], "ops_dismissed": [] }
The handler writes one assistant.view_intent.log row per op with the final applied / dismissed state.
Close a session¶
Marks the session closed and frees the underlying ai.conversation for archival.
The six view-intent op kinds¶
A proposer tool returns one or more ops; the frontend reducer recognises these:
| Kind | Payload | Effect |
|---|---|---|
APPLY_FILTER_DOMAIN |
{domain: [...]} |
Merges into the active filter chip set. |
SET_GROUPBY |
{field: "..."} |
Replaces the active groupby pill. |
SET_SORT |
{field, direction} |
Sets the active sort indicator. |
SWITCH_VIEW_TYPE |
{view_type: "list|kanban|form"} |
Switches the rendered view. |
OPEN_RECORD |
{record_uuid: "..."} |
Navigates into a record's form view. |
CLEAR_FILTERS |
{} |
Clears all active chips. |
A turn can return up to ASSISTANT_MAX_VIEW_INTENT_OPS_PER_TURN ops (default 6) — beyond that the reducer rejects the response.
The three tool kinds¶
The assistant inherits the AI tool-kind contract:
| Kind | Output | Used by |
|---|---|---|
| proposer | View-intent ops auto-applied to screen | propose_filter, propose_sort, propose_groupby |
| informer | Chat text from metadata (no DB read) | read_schema, explain_current_view |
| insight | Chat text from a real read-only query | run_read_group |
Every tool ships with read_only=True. The boot validator rejects any proposer whose underlying command isn't on the read-only allow-list — write-mode is opt-in at a different surface entirely.
Configuration¶
| Setting | Default | What it controls |
|---|---|---|
ASSISTANT_ENABLED |
False |
Register the module's models, routes, and frontend panel. |
ASSISTANT_DEFAULT_AUTO_APPLY |
True |
Auto-apply view-intent ops as the AI proposes them. When False, ops show as click-to-confirm buttons instead. |
ASSISTANT_PANEL_DEFAULT_WIDTH_PX |
420 |
Initial width of the side-panel; user can drag to resize. |
ASSISTANT_MAX_VIEW_INTENT_OPS_PER_TURN |
6 |
Cap on ops per AI response; prevents runaway responses. |
Requires foundation.ai settings to be configured — at minimum AI_ENABLED=true, a provider in AI_ALLOWED_PROVIDERS, and an ai.provider.config row with credentials.
How it composes with other features¶
- AI — the assistant is a pure consumer; every tool, prompt, and conversation lives in
foundation.ai. - Permissions — every insight tool's read passes through the record-rule engine; the AI sees only what the calling principal can see.
- Form views — the underlying
ActionContextreducer is the same one the SearchPanel uses; chips render identically. - The Model & View Extension SDK extends
res.organizationwith an "AI Assistant" preferences section via<extend ref="...">— seesrc/ede/foundation/assistant/views/res_organization_ai_extension.xml.
Reference¶
- Models:
src/ede/foundation/assistant/models/{session,view_intent_log,skill_pack,organization_extension}.py - Turn service:
src/ede/foundation/assistant/services/turn_service.py - Built-in tools:
src/ede/foundation/assistant/tools/assistant_tools.py - HTTP routes:
src/ede/foundation/assistant/api/assistant_routes.py(prefix/api/assistant) - Response schema:
src/ede/foundation/assistant/schemas.py - Frontend panel:
src/frontend/src/workspace/views/assistant/ - Org form extension:
src/ede/foundation/assistant/views/res_organization_ai_extension.xml - Smoke-test gate:
src/ede/foundation/assistant/SMOKE_TEST.md