Back to Baml

Tools / Function Calling

fern/01-guide/06-prompt-engineering/tools.mdx

0.222.021.1 KB
Original Source

"Function calling" is a technique for getting an LLM to choose a function to call for you.

The way it works is:

  1. You define a task with certain function(s)
  2. Ask the LLM to choose which function to call
  3. Get the function parameters from the LLM for the appropriate function it choose
  4. Call the functions in your code with those parameters

It's common for people to think of "function calling" or "tool use" separately from "structured outputs" (even OpenAI has separate parameters for them), but at BAML, we think it's simpler and more impactful to think of them equivalently. This is because, at the end of the day, you are looking to get something processable back from your LLM. Whether it's extracting data from a document or calling the Weather API, you need a standard representation of that output, which is where BAML lives.

<Frame caption="Baml Control Flow"> </Frame>

In BAML, you can get represent a tool or a function you want to call as a BAML class, and make the function output be that class definition.

baml
class WeatherAPI {
  // we can use literals to denote the name of the tool
  // the field can be named anything we want! "api_name" "tool" "function_name"
  // whatever you feel the LLM would understand best
  api_name "weather_request"
  city string @description("the user's city")
  timeOfDay string @description("As an ISO8601 timestamp")
}

function UseTool(user_message: string) -> WeatherAPI {
  client "openai/gpt-5-mini"
  prompt #"
    Given a message, extract info.
    {# special macro to print the functions return type. #}
    {{ ctx.output_format }}

    {{ _.role('user') }}
    {{ user_message }}
  "#
}

Call the function like this:

<CodeGroup> ```python Python import asyncio import datetime from baml_client import b from baml_client.types import WeatherAPI

def get_weather(city: str, time_of_day: datetime.date): ...

def main(): weather_info = b.UseTool("What's the weather like in San Francisco?") print(weather_info) assert isinstance(weather_info, WeatherAPI) print(f"City: {weather_info.city}") print(f"Time of Day: {weather_info.time_of_day}") weather = get_weather(city=weather_info.city, time_of_day=weather_info.timeOfDay)

if name == 'main': main()


```typescript TypeScript
import { b } from './baml_client'
import { WeatherAPI } from './baml_client/types'
import assert from 'assert'

const main = async () => {
  const weatherInfo = await b.UseTool("What's the weather like in San Francisco?")
  console.log(weatherInfo)
  // BAML doesn't generate concrete types in TypeScript
  // so we can only validate the interfaces
  assert("city" in weatherInfo)
  console.log(`City: ${weatherInfo.city}`)
  console.log(`Time of Day: ${weatherInfo.timeOfDay}`)
}
go
package main

import (
    "context"
    "fmt"
    
    b "example.com/myproject/baml_client"
    "example.com/myproject/baml_client/types"
)

func getWeather(city string, timeOfDay string) {
    // Your weather API implementation
}

func main() {
    ctx := context.Background()
    
    weatherInfo, err := b.UseTool(ctx, "What's the weather like in San Francisco?")
    if err != nil {
        panic(err)
    }
    
    fmt.Printf("%+v\n", weatherInfo)
    fmt.Printf("City: %s\n", weatherInfo.City)
    fmt.Printf("Time of Day: %s\n", weatherInfo.TimeOfDay)
    
    getWeather(weatherInfo.City, weatherInfo.TimeOfDay)
}
ruby
require_relative "baml_client/client"

$b = Baml.Client

def main
  weather_info = $b.UseTool(user_message: "What's the weather like in San Francisco?")
  puts weather_info
  raise unless weather_info.is_a?(Baml::Types::WeatherAPI)
  puts "City: #{weather_info.city}"
  puts "Time of Day: #{weather_info.timeOfDay}"
end
</CodeGroup>

Choosing multiple Tools

To choose ONE tool out of many, you can use a union:

baml
function UseTool(user_message: string) -> WeatherAPI | MyOtherAPI {
  .... // same thing
}

<Tip>If you use VSCode Playground, you can see what we inject into the prompt, with full transparency.</Tip>

Call the function like this:

<CodeGroup> ```python Python import asyncio from baml_client import b from baml_client.types import WeatherAPI, MyOtherAPI

async def main(): tool = b.UseTool("What's the weather like in San Francisco?") print(tool)

if isinstance(tool, WeatherAPI):
    print(f"Weather API called:")
    print(f"City: {tool.city}")
    print(f"Time of Day: {tool.timeOfDay}")
elif isinstance(tool, MyOtherAPI):
    print(f"MyOtherAPI called:")
    # Handle MyOtherAPI specific attributes here

if name == 'main': main()


```typescript TypeScript
import { b } from './baml_client'
import { WeatherAPI, MyOtherAPI } from './baml_client/types'

const main = async () => {
  const tool = await b.UseTool("What's the weather like in San Francisco?")
  console.log(tool)
  
  // BAML doesn't generate concrete types in TypeScript
  // We check which tool by checking if certain fields exist
  if ("city" in tool) {
    console.log("Weather API called:")
    console.log(`City: ${tool.city}`)
    console.log(`Time of Day: ${tool.timeOfDay}`)
  } else if ("operation" in tool) {
    console.log("MyOtherAPI called:")
    // Handle MyOtherAPI specific attributes here
  }

  /*
   * Alternatively, we could modify our BAML file as such
   * 
   * class WeatherAPI {
   *   class_name "WeatherAPI"
   *   city string
   *   time string @description("Current time in ISO8601 format")
   * }
   *
   * class MyOtherAPI {
   *   class_name "MyOtherAPI"
   *   operation "add" | "subtract" | "multiply" | "divide"
   *   numbers float[]
   * }
   *
   * Then, in typescript, we could check the class_name to determine which tool to call
   * 
   * if (tool.class_name === "WeatherAPI") {
   *   // Handle WeatherAPI specific attributes here
   * } else if (tool.class_name === "MyOtherAPI") {
   *   // Handle MyOtherAPI specific attributes here
   * }
   */
}

main()
go
package main

import (
    "context"
    "fmt"
    
    b "example.com/myproject/baml_client"
    "example.com/myproject/baml_client/types"
)

func main() {
    ctx := context.Background()
    
    tool, err := b.UseTool(ctx, "What's the weather like in San Francisco?")
    if err != nil {
        panic(err)
    }
    
    fmt.Printf("%+v\n", tool)
    
    // Go generates As{TypeName}() methods for union types
    // Method names correspond to the actual union variant names
    if weatherAPI := tool.AsWeatherAPI(); weatherAPI != nil {
        fmt.Println("Weather API called:")
        fmt.Printf("City: %s\n", weatherAPI.City)
        fmt.Printf("Time of Day: %s\n", weatherAPI.TimeOfDay)
    } else if otherAPI := tool.AsMyOtherAPI(); otherAPI != nil {
        fmt.Println("MyOtherAPI called:")
        // Handle MyOtherAPI specific attributes here
    } else {
        fmt.Println("Unknown tool type")
    }
}
ruby
require_relative "baml_client/client"

$b = Baml.Client

def main
  tool = $b.UseTool(user_message: "What's the weather like in San Francisco?")
  puts tool
  
  case tool
  when Baml::Types::WeatherAPI
    puts "Weather API called:"
    puts "City: #{tool.city}"
    puts "Time of Day: #{tool.timeOfDay}"
  when Baml::Types::MyOtherAPI
    puts "MyOtherAPI called:"
    # Handle MyOtherAPI specific attributes here
  end
end

main
</CodeGroup>

Choosing N Tools

To choose many tools, you can use a union of a list:

baml
function UseTool(user_message: string) -> (WeatherAPI | MyOtherAPI)[] {
  client "openai/gpt-5-mini"
  prompt #"
    Given a message, extract info.
    {# special macro to print the functions return type. #}
    {{ ctx.output_format }}

    {{ _.role('user') }}
    {{ user_message }}
  "#
}

Call the function like this:

<CodeGroup> ```python Python import asyncio from baml_client import b from baml_client.types import WeatherAPI, MyOtherAPI

async def main(): tools = b.UseTool("What's the weather like in San Francisco and New York?") print(tools)

for tool in tools:
    if isinstance(tool, WeatherAPI):
        print(f"Weather API called:")
        print(f"City: {tool.city}")
        print(f"Time of Day: {tool.timeOfDay}")
    elif isinstance(tool, MyOtherAPI):
        print(f"MyOtherAPI called:")
        # Handle MyOtherAPI specific attributes here

if name == 'main': main()


```typescript TypeScript
import { b } from './baml_client'
import { WeatherAPI, MyOtherAPI } from './baml_client/types'

const main = async () => {
  const tools = await b.UseTool("What's the weather like in San Francisco and New York?")
  console.log(tools)
  
  tools.forEach(tool => {
    if ("city" in tool) {
      console.log("Weather API called:")
      console.log(`City: ${tool.city}`)
      console.log(`Time of Day: ${tool.timeOfDay}`)
    } else if ("operation" in tool) {
      console.log("MyOtherAPI called:")
      // Handle MyOtherAPI specific attributes here
    }
  })
}

main()
go
package main

import (
    "context"
    "fmt"
    
    b "example.com/myproject/baml_client"
    "example.com/myproject/baml_client/types"
)

func main() {
    ctx := context.Background()
    
    tools, err := b.UseTool(ctx, "What's the weather like in San Francisco and New York?")
    if err != nil {
        panic(err)
    }
    
    fmt.Printf("%+v\n", tools)
    
    for _, tool := range tools {
        if weatherAPI := tool.AsWeatherAPI(); weatherAPI != nil {
            fmt.Println("Weather API called:")
            fmt.Printf("City: %s\n", weatherAPI.City)
            fmt.Printf("Time of Day: %s\n", weatherAPI.TimeOfDay)
        } else if otherAPI := tool.AsMyOtherAPI(); otherAPI != nil {
            fmt.Println("MyOtherAPI called:")
            // Handle MyOtherAPI specific attributes here
        } else {
            fmt.Println("Unknown tool type")
        }
    }
}
ruby
require_relative "baml_client/client"

$b = Baml.Client

def main
  tools = $b.UseTool(user_message: "What's the weather like in San Francisco and New York?")
  puts tools
  
  tools.each do |tool|
    case tool
    when Baml::Types::WeatherAPI
      puts "Weather API called:"
      puts "City: #{tool.city}"
      puts "Time of Day: #{tool.timeOfDay}"
    when Baml::Types::MyOtherAPI
      puts "MyOtherAPI called:"
      # Handle MyOtherAPI specific attributes here
    end
  end
end

main
</CodeGroup>

Disambiguating Between Similar Tools

When building functions that can call multiple tools (represented as BAML classes), you might encounter situations where different tools accept arguments with the same name. For instance, consider GetWeather and GetTimezone classes, both taking a city: string argument. How does the system determine whether a user query like "What's the time in London?" corresponds to GetTimezone or potentially GetWeather?

You can use string literals to solve this problem:

baml
class GetWeather {
  tool_name "get_weather" @description("Use this tool to get the current weather forecast for a specific city.")
  city string @description("The city for which to get the weather.")
}

class GetTimezone {
  tool_name "get_timezone" @description("Use this tool to find the current timezone of a specific city.")
  city string @description("The city for which to find the timezone.")
}

function ChooseTool(query: string) -> GetWeather | GetTimezone {
  client "openai/gpt-5"
  prompt #"
    Given the user query, determine the primary intent and select the appropriate tool to call.

    {# special macro to add tool structures + descriptions here #}
    {{ ctx.output_format }} 

    {{ _.role('user') }}
    {{ query }}
  "#
}

Dynamically Generate the tool signature

It might be cumbersome to define schemas in baml and code, so you can define them from code as well. Read more about dynamic types here

baml
class WeatherAPI {
  @@dynamic // params defined from code
}

function UseTool(user_message: string) -> WeatherAPI {
  client "openai/gpt-5-mini"
  prompt #"
    Given a message, extract info.
    {# special macro to print the functions return type. #}
    {{ ctx.output_format }}

    {{ _.role('user') }}
    {{ user_message }}
  "#
}

Call the function like this:

<CodeGroup> ```python Python import asyncio import inspect

from baml_client import b from baml_client.type_builder import TypeBuilder from baml_client.types import WeatherAPI

async def get_weather(city: str, time_of_day: str): print(f"Getting weather for {city} at {time_of_day}") return 42

async def main(): tb = TypeBuilder() type_map = {int: tb.int(), float: tb.float(), str: tb.string()} signature = inspect.signature(get_weather) for param_name, param in signature.parameters.items(): tb.WeatherAPI.add_property(param_name, type_map[param.annotation]) tool = b.UseTool("What's the weather like in San Francisco this afternoon?", { "tb": tb }) print(tool) weather = await get_weather(**tool.model_dump()) print(weather)

if name == 'main': asyncio.run(main())

</CodeGroup>

<Warning>Note that the above approach is not fully generic. Recommended you read: [Dynamic JSON Schema](https://www.boundaryml.com/blog/dynamic-json-schemas)</Warning>

## Function-calling APIs vs Prompting
Injecting your function schemas into the prompt, as BAML does, outperforms function-calling across all benchmarks for major providers ([see our Berkeley FC Benchmark results with BAML](https://www.boundaryml.com/blog/sota-function-calling?q=0)).

Amongst other limitations, function-calling APIs will at times:
1. Return a schema when you don't want any (you want an error)
2. Not work for tools with more than 100 parameters.
3. Use [many more tokens than prompting](https://www.boundaryml.com/blog/type-definition-prompting-baml).

Keep in mind that "JSON mode" is nearly the same thing as "prompting", but it enforces the LLM response is ONLY a JSON blob.
BAML does not use JSON mode since it allows developers to use better prompting techniques like chain-of-thought, to allow the LLM to express its reasoning before printing out the actual schema. BAML's parser can find the json schema(s) out of free-form text for you. Read more about different approaches to structured generation [here](https://www.boundaryml.com/blog/schema-aligned-parsing)

BAML will still support native function-calling APIs in the future (please let us know more about your use-case so we can prioritize accordingly)


## Create an Agent that utilizes these Tools 

We can create an Agent or an "agentic loop" that continuously uses tools in a program simply by adding a while loop in our code. 
In this example, we'll have two tools:
1. An API that queries the weather.
2. An API that does basic calculations on numbers. 

This is what it looks in the BAML file:

``` Rust tools.baml
class WeatherAPI {
  intent "weather_request"
  city string
  time string @description("Current time in ISO8601 format")
}

class CalculatorAPI {
  intent "basic_calculator"
  operation "add" | "subtract" | "multiply" | "divide"
  numbers float[]
}

function SelectTool(message: string) -> WeatherAPI | CalculatorAPI {
  client "openai/gpt-5"
  prompt #"
    Given a message, extract info.

    {{ ctx.output_format }}

    {{ _.role("user") }} {{ message }}
  "#
}

In our agent code, we'll:

  1. Implement our APIs
  2. Implement our Agent that continuously will use different tools
<CodeGroup> ```python toolAgent.py from baml_client import b from baml_client.types import WeatherAPI, CalculatorAPI

def handle_weather(weather: WeatherAPI): # Simulate weather API call, but you can implement this with a real API call return f"The weather in {weather.city} at {weather.time} is sunny."

def handle_calculator(calc: CalculatorAPI): numbers = calc.numbers if calc.operation == "add": result = sum(numbers) elif calc.operation == "subtract": result = numbers[0] - sum(numbers[1:]) elif calc.operation == "multiply": result = 1 for n in numbers: result *= n elif calc.operation == "divide": result = numbers[0] for n in numbers[1:]: result /= n return f"The result is {result}"

def main(): print("Agent started! Type 'exit' to quit.")

while True:
    # Get user input
    user_input = input("You: ")
    if user_input.lower() == 'exit':
        break

    # Call the BAML function to select tool
    tool_response = b.SelectTool(user_input)

    # Handle the tool response
    if isinstance(tool_response, WeatherAPI):
        result = handle_weather(tool_response)
        print(f"Agent (Weather): {result}")
    
    elif isinstance(tool_response, CalculatorAPI):
        result = handle_calculator(tool_response)
        print(f"Agent (Calculator): {result}")

if name == "main": main()


```typescript toolAgent.ts
import { b } from "@/baml_client";
import { WeatherAPI, CalculatorAPI } from "@/baml_client/types";

function handleWeather(weather: WeatherAPI): string {
  // Simulate weather API call
  return `The weather in ${weather.city} at ${weather.time} is sunny.`;
}

function handleCalculator(calc: CalculatorAPI): string {
  const numbers = calc.numbers;
  let result: number;

  switch (calc.operation) {
    case "add":
      result = numbers.reduce((a, b) => a + b, 0);
      break;
    case "subtract":
      result = numbers.slice(1).reduce((a, b) => a - b, numbers[0]);
      break;
    case "multiply":
      result = numbers.reduce((a, b) => a * b, 1);
      break;
    case "divide":
      result = numbers.slice(1).reduce((a, b) => a / b, numbers[0]);
      break;
    default:
      return "Unknown operation.";
  }

  return `The result is ${result}`;
}

async function main() {
  console.log("Agent started! Type 'exit' to quit.");

  const readline = await import("readline");

  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  rl.on("line", async (input) => {
    if (input.toLowerCase() === "exit") {
      rl.close();
      return;
    }

    const toolResponse = await b.SelectTool(input);

    switch (toolResponse.intent) {
      case "weather_request":
        const weatherResult = handleWeather(toolResponse);
        console.log(`Agent (Weather): ${weatherResult}`);
        break;
      case "basic_calculator":
        const calcResult = handleCalculator(toolResponse);
        console.log(`Agent (Calculator): ${calcResult}`);
        break;
    }
  });
}

main();

go
package main

import (
    "bufio"
    "context"
    "fmt"
    "os"
    "strings"
    
    b "example.com/myproject/baml_client"
    "example.com/myproject/baml_client/types"
)

func handleWeather(weather *types.WeatherAPI) string {
    // Simulate weather API call
    return fmt.Sprintf("The weather in %s at %s is sunny.", weather.City, weather.Time)
}

func handleCalculator(calc *types.CalculatorAPI) string {
    numbers := calc.Numbers
    var result float64
    
    switch calc.Operation {
    case "add":
        result = 0
        for _, n := range numbers {
            result += n
        }
    case "subtract":
        if len(numbers) > 0 {
            result = numbers[0]
            for _, n := range numbers[1:] {
                result -= n
            }
        }
    case "multiply":
        result = 1
        for _, n := range numbers {
            result *= n
        }
    case "divide":
        if len(numbers) > 0 {
            result = numbers[0]
            for _, n := range numbers[1:] {
                if n != 0 {
                    result /= n
                }
            }
        }
    default:
        return "Unknown operation."
    }
    
    return fmt.Sprintf("The result is %.2f", result)
}

func main() {
    ctx := context.Background()
    fmt.Println("Agent started! Type 'exit' to quit.")
    
    scanner := bufio.NewScanner(os.Stdin)
    
    for {
        fmt.Print("You: ")
        if !scanner.Scan() {
            break
        }
        
        input := scanner.Text()
        if strings.ToLower(input) == "exit" {
            break
        }
        
        // Call the BAML function to select tool
        toolResponse, err := b.SelectTool(ctx, input)
        if err != nil {
            fmt.Printf("Error: %v\n", err)
            continue
        }
        
        // Handle the tool response using generated As methods
        if weatherAPI := toolResponse.AsWeatherAPI(); weatherAPI != nil {
            result := handleWeather(weatherAPI)
            fmt.Printf("Agent (Weather): %s\n", result)
        } else if calcAPI := toolResponse.AsCalculatorAPI(); calcAPI != nil {
            result := handleCalculator(calcAPI)
            fmt.Printf("Agent (Calculator): %s\n", result)
        } else {
            fmt.Println("Agent: Sorry, I couldn't handle that input.")
        }
    }
}
</CodeGroup>

We can test this by asking things like:

  1. What is the weather in Seattle?
  2. What's 5+2?

This is the output:

output.txt
Agent started! Type 'exit' to quit.
You: What's the weather in Seattle
Agent (Weather): The weather in Seattle at 2023-10-02T12:00:00Z is sunny.
You: What's 5+2
Agent (Calculator): The result is 7.0