AI Interfaces
AI Assistant
The AI Assistant (the copilot, /dashboard/aiassistant) is a second user interface of the Studio: a
chat with a language model that can do what the user could do by hand — build and edit agents, add
knowledge and integrations, design evaluations and test suites, explain analytics — while the panels next
to the chat show and edit the same objects. The backend runs the model’s tool loop; the browser renders
what the backend tells it to.
Connection
The assistant talks over Socket.IO, not over the REST API:
| Aspect | Value |
|---|---|
Server |
netty-socketio inside the backend ( |
Path and namespace |
Socket.IO path |
Authentication |
The Keycloak access token in the handshake’s |
Messages |
Every server event is emitted on the Socket.IO event |
On connect the backend resolves the user, sends the latest 50 messages of the user’s chat history as a
history event, and clears approvals of dangerous actions left from an earlier connection.
SocketIOHandler processes each connection’s events in order with the user’s identity in the reactive
security context.
Events
| Direction | Events |
|---|---|
Client → server |
|
Server → client |
|
The UI-driving render.* events are what make the panel follow the conversation: when the model opens
an agent, the backend emits render.agent.detail and the Studio shows it.
A Turn
ClientEventHandler dispatches each incoming event; a user.message goes to
PlatformAssistantService.chat:
-
Prepare — a user with no history enters the onboarding (
REGISTRATION) state; otherwise the message is appended to the model memory. Before it, the backend adds the current date and the user’s session state (open account, project, agent, page) when they changed, and folds in a summary of what the user did by hand since the previous message. Attached files are parsed with the knowledge parsers and added as text. -
Prompt — the system messages are built for this turn: the general assistant prompt, the package-specific guidance of the open project, the section prompt of the page the user is on, the user’s name and email, and a snapshot of the object open on the page.
-
Tools — the tool set is assembled per turn:
NavigationTools,CoreManagementToolsandAdditionalToolsalways, plus the tools of the current section (for exampleKnowledgeToolson the knowledge pages,EvaluationToolson the evaluation pages,TestSuiteToolsin the Test section), account and project creation when enabled by configuration, and the test-agent tools for staff. -
Loop —
LLMService.reactiveCallLLMstreams a chat completion with these tools, executes the tool calls the model makes, feeds the results back and continues until the model answers without a tool call. Text is streamed to the client as it arrives.
Navigation state lives in SessionStateService, in memory, keyed by user. NavigationStatus enumerates
the places the assistant knows — from the account list down to a single evaluation or test suite — and
the map navigationContexts in PlatformAssistantService assigns each a prompt and a tool set.
Tool Loop Safeguards
-
A call identical to the ones right before it in the same turn (same tool, same arguments) is no longer executed once it has been repeated three times; the model is told so, and if it insists the turn ends with an explanation.
-
A turn that ends with a failed step it did not explain, or with no reply at all, gets a fixed reply so the user is never left with an empty bubble (
ToolFailureTracker, the replies inLLMService). -
Optional tool parameters are presented to the model as optional (
llm/AssistantToolCallbacks.kt), so it does not fill them with empty values that the edit tools would then store. -
The stream fails after 180 seconds without a chunk — enough for the long reasoning phase before the first token.
-
Tool references generated into agent fields (
@knowledge:...;) are repaired when the model drops the closing semicolon.
The Model
LLMService uses Spring AI’s OpenAiChatModel on top of an openai-java client configured for Azure
OpenAI (llm/AzureOpenAiClientFactory.kt, promethist.ai.azure-open-ai.*):
| Setting | Value |
|---|---|
|
|
Reasoning effort |
|
Verbosity |
|
These are constants in LLMService, not configuration properties.
Memory and History
Two stores serve two purposes:
| Store | Collection | Content |
|---|---|---|
Model memory |
|
What the model sees: a window of up to 100 messages per user ( |
Chat history |
|
What the user sees: user, assistant and selection messages with a running number, sent on connect and
paged with |
Staff can send RESET to clear their model memory.
Confirmations
Tools that delete, revert, clone or promote run through DangerousToolRunner. Before the action the
backend emits confirmation.request (title, warning, the target object) and waits:
-
The user answers with
confirmation.responsecarryingapprovedand a scope:ONCE,SESSION(the same action on the same kind of resource is approved for the rest of the connection) orALL_TASKS(every dangerous action). Deleting an output data element, deleting a group and deleting a whole evaluation are three kinds: the first two edit a Draft, the last removes every revision. -
Only the connection that received the request can resolve it, and an unanswered request is declined after five minutes.
-
Cancelling the turn or disconnecting declines every pending request.
-
A declined action tells the model it was not performed; the tool card shows "canceled".
Cancelling and Interrupting
cancel.message disposes the running turn, dismisses its open confirmations and ends the loading state.
A new user.message while a turn runs replaces it; in both cases the model is told on the next turn that
its previous answer was cut off. The Studio’s composer holds new messages back until a turn ends, so
interruption is the exception.
Reconnecting
Session state lives in the memory of the backend process that holds the connection. When the connection
drops — a redeploy, a reconnect that lands on another replica — the Studio replays its context with
context.restore (account, project, agent, open object and page). The backend re-applies it and answers
context.set, without starting a turn. Events that arrive before the user is resolved wait for it
instead of being dropped.
Streaming
With promethist.assistant.streaming.enabled (the default), reply chunks are batched — at most 50 ms or 64
chunks per event — and every assistant message gets its own stream id, so text after a tool call starts a
new bubble instead of replacing the previous one.
Voice Input, Web Search and Documentation
| Feature | Implementation |
|---|---|
Dictation |
|
Web search |
The |
Documentation and website |
The |
Adding a Tool
-
Add a method annotated with Spring AI’s
@Toolto the tool class of its area incomponent/, with@ToolParamdescriptions. A nullable Kotlin parameter becomes optional for the model. -
Take a
ToolContextparameter when the tool needs the caller: it carries the authentication, the user id and the Socket.IO session id. Calls into services run with the caller’s identity, so@PreAuthorizeapplies as for the REST API. -
Route destructive operations through
DangerousToolRunner. -
Emit
render.*events throughChatEventServicewhen the result should open in the panel, and keep the section prompt inllm/constants/Prompts.ktconsistent with what the tool does. -
If the tool belongs to a new section, register its prompt and tools in
navigationContexts.
The hosted MCP server exposes similar capabilities through a separate set of tools (Hosted MCP Server and Claude Plugin); a behavior change usually belongs in both.