From 508297d09abf5e5569f0503e2807870e0424b7a0 Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Thu, 6 Jul 2023 19:58:24 +0200 Subject: [PATCH 01/14] feat(core): Add response structure Signed-off-by: marinac-dev --- lib/open_ai/core/response.ex | 8 ++++++ lib/open_ai/core/response/list_models.ex | 28 ++++++++++++++++++ lib/open_ai/core/response/retrieve_model.ex | 32 +++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 lib/open_ai/core/response.ex create mode 100644 lib/open_ai/core/response/list_models.ex create mode 100644 lib/open_ai/core/response/retrieve_model.ex diff --git a/lib/open_ai/core/response.ex b/lib/open_ai/core/response.ex new file mode 100644 index 0000000..ca2a1c4 --- /dev/null +++ b/lib/open_ai/core/response.ex @@ -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 diff --git a/lib/open_ai/core/response/list_models.ex b/lib/open_ai/core/response/list_models.ex new file mode 100644 index 0000000..40ad6f3 --- /dev/null +++ b/lib/open_ai/core/response/list_models.ex @@ -0,0 +1,28 @@ +defmodule OpenAi.Core.Response.ListModels do + @moduledoc """ + Structure for parsing OpenAI API responses for `list_models` + """ + @behaviour OpenAi.Core.Response + + @enforce_keys [:object, :data] + defstruct [:object, :data] + + @type t :: %__MODULE__{ + object: String.t(), + data: list(map()) + } + + @impl true + def parse({:ok, %{body: body}}) do + case Jason.decode(body) do + {:ok, %{"object" => object, "data" => data}} -> + %__MODULE__{ + object: object, + data: data + } + + {:error, _} -> + {:error, %{"message" => "Unable to parse response"}} + end + end +end diff --git a/lib/open_ai/core/response/retrieve_model.ex b/lib/open_ai/core/response/retrieve_model.ex new file mode 100644 index 0000000..4dc399f --- /dev/null +++ b/lib/open_ai/core/response/retrieve_model.ex @@ -0,0 +1,32 @@ +defmodule OpenAi.Core.Response.RetrieveModel do + @moduledoc """ + Structure for parsing OpenAI API responses for `retrieve_model` + """ + @behaviour OpenAi.Core.Response + + @enforce_keys [:id, :object, :owned_by, :permission] + defstruct [:id, :object, :owned_by, :permission] + + @type t :: %__MODULE__{ + id: String.t(), + object: String.t(), + owned_by: String.t(), + permission: list(map()) + } + + @impl true + def parse({:ok, %{body: body}}) do + case Jason.decode(body) do + {:ok, %{"id" => id, "object" => object, "owned_by" => owned_by, "permission" => permission}} -> + %__MODULE__{ + id: id, + object: object, + owned_by: owned_by, + permission: permission + } + + {:error, _} -> + {:error, %{"message" => "Unable to parse response"}} + end + end +end From a042f49254941ae219f85ef2d7dbb9b35d31aad0 Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Thu, 6 Jul 2023 20:00:36 +0200 Subject: [PATCH 02/14] chore: Update .gitignore and add iex prompts for testing Signed-off-by: marinac-dev --- .gitignore | 2 +- .iex.exs | 49 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index e1e002d..2789f64 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,4 @@ config/ .envrc # Ignore Qdrant storage -/qdrant_storage/ \ No newline at end of file +/qdrant_storage/ diff --git a/.iex.exs b/.iex.exs index 32a3af2..b707042 100644 --- a/.iex.exs +++ b/.iex.exs @@ -1,21 +1,52 @@ -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 = %{ + 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: "system", content: "You are friendly."}, %{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."} ], 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: [ From 5a32a3809377cc368244f57245b553ba561ed048 Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Thu, 6 Jul 2023 20:06:23 +0200 Subject: [PATCH 03/14] feat(core): Remove SSE parser (will be moved to ChatCompletition module) Signed-off-by: marinac-dev --- lib/open_ai.ex | 11 +++------ lib/open_ai/utils/sse_parser.ex | 43 --------------------------------- 2 files changed, 4 insertions(+), 50 deletions(-) delete mode 100644 lib/open_ai/utils/sse_parser.ex diff --git a/lib/open_ai.ex b/lib/open_ai.ex index daaa007..d40428f 100644 --- a/lib/open_ai.ex +++ b/lib/open_ai.ex @@ -24,9 +24,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 """ @@ -61,9 +61,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 """ @@ -262,9 +262,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() diff --git a/lib/open_ai/utils/sse_parser.ex b/lib/open_ai/utils/sse_parser.ex deleted file mode 100644 index b82e59e..0000000 --- a/lib/open_ai/utils/sse_parser.ex +++ /dev/null @@ -1,43 +0,0 @@ -defmodule OpenAi.Utils.SseParser do - @moduledoc """ - Parses a server-sent event stream into a string. - """ - - @spec parse(list()) :: String.t() - def parse(response) do - response - |> Enum.reverse() - |> Enum.join() - |> String.split("data: ") - |> Enum.reduce([], fn - "", acc -> acc - "[DONE]" <> _, acc -> acc - line, acc -> [parse_line(line) | acc] - end) - |> Enum.reverse() - - # |> Enum.join() - end - - defp parse_line(data) do - data - |> Jason.decode!() - |> Map.get("choices") - |> List.first() - |> case do - # * This is for streaming text responses - %{"text" => text} -> text - # * This is for streaming chat responses - %{"delta" => delta} -> parse_delta(delta) - end - end - - # * Order matters here - defp parse_delta(%{"function_call" => function_call}) do - IO.inspect(function_call, label: "function_call") - function_call - end - - defp parse_delta(%{"content" => content}), do: content - defp parse_delta(%{}), do: "" -end From 79fcc09ea008cdfa4adaa878a9a64b9070b05a97 Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Thu, 6 Jul 2023 20:06:57 +0200 Subject: [PATCH 04/14] feat(core): Add context and message utility modules Signed-off-by: marinac-dev --- lib/open_ai/utils/context.ex | 135 +++++++++++++++++++++++++++++++++++ lib/open_ai/utils/message.ex | 21 ++++++ 2 files changed, 156 insertions(+) create mode 100644 lib/open_ai/utils/context.ex create mode 100644 lib/open_ai/utils/message.ex diff --git a/lib/open_ai/utils/context.ex b/lib/open_ai/utils/context.ex new file mode 100644 index 0000000..8413a35 --- /dev/null +++ b/lib/open_ai/utils/context.ex @@ -0,0 +1,135 @@ +defmodule OpenAi.Utils.Context do + @moduledoc """ + Documentation for `OpenAi.Utils.Context` module. + + This module contains functions for working with OpenAI chat completion contexts + """ + alias OpenAi.Utils.Message + + @enforce_keys [:last_response, :history] + defstruct [:last_response, :history] + + @type t :: %__MODULE__{ + last_response: Message.t(), + history: list(Message.t()) + } + + @doc """ + Initializes a new context. + """ + def init() do + %__MODULE__{ + last_response: "", + history: [] + } + end + + @doc """ + Adds a system response to the conversation history, returns the updated conversation context.\n + """ + @spec add_system_response(t(), String.t()) :: {:ok, t()} | {:error, String.t()} + def add_system_response(context, content) do + add(context, %Message{role: "system", content: content}) + end + + @doc """ + Adds a system response to the conversation history, returns the updated conversation context.\n + """ + @spec add_system_response!(t(), String.t()) :: t() + def add_system_response!(context, content) do + add!(context, %Message{role: "system", content: content}) + end + + @doc """ + Adds an assistant response to the conversation history, returns the updated conversation context.\n + """ + @spec add_assistant_response(t(), String.t()) :: {:ok, t()} | {:error, String.t()} + def add_assistant_response(context, content) do + add(context, %Message{role: "assistant", content: content}) + end + + @doc """ + Adds an assistant response to the conversation history, returns the updated conversation context.\n + """ + @spec add_assistant_response!(t(), String.t()) :: t() + def add_assistant_response!(context, content) do + add!(context, %Message{role: "assistant", content: content}) + end + + @doc """ + Adds a user response to the conversation history, returns the updated conversation context.\n + """ + @spec add_user_response(t(), String.t()) :: {:ok, t()} | {:error, String.t()} + def add_user_response(context, content) do + add(context, %Message{role: "user", content: content}) + end + + @doc """ + Adds a user response to the conversation history, returns the updated conversation context.\n + """ + @spec add_user_response!(t(), String.t()) :: t() + def add_user_response!(context, content) do + add!(context, %Message{role: "user", content: content}) + end + + @doc """ + Returns the last response from the assistant. + """ + @spec last_assistant_response(t()) :: %Message{} | nil + def last_assistant_response(context) do + context.history + |> Enum.find(fn %Message{role: role} -> role == "assistant" end) + end + + @doc """ + Returns the last response from the user. + """ + @spec last_user_response(t()) :: %Message{} | nil + def last_user_response(context) do + context.history + |> Enum.find(fn %Message{role: role} -> role == "user" end) + end + + @doc """ + Returns the last response from the system. + """ + @spec last_system_response(t()) :: %Message{} | nil + def last_system_response(context) do + context.history + |> Enum.find(fn %Message{role: role} -> role == "system" end) + end + + @doc """ + Adds an item to the conversation history and last response, returns the updated conversation context.\n + Item must have the following fields: `role`, `content`.\n + Role can be either `system`, `assistant` or `user`.\n + + Example: + + iex> add(context, %{role: "user", content: "Hello"}) + """ + @spec add(t(), Message.t()) :: {:ok, t()} | {:error, String.t()} + def add(context, %Message{role: role, content: _} = item) when role in ["system", "assistant", "user"] do + new_context = Map.put(context, :history, [item | context.history]) + new_context = Map.put(new_context, :last_response, item) + {:ok, new_context} + end + + def add(_context, %{role: _}) do + {:error, "Invalid role. Role must be one of the following: system, assistant, or user."} + end + + @doc """ + Adds an item to the conversation history, returns the updated conversation context.\n + """ + @spec add!(t(), Message.t()) :: t() + def add!(context, %Message{role: role, content: _} = item) when role in ["system", "assistant", "user"] do + new_context = Map.put(context, :history, [item | context.history]) + new_context = Map.put(new_context, :last_response, item) + new_context + end + + def add!(_context, %{role: _}) do + raise "Invalid role. Role must be one of the following: system, assistant, or user." + end +end diff --git a/lib/open_ai/utils/message.ex b/lib/open_ai/utils/message.ex new file mode 100644 index 0000000..3330323 --- /dev/null +++ b/lib/open_ai/utils/message.ex @@ -0,0 +1,21 @@ +defmodule OpenAi.Utils.Message do + @moduledoc """ + Documentation for `OpenAi.Utils.Message` module. + + This module hold message data for one OpenAI chat completion. + """ + + @enforce_keys [:content, :role] + defstruct [:content, :role] + + @type t :: %__MODULE__{ + content: String.t(), + role: String.t() + } + + defimpl Jason.Encoder, for: __MODULE__ do + def encode(value, opts) do + Jason.Encode.map(Map.take(value, [:content, :role]), opts) + end + end +end From 6d7addbadacc9a38fadbbf035137192719ec3a62 Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Thu, 6 Jul 2023 21:02:46 +0200 Subject: [PATCH 05/14] feat(openai): Add chat completion response strucutre Signed-off-by: marinac-dev --- lib/open_ai.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/open_ai.ex b/lib/open_ai.ex index d40428f..85c1000 100644 --- a/lib/open_ai.ex +++ b/lib/open_ai.ex @@ -77,8 +77,8 @@ defmodule OpenAi do """ @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() + def chat_completion(prompt, options \\ []) do + OpenAi.ChatCompletion.chat_completion(prompt, options) |> ChatCompletion.parse() end @doc """ From 06db9e29014d9d8ca42c6f5eef199caeb70e4e75 Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Sat, 8 Jul 2023 23:20:25 +0200 Subject: [PATCH 06/14] feat(openai): Add text completion response strucutre Signed-off-by: marinac-dev --- .iex.exs | 28 +++++++++-- lib/open_ai.ex | 11 +++-- lib/open_ai/core/response/text_completion.ex | 50 ++++++++++++++++++++ 3 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 lib/open_ai/core/response/text_completion.ex diff --git a/.iex.exs b/.iex.exs index b707042..e431e6b 100644 --- a/.iex.exs +++ b/.iex.exs @@ -1,8 +1,9 @@ +# Chat Prompt chat_prompt = %{ model: "gpt-4-0613", messages: [ %{role: "system", content: "You are friendly."}, - %{role: "user", content: "Hello!"}, + %{role: "user", content: "Hello!"} ] } @@ -10,7 +11,7 @@ stream_chat = %{ model: "gpt-4-0613", messages: [ %{role: "system", content: "You are friendly."}, - %{role: "user", content: "Hello!"}, + %{role: "user", content: "Hello!"} ], stream: true } @@ -21,7 +22,10 @@ function_prompt = %{ %{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: [ @@ -53,7 +57,10 @@ stream_function_prompt = %{ %{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: [ @@ -77,3 +84,16 @@ stream_function_prompt = %{ } ] } + +# Text Prompt +text_prompt = %{ + model: "text-davinci-003", + prompt: "Say this is a test", + max_tokens: 10, + temperature: 0, + top_p: 1, + n: 1, + stream: false +} + +stream_text_prompt = %{model: "text-davinci-003", prompt: "Hello, my name is", max_tokens: 100, stream: true} diff --git a/lib/open_ai.ex b/lib/open_ai.ex index 85c1000..7ac15db 100644 --- a/lib/open_ai.ex +++ b/lib/open_ai.ex @@ -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.TextCompletition @doc false def start(_type, _args) do @@ -76,9 +77,9 @@ 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, options \\ []) do - OpenAi.ChatCompletion.chat_completion(prompt, options) |> ChatCompletion.parse() + @spec chat_completion(map(), list(), any()) :: ChatCompletion.t() | {:error, map()} + def chat_completion(prompt, options \\ [], stream_cb \\ :default) do + OpenAi.ChatCompletion.chat_completion(prompt, options, stream_cb) |> ChatCompletion.parse() end @doc """ @@ -111,9 +112,9 @@ defmodule OpenAi do } """ - @spec text_completion(map(), function(), list()) :: {:ok, map() | String.t()} | {:error, map()} + @spec text_completion(map(), function(), list()) :: TextCompletition.t() | {:error, map()} def text_completion(prompt, streaming_callback \\ :default, options \\ []) do - OpenAi.TextCompletion.text_completion(prompt, options, streaming_callback) |> parse_response() + OpenAi.TextCompletion.text_completion(prompt, options, streaming_callback) |> TextCompletition.parse() end @doc """ diff --git a/lib/open_ai/core/response/text_completion.ex b/lib/open_ai/core/response/text_completion.ex new file mode 100644 index 0000000..0636b4a --- /dev/null +++ b/lib/open_ai/core/response/text_completion.ex @@ -0,0 +1,50 @@ +defmodule OpenAi.Core.Response.TextCompletition do + @moduledoc """ + Documentation for `TextCompletion` 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 created, but we rename it to created_at + 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_text_sse(body) + struct(__MODULE__, resp) + end + + @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 From ad79174d00ce96510bfe3ef7a238e84d83a5383a Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Sat, 8 Jul 2023 23:21:30 +0200 Subject: [PATCH 07/14] refactor(response): Move parsing back into a separate module Signed-off-by: marinac-dev --- lib/open_ai/core/response/chat_completion.ex | 94 +------------- lib/open_ai/utils/parser.ex | 128 +++++++++++++++++++ 2 files changed, 134 insertions(+), 88 deletions(-) create mode 100644 lib/open_ai/utils/parser.ex diff --git a/lib/open_ai/core/response/chat_completion.ex b/lib/open_ai/core/response/chat_completion.ex index 355362b..2812b70 100644 --- a/lib/open_ai/core/response/chat_completion.ex +++ b/lib/open_ai/core/response/chat_completion.ex @@ -2,7 +2,8 @@ defmodule OpenAi.Core.Response.ChatCompletion do @moduledoc """ Documentation for `ChatCompletion` response. """ - require Logger + alias OpenAi.Utils.Parser + @behaviour OpenAi.Core.Response @enforce_keys [:id, :object, :created_at, :choices, :usage] @@ -14,37 +15,15 @@ defmodule OpenAi.Core.Response.ChatCompletion do # NOTE: Returned field is created, but we rename it to created_at created_at: DateTime.t(), choices: list(map()), - usage: list(map()), + usage: list(map()) | nil, model: String.t() } # * For parsing streaming SSE responses @impl true def parse({:ok, %{body: body, type: :stream}}) do - response = %{ - id: nil, - object: nil, - created_at: nil, - choices: [ - %{ - finish_reason: nil, - content: [], - function_name: nil, - arguments: [] - } - ], - usage: [] - } - - body - |> Enum.join() - |> String.split("data: ") - |> Enum.reverse() - |> Enum.reduce_while(response, fn - "", acc -> {:cont, acc} - "[DONE]" <> _, acc -> {:cont, acc} - data, acc -> parse_data(data, acc) - end) + resp = Parser.parse_chat_sse(body) + struct(__MODULE__, resp) end # * For parsing HTTP responses @@ -60,7 +39,7 @@ defmodule OpenAi.Core.Response.ChatCompletion do |> Map.delete(:created) |> Map.put(:created_at, from_unix(decoded)) - {:ok, struct(__MODULE__, opts)} + struct(__MODULE__, opts) {:error, _} -> {:error, "Invalid response body"} @@ -69,65 +48,4 @@ defmodule OpenAi.Core.Response.ChatCompletion do defp from_unix(%{"created" => timestamp}), do: DateTime.from_unix!(timestamp) - - defp parse_data(data, acc) do - decoded = Jason.decode!(data) - - acc = - acc - |> Map.put(:id, decoded["id"]) - |> Map.put(:object, decoded["object"]) - |> Map.put(:model, decoded["model"]) - |> Map.put(:created_at, from_unix(decoded)) - - decoded - |> Map.get("choices") - |> List.first() - |> parse_choice(acc) - end - - defp parse_choice(%{"finish_reason" => "function_call"}, %{choices: [choice]} = acc) do - string = choice.arguments |> Enum.reverse() |> Enum.join() |> Jason.decode!() - - choice = - choice - |> Map.put(:arguments, string) - |> Map.put(:finish_reason, :function_call) - - {:halt, Map.put(acc, :choices, [choice])} - end - - defp parse_choice(%{"finish_reason" => "stop"}, %{choices: [choice]} = acc) do - string = choice.content |> Enum.reverse() |> Enum.join() - - choice = - choice - |> Map.put(:content, string) - |> Map.put(:finish_reason, :stop) - - {:halt, Map.put(acc, :choices, [choice])} - end - - defp parse_choice(%{"delta" => %{"function_call" => %{"name" => fn_name}}}, %{choices: [choice]} = acc) do - choice = Map.put(choice, :function_name, fn_name) - {:cont, Map.put(acc, :choices, [choice])} - end - - defp parse_choice( - %{"delta" => %{"function_call" => %{"arguments" => arg}}}, - %{choices: [%{arguments: args} = choice]} = acc - ) do - choice = Map.put(choice, :arguments, [arg | args]) - {:cont, Map.put(acc, :choices, [choice])} - end - - defp parse_choice(%{"delta" => %{"content" => new_content}}, %{choices: [%{content: content} = choice]} = acc) do - choice = Map.put(choice, :content, [new_content | content]) - {:cont, Map.put(acc, :choices, [choice])} - end - - defp parse_choice(choice, acc) do - Logger.warning("Unhandled choice: #{inspect(choice)}") - {:cont, acc} - end end diff --git a/lib/open_ai/utils/parser.ex b/lib/open_ai/utils/parser.ex new file mode 100644 index 0000000..b76499f --- /dev/null +++ b/lib/open_ai/utils/parser.ex @@ -0,0 +1,128 @@ +defmodule OpenAi.Utils.Parser do + @moduledoc """ + Documentation for `Parser` module. + + This module is used to parse responses from OpenAI API. + """ + require Logger + + def parse_chat_sse(data) do + init_acc = %{ + id: nil, + object: nil, + created_at: nil, + choices: [ + %{ + finish_reason: nil, + content: [], + function_name: nil, + arguments: [] + } + ], + usage: nil + } + + data + |> Enum.join() + |> String.split("data: ") + |> Enum.reverse() + |> Enum.reduce_while(init_acc, fn + "", acc -> {:cont, acc} + "[DONE]" <> _, acc -> {:cont, acc} + data, acc -> parse_data(data, acc) + end) + end + + def parse_text_sse(data) do + init_acc = %{ + id: nil, + object: nil, + created_at: nil, + choices: [ + %{ + finish_reason: nil, + text: [], + } + ], + usage: nil + } + + data + |> Enum.join() + |> String.split("data: ") + |> Enum.reverse() + |> Enum.reduce_while(init_acc, fn + "", acc -> {:cont, acc} + "[DONE]" <> _, acc -> {:cont, acc} + data, acc -> parse_data(data, acc) + end) + end + + defp parse_data(data, acc) do + decoded = Jason.decode!(data) + + acc = + acc + |> Map.put(:id, decoded["id"]) + |> Map.put(:object, decoded["object"]) + |> Map.put(:model, decoded["model"]) + |> Map.put(:created_at, from_unix(decoded)) + + decoded + |> Map.get("choices") + |> List.first() + |> parse_choice(acc) + end + + defp parse_choice(%{"finish_reason" => "stop"}, %{choices: [%{content: content} = choice]} = acc) do + string = content |> Enum.reverse() |> Enum.join() + + choice = + choice + |> Map.put(:content, string) + |> Map.put(:finish_reason, :stop) + + {:halt, Map.put(acc, :choices, [choice])} + end + + defp parse_choice(%{"finish_reason" => "stop"}, %{choices: [%{text: text} = choice]} = acc) do + string = text |> Enum.reverse() |> Enum.join() + + choice = + choice + |> Map.put(:text, string) + |> Map.put(:finish_reason, :stop) + + {:halt, Map.put(acc, :choices, [choice])} + end + + defp parse_choice(%{"delta" => %{"function_call" => %{"name" => fn_name}}}, %{choices: [choice]} = acc) do + choice = Map.put(choice, :function_name, fn_name) + {:cont, Map.put(acc, :choices, [choice])} + end + + defp parse_choice(%{"delta" => %{"function_call" => %{"arguments" => arg}}}, %{choices: [%{arguments: args} = choice]} = acc) do + choice = Map.put(choice, :arguments, [arg | args]) + {:cont, Map.put(acc, :choices, [choice])} + end + + # * For chat completion response + defp parse_choice(%{"delta" => %{"content" => new_content}}, %{choices: [%{content: content} = choice]} = acc) do + choice = Map.put(choice, :content, [new_content | content]) + {:cont, Map.put(acc, :choices, [choice])} + end + + # * For text completion response + defp parse_choice(%{"text" => new_content}, %{choices: [%{text: content} = choice]} = acc) do + choice = Map.put(choice, :text, [new_content | content]) + {:cont, Map.put(acc, :choices, [choice])} + end + + defp parse_choice(choice, acc) do + Logger.warning("Unhandled choice: #{inspect(choice)}\n#{inspect(acc)}") + {:cont, acc} + end + + defp from_unix(%{"created" => timestamp}), + do: DateTime.from_unix!(timestamp) +end From 783375673fc989195aafb8329b92cef66d36a0ee Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Sat, 8 Jul 2023 23:21:48 +0200 Subject: [PATCH 08/14] style(format): mix format Signed-off-by: marinac-dev --- .formatter.exs | 2 +- lib/open_ai/chat_completion.ex | 5 +++-- lib/open_ai/edit.ex | 14 +++++++------- lib/open_ai/embedding.ex | 8 ++++---- lib/open_ai/moderation.ex | 2 -- lib/open_ai/text_completion.ex | 3 ++- lib/open_ai/utils/parser.ex | 7 +++++-- 7 files changed, 22 insertions(+), 19 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index acb4b66..3cdda92 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -6,6 +6,6 @@ stream: 6, scope: 1, post: 3, - get: 3, + get: 3 ] ] diff --git a/lib/open_ai/chat_completion.ex b/lib/open_ai/chat_completion.ex index d8764f1..96cdce2 100644 --- a/lib/open_ai/chat_completion.ex +++ b/lib/open_ai/chat_completion.ex @@ -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. @@ -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(), 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) diff --git a/lib/open_ai/edit.ex b/lib/open_ai/edit.ex index c40c929..66af4fe 100644 --- a/lib/open_ai/edit.ex +++ b/lib/open_ai/edit.ex @@ -9,13 +9,13 @@ defmodule OpenAi.Edit do scope "/v1/edits" @type edit_body :: %{ - required(:model) => String.t(), - required(:instruction) => String.t(), - optional(:input) => String.t(), - optional(:n) => non_neg_integer(), - optional(:temperature) => float(), - optional(:top_p) => float() - } + required(:model) => String.t(), + required(:instruction) => String.t(), + optional(:input) => String.t(), + optional(:n) => non_neg_integer(), + optional(:temperature) => float(), + optional(:top_p) => float() + } @doc """ Creates a new edit for the provided input, instruction, and parameters. diff --git a/lib/open_ai/embedding.ex b/lib/open_ai/embedding.ex index eef05c4..82d503f 100644 --- a/lib/open_ai/embedding.ex +++ b/lib/open_ai/embedding.ex @@ -9,10 +9,10 @@ defmodule OpenAi.Embedding do scope "/v1/embeddings" @type embed_body :: %{ - required(:input) => String.t(), - required(:model) => String.t(), - optional(:user) => String.t() - } + required(:input) => String.t(), + required(:model) => String.t(), + optional(:user) => String.t() + } @doc """ Creates an embedding vector representing the input text. diff --git a/lib/open_ai/moderation.ex b/lib/open_ai/moderation.ex index 1a70965..012ff8e 100644 --- a/lib/open_ai/moderation.ex +++ b/lib/open_ai/moderation.ex @@ -10,8 +10,6 @@ defmodule OpenAi.Moderation do @doc false scope "/v1/moderations" - - @doc """ Classifies if text violates OpenAI's Content Policy diff --git a/lib/open_ai/text_completion.ex b/lib/open_ai/text_completion.ex index 2aa4744..202bf21 100644 --- a/lib/open_ai/text_completion.ex +++ b/lib/open_ai/text_completion.ex @@ -46,7 +46,8 @@ defmodule OpenAi.TextCompletion do iex> text_completion(prompt) """ - @spec text_completion(text_params(), keyword(), function()) :: {:ok, %Finch.Response{}} | {:error, map()} + @spec text_completion(text_params(), keyword(), function()) :: {:ok, Finch.Response.t()} | {:error, Exception.t()} + def text_completion(%{stream: true} = prompt, options, :default), do: text_completion(prompt, options, &default_stream_callback/2) diff --git a/lib/open_ai/utils/parser.ex b/lib/open_ai/utils/parser.ex index b76499f..7b52233 100644 --- a/lib/open_ai/utils/parser.ex +++ b/lib/open_ai/utils/parser.ex @@ -41,7 +41,7 @@ defmodule OpenAi.Utils.Parser do choices: [ %{ finish_reason: nil, - text: [], + text: [] } ], usage: nil @@ -101,7 +101,10 @@ defmodule OpenAi.Utils.Parser do {:cont, Map.put(acc, :choices, [choice])} end - defp parse_choice(%{"delta" => %{"function_call" => %{"arguments" => arg}}}, %{choices: [%{arguments: args} = choice]} = acc) do + defp parse_choice( + %{"delta" => %{"function_call" => %{"arguments" => arg}}}, + %{choices: [%{arguments: args} = choice]} = acc + ) do choice = Map.put(choice, :arguments, [arg | args]) {:cont, Map.put(acc, :choices, [choice])} end From a6fb691808a9c33af509e97e240347f3b9e98bb6 Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Sat, 8 Jul 2023 23:43:52 +0200 Subject: [PATCH 09/14] fix: Add missing parse_choice and fix version lock Signed-off-by: marinac-dev --- lib/open_ai/utils/parser.ex | 11 +++++++++++ mix.exs | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/open_ai/utils/parser.ex b/lib/open_ai/utils/parser.ex index 7b52233..da1f153 100644 --- a/lib/open_ai/utils/parser.ex +++ b/lib/open_ai/utils/parser.ex @@ -74,6 +74,17 @@ defmodule OpenAi.Utils.Parser do |> parse_choice(acc) end + defp parse_choice(%{"finish_reason" => "function_call"}, %{choices: [choice]} = acc) do + string = choice.arguments |> Enum.reverse() |> Enum.join() |> Jason.decode!() + + choice = + choice + |> Map.put(:arguments, string) + |> Map.put(:finish_reason, :function_call) + + {:halt, Map.put(acc, :choices, [choice])} + end + defp parse_choice(%{"finish_reason" => "stop"}, %{choices: [%{content: content} = choice]} = acc) do string = content |> Enum.reverse() |> Enum.join() diff --git a/mix.exs b/mix.exs index a9e3695..5d6f020 100644 --- a/mix.exs +++ b/mix.exs @@ -23,7 +23,7 @@ defmodule OpenAi.MixProject do defp deps do [ {:ex_doc, "~> 0.29.2", only: :dev}, - {:finch, "~> 0.16.0"}, + {:finch, "~> 0.16.0", override: true}, {:idna, "~> 6.0"}, {:castore, "~> 0.1"}, {:nx, "~> 0.5.2"}, From d6aeafb0aaf1da4a2e4927ac01640527401f1987 Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Wed, 23 Aug 2023 20:38:13 +0200 Subject: [PATCH 10/14] small fixes Signed-off-by: marinac-dev --- lib/open_ai.ex | 32 ++++++++++++++++---- lib/open_ai/chat_completion.ex | 2 +- lib/open_ai/core/response/chat_completion.ex | 2 +- lib/open_ai/core/response/text_completion.ex | 2 +- lib/open_ai/text_completion.ex | 2 +- lib/open_ai/utils/parser.ex | 19 ++++-------- 6 files changed, 36 insertions(+), 23 deletions(-) diff --git a/lib/open_ai.ex b/lib/open_ai.ex index 7ac15db..0b38f98 100644 --- a/lib/open_ai.ex +++ b/lib/open_ai.ex @@ -10,7 +10,7 @@ defmodule OpenAi do alias OpenAi.Core.Response.ListModels alias OpenAi.Core.Response.RetrieveModel alias OpenAi.Core.Response.ChatCompletion - alias OpenAi.Core.Response.TextCompletition + alias OpenAi.Core.Response.TextCompletion @doc false def start(_type, _args) do @@ -77,8 +77,18 @@ defmodule OpenAi do {:ok, "Hello! How may I assist you today?"} """ - @spec chat_completion(map(), list(), any()) :: ChatCompletion.t() | {:error, map()} - def chat_completion(prompt, options \\ [], stream_cb \\ :default) do + @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 @@ -112,9 +122,19 @@ defmodule OpenAi do } """ - @spec text_completion(map(), function(), list()) :: TextCompletition.t() | {:error, map()} - def text_completion(prompt, streaming_callback \\ :default, options \\ []) do - OpenAi.TextCompletion.text_completion(prompt, options, streaming_callback) |> TextCompletition.parse() + @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 """ diff --git a/lib/open_ai/chat_completion.ex b/lib/open_ai/chat_completion.ex index 96cdce2..1fbd318 100644 --- a/lib/open_ai/chat_completion.ex +++ b/lib/open_ai/chat_completion.ex @@ -128,7 +128,7 @@ defmodule OpenAi.ChatCompletion do } """ - @spec chat_completion(chat_params(), keyword(), function()) :: {:ok, Finch.Response.t()} | {:error, Exception.t()} + @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) diff --git a/lib/open_ai/core/response/chat_completion.ex b/lib/open_ai/core/response/chat_completion.ex index 2812b70..b7eec4c 100644 --- a/lib/open_ai/core/response/chat_completion.ex +++ b/lib/open_ai/core/response/chat_completion.ex @@ -12,7 +12,7 @@ defmodule OpenAi.Core.Response.ChatCompletion do @type t :: %__MODULE__{ id: String.t(), object: String.t(), - # NOTE: Returned field is created, but we rename it to created_at + # 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, diff --git a/lib/open_ai/core/response/text_completion.ex b/lib/open_ai/core/response/text_completion.ex index 0636b4a..1e74f82 100644 --- a/lib/open_ai/core/response/text_completion.ex +++ b/lib/open_ai/core/response/text_completion.ex @@ -1,4 +1,4 @@ -defmodule OpenAi.Core.Response.TextCompletition do +defmodule OpenAi.Core.Response.TextCompletion do @moduledoc """ Documentation for `TextCompletion` response. """ diff --git a/lib/open_ai/text_completion.ex b/lib/open_ai/text_completion.ex index 202bf21..3b72824 100644 --- a/lib/open_ai/text_completion.ex +++ b/lib/open_ai/text_completion.ex @@ -46,7 +46,7 @@ defmodule OpenAi.TextCompletion do iex> text_completion(prompt) """ - @spec text_completion(text_params(), keyword(), function()) :: {:ok, Finch.Response.t()} | {:error, Exception.t()} + @spec text_completion(text_params(), keyword(), atom() | function()) :: {:ok, Finch.Response.t()} | {:error, Exception.t()} def text_completion(%{stream: true} = prompt, options, :default), do: text_completion(prompt, options, &default_stream_callback/2) diff --git a/lib/open_ai/utils/parser.ex b/lib/open_ai/utils/parser.ex index da1f153..3bde51f 100644 --- a/lib/open_ai/utils/parser.ex +++ b/lib/open_ai/utils/parser.ex @@ -22,15 +22,7 @@ defmodule OpenAi.Utils.Parser do usage: nil } - data - |> Enum.join() - |> String.split("data: ") - |> Enum.reverse() - |> Enum.reduce_while(init_acc, fn - "", acc -> {:cont, acc} - "[DONE]" <> _, acc -> {:cont, acc} - data, acc -> parse_data(data, acc) - end) + parse_sse(data, init_acc) end def parse_text_sse(data) do @@ -47,6 +39,10 @@ defmodule OpenAi.Utils.Parser do usage: nil } + parse_sse(data, init_acc) + end + + defp parse_sse(data, init_acc) do data |> Enum.join() |> String.split("data: ") @@ -112,10 +108,7 @@ defmodule OpenAi.Utils.Parser do {:cont, Map.put(acc, :choices, [choice])} end - defp parse_choice( - %{"delta" => %{"function_call" => %{"arguments" => arg}}}, - %{choices: [%{arguments: args} = choice]} = acc - ) do + defp parse_choice(%{"delta" => %{"function_call" => %{"arguments" => arg}}}, %{choices: [%{arguments: args} = choice]} = acc) do choice = Map.put(choice, :arguments, [arg | args]) {:cont, Map.put(acc, :choices, [choice])} end From 644c443f20cb2de06f1d5d902b188f4fd3ddc4b3 Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Sun, 29 Oct 2023 15:40:21 +0100 Subject: [PATCH 11/14] fix: Fix parsing of streaming messages Signed-off-by: marinac-dev --- lib/open_ai/utils/parser.ex | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/open_ai/utils/parser.ex b/lib/open_ai/utils/parser.ex index 3bde51f..c853bce 100644 --- a/lib/open_ai/utils/parser.ex +++ b/lib/open_ai/utils/parser.ex @@ -39,14 +39,16 @@ defmodule OpenAi.Utils.Parser do usage: nil } - parse_sse(data, init_acc) + %{choices: [choice]} = parsed = parse_sse(data, init_acc) + text = choice.text |> Enum.reverse() |> Enum.join() + %{parsed | choices: [%{choice | text: text}]} end defp parse_sse(data, init_acc) do data + |> Enum.reverse() |> Enum.join() |> String.split("data: ") - |> Enum.reverse() |> Enum.reduce_while(init_acc, fn "", acc -> {:cont, acc} "[DONE]" <> _, acc -> {:cont, acc} @@ -108,7 +110,10 @@ defmodule OpenAi.Utils.Parser do {:cont, Map.put(acc, :choices, [choice])} end - defp parse_choice(%{"delta" => %{"function_call" => %{"arguments" => arg}}}, %{choices: [%{arguments: args} = choice]} = acc) do + defp parse_choice( + %{"delta" => %{"function_call" => %{"arguments" => arg}}}, + %{choices: [%{arguments: args} = choice]} = acc + ) do choice = Map.put(choice, :arguments, [arg | args]) {:cont, Map.put(acc, :choices, [choice])} end From 5f69d6ea221fd2b0f7caf1075cc51d7130d2a26e Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Sun, 29 Oct 2023 15:41:02 +0100 Subject: [PATCH 12/14] tidbit: Update text model Signed-off-by: marinac-dev --- .iex.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.iex.exs b/.iex.exs index e431e6b..785e843 100644 --- a/.iex.exs +++ b/.iex.exs @@ -87,7 +87,7 @@ stream_function_prompt = %{ # Text Prompt text_prompt = %{ - model: "text-davinci-003", + model: "babbage-002", prompt: "Say this is a test", max_tokens: 10, temperature: 0, @@ -96,4 +96,4 @@ text_prompt = %{ stream: false } -stream_text_prompt = %{model: "text-davinci-003", prompt: "Hello, my name is", max_tokens: 100, stream: true} +stream_text_prompt = %{model: "babbage-002", prompt: "Hello, my name is", max_tokens: 100, stream: true} From ef3c8dc47d3a7c8634d5d91ccbcbb492d910db9c Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Sun, 19 Nov 2023 19:48:25 +0100 Subject: [PATCH 13/14] wip --- README.md | 2 +- lib/open_ai.ex | 2 +- lib/open_ai/core/response/audio.ex | 5 +++ .../response/{chat_completion.ex => chat.ex} | 0 lib/open_ai/core/response/completions.ex | 8 +++++ lib/open_ai/core/response/embeddings.ex | 34 +++++++++++++++++++ lib/open_ai/embedding.ex | 4 +-- lib/open_ai/utils/context.ex | 4 ++- mix.exs | 7 ++-- mix.lock | 24 ++++++------- 10 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 lib/open_ai/core/response/audio.ex rename lib/open_ai/core/response/{chat_completion.ex => chat.ex} (100%) create mode 100644 lib/open_ai/core/response/completions.ex create mode 100644 lib/open_ai/core/response/embeddings.ex diff --git a/README.md b/README.md index df777c6..e12e542 100644 --- a/README.md +++ b/README.md @@ -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} diff --git a/lib/open_ai.ex b/lib/open_ai.ex index 0b38f98..6ff820a 100644 --- a/lib/open_ai.ex +++ b/lib/open_ai.ex @@ -165,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 """ diff --git a/lib/open_ai/core/response/audio.ex b/lib/open_ai/core/response/audio.ex new file mode 100644 index 0000000..8fab4cd --- /dev/null +++ b/lib/open_ai/core/response/audio.ex @@ -0,0 +1,5 @@ +defmodule OpenAi.Core.Response.Audio do + @moduledoc """ + Documentation for `Audio` response. + """ +end diff --git a/lib/open_ai/core/response/chat_completion.ex b/lib/open_ai/core/response/chat.ex similarity index 100% rename from lib/open_ai/core/response/chat_completion.ex rename to lib/open_ai/core/response/chat.ex diff --git a/lib/open_ai/core/response/completions.ex b/lib/open_ai/core/response/completions.ex new file mode 100644 index 0000000..2a35162 --- /dev/null +++ b/lib/open_ai/core/response/completions.ex @@ -0,0 +1,8 @@ +defmodule OpenAi.Core.Response.Completions do + @moduledoc """ + Documentation for `Completions` response. + """ + alias OpenAi.Utils.Parser + + @behaviour OpenAi.Core.Response +end diff --git a/lib/open_ai/core/response/embeddings.ex b/lib/open_ai/core/response/embeddings.ex new file mode 100644 index 0000000..3e09e7f --- /dev/null +++ b/lib/open_ai/core/response/embeddings.ex @@ -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 diff --git a/lib/open_ai/embedding.ex b/lib/open_ai/embedding.ex index 82d503f..944b19a 100644 --- a/lib/open_ai/embedding.ex +++ b/lib/open_ai/embedding.ex @@ -59,7 +59,7 @@ defmodule OpenAi.Embedding do @spec create_embedding(embed_body(), keyword() | list()) :: {:ok, map()} | {:error, map()} def create_embedding(prompt, options \\ []) do - jdata = Jason.encode!(prompt) - post("", jdata, %{}, options) + json = Jason.encode!(prompt) + post("", json, %{}, options) end end diff --git a/lib/open_ai/utils/context.ex b/lib/open_ai/utils/context.ex index 8413a35..439641a 100644 --- a/lib/open_ai/utils/context.ex +++ b/lib/open_ai/utils/context.ex @@ -2,7 +2,9 @@ defmodule OpenAi.Utils.Context do @moduledoc """ Documentation for `OpenAi.Utils.Context` module. - This module contains functions for working with OpenAI chat completion contexts + This module contains functions for working with OpenAI chat completion contexts. + Contexts are used to easily keep track of the conversation history and last response. + Each new message is added as first item to the history list and the last response is updated. """ alias OpenAi.Utils.Message diff --git a/mix.exs b/mix.exs index 5d6f020..20e0dcc 100644 --- a/mix.exs +++ b/mix.exs @@ -4,8 +4,8 @@ defmodule OpenAi.MixProject do def project do [ app: :openai, - version: "0.0.4", - elixir: "~> 1.14", + version: "1.0.0", + elixir: "~> 1.15", start_permanent: Mix.env() == :prod, deps: deps() ] @@ -22,12 +22,13 @@ defmodule OpenAi.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:ex_doc, "~> 0.29.2", only: :dev}, + {:ex_doc, "~> 0.30.1", only: :dev}, {:finch, "~> 0.16.0", override: true}, {:idna, "~> 6.0"}, {:castore, "~> 0.1"}, {:nx, "~> 0.5.2"}, {:multipart, "~> 0.3.1"}, + # * Code quality {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.0", only: [:dev], runtime: false} diff --git a/mix.lock b/mix.lock index 940dedf..30ed3ae 100644 --- a/mix.lock +++ b/mix.lock @@ -2,30 +2,26 @@ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"}, "complex": {:hex, :complex, "0.5.0", "af2d2331ff6170b61bb738695e481b27a66780e18763e066ee2cd863d0b1dd92", [:mix], [], "hexpm", "2683bd3c184466cfb94fad74cbfddfaa94b860e27ad4ca1bffe3bff169d91ef1"}, - "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, - "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"}, - "elixir_make": {:hex, :elixir_make, "0.7.6", "67716309dc5d43e16b5abbd00c01b8df6a0c2ab54a8f595468035a50189f9169", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5a0569756b0f7873a77687800c164cca6dfc03a09418e6fcf853d78991f49940"}, + "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, + "dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.38", "b42252eddf63bda05554ba8be93a1262dc0920c721f1aaf989f5de0f73a2e367", [:mix], [], "hexpm", "2cd0907795aaef0c7e8442e376633c5b3bd6edc8dbbdc539b22f095501c1cdb6"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.29.3", "f07444bcafb302db86e4f02d8bbcd82f2e881a0dcf4f3e4740e4b8128b9353f7", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3dc6787d7b08801ec3b51e9bd26be5e8826fbf1a17e92d1ebc252e1a1c75bfe1"}, - "exla": {:hex, :exla, "0.5.2", "d2c3dc947f2670b28c631bd5803ae09033cbbac468d2dceb3950407b17ecd633", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.5.1", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.4.4", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "0cd561bf45ec2f7b67c3235a380cc85ea09e526f9be130b9395144e176b0e52f"}, + "ex_doc": {:hex, :ex_doc, "0.30.9", "d691453495c47434c0f2052b08dd91cc32bc4e1a218f86884563448ee2502dd2", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d7aaaf21e95dc5cddabf89063327e96867d00013963eadf2c6ad135506a8bc10"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, "multipart": {:hex, :multipart, "0.3.1", "886d77125f5d7ba6be2f86e4be8f6d3556684c8e56a777753f06234885b09cde", [:mix], [{:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "9657783995d2b9b546d9c66e1d497fcb473d813a8a3fb73faf5e411538b1db97"}, "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, - "nx": {:hex, :nx, "0.5.2", "10b047d33646f815eb3bf16353781172f088472587fbf6e9b345e44a1ec3a1c2", [:mix], [{:complex, "~> 0.5", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e08b8b454e65f4ef104b0034a1436a033f2ddf5db9b79408dfc8e92667a6a314"}, + "nx": {:hex, :nx, "0.5.3", "6ad5534f9b82429dafa12329952708c2fdd6ab01b306e86333fdea72383147ee", [:mix], [{:complex, "~> 0.5", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d1072fc4423809ed09beb729e73c200ce177ddecac425d9eb6fba643669623ec"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "tesla": {:hex, :tesla, "1.5.1", "f2ba04f5e6ace0f1954f1fb4375f55809a5f2ff491c18ccb09fbc98370d4280b", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2815d4f6550973d1ed65692d545d079174f6a1f8cb4775f6eb606cbc0666a9de"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, - "xla": {:hex, :xla, "0.4.4", "c3a8ed1f579bda949df505e49ff65415c8281d991fbd6ae1d8f3c5d0fd155f54", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "484f3f9011db3c9f1ff1e98eecefd382f3882a07ada540fd58803db1d2dab671"}, } From dc74240f924bf899eb440a6e4780b2db6565c438 Mon Sep 17 00:00:00 2001 From: marinac-dev Date: Wed, 17 Apr 2024 00:44:17 +0200 Subject: [PATCH 14/14] WIP Signed-off-by: marinac-dev --- lib/open_ai/core/client/completions.ex | 24 +++++++ lib/open_ai/core/response/completions.ex | 33 ++++++++- lib/open_ai/core/types/completion.ex | 70 ++++++++++++++++++++ lib/open_ai/utils/context.ex | 2 +- lib/open_ai/utils/types/completion_choice.ex | 19 ++++++ lib/open_ai/utils/{ => types}/message.ex | 6 +- 6 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 lib/open_ai/core/client/completions.ex create mode 100644 lib/open_ai/core/types/completion.ex create mode 100644 lib/open_ai/utils/types/completion_choice.ex rename lib/open_ai/utils/{ => types}/message.ex (69%) diff --git a/lib/open_ai/core/client/completions.ex b/lib/open_ai/core/client/completions.ex new file mode 100644 index 0000000..7603958 --- /dev/null +++ b/lib/open_ai/core/client/completions.ex @@ -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 diff --git a/lib/open_ai/core/response/completions.ex b/lib/open_ai/core/response/completions.ex index 2a35162..6a25c60 100644 --- a/lib/open_ai/core/response/completions.ex +++ b/lib/open_ai/core/response/completions.ex @@ -1,8 +1,37 @@ defmodule OpenAi.Core.Response.Completions do @moduledoc """ - Documentation for `Completions` response. + Represents a response from the OpenAI Completions API. """ - alias OpenAi.Utils.Parser @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 diff --git a/lib/open_ai/core/types/completion.ex b/lib/open_ai/core/types/completion.ex new file mode 100644 index 0000000..440924d --- /dev/null +++ b/lib/open_ai/core/types/completion.ex @@ -0,0 +1,70 @@ +defmodule OpenAi.Core.Types.Completion do + @moduledoc """ + Type definition for Completion struct. + """ + + # * Subtypes + defmodule CompletionChoice do + @moduledoc """ + Type definition for Completion Choice struct. + """ + + defstruct [ + :finish_reason, + :index, + :logprobs, + :text + ] + + @type t :: %__MODULE__{ + finish_reason: String.t(), + index: Integer.t(), + logprobs: Logprobs.t(), + text: String.t() + } + end + + defmodule Logprobs do + @moduledoc """ + Type definition for Logprobs struct. + """ + + defstruct [ + :tokens, + :textoffset, + :token_logprobs, + :top_logprobs + ] + + @type t :: %__MODULE__{ + tokens: [String.t()], + textoffset: [Integer.t()], + token_logprobs: [Float.t()], + top_logprobs: [Float.t()] + } + end + + # * Struct + + @enforce_keys [:id, :choices, :created, :model, :object, :system_fingerprint, :usage] + + defstruct [ + :id, + :choices, + :created, + :model, + :object, + :system_fingerprint, + :usage + ] + + @type t :: %__MODULE__{ + id: String.t(), + choices: [CompletionChoice.t()], + created: String.t(), + model: String.t(), + object: String.t(), + system_fingerprint: String.t(), + usage: String.t() + } +end diff --git a/lib/open_ai/utils/context.ex b/lib/open_ai/utils/context.ex index 439641a..57bee73 100644 --- a/lib/open_ai/utils/context.ex +++ b/lib/open_ai/utils/context.ex @@ -6,7 +6,7 @@ defmodule OpenAi.Utils.Context do Contexts are used to easily keep track of the conversation history and last response. Each new message is added as first item to the history list and the last response is updated. """ - alias OpenAi.Utils.Message + alias OpenAi.Utils.Types.Message @enforce_keys [:last_response, :history] defstruct [:last_response, :history] diff --git a/lib/open_ai/utils/types/completion_choice.ex b/lib/open_ai/utils/types/completion_choice.ex new file mode 100644 index 0000000..8ea34cd --- /dev/null +++ b/lib/open_ai/utils/types/completion_choice.ex @@ -0,0 +1,19 @@ +defmodule OpenAi.Utils.Types.CompletionChoice do + @moduledoc """ + Type definition for Completion Choice struct. + """ + + defstruct [ + :finish_reason, + :index, + :logprobs, + :message + ] + + @type t :: %__MODULE__{ + finish_reason: String.t(), + index: Integer.t(), + logprobs: Logprobs.t(), + message: String.t() + } +end diff --git a/lib/open_ai/utils/message.ex b/lib/open_ai/utils/types/message.ex similarity index 69% rename from lib/open_ai/utils/message.ex rename to lib/open_ai/utils/types/message.ex index 3330323..9443120 100644 --- a/lib/open_ai/utils/message.ex +++ b/lib/open_ai/utils/types/message.ex @@ -1,8 +1,6 @@ -defmodule OpenAi.Utils.Message do +defmodule OpenAi.Utils.Types.Message do @moduledoc """ - Documentation for `OpenAi.Utils.Message` module. - - This module hold message data for one OpenAI chat completion. + Type definition for Message struct. """ @enforce_keys [:content, :role]