Connect a model to the editor's Assistant

v0.0.1

The visual editor has an Assistant panel where an author describes a change in words. This guide shows how to connect your own model to it: you write one Elixir module that calls the model, and you name that module in the host's config. The CMS does not ship a model client, so the module is yours.

Before you begin

  • You can edit the host project's config and restart the server.
  • You can call your model from Elixir. Your module needs a function that takes the author's text and returns a reply.
  • To test it, sign in with an account that can open the page builder. See Build a page in the visual editor.

Know the two callbacks

Your module implements the MishkaCmsCore.Runtime.PageBuilder.AiBackend behaviour.

CallbackReturnsUsed for
name/0String.t()The label under AI Assistant in the panel.
ask(prompt, scope, opts){:ok, message} or {:error, reason}One request from the author.
  • prompt is the text the author typed. An attachment, such as the selected element's markup, follows it after a line <!-- attached: label -->.
  • scope is a map with node_id, the id of the selected element, and label, its tag such as h1 or text. Both are nil when nothing is selected.
  • opts is a keyword list with :token, :section, :resource_id and :site_id. The token names the editing session. MishkaCmsCore.Runtime.PageBuilder.EditorSession.fetch/1 returns {:ok, editor} for it, and editor.tree is the document.
  • A message is a map with role: :ai, text (the line in the chat bubble), plain (a small note under it, or nil), scope (a string or nil) and ops (a list of edits).

Write the module

This module sends the prompt to a function you provide, MyApp.Model.complete/2. It expects {:ok, %{summary: text, markup: html_or_nil}} back. When the model returns markup, the module inserts it at the top of the page.

elixir
defmodule MyApp.Assistant do
  @behaviour MishkaCmsCore.Runtime.PageBuilder.AiBackend

  @impl true
  def name, do: "Acme model"

  @impl true
  def ask(prompt, scope, _opts) do
    case MyApp.Model.complete(prompt, timeout: 20_000) do
      {:ok, %{summary: summary, markup: nil}} ->
        {:ok, reply(summary, nil, scope, [])}

      {:ok, %{summary: summary, markup: markup}} ->
        ops = [%{kind: :insert, parent_id: "root", index: 0, template: markup}]
        {:ok, reply(summary, "Inserted a section at the top of the page.", scope, ops)}

      {:error, _reason} ->
        {:error, "The model did not answer. Try again."}
    end
  end

  defp reply(text, plain, scope, ops) do
    %{role: :ai, text: text, plain: plain, scope: scope[:label], ops: ops}
  end
end

Return edits as ops

Each op is a map with a kind. The editor checks it with the same rules as a hand edit. It then applies all the ops of one reply as a single undoable step.

KindRequired fields
inserttemplate
move, delete, duplicate, unwrap, set_textnode_id
set_attr, delete_attrnode_id, name
set_stylenode_id, bucket, prop
reset_stylesnode_id, bucket
wrapnode_id, tag
wrap_manynode_ids, tag

The other fields are parent_id, index, value, value_kind and text. A node_id is n followed by eight lowercase letters or digits, and parent_id: "root" means the top level. An op the editor cannot read, or a placement the page does not allow, changes nothing. The bubble then reads Nothing was changed. with the reason, and your text moves to the small line.

Name the module in the config

Add this line to config/config.exs in the host project, or to config/runtime.exs, then restart the server:

elixir
config :mishka_cms_core, :builder_ai_backend, MyApp.Assistant

Check the connection

  1. Open any page in the visual editor.
  2. In the right panel, click the Assistant tab. Until a backend is connected, the tab starts with the amber notice The AI Assistant is not available yet. Your name/0 text appears under AI Assistant. With no backend configured, it reads Echo (no model configured). Under it, a revisions row reads 0 revisions and autosaved.
  3. Find the Advanced settings row. Its badge reads Connected with your backend and Not connected with the stub, even while the row is collapsed. Click the row to open the backend name and a note. With the stub, the note names this config key.
  4. Select an element on the canvas, type a request in the box and click Send.

the Assistant tab of the right panel with no backend configured: the amber notice at the top, the revisions row, Advanced settings open with the Not connected badge, and Echo (no model configured) under AI Assistant

The Advanced settings badge shows whether a backend is connected.

With no backend configured, the tab opens with the amber notice The AI Assistant is not available yet, the backend name is Echo (no model configured), and the revisions row reads 0 revisions and autosaved. The Not connected badge sits on the Advanced settings row, and the note inside that row names the :builder_ai_backend config key. In a development setup that sets builder_demo_data: true, an empty thread starts with a sample chat instead: a request, then an AI reply that carries a revision label and a Restore button, with 2 revisions in the revisions row. The notice then adds that the conversation below is a sample. It is placeholder text, not your history, and your own thread replaces it from your first request.

After you connect a backend, a reply of yours that changed the page shows a revision label and a Restore button, and the revisions row counts it. One request is one undo step: press ⌘Z to take it back. On Windows and Linux, use Ctrl in place of ⌘.

The Audit tab follows the same rule. While no backend is connected, it shows an amber notice, Some checks and automatic fixes are not available yet, and the Fix buttons of findings that are not about forms are disabled. Once a backend is connected, those buttons work. Fix selects the element, switches to the Assistant tab and sends the finding to your backend as a request. The editor repairs forms findings itself, so they need no backend. See Add a form to a page.

Rewrite or refuse prompts

A plugin on the builder:ai:before_ask event runs before ask/3. It can rewrite the prompt, for a house style or to remove private details, or refuse it with a message. See Plugin events.

Next steps