Packages

PromptBuddy is a Livebook Smart Cell that allows pair programming with LLMs.

Current section

Files

Jump to
kino_promptbuddy lib context.ex
Raw

lib/context.ex

defmodule Kino.PromptBuddy.Context do
@moduledoc false
# -- Impure helpers (Livebook/ERPC boundary) --------------------------------
def get_session_id(kino), do: Kino.JS.Live.call(kino, :get_session_id)
def get_current_cell_id() do
Kino.Bridge.get_evaluation_file()
|> String.split("#cell:")
|> List.last()
end
def get_notebook(session_id) do
with {:ok, node_norm, session} <- fetch_session(session_id) do
{:ok, :erpc.call(node_norm, Livebook.Session, :get_notebook, [session.pid])}
end
end
def get_notebook_from_session({:ok, node_norm, session}),
do: {:ok, :erpc.call(node_norm, Livebook.Session, :get_notebook, [session.pid])}
def get_notebook_from_session(_), do: {:error, :invalid_session}
def fetch_session(session_id) when is_binary(session_id) do
node_norm = normalized_node()
Node.set_cookie(node_norm, Node.get_cookie())
sessions = :erpc.call(node_norm, Livebook.Tracker, :list_sessions, [])
case Enum.find(sessions, &(&1.id == session_id)) do
nil -> {:error, :session_not_found}
session -> {:ok, node_norm, session}
end
end
def fetch_session(_), do: {:error, :invalid_session_id}
def normalized_node do
node()
|> Atom.to_string()
|> String.replace(~r/--[^@]+@/, "@")
|> String.to_atom()
end
# -- Pure transforms ---------------------------------------------------------
@cell_code :"Elixir.Livebook.Notebook.Cell.Code"
@cell_markdown :"Elixir.Livebook.Notebook.Cell.Markdown"
@cell_smart :"Elixir.Livebook.Notebook.Cell.Smart"
def build_precedent_messages(nb) when is_map(nb),
do: build_precedent_messages(nb, get_current_cell_id())
def build_precedent_messages(nb, current_cell_id) when is_map(nb) do
nb
|> all_cells()
|> Enum.take_while(&(&1.id != current_cell_id))
|> Enum.flat_map(&cell_to_messages/1)
end
defp all_cells(%{sections: secs}), do: Enum.flat_map(secs, & &1.cells)
def cell_to_messages(%{__struct__: @cell_code, source: src, outputs: outs}) do
source_msg(src) ++ output_msgs(outs)
end
def cell_to_messages(%{__struct__: @cell_markdown, source: md}) do
md = String.trim(md)
cond do
String.starts_with?(md, "**Buddy:**") ->
content = md |> String.replace_prefix("**Buddy:**", "") |> String.trim()
if content == "", do: [], else: [ReqLLM.Context.assistant(content)]
String.starts_with?(md, "**User:**") ->
content = md |> String.replace_prefix("**User:**", "") |> String.trim()
if content == "", do: [], else: [ReqLLM.Context.user(content)]
md != "" -> [ReqLLM.Context.user(md)]
true -> []
end
end
def cell_to_messages(%{__struct__: @cell_smart, outputs: outs}), do: output_msgs(outs)
def cell_to_messages(_), do: []
def source_msg(src) when is_binary(src) do
src = String.trim(src)
if src == "", do: [], else: [ReqLLM.Context.user(src)]
end
def output_msgs(outs) when is_list(outs) do
Enum.flat_map(outs, fn
{_id, %{type: :plain_text, text: text}} -> [ReqLLM.Context.assistant(clean_text(text))]
{_id, %{type: :terminal_text, text: text}} -> [ReqLLM.Context.assistant(clean_text(text))]
{_id, %{text: text}} -> [ReqLLM.Context.assistant(clean_text(text))]
{_id, %{outputs: nested}} -> output_msgs(nested)
_ -> []
end)
end
def clean_text(text), do: text |> String.replace(~r/\e\[[\d;]*m/, "") |> String.trim()
end