Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
stream: 6,
scope: 1,
post: 3,
get: 3,
get: 3
]
]
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ config/
.envrc

# Ignore Qdrant storage
/qdrant_storage/
/qdrant_storage/
73 changes: 62 additions & 11 deletions .iex.exs
Original file line number Diff line number Diff line change
@@ -1,28 +1,66 @@
text_prompt = %{model: "text-davinci-003", prompt: "Hello, my name is", max_tokens: 500, stream: true}
instruction_prompt = %{
model: "text-davinci-edit-001",
input: "What day of the wek is it?",
instruction: "Fix the spelling mistakes"
# Chat Prompt
chat_prompt = %{
model: "gpt-4-0613",
messages: [
%{role: "system", content: "You are friendly."},
%{role: "user", content: "Hello!"}
]
}

chat_prompt = %{
model: "gpt-3.5-turbo",
stream_chat = %{
model: "gpt-4-0613",
messages: [
%{role: "user", content: "Hello!"},
%{role: "assistant", content: "Hello there, how may I assist you today?"},
%{role: "user", content: "I'd like to book a flight to New York City."}
%{role: "system", content: "You are friendly."},
%{role: "user", content: "Hello!"}
],
stream: true
}

function_prompt = %{
model: "gpt-4-0613",
messages: [
%{role: "user", content: "Hello!"},
%{role: "assistant", content: "Hello there, how may I assist you today?"},
%{role: "user", content: "I'd like to book a flight to New York City."},
%{
role: "assistant",
content: "Sure, I can assist you with that. Could you please tell me when you're planning to fly?"
},
%{role: "user", content: "I'd like to fly on the 23rd of June."}
],
functions: [
%{
name: "book_flight",
description: "Book a flight to a destination",
parameters: %{
type: "object",
properties: %{
destination: %{
type: "string",
description: "The destination of the flight"
},
date: %{
type: "string",
description: "The date of the flight"
}
},
required: ["destination", "date"]
}
}
]
}

stream_function_prompt = %{
model: "gpt-4-0613",
stream: true,
messages: [
%{role: "user", content: "Hello!"},
%{role: "assistant", content: "Hello there, how may I assist you today?"},
%{role: "user", content: "I'd like to book a flight to New York City."},
%{role: "assistant", content: "Sure, I can assist you with that. Could you please tell me when you're planning to fly?"},
%{
role: "assistant",
content: "Sure, I can assist you with that. Could you please tell me when you're planning to fly?"
},
%{role: "user", content: "I'd like to fly on the 23rd of June."}
],
functions: [
Expand All @@ -46,3 +84,16 @@ function_prompt = %{
}
]
}

# Text Prompt
text_prompt = %{
model: "babbage-002",
prompt: "Say this is a test",
max_tokens: 10,
temperature: 0,
top_p: 1,
n: 1,
stream: false
}

stream_text_prompt = %{model: "babbage-002", prompt: "Hello, my name is", max_tokens: 100, stream: true}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ config :openai,

## Usage

Once configured in your `config.ex` file, you can use the client to call the OpenAi API instantly.
Once configured in your `config.exs` file, you can use the client to call the OpenAi API instantly.

```elixir
prompt = %{model: "gpt-3.5-turbo", messages: [%{role: "user", content: "Hello!"}], stream: true}
Expand Down
46 changes: 32 additions & 14 deletions lib/open_ai.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule OpenAi do
alias OpenAi.Core.Response.ListModels
alias OpenAi.Core.Response.RetrieveModel
alias OpenAi.Core.Response.ChatCompletion
alias OpenAi.Core.Response.TextCompletion

@doc false
def start(_type, _args) do
Expand All @@ -24,9 +25,9 @@ defmodule OpenAi do
@doc """
Lists the currently available models, and provides basic information about each one such as the owner and availability.
"""
@spec list_models() :: {:ok, map()} | {:error, map()}
@spec list_models() :: ListModels.t() | {:error, map()}
def list_models() do
OpenAi.Models.list_models() |> parse_response()
OpenAi.Models.list_models() |> ListModels.parse()
end

@doc """
Expand Down Expand Up @@ -61,9 +62,9 @@ defmodule OpenAi do
"root" => "gpt-4"
}}
"""
@spec retrieve_model(String.t()) :: {:ok, map()} | {:error, map()}
@spec retrieve_model(String.t()) :: RetrieveModel.t() | {:error, map()}
def retrieve_model(model_id) do
OpenAi.Models.retrieve_model(model_id) |> parse_response()
OpenAi.Models.retrieve_model(model_id) |> RetrieveModel.parse()
end

@doc """
Expand All @@ -76,9 +77,19 @@ defmodule OpenAi do
{:ok, "Hello! How may I assist you today?"}
"""

@spec chat_completion(map(), list()) :: {:ok, String.t()} | {:ok, map()} | {:error, map()}
def chat_completion(prompt, streaming_callback \\ :default, options \\ []) do
OpenAi.ChatCompletion.chat_completion(prompt, streaming_callback, options) |> ChatCompletion.parse()
@spec chat_completion(map()) :: ChatCompletion.t() | {:error, map()}
def chat_completion(prompt) do
chat_completion(prompt, [])
end

@spec chat_completion(map(), list()) :: ChatCompletion.t() | {:error, map()}
def chat_completion(prompt, options) do
OpenAi.ChatCompletion.chat_completion(prompt, options, :default) |> ChatCompletion.parse()
end

@spec chat_completion(map(), function(), list()) :: ChatCompletion.t() | {:error, map()}
def chat_completion(prompt, stream_cb, options) do
OpenAi.ChatCompletion.chat_completion(prompt, options, stream_cb) |> ChatCompletion.parse()
end

@doc """
Expand Down Expand Up @@ -111,9 +122,19 @@ defmodule OpenAi do
}
"""

@spec text_completion(map(), function(), list()) :: {:ok, map() | String.t()} | {:error, map()}
def text_completion(prompt, streaming_callback \\ :default, options \\ []) do
OpenAi.TextCompletion.text_completion(prompt, options, streaming_callback) |> parse_response()
@spec text_completion(map()) :: TextCompletion.t() | {:error, map()}
def text_completion(prompt) do
text_completion(prompt, [])
end

@spec text_completion(map(), list()) :: TextCompletion.t() | {:error, map()}
def text_completion(prompt, options) do
OpenAi.TextCompletion.text_completion(prompt, options, :default) |> TextCompletion.parse()
end

@spec text_completion(map(), function(), list()) :: TextCompletion.t() | {:error, map()}
def text_completion(prompt, streaming_callback, options) do
OpenAi.TextCompletion.text_completion(prompt, options, streaming_callback) |> TextCompletion.parse()
end

@doc """
Expand Down Expand Up @@ -144,7 +165,7 @@ defmodule OpenAi do
"""
@spec embed_text(map(), list()) :: {:ok, map()} | {:error, map()}
def embed_text(prompt, options \\ []) do
OpenAi.Embedding.create_embedding(prompt, options) |> parse_response()
OpenAi.Embedding.create_embedding(prompt, options)
end

@doc """
Expand Down Expand Up @@ -262,9 +283,6 @@ defmodule OpenAi do
{:error, %{status_code: status_code, body: body}}
end

defp parse_response({:ok, %{body: stream, type: :stream}}),
do: {:ok, SseParser.parse(stream)}

defp parse_response({:ok, %{body: body}}),
do: body |> Jason.decode()

Expand Down
5 changes: 3 additions & 2 deletions lib/open_ai/chat_completion.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule OpenAi.ChatCompletion do
@moduledoc """
A client for interacting with the OpenAI Competition API.
A client for interacting with the OpenAI chat completion API.

### Chat Parameters
- `model` - ID of the model to use. You can use the `list_models` API to see all of your available models.
Expand Down Expand Up @@ -128,7 +128,8 @@ defmodule OpenAi.ChatCompletion do
}
"""

@spec chat_completion(chat_params(), keyword(), function()) :: {:ok, map() | %{stream: true}} | {:error, map()}
@spec chat_completion(chat_params(), keyword(), atom() | function()) :: {:ok, Finch.Response.t()} | {:error, Exception.t()}

def chat_completion(%{stream: true} = prompt, options, :default),
do: chat_completion(prompt, options, &default_stream_callback/2)

Expand Down
24 changes: 24 additions & 0 deletions lib/open_ai/core/client/completions.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule OpenAi.Core.Client.Completions do
use OpenAi.Core.Client

@doc false
scope "/v1/completions"

def chat_completion(%{stream: true} = prompt, options, :default),
do: chat_completion(prompt, options, &default_stream_callback/2)

def chat_completion(%{stream: true} = prompt, options, stream_callback) do
json_data = Jason.encode!(prompt)
conn = %{headers: nil, status: nil, body: [], type: :stream}
stream(:post, "", json_data, options, conn, stream_callback)
end

def chat_completion(prompt, options, _) do
json_data = Jason.encode!(prompt)
post("", json_data, %{}, options)
end

defp default_stream_callback({:status, data}, acc), do: %{acc | status: data}
defp default_stream_callback({:headers, headers}, acc), do: %{acc | headers: headers}
defp default_stream_callback({:data, data}, %{body: body} = acc), do: %{acc | body: [data | body]}
end
8 changes: 8 additions & 0 deletions lib/open_ai/core/response.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule OpenAi.Core.Response do
@moduledoc """
Structure for parsing OpenAI API responses
"""

@callback parse({:ok, %Finch.Response{}}) :: struct()
@callback parse({:error, map()}) :: {:error, map()}
end
5 changes: 5 additions & 0 deletions lib/open_ai/core/response/audio.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule OpenAi.Core.Response.Audio do
@moduledoc """
Documentation for `Audio` response.
"""
end
51 changes: 51 additions & 0 deletions lib/open_ai/core/response/chat.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule OpenAi.Core.Response.ChatCompletion do
@moduledoc """
Documentation for `ChatCompletion` response.
"""
alias OpenAi.Utils.Parser

@behaviour OpenAi.Core.Response

@enforce_keys [:id, :object, :created_at, :choices, :usage]
defstruct [:id, :object, :created_at, :choices, :usage, :model]

@type t :: %__MODULE__{
id: String.t(),
object: String.t(),
# NOTE: Returned field is called `created`, but we rename it to `created_at` for consistency
created_at: DateTime.t(),
choices: list(map()),
usage: list(map()) | nil,
model: String.t()
}

# * For parsing streaming SSE responses
@impl true
def parse({:ok, %{body: body, type: :stream}}) do
resp = Parser.parse_chat_sse(body)
struct(__MODULE__, resp)
end

# * For parsing HTTP responses
@impl true
def parse({:ok, %{body: body}}) do
case Jason.decode(body) do
{:ok, decoded} ->
opts =
decoded
|> Enum.reduce(%{}, fn {k, v}, acc ->
Map.put(acc, String.to_atom(k), v)
end)
|> Map.delete(:created)
|> Map.put(:created_at, from_unix(decoded))

struct(__MODULE__, opts)

{:error, _} ->
{:error, "Invalid response body"}
end
end

defp from_unix(%{"created" => timestamp}),
do: DateTime.from_unix!(timestamp)
end
37 changes: 37 additions & 0 deletions lib/open_ai/core/response/completions.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule OpenAi.Core.Response.Completions do
@moduledoc """
Represents a response from the OpenAI Completions API.
"""

@behaviour OpenAi.Core.Response

alias OpenAi.Core.Types.CompletionChoice

@enforce_keys [:id, :choices, :created, :model, :system_fingerprint, :object, :usage]

defstruct [:id, :object, :created, :model, :system_fingerprint, :choices, :usage]

@type t :: %__MODULE__{
id: String.t(),
object: String.t(),
created: Integer.t(),
model: String.t(),
system_fingerprint: String.t(),
choices: [CompletionChoice.t()],
usage: Usage.t()
}

@impl true
def parse({:ok, %Finch.Response{status: 200, body: body}}) do
case Jason.decode(body) do
{:ok, decoded} ->
struct(__MODULE__, decoded)

{:error, _} = error ->
error
end
end

@impl true
def parse({:error, _} = error), do: error
end
34 changes: 34 additions & 0 deletions lib/open_ai/core/response/embeddings.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule OpenAi.Core.Response.Embeddings do
@moduledoc """
Documentation for `Embeddings` response.
"""

@behaviour OpenAi.Core.Response

@enforce_keys [:object, :embedding, :index]
defstruct [:object, :embedding, :index]

@type t :: %__MODULE__{
object: String.t(),
embedding: list(number()),
index: number()
}

# * For parsing HTTP responses
@impl true
def parse({:ok, %{body: body}}) do
case Jason.decode(body, keys: :atoms) do
{:ok, decoded} ->
opts =
decoded
|> Enum.reduce(%{}, fn {k, v}, acc ->
Map.put(acc, String.to_atom(k), v)
end)

struct(__MODULE__, opts)

{:error, _} ->
{:error, "Invalid response body"}
end
end
end
Loading