Class SubjectPlugin

java.lang.Object
com.axelor.ai.chat.plugin.SubjectPlugin
All Implemented Interfaces:
ChatPlugin

public class SubjectPlugin extends Object implements ChatPlugin
Pins the record a conversation is about. The automatic pin fires once and never moves; only the explicit /subject command (set(com.axelor.ai.db.AiChat, java.lang.Object), clear(com.axelor.ai.db.AiChat)) changes it afterwards. Users still open popups and switch tabs freely; that only changes the focused view the prompt reports for the turn.

The pin fires on first engagement, not first sight: a tool call has to name the record the user has in front of them, the exchange has to be about it, or the user has to ask from one of its fields, which also records that field. Starting a conversation on a project task and then asking about product sales therefore pins nothing, where pinning on sight would have labelled that conversation with a record it never discusses — and, since the subject outranks the focused view, would have misdirected a later "this record".

The pinned subject, the active tab and the focused field are stamped into the request context, where AgentResolverPlugin renders them; a consulted agent's request carries them on with the rest of its caller's context. Every key it writes is server-owned and cleared on entry, so nothing a client sends reaches the prompt unvalidated.

  • Method Details

    • priority

      public int priority()
      Description copied from interface: ChatPlugin
      Execution priority. Lower values run first. Default is 100.
      Specified by:
      priority in interface ChatPlugin
    • onRequest

      public ChatRequest onRequest(ChatRequest request)
      Description copied from interface: ChatPlugin
      Called before the loop starts. May inspect or transform the request (e.g. resolve agent config, check rate limits).
      Specified by:
      onRequest in interface ChatPlugin
    • decorateToolExecutor

      public ToolExecutor decorateToolExecutor(ChatRequest request, ToolExecutor executor)
      Watches tool calls for the first one that names the record on screen, and pins it. Runs for sub-agents too: they inherit the parent's active tab, and work they do counts as engagement.
      Specified by:
      decorateToolExecutor in interface ChatPlugin
    • onComplete

      public void onComplete(ChatRequest request)
      The fallback for a turn no tool call named: asks whether the words themselves were about the record on screen, rather than assuming either way. touches answers "no" to plenty of genuine engagement - the active-form tools take no target of their own to declare, so a question the person is visibly asking about the record in front of them still names nothing mechanically. Silence there is not evidence either way, so this asks instead of assuming.

      Only reached while unpinned: a tool call may have already pinned earlier in the same turn, so the chat's own stored state is re-read rather than trusted from the request's snapshot of it at the top of the turn.

      Specified by:
      onComplete in interface ChatPlugin
    • simpleName

      public static String simpleName(String model)
      e.g. "com.axelor.sale.db.Order" -> "Order"
    • pinnedValue

      public static Map<String,Object> pinnedValue(AiChat chat, Access access)
      The pinned subject as chatToMap serializes it, or null: the context value, plus the field the conversation started from while it still resolves for user.
    • pinnedValue

      public static Map<String,Object> pinnedValue(AiChat chat, SubjectPlugin.PinnedFields fields)
      As pinnedValue(AiChat, Access), resolving fields through a listing's shared cache.
    • set

      public static String set(AiChat chat, Object candidate)
      Explicit re-pin for /subject set. Unlike the engagement pin this arrives with no successful tool call behind it, so it re-establishes that gate itself: the record is resolved under the caller's Access to read it, and a record that does not exist and one the caller cannot read are rejected identically (null), so the command cannot probe existence. Unlike pin() it may overwrite — that is what makes it a re-pin. Call inside a transaction, after the resource's membership and canDrive checks.
      Returns:
      the server-derived label of the pinned record (for the event chip), or null when rejected: bad shape, nonexistent, or unreadable
    • pinnedLabel

      public static String pinnedLabel(AiChat chat)
      How to name the pinned record to a reader, or null when there is no pin - or when the caller cannot read what was pinned.

      That second case matters: a chat's pin is the owner's, and a member may not share their access to it. A caller who cannot read the record gets no label, which is what keeps the destination out of reach for someone who should not be publishing to it.

    • clear

      public static void clear(AiChat chat)
      Clears the pin; the next engagement re-arms the automatic one (pin() checks for null).