defmodule Cmd.Tool do @behaviour Cmd @impl Cmd def spec() do [ tool: [ name: "tool", about: "Performs a tool call", allow_unknown_args: true, subcommands: get_subcommands(), options: [ project: [ value_name: "PROJECT", long: "--project", short: "-p", help: "Project name" ], workers: [ value_name: "WORKERS", long: "--workers", short: "-w", help: "Limits the number of concurrent OpenAI requests", parser: :integer, default: Cmd.default_workers() ], tool: [ value_name: "TOOL", long: "--tool", short: "-t", help: "The name of the tool to call; use --help to list options.", required: true ] ] ] ] end @impl Cmd def run(opts, _subcommands, unknown) do with {:ok, tool} <- Map.fetch(opts, :tool), {:ok, tool_args} <- parse_tool_args(tool, unknown), _ <- Store.get_project() do state = %{ai: AI.new()} AI.Tools.perform_tool_call(state, tool, tool_args, AI.Tools.all_tools()) |> case do {:ok, response} -> IO.puts(response) {:error, error} -> IO.puts(:stderr, "Error: #{error}") end else {:error, :unknown_tool, tool} -> IO.puts(:stderr, "Error: Unknown tool '#{tool}'") System.halt(1) error -> IO.puts(:stderr, "Error: #{inspect(error)}") System.halt(1) end end defp parse_tool_args(tool, args) do with {:ok, spec} <- AI.Tools.tool_spec(tool, AI.Tools.all_tools()) do build_optimus(tool, spec) |> Optimus.parse!(args) |> case do %{options: args} -> args |> Enum.map(fn {k, v} -> {Atom.to_string(k), v} end) |> Map.new() |> then(&{:ok, &1}) other -> other end end end defp build_optimus(tool, spec) do Optimus.new!( name: tool, description: spec.function.description, options: tool_spec_to_optimus_spec(spec) ) end defp tool_spec_to_optimus_spec(spec) do %{required: required, properties: args} = spec |> Map.get(:function) |> Map.get(:parameters) is_required? = required |> Enum.map(fn arg -> String.to_atom(arg) end) |> Enum.map(fn arg -> {arg, true} end) |> Map.new() args |> Enum.reduce([], fn {arg, arg_spec}, acc -> long = "--#{arg}" help = arg_spec.description required = Map.get(is_required?, arg, false) Keyword.put(acc, arg, long: long, help: help, required: required ) end) end defp get_subcommands() do tools = AI.Tools.all_tools() tools |> Enum.map(fn {tool, _module} -> spec = AI.Tools.tool_spec!(tool, tools) desc = spec.function.description |> String.split("\n") |> Enum.map(&String.trim/1) |> Enum.join(" ") {String.to_atom(tool), [ name: tool, about: desc, options: tool_spec_to_optimus_spec(spec) ]} end) end end