Skip to content

AI Assistant (Vernis)

The assistant module provides Vernis, an AI chat assistant embedded in the Loquent UI. Users interact through a slide-over panel or full-screen view. Vernis streams responses in real time, calls tools to read and write platform data, and persists conversations across sessions.

The assistant uses a WebSocket connection for bidirectional streaming. The client sends user messages and session commands; the server streams text deltas and tool-call events back.

User types message
→ WsClientMessage::UserMessage { content, page_context }
→ Rate-limit check (10 msgs / 60s)
→ Lazy conversation creation (if first message)
→ Auto-rollover at 50 messages
→ Build system prompt (org context, org AI rules, user role, page context, tool list)
→ Collect tools filtered by session permissions
→ Stream LLM response via aisdk
→ WsServerMessage::TextDelta (streamed chunks)
→ WsServerMessage::ToolCallStarted / ToolCallCompleted
→ WsServerMessage::AssistantEnd
→ Persist assistant message + tool calls to DB
→ Generate title after first exchange (background task)

The module lives at src/mods/assistant/ with sub-directories for api/, components/, hooks/, services/, tools/, and types/.

ColumnTypeDescription
idUUIDPrimary key
organization_idUUIDFK → organization
member_idUUIDFK → member
titlevarchar(255), nullableAI-generated after first exchange
created_attimestamptzDefaults to now()
updated_attimestamptzBumped on every new message

Indexed on (member_id, organization_id) for fast conversation listing.

ColumnTypeDescription
idUUIDPrimary key
conversation_idUUIDFK → assistant_conversation
rolevarcharuser or assistant
contenttextMessage text
tool_calls_jsonjsonb, nullableArray of StoredToolCall objects
created_attimestamptzInsertion time

Connect to GET /api/assistant/ws (requires authenticated session with AssistantInstanceAction::Use permission).

VariantFieldsDescription
UserMessagecontent, page_context?Send a chat message with optional page context
NewConversationReset to a pending state (lazy creation)
ListConversationsRequest the 20 most recent conversations
LoadConversationconversation_idSwitch to an existing conversation
VariantFieldsDescription
AssistantStartSignals the start of a response
TextDeltacontentStreamed text chunk
ToolCallStartedtool_name, argumentsTool invocation begins
ToolCallCompletedtool_name, resultTool finished with result
AssistantEndResponse complete
ErrormessageError occurred
ConversationLoadedconversation_id, title?Sent on connect or session switch
TitleUpdatedtitleAI-generated title ready
ConversationListconversationsResponse to ListConversations
HistoryMessagerole, content, tool_calls?Replayed message during load
HistoryEndHistory replay complete

The assistant is page-aware. The client sends a PageContext with each message:

pub struct PageContext {
pub route: String, // e.g., "/contacts/abc-123"
pub entity_type: Option<String>, // e.g., "contact"
pub entity_id: Option<String>,
}

The server uses this to inject a “Current Page Context” section into the system prompt. For entity detail pages, it fetches the entity name from the database so Vernis can resolve pronouns like “this contact” without asking.

Supported entity types: contact, call, plan, plan_template, agent, task, phone, knowledge, analyzer, text_agent.

Tools are collected at connection time based on the user’s permissions. The collect_assistant_tools() function checks each permission gate and only includes tools the user can use. Runtime permission checks remain as defense-in-depth.

CategoryToolsPermission Gate
Contactssearch_contacts, get_contact_details, create_contact, update_contact, add_contact_note, manage_contact_tags/emails/phonesContact collection/instance permissions
Callsget_recent_calls, get_call_detailsCall list/view permissions
Messagesget_contact_messages, send_sms, get_unanswered_contactsMessage list/create permissions
Analyticsget_dashboard_stats, get_engagement_stats, get_plan_stats, get_activity_countsDashboard view permissions
Workflowget_plan_templates, create_plan, create_plan_template, update_plan_templatePlanTemplate collection/instance permissions
Infrastructureget_phone_numbers, get_agents, create_agent, update_agent, get_analyzers, create_analyzer, assign_phone_agent/analyzers/text_agentPhone/Agent/Analyzer permissions
Tasksget_tasks, create_task, update_task, complete_task, dismiss_task, reopen_taskTask collection/instance permissions
Notificationsget_notifications, get_unread_count, mark_notification_read, mark_all_notifications_readNotification list/markRead permissions

All write tools use a two-step confirmation flow:

  1. Call with confirmed: false → returns a preview
  2. User approves → call with confirmed: true → executes the action

Each tool is wrapped with instrument_tool() which:

  • Sends ToolCallStarted before execution so the UI shows a loading card
  • Deduplicates identical calls using a DashMap cache (guards against duplicate SSE chunks from OpenRouter)
  • Lazy creation — conversations are only persisted when the user sends their first message, not on “New Chat”
  • Auto-rollover — when a conversation reaches 50 messages, a new one is created automatically
  • Sliding window — the server maintains a ConversationState with a VecDeque of the last 50 messages for AI context
  • Title generation — after the first user↔assistant exchange, a background task generates a short title using a lightweight model (DeepSeek v3.2)
  • History replay — on connect or conversation switch, all stored messages are replayed to the client and the AI context window is rebuilt
ComponentPurpose
AssistantPanel400px slide-over sidebar with width transition
AssistantFullScreenFull-screen overlay mode
AssistantProviderContext provider managing AssistantViewMode
AssistantChatBodyShared layout: header + message list + input bar
AssistantHeaderTitle display, expand/close buttons, session picker trigger
AssistantMessageListScrollable message container
ChatMessageIndividual message bubble with markdown rendering
AssistantInputBarText input with send button
ToolCallCardCollapsible card showing tool name, arguments, and result
QuickActionsEmpty-state action grid (9 predefined shortcuts)
SessionPickerDropdownConversation history list for switching sessions

The use_assistant_chat() hook manages all reactive state: WebSocket connection, message streaming, history replay, and session switching.

The assistant shows predefined quick actions when the conversation is empty:

LabelCategory
Search contactsContacts
Recent callsCalls
Send an SMSMessages
Dashboard statsAnalytics
Follow up with a contactWorkflow
Review today’s activityWorkflow
Create a new contactContacts
Check engagement statsAnalytics
View my plansWorkflow

The WebSocket handler enforces a sliding-window rate limit: 10 messages per 60 seconds per connection. Exceeding the limit returns a WsServerMessage::Error without processing the message.

PathPurpose
src/mods/assistant/mod.rsModule root, re-exports
src/mods/assistant/api/assistant_ws_api.rsWebSocket endpoint and message loop
src/mods/assistant/services/assistant_service.rsstream_assistant_message() — LLM streaming orchestrator
src/mods/assistant/services/build_system_prompt_service.rsDynamic system prompt builder
src/mods/assistant/services/tool_registry_service.rsPermission-gated tool collection
src/mods/assistant/services/conversation_persistence_service.rsCRUD for conversations and messages
src/mods/assistant/services/generate_title_service.rsBackground title generation
src/mods/assistant/hooks/use_assistant_chat.rsClient-side WebSocket hook and state management
src/mods/assistant/types/assistant_types.rsCore types: WsClientMessage, WsServerMessage, PageContext
src/mods/assistant/types/assistant_tool_name_type.rsAssistantToolName enum with api/display mappings
src/mods/assistant/page_context_mapper.rsRoute → entity type mapping
src/mods/assistant/tools/Tool implementations organized by domain