Tool Use (Function Calling)

Copy Markdown View Source

Ollama supports tool use for models that are trained for it. This allows the model to request function calls that your code executes.

Overview

The tool use workflow:

  1. Define tools with JSON Schema
  2. Send chat with tool definitions
  3. Model returns tool_calls (if needed)
  4. Execute the functions
  5. Send results back to model
  6. Model generates final response

Defining Tools

Tools use JSON Schema format:

weather_tool = %{
  type: "function",
  function: %{
    name: "get_weather",
    description: "Get current weather for a location",
    parameters: %{
      type: "object",
      properties: %{
        location: %{
          type: "string",
          description: "City name, e.g., 'San Francisco'"
        },
        unit: %{
          type: "string",
          enum: ["celsius", "fahrenheit"],
          description: "Temperature unit"
        }
      },
      required: ["location"]
    }
  }
}

Tool Helpers

You can define tools programmatically:

tool =
  Ollixir.Tool.define(:get_weather,
    description: "Get current weather for a location",
    parameters: [
      location: [type: :string, required: true, description: "City name"],
      unit: [type: :string, enum: ["celsius", "fahrenheit"]]
    ]
  )

Or pass functions directly and let Ollama convert them:

defmodule WeatherTools do
  @doc "Get weather for a city."
  @spec get_weather(String.t(), String.t()) :: String.t()
  def get_weather(city, unit), do: "#{city} in #{unit}: 72 degrees"
end

{:ok, response} = Ollixir.chat(client,
  model: "llama3.2",
  messages: [%{role: "user", content: "Weather in Tokyo?"}],
  tools: [&WeatherTools.get_weather/2]
)

Ollama also ships predefined tool definitions for web search and fetch:

tools = Ollixir.Web.Tools.all()

Best Practices for Tool Definitions

  1. Clear descriptions - Help the model understand when to use the tool
  2. Specific parameter descriptions - Include examples and constraints
  3. Required fields - Mark truly required parameters
  4. Enum values - Use enums for constrained choices

Basic Workflow

defmodule WeatherAgent do
  def run(question) do
    client = Ollixir.init()
    tools = [weather_tool()]

    # Step 1: Initial request
    {:ok, response} = Ollixir.chat(client,
      model: "llama3.2",
      messages: [%{role: "user", content: question}],
      tools: tools
    )

    # Step 2: Check for tool calls
    case get_in(response, ["message", "tool_calls"]) do
      nil ->
        # No tool call, return direct response
        response["message"]["content"]

      tool_calls ->
        # Step 3: Execute tools
        results = execute_tools(tool_calls)

        # Step 4: Send results back
        {:ok, final} = Ollixir.chat(client,
          model: "llama3.2",
          messages: [
            %{role: "user", content: question},
            %{role: "assistant", content: "", tool_calls: tool_calls},
            %{role: "tool", content: results}
          ],
          tools: tools
        )

        final["message"]["content"]
    end
  end

  defp execute_tools(tool_calls) do
    # Execute each tool and collect results
    tool_calls
    |> Enum.map(&execute_tool/1)
    |> Enum.join("\n")
  end

  defp execute_tool(%{"function" => %{"name" => "get_weather", "arguments" => args}}) do
    # Your actual weather API call here
    location = args["location"]
    "Weather in #{location}: 72°F, sunny"
  end
end

Multi-Turn Tool Use

For complex tasks, the model may need multiple tool calls:

defmodule Agent do
  @max_iterations 5

  def run(prompt, tools) do
    client = Ollixir.init()
    messages = [%{role: "user", content: prompt}]
    loop(client, messages, tools, 0)
  end

  defp loop(_client, messages, _tools, @max_iterations) do
    {:error, :max_iterations}
  end

  defp loop(client, messages, tools, iteration) do
    {:ok, response} = Ollixir.chat(client,
      model: "llama3.2",
      messages: messages,
      tools: tools
    )

    case get_in(response, ["message", "tool_calls"]) do
      nil ->
        {:ok, response["message"]["content"]}

      tool_calls ->
        results = execute_all(tool_calls)

        messages = messages ++
          [%{role: "assistant", content: "", tool_calls: tool_calls}] ++
          Enum.map(results, &%{role: "tool", content: &1})

        loop(client, messages, tools, iteration + 1)
    end
  end
end

Web Tools Integration

Use predefined web tools for search and fetch.

For an MCP (Model Context Protocol) stdio server example, see examples/mcp/mcp_server.exs. This exposes web_search and web_fetch to MCP clients like Cursor, Cline, and Open WebUI.

# Get all web tool definitions
tools = Ollixir.Web.Tools.all()

{:ok, response} = Ollixir.chat(client,
  model: "llama3.2",
  messages: [%{role: "user", content: "Search for Elixir news"}],
  tools: tools
)

# Execute web tool calls
case get_in(response, ["message", "tool_calls"]) do
  [%{"function" => %{"name" => "web_search", "arguments" => args}}] ->
    {:ok, results} = Ollixir.web_search(client, query: args["query"])
    # Continue with results...

  [%{"function" => %{"name" => "web_fetch", "arguments" => args}}] ->
    {:ok, page} = Ollixir.web_fetch(client, url: args["url"])
    # Continue with page content...

  _ ->
    response["message"]["content"]
end

Thinking with Tools

Combine tool use with thinking mode (supported models only):

{:ok, response} = Ollixir.chat(client,
  model: "gpt-oss:20b-cloud",
  messages: [%{role: "user", content: "Calculate the weather impact on crops"}],
  tools: [weather_tool, crop_tool],
  think: true
)

# Access thinking process
IO.puts("Thinking: #{response["message"]["thinking"]}")

# Handle tool calls as usual
tool_calls = get_in(response, ["message", "tool_calls"])

Tool Argument Handling

Tool arguments may arrive as a map or JSON string. Handle both:

defp parse_arguments(args) when is_map(args), do: args
defp parse_arguments(args) when is_binary(args) do
  case Jason.decode(args) do
    {:ok, parsed} -> parsed
    {:error, _} -> %{}
  end
end

defp execute_tool(%{"function" => %{"name" => name, "arguments" => args}}) do
  parsed_args = parse_arguments(args)
  # Use parsed_args...
end

Error Handling

Handle tool execution failures gracefully:

defp execute_tool_safely(tool_call) do
  try do
    result = execute_tool(tool_call)
    Jason.encode!(%{success: true, result: result})
  rescue
    e ->
      Jason.encode!(%{success: false, error: Exception.message(e)})
  end
end

# In the agent loop, send error back to model
{:ok, response} = Ollixir.chat(client,
  model: "llama3.2",
  messages: messages ++ [
    %{role: "assistant", content: "", tool_calls: tool_calls},
    %{role: "tool", content: ~s({"success": false, "error": "API unavailable"})}
  ],
  tools: tools
)

Limitations

  • No streaming with tools - stream: true is not supported when using tools
  • Model dependent - Not all models support tool use
  • Tool arguments - Arguments may arrive as a JSON string or a map
  • Non-deterministic - Tool calls may vary between runs

Compatible Models

ModelTool SupportThinking + Tools
llama3.2YesNo
mistralYesNo
mixtralYesNo
command-rYesNo
qwen2.5YesNo
qwen3YesYes
gpt-oss:20b-cloudYesYes
gpt-oss:120b-cloudYesYes

See Also