Understanding Tool Calling
In BAML, tools are represented as classes. The LLM chooses which tool to call and provides the parameters:- Define tools as BAML classes
- LLM selects which tool to call
- LLM extracts parameters for the tool
- Your code executes the function with those parameters
Single Tool Selection
Weather API Example
Let’s start with a simple weather API tool:weather_tool.baml
class WeatherAPI {
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-4o-mini"
prompt #"
Given a message, extract info.
{{ ctx.output_format }}
{{ _.role('user') }}
{{ user_message }}
"#
}
api_name field uses a string literal to identify the tool.
Calling the Tool
- Python
- TypeScript
from baml_client import b
from baml_client.types import WeatherAPI
import datetime
def get_weather(city: str, time_of_day: str):
# Your actual weather API implementation
return f"Weather in {city} at {time_of_day}: Sunny, 72°F"
def main():
# Extract tool parameters
weather_info = b.UseTool("What's the weather like in San Francisco?")
print(f"Tool: {weather_info.api_name}")
print(f"City: {weather_info.city}")
print(f"Time: {weather_info.timeOfDay}")
# Call your actual function
result = get_weather(
city=weather_info.city,
time_of_day=weather_info.timeOfDay
)
print(f"Result: {result}")
if __name__ == '__main__':
main()
import { b } from './baml_client'
import { WeatherAPI } from './baml_client/types'
function getWeather(city: string, timeOfDay: string): string {
// Your actual weather API implementation
return `Weather in ${city} at ${timeOfDay}: Sunny, 72°F`
}
async function main() {
// Extract tool parameters
const weatherInfo = await b.UseTool("What's the weather like in San Francisco?")
console.log(`Tool: ${weatherInfo.api_name}`)
console.log(`City: ${weatherInfo.city}`)
console.log(`Time: ${weatherInfo.timeOfDay}`)
// Call your actual function
const result = getWeather(weatherInfo.city, weatherInfo.timeOfDay)
console.log(`Result: ${result}`)
}
main()
Multiple Tool Selection
Use unions to let the LLM choose from multiple tools:multi_tool.baml
class WeatherAPI {
tool_name "get_weather" @description("Get current weather forecast")
city string @description("The city for which to get weather")
}
class CalculatorAPI {
tool_name "basic_calculator" @description("Perform basic calculations")
operation "add" | "subtract" | "multiply" | "divide"
numbers float[]
}
function SelectTool(message: string) -> WeatherAPI | CalculatorAPI {
client "openai/gpt-4o"
prompt #"
Given a message, select the appropriate tool and extract parameters.
{{ ctx.output_format }}
{{ _.role("user") }}
{{ message }}
"#
}
Handling Multiple Tools
- Python
- TypeScript
from baml_client import b
from baml_client.types import WeatherAPI, CalculatorAPI
def handle_weather(weather: WeatherAPI) -> str:
return f"The weather in {weather.city} is sunny."
def handle_calculator(calc: CalculatorAPI) -> str:
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():
user_input = input("What would you like to do? ")
# Get tool selection from LLM
tool_response = b.SelectTool(user_input)
# Handle based on tool type
if isinstance(tool_response, WeatherAPI):
result = handle_weather(tool_response)
print(f"Weather: {result}")
elif isinstance(tool_response, CalculatorAPI):
result = handle_calculator(tool_response)
print(f"Calculator: {result}")
if __name__ == "__main__":
main()
import { b } from './baml_client'
import { WeatherAPI, CalculatorAPI } from './baml_client/types'
function handleWeather(weather: WeatherAPI): string {
return `The weather in ${weather.city} 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() {
const readline = await import('readline')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question("What would you like to do? ", async (userInput) => {
// Get tool selection from LLM
const toolResponse = await b.SelectTool(userInput)
// Handle based on tool type (check for discriminator field)
if ('city' in toolResponse) {
const result = handleWeather(toolResponse as WeatherAPI)
console.log(`Weather: ${result}`)
} else if ('operation' in toolResponse) {
const result = handleCalculator(toolResponse as CalculatorAPI)
console.log(`Calculator: ${result}`)
}
rl.close()
})
}
main()
Multiple Tool Calls
To allow the LLM to call multiple tools in a single response:multi_call.baml
function UseMultipleTools(message: string) -> (WeatherAPI | CalculatorAPI)[] {
client "openai/gpt-4o-mini"
prompt #"
Given a message, extract all tool calls needed.
{{ ctx.output_format }}
{{ _.role('user') }}
{{ message }}
"#
}
- Python
- TypeScript
from baml_client import b
from baml_client.types import WeatherAPI, CalculatorAPI
def main():
message = "What's the weather in SF and NY? Also calculate 5 + 3."
tools = b.UseMultipleTools(message)
for tool in tools:
if isinstance(tool, WeatherAPI):
result = handle_weather(tool)
print(f"Weather: {result}")
elif isinstance(tool, CalculatorAPI):
result = handle_calculator(tool)
print(f"Calculator: {result}")
if __name__ == "__main__":
main()
import { b } from './baml_client'
import { WeatherAPI, CalculatorAPI } from './baml_client/types'
async function main() {
const message = "What's the weather in SF and NY? Also calculate 5 + 3."
const tools = await b.UseMultipleTools(message)
tools.forEach(tool => {
if ('city' in tool) {
const result = handleWeather(tool as WeatherAPI)
console.log(`Weather: ${result}`)
} else if ('operation' in tool) {
const result = handleCalculator(tool as CalculatorAPI)
console.log(`Calculator: ${result}`)
}
})
}
main()
Disambiguating Similar Tools
When tools have overlapping parameters, use descriptive fields:disambiguate.baml
class GetWeather {
tool_name "get_weather" @description("Get current weather forecast for a city")
city string @description("The city for which to get weather")
}
class GetTimezone {
tool_name "get_timezone" @description("Find the current timezone of a city")
city string @description("The city for which to find timezone")
}
function ChooseTool(query: string) -> GetWeather | GetTimezone {
client "openai/gpt-4o"
prompt #"
Determine the primary intent and select the appropriate tool.
{{ ctx.output_format }}
{{ _.role('user') }}
{{ query }}
"#
}
Building an Agent
Create an agentic loop that continuously uses tools:agent.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-4o"
prompt #"
Given a message, extract the appropriate tool info.
{{ ctx.output_format }}
{{ _.role("user") }}
{{ message }}
"#
}
- Python
- TypeScript
from baml_client import b
from baml_client.types import WeatherAPI, CalculatorAPI
def handle_weather(weather: WeatherAPI):
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.\n")
while True:
user_input = input("You: ")
if user_input.lower() == 'exit':
break
# Call BAML 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}\n")
elif isinstance(tool_response, CalculatorAPI):
result = handle_calculator(tool_response)
print(f"Agent (Calculator): {result}\n")
if __name__ == "__main__":
main()
import { b } from './baml_client'
import { WeatherAPI, CalculatorAPI } from './baml_client/types'
import * as readline from 'readline'
function handleWeather(weather: WeatherAPI): string {
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.\n")
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)
if (toolResponse.intent === "weather_request") {
const result = handleWeather(toolResponse as WeatherAPI)
console.log(`Agent (Weather): ${result}\n`)
} else if (toolResponse.intent === "basic_calculator") {
const result = handleCalculator(toolResponse as CalculatorAPI)
console.log(`Agent (Calculator): ${result}\n`)
}
})
}
main()
Example Output
Agent started! Type 'exit' to quit.
You: What's the weather in Seattle?
Agent (Weather): The weather in Seattle at 2024-03-15T12:00:00Z is sunny.
You: What's 5+2?
Agent (Calculator): The result is 7.0
You: exit
Dynamic Tool Schemas
You can define tool schemas dynamically from your Python/TypeScript code:dynamic.baml
class WeatherAPI {
@@dynamic // params defined from code
}
function UseTool(user_message: string) -> WeatherAPI {
client "openai/gpt-4o-mini"
prompt #"
Given a message, extract info.
{{ ctx.output_format }}
{{ _.role('user') }}
{{ user_message }}
"#
}
- Python
import inspect
from baml_client import b
from baml_client.type_builder import TypeBuilder
async def get_weather(city: str, time_of_day: str):
print(f"Getting weather for {city} at {time_of_day}")
return {"temp": 72, "condition": "sunny"}
def main():
# Build schema from function signature
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])
# Use the dynamic schema
tool = b.UseTool(
"What's the weather in San Francisco this afternoon?",
{"tb": tb}
)
print(tool)
# Call the actual function
weather = get_weather(**tool.model_dump())
print(weather)
if __name__ == '__main__':
main()
Advanced: Todo List Agent
A more complex example with multiple tool types:todo.baml
class AddTodoItem {
type "add_todo_item"
item string
time string
description string @description("20 word description")
}
class TodoMessageToUser {
type "todo_message_to_user"
message string @description("A message to the user, about 50 words")
}
type TodoTool = AddTodoItem | TodoMessageToUser
function ChooseTodoTools(query: string) -> TodoTool[] {
client "openai/gpt-4o"
prompt #"
Choose tools to satisfy the user query.
For example, if they ask for "5 todo items for learning chess",
return a list of 5 "add_todo_item" objects and a single
"todo_message_to_user" object.
All requests should end with a "todo_message_to_user" object.
{{ ctx.output_format }}
{{ _.role('user') }}
{{ query }}
"#
}
- Python
- TypeScript
from baml_client import b
from baml_client.types import AddTodoItem, TodoMessageToUser
def main():
query = "Give me 5 todo items for learning chess"
tools = b.ChooseTodoTools(query)
for tool in tools:
if isinstance(tool, AddTodoItem):
print(f"📝 Add: {tool.item}")
print(f" Time: {tool.time}")
print(f" Description: {tool.description}")
# Add to your todo database
elif isinstance(tool, TodoMessageToUser):
print(f"\n💬 {tool.message}")
if __name__ == "__main__":
main()
import { b } from './baml_client'
import { AddTodoItem, TodoMessageToUser } from './baml_client/types'
async function main() {
const query = "Give me 5 todo items for learning chess"
const tools = await b.ChooseTodoTools(query)
tools.forEach(tool => {
if (tool.type === "add_todo_item") {
const item = tool as AddTodoItem
console.log(`📝 Add: ${item.item}`)
console.log(` Time: ${item.time}`)
console.log(` Description: ${item.description}`)
// Add to your todo database
} else if (tool.type === "todo_message_to_user") {
const msg = tool as TodoMessageToUser
console.log(`\n💬 ${msg.message}`)
}
})
}
main()
Best Practices
1. Use Clear Tool Names
class GetWeather {
tool_name "get_weather" // Clear and descriptive
// ...
}
2. Add Descriptions
class SearchDatabase {
tool_name "search_database" @description("Search the product database")
query string @description("The search query")
limit int @description("Maximum number of results (1-100)")
}
3. Validate Tool Parameters
def handle_search(search: SearchDatabase):
# Validate limit
if not 1 <= search.limit <= 100:
raise ValueError("Limit must be between 1 and 100")
# Validate query
if len(search.query.strip()) == 0:
raise ValueError("Query cannot be empty")
# Proceed with search
return perform_search(search.query, search.limit)
4. Handle Tool Errors Gracefully
try:
tool = b.SelectTool(user_input)
result = execute_tool(tool)
except ValueError as e:
print(f"Invalid parameters: {e}")
except Exception as e:
print(f"Tool execution failed: {e}")
Why BAML for Tool Calling?
- Type Safety: Full type checking for tool parameters
- Transparency: See exactly what gets sent to the LLM (use VS Code Playground)
- Better Performance: Prompting outperforms native function-calling APIs
- No Token Waste: More efficient than OpenAI’s function calling format
- Flexibility: Works with any LLM, not just those with function-calling APIs
Next Steps
- Learn about Dynamic Types for runtime schema generation
- Explore RAG to combine tool calling with knowledge retrieval
- Check out Streaming for real-time tool selection