Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

OpenAI Advanced Features

openai-advanced-features.livemd

OpenAI Advanced Features

Mix.install(
  [
    {:instructor_lite, "~> 1.0"},
    {:req, "~> 0.5"},
    {:kino, "~> 0.16"}
  ]
)

Motivation

OpenAI has introduced a lot of features since InstructorLite was first released. Let’s explore some of them and see if they can be of any help.

Setup

In order to run code in this notebook, you need to add your OpenAI API key as an OPENAI_KEY Livebook secret. It will then be accessible through an environment variable.

secret_key = System.fetch_env!("LB_OPENAI_KEY")
:ok
:ok

Image Input

file = Kino.FS.file_path("shopify-screenshot.png") |> File.read!()
base64_image = "data:image/png;base64," <> Base.encode64(file)

defmodule Product do
  use Ecto.Schema

  @primary_key false
  embedded_schema do
    field(:name, :string)
    field(:price, :decimal)
    field(:currency, Ecto.Enum, values: [:usd, :gbp, :eur, :cny])
    field(:color, :string)
  end
end

{:ok, result} =
  InstructorLite.instruct(%{
      model: "gpt-4o-mini",
      input: [
        %{
          role: "user",
          content: [
            %{type: "input_text", text: "What is the product details of the following image?"},
            %{type: "input_image", image_url: base64_image}
          ]
        }
      ]
    },
    response_model: Product,
    adapter_context: [api_key: secret_key]
  )

result
%Product{
  name: "Thomas Wooden Railway Thomas the Tank Engine",
  price: Decimal.new("33.0"),
  currency: :usd,
  color: "blue"
}

Built-in Tool Use

OpenAI supports a number of built-in tools, such as web search!

defmodule Package do
  use Ecto.Schema

  @primary_key false
  embedded_schema do
    field(:name, :string)
    field(:github_link, :string)
    field(:latest_version, :string)
  end
end

{:ok, result} =
  InstructorLite.instruct(%{
      model: "gpt-4o-mini",
      tools: [%{type: "web_search_preview"}],
      input: "We want to know about: InstructorLite Hex package"
    },
    response_model: Package,
    adapter_context: [api_key: secret_key]
  )

result
%Package{
  name: "InstructorLite",
  github_link: "https://github.com/martosaur/instructor_lite",
  latest_version: "1.0.0"
}

Reasoning

Reasoning models are powerful, but ultimately, reasoning doesn’t affect structured output in any way. However, they can be very useful to keep track of the reasoning behind the output. InstructorLite.instruct/2 does not give you access to the raw output, but you can use lower-level functions to sidestep this.

require Logger

adventure_description = """
Something strange is happening to your hometown of Willowshore! Nestled on the
banks of a river winding through the legendary Specterwood in Tian Xia’s haunted
land of Shenmen, the people of Willowshore are no strangers to supernatural
threat, but the danger that comes to town on the first day of summer is unlike
anything you’ve ever seen before. Over the four seasons to come, you and your
fellow home-grown heroes must face evil spirits, sinister fiends, and
frightening curses, lest the town of Willowshore succumb to the Season of
Ghosts!
"""

opts = [
  response_model: %{toponyms: {:array, :string}},
  adapter_context: [api_key: secret_key]
]

params = InstructorLite.prepare_prompt(%{
      model: "o4-mini",
      reasoning: %{effort: "medium", summary: "auto"},
      input: [
        %{role: "system", content: "Here's an adventure description, please extract all toponyms"},
        %{role: "user", content: adventure_description}
      ]
  }, opts)

{:ok, response} = InstructorLite.Adapters.OpenAI.send_request(params, opts)
{:ok, result} = InstructorLite.consume_response(response, params, opts)

for %{"summary" => summaries} <- response["output"] do
  for %{"type" => "summary_text", "text" => text} <- summaries do
    Logger.info(text)
  end
end

result

13:34:05.078 [info] **Extracting toponyms**

The user is interested in extracting all toponyms from a prompt about Willowshore. I’ll identify the place names, which include: Willowshore, Specterwood, Tian Xia, and Shenmen. Event names, like "the Season of Ghosts," aren't toponyms. The phrase "the banks of a river" doesn't name the river either. 

I’ll ensure to include only unique entries, ultimately returning this list in a structured format as JSON: {"toponyms":["Willowshore","Specterwood","Tian Xia","Shenmen"]}.
%{toponyms: ["Willowshore", "Specterwood", "Tian Xia", "Shenmen"]}