Powered by AppSignal & Oban Pro

Open AI Text Embedings

books/ai/openai/text_embedding.livemd

Open AI Text Embedings

Mix.install([
  {:req, "~> 0.3.6"}
])

Library

defmodule OpenAi.Client do
  @type ok_tuple(value) :: {:ok, value}
  @type error_tuple :: {:error, any()}

  require Logger

  @openai_timeout 120 * 1000

  @spec build_req :: Req.Request.t()
  defp build_req do
    host = "https://api.openai.com"
    secret_key = System.fetch_env!("LB_" <> "OPEN_AI_API_KEY")

    headers = %{
      "Authorization" => "Bearer #{secret_key}"
    }

    Req.new(
      base_url: host,
      headers: headers,
      receive_timeout: @openai_timeout,
      max_retries: 2
    )
  end

  @spec fetch_embedding(String.t(), String.t() | list(String.t())) ::
          ok_tuple(list()) | error_tuple()
  def fetch_embedding(model, input) do
    payload = %{"model" => model, "input" => input}

    build_req()
    |> Req.post(
      url: "/v1/embeddings",
      json: payload,
      retry: fn res -> retry_fun(res, "EMBEDDINGS") end
    )
    |> process_response(:text_embedding_failed)
    |> then(fn result ->
      case result do
        {:ok, data} ->
          data
          |> Map.get("data")
          |> Enum.map(fn embedding ->
            %{index: embedding["index"], embedding: embedding["embedding"]}
          end)
          |> then(fn result -> {:ok, result} end)

        {:error, error} ->
          {:error, error}
      end
    end)
  end

  @spec process_response({:ok, Req.Response.t()} | error_tuple(), atom()) ::
          ok_tuple(binary() | term()) | error_tuple()
  defp process_response({:ok, res}, _error_code) when res.status < 400, do: {:ok, res.body}

  defp process_response({:ok, res}, error_code) do
    Logger.error("[OPENAI_REQUEST] #{inspect(error_code)}: res #{inspect(res)}")
    {:error, error_code}
  end

  defp process_response({:error, error}, error_code) do
    error |> Kernel.inspect() |> Logger.error()
    {:error, error_code}
  end

  defp retry_fun(%Req.Response{} = res, source) do
    if res.status >= 400 do
      Logger.error("[OPENAI_REQUEST][#{source}] HTTP Error: #{inspect(res.status)}, retrying...")
      # retry on 4xx or 5xx
      true
    else
      false
    end
  end

  defp retry_fun(%Mint.TransportError{} = e, source) do
    # retry on transport errors
    Logger.error("[OPENAI_REQUEST][#{source}] Transport Error: #{inspect(e)}")
    true
  end

  defp retry_fun(x, source) do
    Logger.debug("[OPENAI_REQUEST][#{source}] Unknown retry function param: #{inspect(x)}")
    false
  end
end
defmodule OpenAi do
  @type ok_tuple(value) :: {:ok, value}
  @type error_tuple :: {:error, any()}

  require Logger

  alias OpenAi.Client

  @text_embedding_model "text-embedding-ada-002"

  @spec text_embedding(String.t() | list(String.t())) :: ok_tuple(list()) | error_tuple()
  def text_embedding(input) do
    case Client.fetch_embedding(@text_embedding_model, input) do
      {:ok, result} ->
        Logger.debug("[OPENAI][TEXT_EMBEDDING] fetch_text_embedding success")
        {:ok, result}

      {:error, reason} ->
        Logger.error("[OPENAI][ERROR] fetch_text_embedding failed, #{inspect(reason)}")
        {:error, reason}
    end
  end
end

Usage

OpenAi.text_embedding(["Hello", "world"])