Skip to main content

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_question tool and the user answers on an options card
  • Per-role opt-in — the is_catalyst_ai_enabled flag on CmRole
  • 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):

SettingSlugPurposeDefault
CT Bot Greetingct-bot-greetingMessage shown at the start of every new chatHi! How can I help you today?
CT Bot Modelct-bot-modelOpenRouter model ID (e.g. openai/gpt-5.6)openai/gpt-5.6-sol
CT Bot Queuect-bot-queueSidekiq queue used for CtBotReplyJob and CtBotTranscriptionJobdefault
CT Bot Transcription Modelct-bot-transcription-modelOpenRouter model ID used to transcribe voice inputopenai/gpt-transcribe
note

Platform settings are loaded into PLATFORM_SETTINGS at boot. Restart the app and Sidekiq after changing them.


How it works​

  1. The widget renders in layouts/cm_admin when Current.user.cm_role.catalyst_ai_enabled?. It opens on the user's most recent conversation (or a fresh one) inside a lazy turbo frame.
  2. Sending a message creates a CtBotMessage (role: :user), which triggers request_reply: open artifacts are closed, a pending assistant message is created, and CtBotReplyJob is enqueued to answer it.
  3. The job builds a CtBotAgent — a ruby_llm agent connected in-process to CmAdmin::Mcp::Server — loads the conversation history, and calls ask. Current.user is set to the conversation owner, so every MCP tool call is scoped by CmAdmin::<Model>PolicyIndexScope for that user.
  4. Streamed chunks are broadcast over the signed CmAdmin::CtBotChannel stream (throttled to ~4/second); the pending message partial re-renders live until the reply finishes.
  5. If the agent calls ask_user_question, the pending tool call is saved as a CtBotArtifact and rendered as a radio/checkbox card. Submitting an answer creates a user message, which triggers a fresh reply.
  6. ruby_llm usage entries are persisted as CtCostTracking records with the request payload attached. Each tracking refreshes the message's total_cost (sum of its trackings), which in turn refreshes the conversation's total_cost — visible per message and in the Cost Trackings index (filterable by provider and model).
  7. On failure the reply is marked failed with a friendly error message and the exception is reported via Airbrake.
  8. Voice input uploads the recording as a CtBotTranscription (audio attached, linked to the open conversation if any) and enqueues CtBotTranscriptionJob. The job stores the transcribed text, status and cost (as CtCostTracking), then broadcasts the result over the signed CmAdmin::CtBotTranscriptionChannel stream. When a message is sent in that conversation, the transcription is linked to the CtBotMessage.

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.