Catalyst AI (CT Bot)
cm-admin ships with Catalyst AI, an opt-in AI assistant that lives inside the admin panel. Users with an enabled role get a floating chat widget on every admin page and can ask questions in natural language â the assistant answers using read-only MCP tools against your admin-registered models, streams the reply live, and can ask clarifying questions back when a request is ambiguous.
What you getâ
- Floating chat widget on every admin page â conversation history, unread badges, paginated messages, file attachments, and a Lexxy rich-text composer
- Streaming replies over ActionCable â the answer renders chunk by chunk
- Clarifying questions â when the request is ambiguous, the assistant asks
via an
ask_user_questiontool and the user answers on an options card - Per-role opt-in â the
is_catalyst_ai_enabledflag onCmRole - Cost tracking â every LLM request is recorded as a
CtCostTracking(tokens per type, total cost, and the raw request payload for audit), with totals rolled up onto each message and conversation - Permission-scoped answers â tool calls run through the same Pundit policy scopes as the web UI, evaluated for the asking user
Setupâ
1. Generate and run the migrationsâ
rails generate cm_admin:add_bot_conversations
rails generate cm_admin:add_bot_transcriptions
rails db:migrate
add_bot_conversations creates ct_bot_conversations, ct_bot_messages,
ct_bot_artifacts, ct_cost_trackings, and adds is_catalyst_ai_enabled to
cm_roles. add_bot_transcriptions (added in v8.4.1) creates
ct_bot_transcriptions for voice input. See the generator reference for
Add Bot Conversations
and Add Bot Transcriptions.
2. Register the modelsâ
# config/initializers/zcm_admin.rb
config.included_models = [..., CtBotConversation, CtBotMessage, CtBotTranscription, CtCostTracking]
3. Add your OpenRouter API keyâ
rails credentials:edit
catalyst_ai:
openrouter_api_key: sk-or-...
4. Configure ActionCableâ
Replies stream over CmAdmin::CtBotChannel. Make sure config/cable.yml
exists â Redis is recommended outside development:
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
5. Enable the assistant on rolesâ
Edit each CmRole in the admin panel and switch on Catalyst AI. The
widget renders only for users whose role has the flag â and the chat
endpoints return 404 without it.
6. Expose models to the assistantâ
The assistant can only query models that declare mcp_actions (same
mechanism as the external MCP Server):
class Article < ApplicationRecord
cm_admin do
mcp_actions
end
end
Models without mcp_actions are invisible to the assistant.
7. (Optional) Give the assistant domain contextâ
config.mcp_system_prompt doubles as the bot's system prompt â set it to
explain your data model and business semantics:
# config/initializers/zcm_admin.rb
CmAdmin.configure do |config|
config.mcp_system_prompt = <<~PROMPT
You are the Acme Platform admin assistant.
- `assignee` is the current owner of a work item.
- Prefer aggregate tools for counts and totals.
PROMPT
end
Platform settingsâ
Four admin-managed settings under the Catalyst AI category configure
runtime behavior (create them via
CmPlatformSetting.add_cm_default_settings or the Platform Settings page):
| Setting | Slug | Purpose | Default |
|---|---|---|---|
| CT Bot Greeting | ct-bot-greeting | Message shown at the start of every new chat | Hi! How can I help you today? |
| CT Bot Model | ct-bot-model | OpenRouter model ID (e.g. openai/gpt-5.6) | openai/gpt-5.6-sol |
| CT Bot Queue | ct-bot-queue | Sidekiq queue used for CtBotReplyJob and CtBotTranscriptionJob | default |
| CT Bot Transcription Model | ct-bot-transcription-model | OpenRouter model ID used to transcribe voice input | openai/gpt-transcribe |
Platform settings are loaded into PLATFORM_SETTINGS at boot. Restart the
app and Sidekiq after changing them.
How it worksâ
- The widget renders in
layouts/cm_adminwhenCurrent.user.cm_role.catalyst_ai_enabled?. It opens on the user's most recent conversation (or a fresh one) inside a lazy turbo frame. - Sending a message creates a
CtBotMessage(role: :user), which triggersrequest_reply: open artifacts are closed, a pending assistant message is created, andCtBotReplyJobis enqueued to answer it. - The job builds a
CtBotAgentâ aruby_llmagent connected in-process toCmAdmin::Mcp::Serverâ loads the conversation history, and callsask.Current.useris set to the conversation owner, so every MCP tool call is scoped byCmAdmin::<Model>PolicyIndexScopefor that user. - Streamed chunks are broadcast over the signed
CmAdmin::CtBotChannelstream (throttled to ~4/second); the pending message partial re-renders live until the reply finishes. - If the agent calls
ask_user_question, the pending tool call is saved as aCtBotArtifactand rendered as a radio/checkbox card. Submitting an answer creates a user message, which triggers a fresh reply. ruby_llmusage entries are persisted asCtCostTrackingrecords with the request payload attached. Each tracking refreshes the message'stotal_cost(sum of its trackings), which in turn refreshes the conversation'stotal_costâ visible per message and in the Cost Trackings index (filterable by provider and model).- On failure the reply is marked
failedwith a friendly error message and the exception is reported via Airbrake. - Voice input uploads the recording as a
CtBotTranscription(audio attached, linked to the open conversation if any) and enqueuesCtBotTranscriptionJob. The job stores the transcribed text, status and cost (asCtCostTracking), then broadcasts the result over the signedCmAdmin::CtBotTranscriptionChannelstream. When a message is sent in that conversation, the transcription is linked to theCtBotMessage.
Security notesâ
- All chat endpoints require a Catalyst-AI-enabled role and scope every
query to
Current.userâ users can only read their own conversations. - Channel subscriptions use signed stream names; a stream for another user's conversation can't be forged.
- Attachment previews are served inline only for non-SVG images; everything else downloads as an attachment.
- The bot's data access is read-only and goes through the same Pundit policy scopes as the web UI â it cannot see records the asking user couldn't.
Admin viewsâ
- Bot Conversations â index + show (with a Messages tab) for auditing chats, including a rolled-up Total Cost column
- Bot Messages â index/show per message with Total Cost, including a Cost Trackings tab
- Bot Transcriptions â index/show per voice recording with its audio, text, status and Total Cost, including a Cost Trackings tab
- Cost Trackings â global index with provider/model filters
Permission rows for CtBotConversation/CtBotMessage appear on a role only
while Catalyst AI is enabled; disabling the flag removes them.