Skip to content

Commit

Permalink
Merge pull request #25 from benitezhm/main
Browse files Browse the repository at this point in the history
Adding support for new OpanAI API
  • Loading branch information
dvcrn authored Jul 13, 2024
2 parents db4e95b + 3493d66 commit 985a1bf
Show file tree
Hide file tree
Showing 12 changed files with 13,909 additions and 9,466 deletions.
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.13.4-otp-23
erlang 23.3.4.19
elixir 1.16.1
erlang 26.2
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ config :ex_openai,
# optional, default request headers. The following header is required for Assistant endpoints, which are in beta as of December 2023.
http_headers: [
{"OpenAI-Beta", "assistants=v1"}
]
],
# optional http client, useful for testing purposes on dependent projects
# if unset the default client is ExOpenAI.Client
http_client: ExOpenAI.Client
```

You can also pass `api_key` and `organization_key` directly by passing them into the `opts` argument when calling the openai apis:
Expand Down
27 changes: 24 additions & 3 deletions lib/ex_openai.ex
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ end)
@moduledoc "#{docstring_head}
Use any of these components: #{inspect(component.components |> Enum.map(&Kernel.elem(&1, 1)))}"

:allOf ->
@type t :: unquote(ExOpenAI.Codegen.type_to_spec({:oneOf, component.components}))
@typespec quote(
do: unquote(ExOpenAI.Codegen.type_to_spec({:oneOf, component.components}))
)

@moduledoc "#{docstring_head}
Use any of these components: #{inspect(component.components |> Enum.map(&Kernel.elem(&1, 1)))}"
end

use ExOpenAI.Codegen.AstUnpacker
Expand Down Expand Up @@ -175,7 +185,7 @@ end)
# POST methods have body arguments on top of positional URL ones
:post ->
args ++
if(is_nil(fx.request_body),
if(is_nil(get_in(fx, [:request_body, :request_schema, :required_props])),
do: [],
else: fx.request_body.request_schema.required_props
)
Expand Down Expand Up @@ -210,7 +220,7 @@ end)
case method do
:post ->
Enum.filter(args, &(!Map.get(&1, :required?))) ++
if(is_nil(fx.request_body),
if(is_nil(get_in(fx, [:request_body, :request_schema, :optional_props])),
do: [],
else: fx.request_body.request_schema.optional_props
)
Expand Down Expand Up @@ -383,6 +393,17 @@ end)
ExOpenAI.Codegen.keys_to_atoms(res)
)}

# TODO figure it out a better way to understand what type is getting here
types when is_list(types) ->
{:component, comp} = types |> List.first()
ExOpenAI.Codegen.string_to_component(comp).unpack_ast()

{:ok,
struct(
ExOpenAI.Codegen.string_to_component(comp),
ExOpenAI.Codegen.keys_to_atoms(res)
)}

_ ->
{:ok, res}
end
Expand All @@ -392,7 +413,7 @@ end)
end
end

ExOpenAI.Client.api_call(
ExOpenAI.Config.http_client().api_call(
method,
url,
body_params,
Expand Down
13 changes: 12 additions & 1 deletion lib/ex_openai/client.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule ExOpenAI.Client do
@moduledoc false
alias ExOpenAI.Config
use HTTPoison.Base
alias ExOpenAI.Config

def process_url(url), do: Config.api_url() <> url

Expand All @@ -18,6 +18,9 @@ defmodule ExOpenAI.Client do
{:ok, %HTTPoison.Response{status_code: 200, body: {:ok, body}}} ->
{:ok, body}

{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, body}

{:ok, %HTTPoison.Response{body: {:ok, body}}} ->
{:error, body}

Expand Down Expand Up @@ -200,6 +203,14 @@ defmodule ExOpenAI.Client do
|> convert_response.()
end

@callback api_call(
method :: atom(),
url :: String.t(),
params :: Keyword.t(),
request_content_type :: Keyword.t(),
request_options :: Keyword.t(),
convert_response :: any()
) :: {:ok, res :: term()} | {:error, res :: term()}
def api_call(:get, url, _params, _request_content_type, request_options, convert_response),
do: api_get(url, request_options, convert_response)

Expand Down
51 changes: 40 additions & 11 deletions lib/ex_openai/codegen.ex
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,15 @@ defmodule ExOpenAI.Codegen do
}
end

def parse_property(%{"allOf" => allOf} = args) do
%{
name: Map.get(args, "name"),
description: Map.get(args, "description"),
required: Map.get(args, "required", false),
type: {:allOf, Enum.map(allOf, fn x -> ExOpenAI.Codegen.parse_type(x) end)}
}
end

# %{
# "default" => "auto",
# "description" =>
Expand Down Expand Up @@ -425,6 +434,16 @@ defmodule ExOpenAI.Codegen do
}
end

def parse_component_schema(%{"allOf" => _allOf} = args) do
%{
kind: :allOf,
# piggybacking parse_property which handles parsing of "oneOf" already
components: parse_property(args) |> Map.get(:type) |> elem(1),
required_props: [],
optional_props: []
}
end

def parse_component_schema(%{"properties" => props}),
do: parse_component_schema(%{"properties" => props, "required" => []})

Expand All @@ -448,7 +467,7 @@ defmodule ExOpenAI.Codegen do

# resolve the object ref to the actual component to get the schema
ref =
rest["schema"]["$ref"]
(rest["schema"]["$ref"] || "")
|> String.replace_prefix("#/components/schemas/", "")

%{
Expand Down Expand Up @@ -484,17 +503,26 @@ defmodule ExOpenAI.Codegen do
end

defp extract_response_type(%{"200" => %{"content" => content}}) do
case content
# [["application/json", %{}]]
|> Map.to_list()
# ["application/json", %{}]
|> List.first()
# %{}
|> Kernel.elem(1)
|> Map.get("schema") do
content
# [["application/json", %{}]]
|> Map.to_list()
# ["application/json", %{}]
|> List.first()
# %{}
|> Kernel.elem(1)
|> Map.get("schema")
|> case do
# no ref
%{"type" => type} -> String.to_atom(type)
%{"$ref" => ref} -> {:component, String.replace(ref, "#/components/schemas/", "")}
%{"type" => type} ->
String.to_atom(type)

%{"$ref" => ref} ->
{:component, String.replace(ref, "#/components/schemas/", "")}

%{"oneOf" => list} ->
Enum.map(list, fn %{"$ref" => ref} ->
{:component, String.replace(ref, "#/components/schemas/", "")}
end)
end
end

Expand Down Expand Up @@ -675,6 +703,7 @@ defmodule ExOpenAI.Codegen do
def type_to_spec("array"), do: quote(do: list())
def type_to_spec("object"), do: quote(do: map())
def type_to_spec("oneOf"), do: quote(do: any())
def type_to_spec("allOf"), do: quote(do: any())
def type_to_spec("anyOf"), do: quote(do: any())

def type_to_spec({:anyOf, nested}) do
Expand Down
10 changes: 7 additions & 3 deletions lib/ex_openai/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ defmodule ExOpenAI.Config do
:api_key,
:organization_key,
:http_options,
:http_headers
:http_headers,
:http_client
]

def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
Expand All @@ -32,15 +33,18 @@ defmodule ExOpenAI.Config do
def org_key, do: get_config_value(:organization_key)

# API Url
# Other clients allow overriding via the OPENAI_API_URL/OPENAI_API_BASE environment variable,
# but this is set via config with the default being https://api.openai.com/v1
# Other clients allow overriding via the OPENAI_API_URL/OPENAI_API_BASE environment variable,
# but this is set via config with the default being https://api.openai.com/v1
def api_url, do: get_config_value(:base_url, @openai_url)

# HTTP Options
def http_options, do: get_config_value(:http_options, [])

def http_headers, do: get_config_value(:http_headers, [])

# HTTP client can be customized to facilitate testing
def http_client, do: get_config_value(:http_client, ExOpenAI.Client)

defp get_config_value(key, default \\ nil) do
value =
:ex_openai
Expand Down
Loading

0 comments on commit 985a1bf

Please sign in to comment.