Current section

Files

Jump to
codex_sdk lib codex tools hosted_tools.ex
Raw

lib/codex/tools/hosted_tools.ex

defmodule Codex.Tools.Hosted do
@moduledoc false
def metadata_value(metadata, key, default \\ nil) do
Map.get(metadata, key) ||
Map.get(metadata, to_string(key)) ||
default
end
def file_search_value(metadata, key, default \\ nil) do
case metadata_value(metadata, key) do
nil -> nested_file_search_value(metadata, key, default)
value -> value
end
end
defp nested_file_search_value(metadata, key, default) do
case metadata_value(metadata, :file_search) do
%{} = file_search ->
Map.get(file_search, key) ||
Map.get(file_search, to_string(key)) ||
default
_ ->
default
end
end
def callback(metadata, key) do
metadata_value(metadata, key)
end
def require_callback(metadata, key) do
case callback(metadata, key) do
fun when is_function(fun) -> {:ok, fun}
_ -> {:error, {:missing_callback, key}}
end
end
def safe_call(fun, args, context, metadata)
when is_function(fun, 3),
do: fun.(args, context, metadata)
def safe_call(fun, args, context, _metadata) when is_function(fun, 2), do: fun.(args, context)
def safe_call(fun, args, _context, _metadata) when is_function(fun, 1), do: fun.(args)
def safe_call(fun, _args, _context, _metadata) when is_function(fun, 0), do: fun.()
def safe_call(_fun, _args, _context, _metadata), do: {:error, :invalid_callback}
def maybe_truncate_output(output, nil), do: output
def maybe_truncate_output(output, limit) when is_integer(limit) and limit > 0 do
cond do
is_binary(output) -> String.slice(output, 0, limit)
is_map(output) -> truncate_map(output, limit)
true -> output
end
end
def maybe_truncate_output(output, _limit), do: output
def check_approval(metadata, args, context) do
case callback(metadata, :approval) do
nil ->
:ok
fun when is_function(fun) ->
case safe_call(fun, args, context, metadata) do
{:deny, reason} -> {:error, {:approval_denied, reason}}
false -> {:error, {:approval_denied, :denied}}
:deny -> {:error, {:approval_denied, :denied}}
_ -> :ok
end
_ ->
:ok
end
end
defp truncate_map(map, limit) do
map
|> maybe_truncate_key(:output, limit)
|> maybe_truncate_key("output", limit)
|> maybe_truncate_key(:stdout, limit)
|> maybe_truncate_key("stdout", limit)
end
defp maybe_truncate_key(map, key, limit) do
case Map.fetch(map, key) do
{:ok, value} when is_binary(value) ->
Map.put(map, key, String.slice(value, 0, limit))
_ ->
map
end
end
end
defmodule Codex.Tools.ComputerTool do
@moduledoc """
Hosted computer action tool with safety callback.
"""
@behaviour Codex.Tool
alias Codex.Tools.Hosted
@impl true
def metadata do
%{
name: "computer",
description: "Perform computer control actions",
schema: %{
"type" => "object",
"properties" => %{"action" => %{"type" => "string"}},
"required" => ["action"],
"additionalProperties" => false
}
}
end
@impl true
def invoke(args, context) do
metadata = Map.get(context, :metadata, %{})
safety = Hosted.callback(metadata, :safety)
with :ok <- Hosted.check_approval(metadata, args, context),
:ok <- run_safety_check(safety, args, context, metadata),
{:ok, executor} <- Hosted.require_callback(metadata, :executor) do
case Hosted.safe_call(executor, args, context, metadata) do
{:ok, result} -> {:ok, result}
{:error, reason} -> {:error, reason}
other -> {:ok, other}
end
end
end
defp run_safety_check(nil, _args, _context, _metadata), do: :ok
defp run_safety_check(fun, args, context, metadata) when is_function(fun) do
case Hosted.safe_call(fun, args, context, metadata) do
{:deny, reason} -> {:error, {:computer_denied, reason}}
:deny -> {:error, {:computer_denied, :denied}}
false -> {:error, {:computer_denied, :denied}}
_ -> :ok
end
end
defp run_safety_check(_other, _args, _context, _metadata), do: :ok
end
defmodule Codex.Tools.VectorStoreSearchTool do
@moduledoc """
Hosted vector store search tool for searching indexed documents.
This tool integrates with OpenAI's vector store file search capabilities,
allowing searches across indexed documents with optional filtering and ranking.
## Options
Options can be passed during registration or via context metadata:
* `:searcher` - Required callback function to execute the search
* `:vector_store_ids` - List of vector store IDs to search
* `:filters` - Search filters to apply
* `:ranking_options` - Options for result ranking
* `:include_search_results` - Whether to include full search results
## Usage
searcher = fn args, _ctx, _meta ->
{:ok, %{results: [%{text: args["query"]}]}}
end
{:ok, _} = Codex.Tools.register(VectorStoreSearchTool,
searcher: searcher,
vector_store_ids: ["vs_123"]
)
"""
@behaviour Codex.Tool
alias Codex.Tools.Hosted
@impl true
def metadata do
%{
name: "vector_store_search",
description: "Search indexed documents in vector stores",
schema: %{
"type" => "object",
"properties" => %{
"query" => %{"type" => "string"},
"filters" => %{"type" => "object"}
},
"required" => ["query"],
"additionalProperties" => false
}
}
end
@impl true
def invoke(args, context) do
metadata = Map.get(context, :metadata, %{})
file_search = Map.get(context, :file_search)
vector_store_ids =
Hosted.file_search_value(
metadata,
:vector_store_ids,
file_search_field(file_search, :vector_store_ids, [])
)
filters =
Hosted.file_search_value(metadata, :filters, file_search_field(file_search, :filters))
ranking_options =
Hosted.file_search_value(
metadata,
:ranking_options,
file_search_field(file_search, :ranking_options)
)
include_results =
Hosted.file_search_value(
metadata,
:include_search_results,
file_search_field(file_search, :include_search_results)
)
search_args =
args
|> Map.put_new("filters", filters)
|> Map.put_new("vector_store_ids", vector_store_ids)
|> maybe_put_arg("include_search_results", include_results)
|> maybe_put_arg("ranking_options", ranking_options)
enriched_metadata =
metadata
|> maybe_put_arg(:filters, filters)
|> maybe_put_arg(:vector_store_ids, vector_store_ids)
|> maybe_put_arg(:include_search_results, include_results)
|> maybe_put_arg(:ranking_options, ranking_options)
with {:ok, fun} <- Hosted.require_callback(enriched_metadata, :searcher) do
case Hosted.safe_call(fun, search_args, context, enriched_metadata) do
{:ok, result} -> {:ok, result}
{:error, reason} -> {:error, reason}
other -> {:ok, other}
end
end
end
defp file_search_field(file_search, key, default \\ nil)
defp file_search_field(%Codex.FileSearch{} = fs, key, default), do: Map.get(fs, key) || default
defp file_search_field(%{} = map, key, default),
do: Map.get(map, key) || Map.get(map, to_string(key)) || default
defp file_search_field(_other, _key, default), do: default
defp maybe_put_arg(map, _key, nil), do: map
defp maybe_put_arg(map, key, value), do: Map.put(map, key, value)
end
# WebSearchTool is now defined in lib/codex/tools/web_search_tool.ex
# with support for Tavily, Serper, and mock providers
defmodule Codex.Tools.ImageGenerationTool do
@moduledoc """
Hosted image generation tool.
"""
@behaviour Codex.Tool
alias Codex.Tools.Hosted
@impl true
def metadata do
%{
name: "image_generation",
description: "Generate images from prompts",
schema: %{
"type" => "object",
"properties" => %{
"prompt" => %{"type" => "string"},
"size" => %{"type" => "string"},
"quality" => %{"type" => "string"}
},
"required" => ["prompt"],
"additionalProperties" => false
}
}
end
@impl true
def invoke(args, context) do
metadata = Map.get(context, :metadata, %{})
with {:ok, generator} <- Hosted.require_callback(metadata, :generator) do
case Hosted.safe_call(generator, args, context, metadata) do
{:ok, result} -> {:ok, result}
{:error, reason} -> {:error, reason}
other -> {:ok, other}
end
end
end
end
defmodule Codex.Tools.CodeInterpreterTool do
@moduledoc """
Hosted code interpreter tool.
"""
@behaviour Codex.Tool
alias Codex.Tools.Hosted
@impl true
def metadata do
%{
name: "code_interpreter",
description: "Run code in a sandbox",
schema: %{
"type" => "object",
"properties" => %{"code" => %{"type" => "string"}},
"required" => ["code"],
"additionalProperties" => false
}
}
end
@impl true
def invoke(args, context) do
metadata = Map.get(context, :metadata, %{})
with {:ok, runner} <- Hosted.require_callback(metadata, :runner) do
case Hosted.safe_call(runner, args, context, metadata) do
{:ok, result} -> {:ok, result}
{:error, reason} -> {:error, reason}
other -> {:ok, other}
end
end
end
end
defmodule Codex.Tools.HostedMcpTool do
@moduledoc """
Hosted MCP tool wrapper that delegates to `Codex.MCP.Client`.
"""
@behaviour Codex.Tool
alias Codex.MCP.Client
alias Codex.Tools.Hosted
@impl true
def metadata do
%{
name: "hosted_mcp",
description: "Call a tool exposed by an MCP server"
}
end
@impl true
def invoke(args, context) do
metadata = Map.get(context, :metadata, %{})
tool = resolve_tool(args, metadata)
client = Hosted.metadata_value(metadata, :client)
retries = Hosted.metadata_value(metadata, :retries, 0)
backoff = Hosted.callback(metadata, :backoff)
call_args =
Map.get(args, "arguments") ||
Map.get(args, :arguments) ||
args
with true <- not is_nil(tool) or {:error, :missing_tool},
{:ok, %Client{} = client} <- ensure_client(client),
:ok <- Hosted.check_approval(metadata, args, context),
{:ok, result} <-
Client.call_tool(client, tool, call_args,
retries: retries,
backoff: backoff
) do
{:ok, result}
else
{:error, _} = error -> error
end
end
defp ensure_client(%Client{} = client), do: {:ok, client}
defp ensure_client(_), do: {:error, :missing_client}
defp resolve_tool(args, metadata) do
Hosted.metadata_value(metadata, :tool) ||
Hosted.metadata_value(metadata, :tool_name) ||
Map.get(args, "tool") ||
Map.get(args, :tool)
end
end