Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pulse

Application created while I was learning how to Streaming OpenAI in Elixir and Phoenix.

Learn more

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")
end
config :pulse, :openai, api_key: System.fetch_env!("OPENAI_API_KEY")

Setting up the project

To start this Phoenix server:

  • Run mix setup to install and setup dependencies
  • Start Phoenix endpoint with mix phx.server or inside IEx with iex -S mix phx.server

Now you can visit localhost:4000 from your browser.

Working with data

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
)

Playing with the chat_completion/1 function.

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}]} = response

Type finish_reason and you will see the value "stop".

And to obtian the message content:

%{"choices" => [%{"message" => %{ "content" => content }}]} = response

Type 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.

Organizing the sequence of conversarion

  1. Define the messages Map
messages = [%{role: "user", content: "O que é uma maçã em até 5 palavras?"}]
  1. 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"
      }
  1. Pattern matching the response to obtain the message node.
%{"choices" => [%{"message" => message}]} = response
  1. 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"
  }
]
  1. 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"
         }

Livebook

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.server

See below the configs with the Remote execution smart cell.

The Livebook conection to the Phoenix Application node

The final project. How it looks like

The chat is available on chats router. You can visit localhost:4000/chats from your browser.

The text form to with your questions.

The text form to with your questions!

The response streaming from OpenAI.

The response streaming from OpenAI!

Dependecies

This project relies on these dependencies:

  • Req (link), for HTTP requests
  • DotenvParser (link), for parsing .env files

Dealing with building artifacts problems

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 --force

Clear build artifacts and compile:

rm -rf _build
mix deps.compile
mix compile

About

Learning how to streaming OpenAI in Elixir Phoenix ⚡️

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages