Current section

Files

Jump to
codex_sdk examples live_structured_hosted_tools.exs
Raw

examples/live_structured_hosted_tools.exs

# Covers ADR-004, ADR-005, ADR-012 (function tools, hosted tools, structured outputs)
Mix.Task.run("app.start")
Code.require_file(Path.expand("support/example_helper.exs", __DIR__))
alias CodexExamples.Support
Support.init!()
alias Codex.{Agent, AgentRunner, FileSearch, RunConfig, ToolOutput, Tools}
alias Codex.FunctionTool
alias Codex.Items.AgentMessage
defmodule CodexExamples.StructuredBundleTool do
use FunctionTool,
name: "structured_bundle",
description: "Returns structured text, image, and file outputs for a topic",
parameters: %{topic: :string},
handler: fn %{"topic" => topic}, _ctx ->
{:ok,
[
ToolOutput.text("Structured summary for #{topic}"),
ToolOutput.image(%{
url: "https://example.com/#{topic}.png",
detail: "low"
}),
ToolOutput.file(%{
data: Base.encode64("demo file payload for #{topic}"),
filename: "#{topic}.txt",
mime_type: "text/plain"
})
]}
end
end
defmodule CodexExamples.LiveStructuredHostedTools do
@moduledoc false
def main(argv) do
prompt =
case argv do
[] ->
"Summarize this repository in one or two sentences."
values ->
Enum.join(values, " ")
end
Tools.reset!()
{:ok, _} = Tools.register(CodexExamples.StructuredBundleTool)
{:ok, _} =
Codex.Tools.ShellTool
|> Tools.register(Keyword.merge([name: "hosted_shell"], shell_options()))
{:ok, _} =
Codex.Tools.ApplyPatchTool
|> Tools.register(Keyword.merge([name: "apply_patch"], apply_patch_options()))
{:ok, _} =
Codex.Tools.ComputerTool
|> Tools.register(Keyword.merge([name: "computer"], computer_options()))
{:ok, _} =
Codex.Tools.VectorStoreSearchTool
|> Tools.register(Keyword.merge([name: "file_search"], file_search_options()))
{:ok, _} =
Codex.Tools.ImageGenerationTool
|> Tools.register(Keyword.merge([name: "image_generation"], image_options()))
{:ok, file_search} =
FileSearch.new(%{
vector_store_ids: ["demo-store"],
filters: %{"source" => "docs"},
include_search_results: true
})
{:ok, agent} =
Agent.new(%{
name: "StructuredHostAgent",
instructions: "Provide a short summary. Use hosted tools only if they are available.",
tools: [
"structured_bundle",
"hosted_shell",
"apply_patch",
"computer",
"file_search",
"image_generation"
],
reset_tool_choice: true
})
{:ok, run_config} =
RunConfig.new(%{
max_turns: 3,
file_search: file_search
})
codex_opts =
Support.codex_options!(%{})
{:ok, thread_opts} =
Codex.Thread.Options.new(%{
file_search: file_search
})
{:ok, thread} = Codex.start_thread(codex_opts, Support.thread_opts!(thread_opts))
IO.puts("""
Running live structured/hosted tools demo.
Prompt: #{prompt}
File search: #{inspect(file_search)}
""")
case AgentRunner.run(thread, prompt, %{agent: agent, run_config: run_config}) do
{:ok, result} ->
tool_outputs = result.raw[:tool_outputs] || []
print_tool_outputs(tool_outputs)
if tool_outputs == [] do
demo_tool_invocations()
end
IO.puts("Usage: #{inspect(result.thread.usage || %{})}")
IO.puts("Final response:\n#{render_response(result.final_response)}")
{:error, reason} ->
Mix.raise("Run failed: #{inspect(reason)}")
end
end
defp print_tool_outputs(outputs) do
outputs
|> List.wrap()
|> Enum.each(fn output ->
IO.puts("Tool output: #{inspect(output)}")
end)
end
defp demo_tool_invocations do
IO.puts("No tool calls observed; invoking hosted tools locally.")
invoke_tool("structured_bundle", %{"topic" => "codex repo"})
invoke_tool("hosted_shell", %{"command" => "echo ok"})
invoke_tool("file_search", %{"query" => "docs"})
end
defp invoke_tool(name, args) do
case Tools.invoke(name, args, %{}) do
{:ok, output} -> IO.puts("Tool #{name} output: #{inspect(output)}")
{:error, reason} -> IO.puts("Tool #{name} error: #{inspect(reason)}")
end
end
defp render_response(%AgentMessage{text: text}), do: text
defp render_response(%{"text" => text}), do: text
defp render_response(nil), do: "<no response>"
defp render_response(other), do: inspect(other)
defp shell_options do
[
# Executor receives (args_map, context, metadata) or (args_map, context)
executor: fn %{"command" => command}, _context, _metadata ->
{:ok, %{"command" => command, "stdout" => "simulated shell: #{command}"}}
end,
# Approval receives (command_string, context, metadata) or (command_string, context)
# Note: first arg is the command STRING, not the args map
approval: fn command, _context, _metadata ->
if String.contains?(command, "rm"), do: {:deny, "blocked dangerous command"}, else: :allow
end,
max_output_bytes: 400
]
end
defp apply_patch_options do
[
editor: fn %{"patch" => patch}, _context ->
{:ok, %{"applied" => String.slice(patch, 0, 60)}}
end
]
end
defp computer_options do
[
safety: fn args, _context, _metadata ->
action = Map.get(args, "action", "")
if String.contains?(action, "destructive") do
{:deny, "computer action blocked"}
else
:ok
end
end,
executor: fn args, _context ->
{:ok, %{"action" => Map.get(args, "action", "noop"), "status" => "simulated"}}
end
]
end
defp file_search_options do
[
searcher: fn args, _context, _metadata ->
{:ok,
%{
"query" => Map.get(args, "query"),
"results" => [%{"title" => "demo result", "score" => 0.99}]
}}
end
]
end
defp image_options do
[
generator: fn args, _context, _metadata ->
{:ok,
%{"prompt" => Map.get(args, "prompt"), "url" => "https://example.com/generated.png"}}
end
]
end
end
CodexExamples.LiveStructuredHostedTools.main(System.argv())