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.
Architecture
Section titled “Architecture”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/.
Data Model
Section titled “Data Model”assistant_conversation table
Section titled “assistant_conversation table”| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
organization_id | UUID | FK → organization |
member_id | UUID | FK → member |
title | varchar(255), nullable | AI-generated after first exchange |
created_at | timestamptz | Defaults to now() |
updated_at | timestamptz | Bumped on every new message |
Indexed on (member_id, organization_id) for fast conversation listing.
assistant_message table
Section titled “assistant_message table”| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
conversation_id | UUID | FK → assistant_conversation |
role | varchar | user or assistant |
content | text | Message text |
tool_calls_json | jsonb, nullable | Array of StoredToolCall objects |
created_at | timestamptz | Insertion time |
WebSocket Protocol
Section titled “WebSocket Protocol”Connect to GET /api/assistant/ws (requires authenticated session with AssistantInstanceAction::Use permission).
Client → Server (WsClientMessage)
Section titled “Client → Server (WsClientMessage)”| Variant | Fields | Description |
|---|---|---|
UserMessage | content, page_context? | Send a chat message with optional page context |
NewConversation | — | Reset to a pending state (lazy creation) |
ListConversations | — | Request the 20 most recent conversations |
LoadConversation | conversation_id | Switch to an existing conversation |
Server → Client (WsServerMessage)
Section titled “Server → Client (WsServerMessage)”| Variant | Fields | Description |
|---|---|---|
AssistantStart | — | Signals the start of a response |
TextDelta | content | Streamed text chunk |
ToolCallStarted | tool_name, arguments | Tool invocation begins |
ToolCallCompleted | tool_name, result | Tool finished with result |
AssistantEnd | — | Response complete |
Error | message | Error occurred |
ConversationLoaded | conversation_id, title? | Sent on connect or session switch |
TitleUpdated | title | AI-generated title ready |
ConversationList | conversations | Response to ListConversations |
HistoryMessage | role, content, tool_calls? | Replayed message during load |
HistoryEnd | — | History replay complete |
Page Context
Section titled “Page Context”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.
Tool System
Section titled “Tool System”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.
Tool Categories
Section titled “Tool Categories”| Category | Tools | Permission Gate |
|---|---|---|
| Contacts | search_contacts, get_contact_details, create_contact, update_contact, add_contact_note, manage_contact_tags/emails/phones | Contact collection/instance permissions |
| Calls | get_recent_calls, get_call_details | Call list/view permissions |
| Messages | get_contact_messages, send_sms, get_unanswered_contacts | Message list/create permissions |
| Analytics | get_dashboard_stats, get_engagement_stats, get_plan_stats, get_activity_counts | Dashboard view permissions |
| Workflow | get_plan_templates, create_plan, create_plan_template, update_plan_template | PlanTemplate collection/instance permissions |
| Infrastructure | get_phone_numbers, get_agents, create_agent, update_agent, get_analyzers, create_analyzer, assign_phone_agent/analyzers/text_agent | Phone/Agent/Analyzer permissions |
| Tasks | get_tasks, create_task, update_task, complete_task, dismiss_task, reopen_task | Task collection/instance permissions |
| Notifications | get_notifications, get_unread_count, mark_notification_read, mark_all_notifications_read | Notification list/markRead permissions |
Confirmation Pattern
Section titled “Confirmation Pattern”All write tools use a two-step confirmation flow:
- Call with
confirmed: false→ returns a preview - User approves → call with
confirmed: true→ executes the action
Tool Instrumentation
Section titled “Tool Instrumentation”Each tool is wrapped with instrument_tool() which:
- Sends
ToolCallStartedbefore execution so the UI shows a loading card - Deduplicates identical calls using a
DashMapcache (guards against duplicate SSE chunks from OpenRouter)
Conversation Management
Section titled “Conversation Management”- 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
ConversationStatewith aVecDequeof 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
UI Components
Section titled “UI Components”| Component | Purpose |
|---|---|
AssistantPanel | 400px slide-over sidebar with width transition |
AssistantFullScreen | Full-screen overlay mode |
AssistantProvider | Context provider managing AssistantViewMode |
AssistantChatBody | Shared layout: header + message list + input bar |
AssistantHeader | Title display, expand/close buttons, session picker trigger |
AssistantMessageList | Scrollable message container |
ChatMessage | Individual message bubble with markdown rendering |
AssistantInputBar | Text input with send button |
ToolCallCard | Collapsible card showing tool name, arguments, and result |
QuickActions | Empty-state action grid (9 predefined shortcuts) |
SessionPickerDropdown | Conversation history list for switching sessions |
The use_assistant_chat() hook manages all reactive state: WebSocket connection, message streaming, history replay, and session switching.
Quick Actions
Section titled “Quick Actions”The assistant shows predefined quick actions when the conversation is empty:
| Label | Category |
|---|---|
| Search contacts | Contacts |
| Recent calls | Calls |
| Send an SMS | Messages |
| Dashboard stats | Analytics |
| Follow up with a contact | Workflow |
| Review today’s activity | Workflow |
| Create a new contact | Contacts |
| Check engagement stats | Analytics |
| View my plans | Workflow |
Rate Limiting
Section titled “Rate Limiting”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.
Key Files
Section titled “Key Files”| Path | Purpose |
|---|---|
src/mods/assistant/mod.rs | Module root, re-exports |
src/mods/assistant/api/assistant_ws_api.rs | WebSocket endpoint and message loop |
src/mods/assistant/services/assistant_service.rs | stream_assistant_message() — LLM streaming orchestrator |
src/mods/assistant/services/build_system_prompt_service.rs | Dynamic system prompt builder |
src/mods/assistant/services/tool_registry_service.rs | Permission-gated tool collection |
src/mods/assistant/services/conversation_persistence_service.rs | CRUD for conversations and messages |
src/mods/assistant/services/generate_title_service.rs | Background title generation |
src/mods/assistant/hooks/use_assistant_chat.rs | Client-side WebSocket hook and state management |
src/mods/assistant/types/assistant_types.rs | Core types: WsClientMessage, WsServerMessage, PageContext |
src/mods/assistant/types/assistant_tool_name_type.rs | AssistantToolName enum with api/display mappings |
src/mods/assistant/page_context_mapper.rs | Route → entity type mapping |
src/mods/assistant/tools/ | Tool implementations organized by domain |