Application created while I was learning how to Streaming OpenAI in Elixir and Phoenix.
I walked through this tutorial's series by Ben Reinhart following each step to build this project.
Here is the sequence of tutorials:
In order to develop this project, you will need to create an OpenAI account and obtain your API key.
I'm storing my API key in a .env file in the root of the project. You can create your own .env file and add the following content:
OPENAI_API_KEY="your-openai-key"Add the following dependencies to your mix.exs file:
{:dotenv_parser, "~> 2.0"},And these instructions to your runtime.exs:
if config_env() == :dev do
DotenvParser.load_file(".env")
endconfig :pulse, :openai, api_key: System.fetch_env!("OPENAI_API_KEY")To start this Phoenix server:
- Run
mix setupto install and setup dependencies - Start Phoenix endpoint with
mix phx.serveror inside IEx withiex -S mix phx.server
Now you can visit localhost:4000 from your browser.
During the developemnt, you can use the instructions below to interact wit the chat_completion function.
{:ok, %{body: response}} = Pulse.Openai.chat_completion(%{ model: "gpt-3.5-turbo", messages: [%{role: "user", content: "Hello 3.5!"}] })
{:ok, %{body: response}} =
Pulse.Openai.chat_completion(
%{
model: "gpt-3.5-turbo",
messages: [%{role: "user", content: "Hello 3.5!"}]
},
&IO.puts/1
)
Open your iex terminal.
Define the messages variable:
messages = [%{role: "user", content: "O que é uma maçã em 5 palavras?"}]Passing the messages variable to the function calling:
{:ok, %{body: response}} = Pulse.Openai.chat_completion(%{ model: "gpt-3.5-turbo", messages: messages })You can add more parameters as max_tokens, temperature, etc.
{:ok, %{body: response}} = Pulse.Openai.chat_completion(%{ model: "gpt-3.5-turbo", max_tokens: 1000, temperature: 0, messages: messages })Here is a simple way you can obtain the response content utilizing the Map functions:
content = response |> Map.get("choices") |> Enum.at(0) |> Map.get("message") |> Map.get("content")And there's a better way to perform it, utilizing the pattern matching.
To obtain the finish_reason:
%{"choices" => [%{"finish_reason" => finish_reason}]} = responseType finish_reason and you will see the value "stop".
And to obtian the message content:
%{"choices" => [%{"message" => %{ "content" => content }}]} = responseType content and you will see the value "Fruta redonda e saborosa.".
As you can see, pattern matching is a powerful tool in Elixir for parsing nested data structures. So, you can use it to extract the data you need. Get used to it.
- Define the messages Map
messages = [%{role: "user", content: "O que é uma maçã em até 5 palavras?"}]- Call the prompt with our initial messages Map
{:ok, %{body: response}} = Pulse.Openai.chat_completion(%{ model: "gpt-3.5-turbo", max_tokens: 1000, temperature: 0, messages: messages })It will generate our response. Note the message Map returned:
"message" => %{
"content" => "Fruta redonda e saborosa.",
"refusal" => nil,
"role" => "assistant"
}- Pattern matching the response to obtain the message node.
%{"choices" => [%{"message" => message}]} = response- Add the message to the messages Map.
messages = messages ++ [message]This will be the result of the new messages Map
[
%{role: "user", content: "O que é uma maçã em até 5 palavras?"},
%{
"content" => "Fruta redonda e saborosa.",
"refusal" => nil,
"role" => "assistant"
}
]- Add the map for the next question to the messages Map.
messages = messages ++ [%{role: "user", content: "E qual a sua cor?"}]The new result of messages Map:
[
%{role: "user", content: "O que é uma maçã em até 5 palavras?"},
%{
"content" => "Fruta redonda e saborosa.",
"refusal" => nil,
"role" => "assistant"
},
%{role: "user", content: "E qual a sua cor?"}
]And process the prompt again, now with the messages Map containing the first and the last message. Processa novamente o prompt.
{:ok, %{body: response}} = Pulse.Openai.chat_completion(%{ model: "gpt-3.5-turbo", max_tokens: 1000, temperature: 0, messages: messages })Note the new content returned on the message Map.
"message" => %{
"content" => "Vermelha ou verde.",
"refusal" => nil,
"role" => "assistant"
}For an interactive example, you can utilize the Livebook Getting Started - Consuming the Pulse functions. More instructions will be provided soon.
Start the application with a fully qualified node name (the --name command-line option) with a cookie, and then conect the Livebook to it.
iex --name pulse-app@127.0.0.1 --cookie pulse-secret -S mix phx.serverSee below the configs with the Remote execution smart cell.
The chat is available on chats router. You can visit localhost:4000/chats from your browser.
The text form to with your questions.
The response streaming from OpenAI.
This project relies on these dependencies:
Sometimes there's some incompatibilies with the files on _build folder. In this case, proceed with one of the steps below.
Clear and recompile modules with:
mix compile --forceClear build artifacts and compile:
rm -rf _build
mix deps.compile
mix compile