Streaming Orderbot
Mix.install([
{:openai_ex, "~> 0.8.5"},
{:kino, "~> 0.13.2"}
])
alias OpenaiEx
alias OpenaiEx.Chat
alias OpenaiEx.ChatMessage
Setup
This notebook creates an Orderbot, similar to the one in Deeplearning.AI Orderbot, but using the streaming version of the Chat Completion API.
openai = System.fetch_env!("LB_OPENAI_API_KEY") |> OpenaiEx.new()
# We need to make the request task pid available to the cancel button by sharing state
{:ok, pid} = Agent.start_link(fn -> nil end, name: :shared_task_pid)
defmodule OpenaiEx.Notebooks.StreamingOrderbot do
alias OpenaiEx
require Logger
def set_task_pid(task_pid) do
Agent.update(:shared_task_pid, fn _ -> task_pid end)
end
def get_task_pid do
Agent.get(:shared_task_pid, fn pid -> pid end)
end
def create_chat_req(args = [_ | _]) do
args
|> Enum.into(%{
model: "gpt-4o-mini",
temperature: 0
})
|> Chat.Completions.new()
end
def get_stream(openai = %OpenaiEx{}, messages) do
openai |> Chat.Completions.create!(create_chat_req(messages: messages), stream: true)
end
def stream_to_completions(%{body_stream: body_stream}) do
body_stream
|> Stream.flat_map(& &1)
|> Stream.map(fn %{data: d} -> d |> Map.get("choices") |> List.first() |> Map.get("delta") end)
|> Stream.filter(fn map -> map |> Map.has_key?("content") end)
|> Stream.map(fn map -> map |> Map.get("content") end)
end
def stream_completion_to_frame(stream, frame) do
try do
result =
stream
|> stream_to_completions()
|> Enum.reduce("", fn token, text ->
next = text <> token
Kino.Frame.render(frame, Kino.Text.new(next))
next
end)
{:ok, result}
rescue
e in OpenaiEx.Error ->
case e do
%{kind: :sse_cancellation} ->
message = "Request was canceled."
Kino.Frame.render(frame, Kino.Text.new(message))
{:canceled, message}
_ ->
message = "An error occurred: #{e.message}"
Kino.Frame.render(frame, Kino.Text.new(message))
{:error, message}
end
end
end
def create_orderbot(openai = %OpenaiEx{}, context) do
chat_frame = Kino.Frame.new()
last_frame = Kino.Frame.new()
inputs = [prompt: Kino.Input.textarea("You")]
form = Kino.Control.form(inputs, submit: "Send", reset_on_submit: [:prompt])
cancel_button = Kino.Control.button("Cancel")
Kino.Frame.render(chat_frame, Kino.Markdown.new("### Orderbot Chat"))
Kino.Layout.grid([chat_frame, cancel_button, last_frame, form], boxed: true, gap: 16)
|> Kino.render()
stream_o = openai |> get_stream(context)
{status, bot_says} = stream_o |> stream_completion_to_frame(last_frame)
Kino.listen(
form,
context ++ if(status == :ok, do: [ChatMessage.assistant(bot_says)], else: []),
fn %{data: %{prompt: you_say}}, history ->
Kino.Frame.render(last_frame, Kino.Text.new(""))
Kino.Frame.append(chat_frame, Kino.Text.new(List.last(history).content))
Kino.Frame.append(chat_frame, Kino.Markdown.new("**You** #{you_say}"))
stream = openai |> get_stream(history ++ [ChatMessage.user(you_say)])
set_task_pid(stream.task_pid)
{status, bot_says} = stream |> stream_completion_to_frame(last_frame)
case status do
:ok -> {:cont, history ++ [ChatMessage.user(you_say), ChatMessage.assistant(bot_says)]}
_ -> {:cont, history}
end
end
)
Kino.listen(
cancel_button,
fn _event ->
pid = get_task_pid()
OpenaiEx.HttpSse.cancel_request(pid)
end
)
end
end
alias OpenaiEx.Notebooks.StreamingOrderbot
Orderbot
context = [
ChatMessage.system("""
You are OrderBot, an automated service to collect orders for a pizza restaurant. \
You first greet the customer, then collects the order, \
and then asks if it's a pickup or delivery. \
You wait to collect the entire order, then summarize it and check for a final \
time if the customer wants to add anything else. \
If it's a delivery, you ask for an address. \
Finally you collect the payment.\
Make sure to clarify all options, extras and sizes to uniquely \
identify the item from the menu.\
You respond in a short, very conversational friendly style. \
The menu includes \
pepperoni pizza 12.95, 10.00, 7.00 \
cheese pizza 10.95, 9.25, 6.50 \
eggplant pizza 11.95, 9.75, 6.75 \
fries 4.50, 3.50 \
greek salad 7.25 \
Toppings: \
extra cheese 2.00, \
mushrooms 1.50 \
sausage 3.00 \
canadian bacon 3.50 \
AI sauce 1.50 \
peppers 1.00 \
Drinks: \
coke 3.00, 2.00, 1.00 \
sprite 3.00, 2.00, 1.00 \
bottled water 5.00 \
""")
]
openai |> StreamingOrderbot.create_orderbot(context)