View Source Completions Bot

Mix.install([
  {:openai_ex, "~> 0.3.0"},
  {:kino, "~> 0.11.0"}
])

alias OpenaiEx
alias OpenaiEx.Completion

Model Choices

openai = System.fetch_env!("LB_OPENAI_API_KEY") |> OpenaiEx.new()
comp_models = [
  "text-davinci-003",
  "text-davinci-002",
  "text-curie-001",
  "text-babbage-001",
  "text-ada-001"
]

Normal Completion

This function calls the completion API and renders the result in the given Kino frame.

completion = fn model, prompt, max_tokens, temperature, last_frame ->
  text =
    openai
    |> Completion.create(%{
      model: model,
      prompt: prompt,
      max_tokens: max_tokens,
      temperature: temperature
    })
    |> Map.get("choices")
    |> Enum.at(0)
    |> Map.get("text")

  Kino.Frame.render(last_frame, Kino.Markdown.new("**Bot** #{text}"))
  text
end

Streaming Completion

This function calls the streaming completion API and continously updates the Kino frame with the latest tokens

completion_stream = fn model, prompt, max_tokens, temperature, last_frame ->
  token_stream =
    openai
    |> Completion.create(
      %{
        model: model,
        prompt: prompt,
        max_tokens: max_tokens,
        temperature: temperature
      },
      stream: true
    )
    |> Stream.flat_map(& &1)
    |> Stream.map(fn %{data: data} ->
      data |> Map.get("choices") |> Enum.at(0) |> Map.get("text")
    end)

  token_stream
  |> Enum.reduce("", fn out, acc ->
    next = acc <> out
    Kino.Frame.render(last_frame, Kino.Markdown.new("**Bot** #{next}"))
    next
  end)
end

Create a Form UI

This is a function to create a Form UI that can be used to call the completion API. The 2nd parameter determines whether the normal or streaming API is called.

create_form = fn title, completion_fn ->
  chat_frame = Kino.Frame.new()
  last_frame = Kino.Frame.new()

  Kino.Frame.render(chat_frame, Kino.Markdown.new(title))

  inputs = [
    model: Kino.Input.select("Model", comp_models |> Enum.map(fn x -> {x, x} end)),
    max_tokens: Kino.Input.number("Max Tokens", default: 400),
    temperature: Kino.Input.number("Temperature", default: 1),
    prompt: Kino.Input.textarea("Prompt")
  ]

  form = Kino.Control.form(inputs, submit: "Send", reset_on_submit: [:prompt])

  Kino.listen(
    form,
    fn %{
         data: %{
           prompt: prompt,
           model: model,
           max_tokens: max_tokens,
           temperature: temperature
         }
       } ->
      Kino.Frame.render(chat_frame, Kino.Markdown.new(title))
      Kino.Frame.append(chat_frame, Kino.Markdown.new("**Me** #{prompt}"))

      completion_fn.(model, prompt, max_tokens, temperature, last_frame)
    end
  )

  Kino.Layout.grid([chat_frame, last_frame, form], boxed: true, gap: 16)
end

Normal Chatbot

Create the Form for the non-streaming completion API and use it.

create_form.("## Completion Chatbot", completion)

Streaming Chatbot

Create the form for the streaming completion API, and use it.

create_form.("## Streaming Chatbot", completion_stream)