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/.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..785e843 100644 --- a/.iex.exs +++ b/.iex.exs @@ -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: [ @@ -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} 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 daaa007..6ff820a 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.TextCompletion @doc false def start(_type, _args) do @@ -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 """ @@ -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 """ @@ -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 """ @@ -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 """ @@ -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 """ @@ -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() diff --git a/lib/open_ai/chat_completion.ex b/lib/open_ai/chat_completion.ex index d8764f1..1fbd318 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(), 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/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.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/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.ex b/lib/open_ai/core/response/chat.ex new file mode 100644 index 0000000..b7eec4c --- /dev/null +++ b/lib/open_ai/core/response/chat.ex @@ -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 diff --git a/lib/open_ai/core/response/completions.ex b/lib/open_ai/core/response/completions.ex new file mode 100644 index 0000000..6a25c60 --- /dev/null +++ b/lib/open_ai/core/response/completions.ex @@ -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 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/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 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..1e74f82 --- /dev/null +++ b/lib/open_ai/core/response/text_completion.ex @@ -0,0 +1,50 @@ +defmodule OpenAi.Core.Response.TextCompletion 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 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/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..944b19a 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. @@ -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/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..3b72824 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(), 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/context.ex b/lib/open_ai/utils/context.ex new file mode 100644 index 0000000..57bee73 --- /dev/null +++ b/lib/open_ai/utils/context.ex @@ -0,0 +1,137 @@ +defmodule OpenAi.Utils.Context do + @moduledoc """ + Documentation for `OpenAi.Utils.Context` module. + + 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.Types.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/core/response/chat_completion.ex b/lib/open_ai/utils/parser.ex similarity index 59% rename from lib/open_ai/core/response/chat_completion.ex rename to lib/open_ai/utils/parser.ex index 355362b..c853bce 100644 --- a/lib/open_ai/core/response/chat_completion.ex +++ b/lib/open_ai/utils/parser.ex @@ -1,27 +1,13 @@ -defmodule OpenAi.Core.Response.ChatCompletion do +defmodule OpenAi.Utils.Parser do @moduledoc """ - Documentation for `ChatCompletion` response. + Documentation for `Parser` module. + + This module is used to parse responses from OpenAI API. """ require Logger - @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()), - model: String.t() - } - # * For parsing streaming SSE responses - @impl true - def parse({:ok, %{body: body, type: :stream}}) do - response = %{ + def parse_chat_sse(data) do + init_acc = %{ id: nil, object: nil, created_at: nil, @@ -33,43 +19,43 @@ defmodule OpenAi.Core.Response.ChatCompletion do arguments: [] } ], - usage: [] + usage: nil } - body + parse_sse(data, init_acc) + end + + def parse_text_sse(data) do + init_acc = %{ + id: nil, + object: nil, + created_at: nil, + choices: [ + %{ + finish_reason: nil, + text: [] + } + ], + usage: nil + } + + %{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(response, fn + |> Enum.reduce_while(init_acc, fn "", acc -> {:cont, acc} "[DONE]" <> _, acc -> {:cont, acc} data, acc -> parse_data(data, acc) end) 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)) - - {:ok, struct(__MODULE__, opts)} - - {:error, _} -> - {:error, "Invalid response body"} - end - end - - defp from_unix(%{"created" => timestamp}), - do: DateTime.from_unix!(timestamp) - defp parse_data(data, acc) do decoded = Jason.decode!(data) @@ -97,8 +83,8 @@ defmodule OpenAi.Core.Response.ChatCompletion do {:halt, Map.put(acc, :choices, [choice])} end - defp parse_choice(%{"finish_reason" => "stop"}, %{choices: [choice]} = acc) do - string = choice.content |> Enum.reverse() |> Enum.join() + defp parse_choice(%{"finish_reason" => "stop"}, %{choices: [%{content: content} = choice]} = acc) do + string = content |> Enum.reverse() |> Enum.join() choice = choice @@ -108,6 +94,17 @@ defmodule OpenAi.Core.Response.ChatCompletion do {: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])} @@ -121,13 +118,23 @@ defmodule OpenAi.Core.Response.ChatCompletion do {: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)}") + Logger.warning("Unhandled choice: #{inspect(choice)}\n#{inspect(acc)}") {:cont, acc} end + + defp from_unix(%{"created" => timestamp}), + do: DateTime.from_unix!(timestamp) end 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 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/types/message.ex b/lib/open_ai/utils/types/message.ex new file mode 100644 index 0000000..9443120 --- /dev/null +++ b/lib/open_ai/utils/types/message.ex @@ -0,0 +1,19 @@ +defmodule OpenAi.Utils.Types.Message do + @moduledoc """ + Type definition for Message struct. + """ + + @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 diff --git a/mix.exs b/mix.exs index a9e3695..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}, - {:finch, "~> 0.16.0"}, + {: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"}, }