Skip to main content

Agentic Page Assistant

This document describes the architecture of the Agentic Page Assistant — the contextual AI panel that opens alongside any admin page — and explains how to extend it with per-page context.

Agentic Page Assistant

For the user-facing description, see Agentic Page Assistant (User Guide).


Architecture Overview​

The agentic assistant reuses the existing chat infrastructure (useConversation, useChatStream, useAiModels) and adds a parallel state layer that tracks panel visibility, page context, and a per-navigation conversation ID.

AdminLayout
├── AiPanelProvider ← global state (context, panel open/close)
│ └── AdminLayoutInner
│ ├── AppHeader (✦ button)
│ ├── ResizablePanel ← splits screen when panel is open
│ │ ├── left: page content (children)
│ │ └── right: InPageChat
│ └── [page components]
│ └── useAiPageContext() ← pages publish context here

Panel conversations land in the same global history as the full-screen assistant — no separate storage.


Key Files​

FileRole
frontend/src/context/AiPanelContext.tsxGlobal state: panel open/close, page context and system prompt, panel conversation ID, exclusion list
frontend/src/components/ai-panel/InPageChat.tsxCompact chat panel; reuses useConversation + useChatStream; always sends tools: "*"
frontend/src/components/ai-panel/ResizablePanel.tsxTwo-pane split with drag handle; starts 2/3 – 1/3, clamped 25–80 %
frontend/src/hooks/useAiPageContext.tsDrop-in hook for pages to publish context and systemPrompt
frontend/src/app/(admin)/layout.tsxWraps everything in AiPanelProvider; resets panel and conversation ID on navigation
backend/instacrud/api/ai_api.pyPOST /chat — substitutes $PATH/$CONTEXT, then either runs the agentic loop or plain completion
backend/instacrud/api/ai_dto.pyChatRequest DTO with system_prompt, path, context, tools

Context and System Prompt​

The two most important customization points are context (what the assistant knows) and systemPrompt (how it behaves).

How pages publish them​

Call useAiPageContext anywhere in a page or component:

import { useAiPageContext } from '@/hooks/useAiPageContext';

useAiPageContext({
context: JSON.stringify(myData), // serialized snapshot of the page's data
systemPrompt: `...`, // optional; falls back to the generic template
});

The hook pushes both values into AiPanelContext and clears them on unmount, so stale context never leaks to the next page.

Where context is hooked in​

Context is registered at the generic component level so individual pages don't need changes:

ComponentWhat is registered
EntityGridJSON.stringify(rows) — the full visible row set, updated when rows change
EntityDetailViewJSON.stringify(item) — the open record, updated when the item changes
CalendarArray of { id, title, start, end, type, entityId } for all loaded events

Adding context to a new page follows the same pattern: call useAiPageContext with the data the assistant should know about.

System prompt: generic vs. page-provided​

InPageChat picks the effective system prompt this way:

  1. If the current page published a non-empty systemPrompt via useAiPageContext → use that.
  2. Otherwise → use the generic template defined in AiPanelContext:
The user is on $PATH and is seeing the following data:
$CONTEXT

Answer their question helpfully and concisely.

Before the request reaches the LLM, the backend substitutes:

VariableReplaced with
$PATHCurrent page route, e.g. /clients/abc123
$CONTEXTThe serialized context string published by the page

Substitution is a single-pass regex replace (injection-safe). If the page passed no context, $CONTEXT is simply omitted from the prompt.

To give a page its own agentic persona, supply a systemPrompt that positions the assistant for that context:

useAiPageContext({
context: JSON.stringify(project),
systemPrompt: `You are an agentic project assistant.
The user is reviewing a project at $PATH.
Project data:
$CONTEXT

You can read and update project records using your tools.`,
});

The generic template is intentionally minimal — page-level overrides are the right place to add domain instructions, tone, or tool guidance.


Backend: /chat Endpoint​

The endpoint has two execution paths depending on the tools field.

With tools == "*" (what InPageChat always sends):

POST /chat
→ build SystemMessage from system_prompt (with $PATH/$CONTEXT substitution)
→ append _TOOL_DIRECTIVE to the system message
→ bind ALL_TOOLS to the model
→ run LangChain agentic loop (max 10 iterations)
→ return final answer (buffered, then emitted as a single streaming chunk if stream=True)

Any tool added to ALL_TOOLS in backend/instacrud/ai/functions/crud.py is automatically available to the agentic panel — no wiring needed.

With tools == null (full-screen assistant in plain-chat mode):

POST /chat
→ build SystemMessage (same substitution)
→ plain streaming/non-streaming completion, no tools

_TOOL_DIRECTIVE​

When the agentic loop is active, a fixed directive string (_TOOL_DIRECTIVE) is appended to whatever system message is in place (or prepended as a new SystemMessage if there is none). It instructs the model to:

  • Fetch data autonomously — when answering a question that requires data not already in the context (e.g. resolving an ID, looking up a related record), call the appropriate CRUD tool immediately rather than asking the user to navigate elsewhere.
  • Confirm before writes — before any create, update, or delete operation, describe the exact change and wait for explicit user confirmation.

This directive is invisible to the frontend; it is injected server-side on every agentic request.

ChatRequest DTO​

class ChatRequest(BaseModel):
prompt: str
model_id: str
stream: bool = False
reasoning: bool = False
system_prompt: Optional[str] = None # template; $PATH / $CONTEXT substituted server-side
path: Optional[str] = None
context: Optional[str] = None
tools: Optional[str] = None # "*" → agentic loop; None → plain completion

All user-supplied fields are validated for prompt injection before any LLM call.


Conversation History​

Panel conversations use browser-generated UUIDs as external_uuid and are stored and synced via the same serverSync + IndexedDB stack as the full-screen assistant. Every panel conversation appears in the full assistant's conversation dropdown and can be continued there.

The four new fields (system_prompt, path, context, tools) are persisted on the Conversation model (backend) and LocalConversation (IndexedDB) so the full assistant can restore the original agentic context when the user reopens a panel conversation.


Exclusion List​

The sparkles button is grayed out (disabled) on paths where opening a second chat panel makes no sense. The exclusion list lives in AiPanelContext and currently covers /ai-assistant and /ai-assistant/[id]. Add new paths to that list if you add other full-screen AI views.