Adding LiveBook secrets to custom Smart Cells


I came across the lack of documentation on how to pass/trigger the modal dialog used by the Kino.DB smart cell when setting up an interactive chat tree widget for working with my GenAI multi provider client wrapper https://github.com/noizu-labs-ml/genai

It turns out to be pretty straight forward, the javscript ctx module exposes a function for launching the modal which can then be passed on down to your smart cell for use in code generation. The livebook the below was generated from is here: Google Gist

SmartCell Secret Input Modal Example

Mix.install([
  {:kino, "~> 0.12.0"}
])

Setup Frame

Quick hack to expose frame to smart cell demo.

frame = Kino.Frame.new()
:persistent_term.put({:demo, :frame}, frame)
:persistent_term.put({:demo, :log}, true)

Break Down

Now here is a quick break down of our of the steps needed to hook up to the LiveBook secret and secret selector modal pop up in our own smart cell provider.

First: we add a secret_key field to our context
@impl true
def init(attrs, ctx) do    
  ctx =
    assign(ctx,
      secret_key: attrs["secret_key"]
    )
  {:ok, ctx, []}
end
Second: Event Handler for secret update

We listen for incoming updates to this key and push back a field updated message to inform the frontend of success.

@impl true
def handle_event("set_secret_key" = event, variable, ctx) do
  ctx = assign(ctx, secret_key: variable["value"])
  broadcast_event(ctx, "update_secret_key", ctx.assigns.secret_key)
  {:noreply, ctx}
end
Third: Evaluate output code gen

Once our smart cell is evaluated we fetch and return the provided key from livebooks secret storage system or return a not set tuple. You could alternatively in this section check if all required inputs are set and return a blank string response wehn unavailable as the Kino DB connection_cell does.

@impl true
def to_source(attrs) do
  secret = attrs["secret_key"]
  if is_bitstring(secret) and secret != "" do      
    quote do
      {:all_your_scrests_belong_to_me, System.fetch_env!(unquote("LB_#{secret}"))}
    end
  else
    quote do         
      {:oh, :you_havent_set_the_secret_yet}
    end
  end
  |> Kino.SmartCell.quoted_to_string()
end

Front End

Finally we define a basic html form to accept the required params.
We wrap the label and input for the secret input field with a on click event that invokes the javascript’s ctx.selectSecret method. It pops the input modal and once selected the provided secret hanlde is pushed back to our live cell’s set_secret_key event handler.

ctx.selectSecret(
     (secretName) => {
       ctx.pushEvent("set_secret_key", {
         field: "if_you_wanted_to_impelment_a_handler_method_supporting_multiple_inputs",
         value: secretName,
       });
     },
     "YOUR_SECRET_KEY_DEFAULT",
     { title: "TELL ME YOUR SECRET" }
   );
const el = ctx.root.querySelector(`.secret-modal`);   
    const el_input = ctx.root.querySelector(`.secret-modal [name="secret"]`);   
    ctx.handleEvent("update_secret_key", (text) => {
      alert("hey")
      el_input.value = text;
    });

Logging

To assist with inspecting the internal state of our smart cell I’ve added jury rigged (qiocl amd dirty) persistent_term backed toggler for logs and log output statements at the top of our smart cell methods.
method for passing our output frame and log on/off toggle to our smart cell.
An obvious improvement here for a longer term solution would be to pass in the frame as an input and, and track the log on/flag in the per instance ctx context state. And add the frame as the output of a cell on our live book.

frame = Kino.Frame.new()
:persistent_term.put({:demo, :frame}, frame)
:persistent_term.put({:demo, :log}, false)

To indicate the value has been assigned we add a listener for the handle_event update_secret_key broadcast event.

Here it is all together:

defmodule TheRobotLives.AddingSecretsToKinoSmartCell do
  use Kino.JS
  use Kino.JS.Live
  use Kino.SmartCell, name: "Smart Cell Secret Selection Demo"

  @impl true
  def init(attrs, ctx) do
    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([attrs: attrs, ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    :persistent_term.get({:demo, :log}) &&
      Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    ctx =
      assign(ctx,
        secret_key: attrs["secret_key"]
      )

    {:ok, ctx, []}
  end

  @impl true
  def handle_connect(ctx) do
    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([actx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    :persistent_term.get({:demo, :log}) &&
      Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    {:ok, %{secret_key: ctx.assigns.secret_key}, ctx}
  end

  @impl true
  def handle_event("set_secret_key" = event, variable, ctx) do
    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([event: event, variable: variable, ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    :persistent_term.get({:demo, :log}) &&
      Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    ctx = assign(ctx, secret_key: variable["value"])
    broadcast_event(ctx, "update_secret_key", ctx.assigns.secret_key)
    {:noreply, ctx}
  end

  @impl true
  def handle_event("toggle_log" = event, args, ctx) do
    s = :persistent_term.get({:demo, :log})

    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)} - Set To #{inspect(!s)}\n\n```elixir\n#{inspect([event: event, args: args, ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    :persistent_term.put({:demo, :log}, !s)
    {:noreply, ctx}
  end

  @impl true
  def to_attrs(ctx) do
    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    :persistent_term.get({:demo, :log}) &&
      Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    %{"secret_key" => ctx.assigns.secret_key}
  end

  @impl true
  def to_source(attrs) do
    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([attrs: attrs], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    :persistent_term.get({:demo, :log}) &&
      Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    secret = attrs["secret_key"]

    if is_bitstring(secret) and secret != "" do
      quote do
        {:all_your_scrests_belong_to_me, System.fetch_env!(unquote("LB_#{secret}"))}
      end
    else
      quote do
        {:oh, :you_havent_set_the_secret_yet}
      end
    end
    |> Kino.SmartCell.quoted_to_string()
  end

  asset "main.js" do
    """
    export function init(ctx, payload) {
      ctx.importCSS("main.css");
      ctx.importCSS("https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap");
      root.innerHTML = `
        <div class="app">
          <div class="secret-modal">
            <label class="label">Secret</label>
            <input class="input" type="password" name="secret" />
          </div> 
          <label class="label">Enable Frame Logs</label>
          <input class="input" type="checkbox" name="log" />
        </div>
      `;
      
      const el = ctx.root.querySelector(`.secret-modal`);   
      const el_input = ctx.root.querySelector(`.secret-modal [name="secret"]`);   
      ctx.handleEvent("update_secret_key", (text) => {
        el_input.value = text;
      });
      


      el.addEventListener("click", (event) => {
        const preselectName = "DEFAULT_ENV_VAR";
        ctx.selectSecret(
          (secretName) => {
            ctx.pushEvent("set_secret_key", {
              field: "if_you_wanted_to_impelment_a_handler_method_supporting_multiple_inputs",
              value: secretName,
            });
          },
          preselectName,
          { title: "TELL ME YOUR SECRET" }
        );
      });

      const el2 = ctx.root.querySelector(`[name="log"]`);   

      el2.addEventListener("click", (event) => {
      ctx.pushEvent("toggle_log", {});
      });


      ctx.handleSync(() => {
        // Synchronously invokes change listeners
        document.activeElement &&
          document.activeElement.dispatchEvent(new Event("change"));
      });
    }
    """
  end

  asset "main.css" do
    """
    // make it pretty
    """
  end
end

Kino.SmartCell.register(TheRobotLives.AddingSecretsToKinoSmartCell)

And here it is in action LiveBook file


Leave a Reply

Your email address will not be published. Required fields are marked *