Mix.install([
{:kino, "~> 0.9.1"},
{:poison, "~> 4.0"},
{:httpoison, "~> 1.8"},
{:explorer, "~> 0.5.0"},
{:kino_explorer, "~> 0.1.7"}
])
defmodule OpenAIClient do
require HTTPoison
defstruct [:openai_key, :model]
def new(openai_key \\ nil, openai_key_path \\ nil) do
key =
case openai_key do
nil -> load_openai_key(openai_key_path)
_ -> openai_key
end
%OpenAIClient{:openai_key => key}
end
def load_openai_key(path \\ "openai_medsi_key.txt") do
{:ok, key} = File.read(Path.expand(path))
String.trim(key)
end
defp get_completion_body(model, prompt) do
Poison.encode!(%{
"model" => model,
"prompt" => prompt,
"temperature" => 0.9,
"max_tokens" => 150,
"top_p" => 1,
"frequency_penalty" => 0.0,
"presence_penalty" => 0.6,
"stop" => [" Human:", " AI:"]
})
end
defp get_chat_body(model, prompt) do
Poison.encode!(%{
"model" => model,
"messages" => [%{"role" => "user", "content" => prompt}],
"temperature" => 0.9,
"max_tokens" => 15
})
end
defp is_completion_model_name(model) do
model |> String.contains?("text-davinci")
end
defp get_openai_params(model, prompt) do
if is_completion_model_name(model) do
{"https://api.openai.com/v1/completions", get_completion_body(model, prompt)}
else
{"https://api.openai.com/v1/chat/completions", get_chat_body(model, prompt)}
end
end
defp _get_response(client, prompt, model) do
headers = [
{"Content-Type", "application/json"},
{"Authorization", "Bearer #{client.openai_key}"}
]
{url, request_body} = get_openai_params(model, prompt)
options = [recv_timeout: 10000, timeout: 10000]
HTTPoison.post(url, request_body, headers, options)
end
defp get_response(%{"choices" => choices}) do
[result | _] = choices
{:ok, get_response_text(result)}
end
defp get_response(%{"error" => error}) do
{:error, error["message"]}
end
defp get_response_text(%{"message" => message}) do
message["content"]
end
defp get_response_text(%{"text" => text}) do
text
end
def get_completion_response(client, prompt, model) do
maybe_api_response = _get_response(client, prompt, model)
case maybe_api_response do
{:ok, %_{body: body}} ->
body = Poison.decode!(body)
response = get_response(body)
response
{status, %_{reason: reason}} ->
{status, Atom.to_string(reason)}
_ ->
maybe_api_response
end
end
end
defmodule Generator do
def openai_client do
OpenAIClient.new(nil, "openai_key.txt")
end
def generate(prompt, model \\ "gpt-3.5-turbo-0613") do
maybe_result = openai_client() |> OpenAIClient.get_completion_response(prompt, model)
case maybe_result do
{:error, result} -> "Error: " <> result
{:ok, result} -> result
_ -> "OpenAI Error"
end
end
end
response = Generator.generate("a cat walks into a bar")
Kino.Markdown.new("### Chat\n----------")
models = ["text-davinci-003", "gpt-3.5-turbo-0613", "gpt-4-0613"]
inputs = [
prompt: Kino.Input.text("Prompt"),
model: Kino.Input.select("model", for(m <- models, do: {m, m}))
]
chat_form = Kino.Control.form(inputs, submit: "Send", reset_on_submit: [:prompt])
Kino.Markdown.new("### Viewable table results\n------------")
json_frame = Kino.Frame.new()
Kino.Markdown.new("### Exportable results in JSONLines\n----------")
table_frame = Kino.Frame.new()
defmodule KinoUtils do
def prettyprint(record) do
~s"""
#### prompt:
#{record["prompt"]},
#### response:
#{record["response"]}
"""
|> String.trim()
end
def prepare_outputs(prompt, response) do
%{"prompt" => prompt, "response" => response}
end
def display_tables(prompt, response, table_frame, json_frame) do
record = prepare_outputs(prompt, response)
Kino.Frame.append(table_frame, Kino.Text.new(Poison.encode!(record)))
Kino.Frame.append(json_frame, Kino.Markdown.new(prettyprint(record)))
end
end
Kino.listen(chat_form, fn %{data: %{prompt: prompt, model: model}, origin: origin} ->
if prompt != "" do
prompt_md = Kino.Markdown.new("**user**: #{prompt}. Generating...")
Kino.Frame.append(frame, prompt_md)
response = Generator.generate(prompt, model)
content_md = Kino.Markdown.new("**bot**: #{response}")
Kino.Frame.append(frame, content_md)
KinoUtils.display_tables(prompt, response, table_frame, json_frame)
else
content = Kino.Markdown.new("_ERROR! You need a name and message to submit..._")
Kino.Frame.append(frame, content, to: origin)
end
end)