Complete guide to using tools (function calling) with cascadeflow.
Essential tool calling patterns for cascadeflow.
Tools (also called "function calling") allow AI models to interact with external systems, APIs, and data sources. cascadeflow provides a complete tool system that works seamlessly with cascades, streaming, and quality validation.
- 🌐 Call External APIs: Weather, stocks, search, etc.
- 🧮 Perform Calculations: Math, data analysis, conversions
- 💾 Access Databases: Query, update, retrieve data
- 🔧 Execute Code: Run scripts, process files, transform data
- 🤖 Multi-Step Workflows: Chain multiple tools together
- ✅ Universal Format: Works with all providers (OpenAI, Anthropic, Groq)
- ✅ Automatic Conversion: Provider-specific format handling
- ✅ Quality Validation: Built-in tool call validation
- ✅ Cascade Compatible: Tools work with all cascade types
- ✅ Streaming Support: Watch tool calls form in real-time
- ✅ Parallel Execution: Run multiple tools concurrently
- ✅ Error Handling: Robust error recovery
from cascadeflow import CascadeAgent, ModelConfig
from cascadeflow.tools import ToolConfig, ToolExecutor
# Step 1: Define your function
def get_weather(location: str, unit: str = "celsius") -> dict:
"""Get weather for a location."""
# Your implementation here
return {"temp": 22, "condition": "sunny"}
# Step 2: Create ToolConfig
tool_config = ToolConfig(
name="get_weather",
description="Get current weather for a location",
parameters={
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
},
function=get_weather # Link to actual function
)
# Step 3: Create executor and use with agent
executor = ToolExecutor([tool_config])
agent = CascadeAgent(models=[...])
result = await agent.run(
"What's the weather in Paris?",
tools=[{
"name": "get_weather",
"description": "Get current weather for a location",
"parameters": {...}
}]
)
# Execute tool calls
for tool_call in result.tool_calls:
tool_result = await executor.execute(tool_call)
print(f"Result: {tool_result.result}")Important Distinction:
-
ToolConfig (Python object with function):
- Used by
ToolExecutorto run tools - Contains actual function reference
- Never sent to model
- Used by
-
Tool Schema (JSON dict):
- Sent to model to describe available tools
- Contains only name, description, parameters
- No function reference
# ToolConfig - for execution
tool_config = ToolConfig(
name="get_weather",
description="Get weather",
parameters={...},
function=get_weather # ← Actual function
)
# Tool Schema - for model
tool_schema = {
"name": "get_weather",
"description": "Get weather",
"parameters": {...}
# No function! Model doesn't see implementation
}1. User Query
↓
2. Model Generates Tool Calls
↓
3. Parse Tool Calls (ToolCall objects)
↓
4. Execute with ToolExecutor
↓
5. Format Results (ToolResult objects)
↓
6. Feed Back to Model
↓
7. Model Generates Final Answer
| Class | Purpose | Contains |
|---|---|---|
ToolConfig |
Tool definition | Schema + function |
ToolCall |
Model's request | Tool name + arguments |
ToolResult |
Execution result | Result or error |
ToolExecutor |
Execution engine | Runs tools |
from cascadeflow.tools import ToolConfig
def calculate(operation: str, x: float, y: float) -> dict:
"""Perform a calculation."""
ops = {
"add": x + y,
"subtract": x - y,
"multiply": x * y,
"divide": x / y if y != 0 else None
}
return {"result": ops[operation], "operation": operation}
tool = ToolConfig(
name="calculate",
description="Perform basic math operations",
parameters={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"]
},
"x": {"type": "number"},
"y": {"type": "number"}
},
"required": ["operation", "x", "y"]
},
function=calculate
)from cascadeflow.tools import ToolConfig
def get_stock_price(symbol: str, currency: str = "USD") -> dict:
"""Get current stock price for a symbol."""
# Implementation...
return {"symbol": symbol, "price": 150.25, "currency": currency}
# Automatically extracts: name, description, parameters, required fields
tool = ToolConfig.from_function(get_stock_price)from cascadeflow.tools import tool
@tool
def search_documents(query: str, limit: int = 10) -> list:
"""Search documents by keyword."""
# Implementation...
return [{"title": "Doc 1", "score": 0.95}]
# search_documents is now a ToolConfig object!cascadeflow supports all JSON Schema types:
parameters = {
"type": "object",
"properties": {
# String
"name": {"type": "string"},
# Number (int or float)
"age": {"type": "integer"},
"price": {"type": "number"},
# Boolean
"active": {"type": "boolean"},
# Enum (constrained values)
"status": {
"type": "string",
"enum": ["pending", "approved", "rejected"]
},
# Array
"tags": {
"type": "array",
"items": {"type": "string"}
},
# Nested object
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
}
}
},
"required": ["name", "age"] # Required fields
}from cascadeflow.tools import ToolExecutor, ToolCall, ToolCallFormat
# Create executor with tool configs
executor = ToolExecutor([tool1, tool2, tool3])
# Execute a tool call
tool_call = ToolCall(
id="call_123",
name="get_weather",
arguments={"location": "Paris", "unit": "celsius"},
provider_format=ToolCallFormat.OPENAI
)
result = await executor.execute(tool_call)
if result.success:
print(f"Result: {result.result}")
else:
print(f"Error: {result.error}")Execute multiple tools concurrently for better performance:
tool_calls = [call1, call2, call3, call4]
# Execute up to 5 tools in parallel
results = await executor.execute_parallel(
tool_calls,
max_parallel=5
)
for result in results:
if result.success:
print(f"{result.name}: {result.result}")result = await executor.execute(tool_call)
if not result.success:
# Tool execution failed
error_type = type(result.error).__name__
error_msg = result.error
if "not found" in error_msg:
# Tool doesn't exist
print(f"Unknown tool: {tool_call.name}")
elif "arguments" in error_msg:
# Invalid arguments
print(f"Bad arguments: {tool_call.arguments}")
else:
# Other error
print(f"Tool failed: {error_msg}")ToolExecutor handles both automatically:
# Synchronous function
def sync_tool(x: int) -> int:
return x * 2
# Asynchronous function
async def async_tool(url: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()
# Both work with ToolExecutor!
executor = ToolExecutor([
ToolConfig.from_function(sync_tool),
ToolConfig.from_function(async_tool)
])Tools typically require multiple turns: request → execute → respond.
async def tool_conversation(agent, executor, query, tools):
"""Run a complete tool conversation."""
messages = [{"role": "user", "content": query}]
max_turns = 3
turn = 0
while turn < max_turns:
turn += 1
# Get model response
result = await agent.run(
query=" ".join([m["content"] for m in messages if m["role"] == "user"]),
tools=tools
)
# Check if model wants to use tools
if result.tool_calls:
# Execute tools
tool_results = []
for tool_call in result.tool_calls:
tool_result = await executor.execute(tool_call)
tool_results.append(tool_result)
# Add assistant message with tool calls
messages.append({
"role": "assistant",
"content": result.content or "",
"tool_calls": result.tool_calls
})
# Add tool results
for tool_result in tool_results:
messages.append({
"role": "tool",
"tool_call_id": tool_result.call_id,
"name": tool_result.name,
"content": str(tool_result.result)
})
# Continue to next turn
continue
else:
# Model generated final answer
return result.content
return "Max turns reached"Different providers use different formats for tool results:
OpenAI Format:
{
"role": "tool",
"tool_call_id": "call_123",
"name": "get_weather",
"content": "{'temp': 22, 'condition': 'sunny'}"
}Anthropic Format:
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": "toolu_123",
"content": "{'temp': 22, 'condition': 'sunny'}"
}]
}cascadeflow handles this automatically:
# Automatically converts to correct format
tool_result.to_provider_message("openai") # OpenAI format
tool_result.to_provider_message("anthropic") # Anthropic formatPower features for tool calling in production.
Watch tool calls form in real-time as the model generates them.
from cascadeflow.streaming import ToolStreamEventType
async for event in agent.stream_events(query, tools=tools):
match event.type:
case ToolStreamEventType.TOOL_CALL_START:
print(f"\n🔧 Calling: {event.tool_call['name']}")
case ToolStreamEventType.TOOL_CALL_DELTA:
# Arguments streaming in progressively
print(event.delta, end='')
case ToolStreamEventType.TOOL_CALL_COMPLETE:
# Full tool call parsed
tool = event.tool_call
print(f"\n✓ Complete: {tool['name']}({tool['arguments']})")
case ToolStreamEventType.TEXT_CHUNK:
# Regular text response
print(event.content, end='')async for event in agent.stream_events(query, tools=tools):
if event.type == ToolStreamEventType.TOOL_CALL_DELTA:
# Show arguments as they arrive
print(f"\rArguments: {event.partial_arguments}", end='')See examples/streaming_tools.py for complete streaming example.
Select tools based on query or context:
def get_tools_for_query(query: str) -> list:
"""Select relevant tools for query."""
all_tools = {
"weather": weather_tools,
"calculation": math_tools,
"search": search_tools
}
# Simple keyword matching (use embedding similarity in production)
if "weather" in query.lower():
return all_tools["weather"]
elif any(word in query.lower() for word in ["calculate", "multiply", "add"]):
return all_tools["calculation"]
else:
return all_tools["search"]
# Use with agent
tools = get_tools_for_query(user_query)
result = await agent.run(user_query, tools=tools)Chain multiple tools together:
async def chain_tools(agent, executor, query):
"""Execute tools in sequence."""
# Step 1: Get weather
result1 = await agent.run(
"Get weather for Paris",
tools=[weather_tool]
)
weather_data = await executor.execute(result1.tool_calls[0])
# Step 2: Use weather in next query
result2 = await agent.run(
f"Given weather is {weather_data.result}, should I bring an umbrella?",
tools=[]
)
return result2.contentExecute tools only if certain conditions are met:
async def conditional_execution(agent, executor, query, tools):
"""Execute tools with validation."""
result = await agent.run(query, tools=tools)
for tool_call in result.tool_calls:
# Validate before execution
if not validate_tool_call(tool_call):
print(f"Skipping unsafe tool: {tool_call.name}")
continue
# Check budget
if estimate_cost(tool_call) > budget:
print(f"Tool too expensive: {tool_call.name}")
continue
# Execute
tool_result = await executor.execute(tool_call)Add custom validation logic:
def validate_tool_arguments(tool_call: ToolCall, tool_config: ToolConfig) -> bool:
"""Custom validation logic."""
# Check required fields
for required in tool_config.parameters.get("required", []):
if required not in tool_call.arguments:
return False
# Custom business logic
if tool_call.name == "transfer_money":
amount = tool_call.arguments.get("amount", 0)
if amount > 10000:
return False # Block large transfers
return True# ❌ Bad
description = "Gets weather"
# ✅ Good
description = "Get current weather information for a specific location, including temperature, conditions, and humidity"def get_weather(location: str, unit: str = "celsius") -> dict:
# Validate unit
if unit not in ["celsius", "fahrenheit"]:
raise ValueError(f"Invalid unit: {unit}. Must be 'celsius' or 'fahrenheit'.")
# Validate location
if not location or len(location) < 2:
raise ValueError("Location must be at least 2 characters")
# ... rest of implementation# ❌ Bad - string response
def get_weather(location: str) -> str:
return "It's 22°C and sunny"
# ✅ Good - structured dict
def get_weather(location: str) -> dict:
return {
"location": location,
"temperature": 22,
"unit": "celsius",
"condition": "sunny",
"humidity": 65,
"timestamp": "2025-10-21T14:30:00Z"
}def call_external_api(url: str) -> dict:
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
except requests.Timeout:
return {"error": "API timeout", "code": "TIMEOUT"}
except requests.HTTPError as e:
return {"error": f"HTTP {e.response.status_code}", "code": "HTTP_ERROR"}
except Exception as e:
return {"error": str(e), "code": "UNKNOWN"}async def slow_tool(query: str) -> dict:
"""Tool with timeout protection."""
try:
async with asyncio.timeout(10): # 10 second timeout
result = await expensive_operation(query)
return result
except asyncio.TimeoutError:
return {"error": "Operation timed out", "code": "TIMEOUT"}import logging
logger = logging.getLogger(__name__)
def get_weather(location: str) -> dict:
logger.info(f"get_weather called with location={location}")
try:
result = fetch_weather_data(location)
logger.info(f"get_weather succeeded: {result['condition']}")
return result
except Exception as e:
logger.error(f"get_weather failed: {e}")
raiseError: Tool 'xyz' not found
Cause: Tool name in ToolCall doesn't match any ToolConfig
Solution:
# Check available tools
print(f"Available tools: {list(executor.tools.keys())}")
# Verify tool name matches exactly
tool_config = ToolConfig(name="get_weather", ...) # Must match exactlyError: TypeError: missing required argument
Cause: Model didn't provide required parameters
Solution:
# Add validation
def get_weather(location: str, unit: str = "celsius") -> dict:
if not location:
raise ValueError("location is required")
# ... restError: Tool hangs indefinitely
Solution:
async def safe_tool(param: str) -> dict:
try:
async with asyncio.timeout(30):
return await slow_operation(param)
except asyncio.TimeoutError:
return {"error": "Timeout", "code": "TIMEOUT"}Error: KeyError: 'name' or tools not working
Cause: Using OpenAI format instead of universal format
Solution:
# ❌ Wrong - OpenAI format
tools = [{
"type": "function",
"function": {"name": "...", ...}
}]
# ✅ Correct - Universal format
tools = [{
"name": "get_weather",
"description": "...",
"parameters": {...}
}]Issue: Tool queries cost more than expected
Cause: Token-based pricing + multiple turns
Solution:
# Track per-turn costs
total_cost = 0
for turn in range(max_turns):
result = await agent.run(query, tools=tools)
total_cost += result.total_cost
print(f"Turn {turn}: ${result.total_cost:.6f}")
print(f"Total: ${total_cost:.6f}")- Examples: See
examples/tool_execution.pyfor complete working code - Streaming: Read Streaming Guide for real-time tool streaming
- API Reference: Check API docs for detailed class documentation
Questions? Open an issue on GitHub.