Tools API Reference¶
The Tools module provides a powerful system for creating callable functions that LLM agents can use. It supports JSON Schema validation, approval workflows, and platform context injection.
Table of Contents¶
- Overview
- Class: Tool
- Decorator: @tool
- Function: create_tool
- Schema Definition Options
- Platform Context
- Approval Workflows
- Examples
- Best Practices
Overview¶
The Tools module provides two ways to create tools:
@tooldecorator - Transform a function into a Tool objectcreate_tool()function - Programmatically create tools
And three ways to define tool schemas:
- Auto-generate - Schema is inferred from function signature (simplest)
- Dict schema - Pass a JSON Schema dict (full control)
- Pydantic model - Pass a Pydantic model class (type-safe with IDE support)
Import¶
Key Features¶
- Flexible schema definition - Auto-generate, dict, or Pydantic model
- JSON Schema validation for tool inputs
- Automatic platform context detection
- Approval workflow support
- LLM-ready schema generation
- Tool inspection and debugging
Class: Tool¶
The Tool class is a Pydantic model that represents a callable tool.
class Tool(BaseModel):
"""Container for tool metadata and configuration."""
func: Callable # The wrapped function
name: str # Tool name for LLM
description: str # Tool description
schema: Dict[str, Any] # JSON schema for inputs
requires_approval: bool # Whether approval is needed
requires_platform_context: bool # Whether context is needed
Attributes¶
| Attribute | Type | Description |
|---|---|---|
func |
Callable |
The underlying Python function |
name |
str |
Tool name (used by LLM) |
description |
str |
Human-readable description |
schema |
Dict |
Full JSON schema specification |
requires_approval |
bool |
If True, requires user approval |
requires_platform_context |
bool |
If True, expects platform context |
Methods¶
get_schema() -> Dict[str, Any]¶
Returns the tool's JSON schema in LLM-ready format.
schema = my_tool.get_schema()
# {
# "name": "my_tool",
# "description": "Tool description",
# "input_schema": {...}
# }
execute(input_args: Dict, platform_context: Dict = None) -> str¶
Execute the tool with given inputs.
# Without platform context
result = my_tool.execute({"param": "value"})
# With platform context
result = my_tool.execute(
{"param": "value"},
{"user_id": "alice", "tenant": "prod"}
)
describe() -> None¶
Print detailed information about the tool.
my_tool.describe()
# Tool: my_tool
# Description: Tool description
# Requires Approval: False
# Has Platform Context: True
# Schema: {...}
__repr__() -> str¶
Pretty representation for debugging.
Decorator: @tool¶
Transform a function into a Tool object.
def tool(
func: Optional[Callable] = None,
*,
description: Optional[str] = None,
name: Optional[str] = None,
requires_approval: bool = False,
schema: Optional[Union[Dict[str, Any], Type[BaseModel]]] = None,
) -> Tool
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
description |
str |
Docstring | Tool description shown to LLM |
name |
str |
Function name | Override the tool name |
requires_approval |
bool |
False |
Require user approval before execution |
schema |
Dict or BaseModel |
Auto-generated | JSON schema dict OR Pydantic model class |
Schema Options¶
The @tool decorator supports three ways to define the input schema:
Option 1: Auto-Generate (Simplest)¶
Let DCAF generate the schema from your function signature:
from dcaf.tools import tool
@tool(description="Generate a personalized greeting")
def greet(name: str, language: str = "english") -> str:
"""Generate a greeting."""
greetings = {"english": "Hello", "spanish": "Hola", "french": "Bonjour"}
return f"{greetings.get(language, 'Hello')}, {name}!"
# Schema is auto-generated from function signature
print(greet.input_schema)
# {
# "type": "object",
# "properties": {
# "name": {"type": "string"},
# "language": {"type": "string", "default": "english"}
# },
# "required": ["name"]
# }
Option 2: Dict Schema (Full Control)¶
Pass an explicit JSON Schema dict:
@tool(
description="Generate a greeting message",
schema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the person to greet",
"minLength": 1,
"maxLength": 100
},
"language": {
"type": "string",
"enum": ["english", "spanish", "french"],
"description": "Language for the greeting",
"default": "english"
}
},
"required": ["name"]
},
requires_approval=False
)
def greet(name: str, language: str = "english") -> str:
"""Generate a greeting."""
greetings = {"english": "Hello", "spanish": "Hola", "french": "Bonjour"}
return f"{greetings.get(language, 'Hello')}, {name}!"
Option 3: Pydantic Model (Type-Safe)¶
Pass a Pydantic model class for type-safe schemas with IDE support:
from pydantic import BaseModel, Field
from typing import Literal
class GreetInput(BaseModel):
"""Input schema for the greet tool."""
name: str = Field(..., description="Name of the person to greet", min_length=1, max_length=100)
language: Literal["english", "spanish", "french"] = Field(
default="english",
description="Language for the greeting"
)
@tool(description="Generate a greeting message", schema=GreetInput)
def greet(name: str, language: str = "english") -> str:
"""Generate a greeting."""
greetings = {"english": "Hello", "spanish": "Hola", "french": "Bonjour"}
return f"{greetings.get(language, 'Hello')}, {name}!"
Pydantic Benefits
Using Pydantic models gives you:
- IDE autocomplete when defining the schema
- Type checking at development time
- Reusable schemas across multiple tools
- Built-in validation if you want to validate inputs in your tool
Basic Usage¶
from dcaf.tools import tool
# Simplest - auto-generate schema
@tool(description="Generate a greeting")
def greet(name: str) -> str:
"""Generate a greeting."""
return f"Hello, {name}!"
# Use the tool
result = greet.execute({"name": "Alice"})
print(result) # "Hello, Alice!"
With Platform Context¶
If your function has a platform_context parameter, DCAF automatically detects it:
@tool(
schema={
"name": "log_action",
"description": "Log an action with user context",
"input_schema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "Action to log"
}
},
"required": ["action"]
}
}
)
def log_action(action: str, platform_context: dict) -> str:
"""Log an action with the current user."""
user = platform_context.get("user_id", "unknown")
tenant = platform_context.get("tenant_name", "default")
return f"[{tenant}/{user}] Action: {action}"
# Verify detection
print(log_action.requires_platform_context) # True
# Execute with context
result = log_action.execute(
{"action": "login"},
{"user_id": "alice", "tenant_name": "prod"}
)
print(result) # "[prod/alice] Action: login"
With Approval Required¶
@tool(
schema={
"name": "delete_resource",
"description": "Delete a resource (requires approval)",
"input_schema": {
"type": "object",
"properties": {
"resource_id": {
"type": "string",
"description": "ID of resource to delete"
},
"force": {
"type": "boolean",
"description": "Force deletion",
"default": False
}
},
"required": ["resource_id"]
}
},
requires_approval=True # User must approve
)
def delete_resource(resource_id: str, force: bool = False) -> str:
"""Delete a resource from the system."""
mode = "force-deleted" if force else "deleted"
return f"Resource {resource_id} has been {mode}"
# This tool will require approval before execution
print(delete_resource.requires_approval) # True
Custom Name and Description¶
@tool(
schema={
"name": "search_db",
"description": "Search the database",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": ["query"]
}
},
name="database_search", # Override name
description="Search for records" # Override docstring
)
def internal_search(query: str) -> str:
"""This docstring is overridden."""
return f"Found 5 results for: {query}"
print(internal_search.name) # "database_search"
print(internal_search.description) # "Search for records"
Function: create_tool¶
Create a tool programmatically without using a decorator.
def create_tool(
func: Callable,
description: Optional[str] = None,
name: Optional[str] = None,
requires_approval: bool = False,
schema: Optional[Union[Dict[str, Any], Type[BaseModel]]] = None,
) -> Tool
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
func |
Callable |
Required | Function to wrap |
description |
str |
Docstring | Tool description |
name |
str |
Function name | Tool name |
requires_approval |
bool |
False |
Require approval |
schema |
Dict or BaseModel |
Auto-generated | JSON schema dict OR Pydantic model class |
Usage¶
from dcaf.tools import create_tool
# Define a function
def multiply(a: int, b: int) -> str:
"""Multiply two numbers."""
return f"{a} × {b} = {a * b}"
# Option 1: Auto-generate schema
multiply_tool = create_tool(multiply, description="Multiply two integers")
# Option 2: With explicit dict schema
multiply_tool = create_tool(
func=multiply,
description="Multiply two integers",
schema={
"type": "object",
"properties": {
"a": {"type": "integer", "description": "First number", "minimum": 0},
"b": {"type": "integer", "description": "Second number", "minimum": 0}
},
"required": ["a", "b"]
}
)
# Option 3: With Pydantic model
from pydantic import BaseModel, Field
class MultiplyInput(BaseModel):
a: int = Field(..., ge=0, description="First number")
b: int = Field(..., ge=0, description="Second number")
multiply_tool = create_tool(
func=multiply,
description="Multiply two integers",
schema=MultiplyInput
)
# Use the tool
result = multiply_tool.execute({"a": 6, "b": 7})
print(result) # "6 × 7 = 42"
When to Use create_tool¶
- Creating tools from existing functions you can't modify
- Dynamic tool generation at runtime
- Building tools from configuration files
- Testing and mocking
Schema Definition Options¶
DCAF supports three ways to define tool input schemas, giving you flexibility based on your needs.
Comparison¶
| Approach | Best For | Pros | Cons |
|---|---|---|---|
| Auto-generate | Simple tools | Zero config, fast | Limited validation |
| Dict schema | Full control | Any JSON Schema feature | Verbose, no IDE help |
| Pydantic model | Production tools | Type-safe, IDE support, reusable | Extra class definition |
Option 1: Auto-Generated Schema¶
When you don't provide a schema, DCAF generates one from your function signature:
@tool(description="Search for users")
def search_users(query: str, limit: int = 10, active_only: bool = True) -> str:
...
# Auto-generates:
# {
# "type": "object",
# "properties": {
# "query": {"type": "string"},
# "limit": {"type": "integer", "default": 10},
# "active_only": {"type": "boolean", "default": true}
# },
# "required": ["query"]
# }
Option 2: Dict Schema¶
For full control over the JSON Schema:
schema = {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query",
"minLength": 1,
"maxLength": 100
},
"limit": {
"type": "integer",
"description": "Max results",
"minimum": 1,
"maximum": 100,
"default": 10
},
"active_only": {
"type": "boolean",
"description": "Only return active users",
"default": True
}
},
"required": ["query"]
}
@tool(description="Search for users", schema=schema)
def search_users(query: str, limit: int = 10, active_only: bool = True) -> str:
...
Option 3: Pydantic Model¶
For type-safe schemas with IDE support and validation:
from pydantic import BaseModel, Field
class SearchUsersInput(BaseModel):
"""Input schema for user search."""
query: str = Field(..., description="Search query", min_length=1, max_length=100)
limit: int = Field(default=10, ge=1, le=100, description="Max results")
active_only: bool = Field(default=True, description="Only return active users")
@tool(description="Search for users", schema=SearchUsersInput)
def search_users(query: str, limit: int = 10, active_only: bool = True) -> str:
...
How It Works
When you pass a Pydantic model, DCAF automatically calls model.model_json_schema()
to convert it to a JSON Schema dict. You don't need to do anything special.
JSON Schema Structure¶
Regardless of how you define it, the final schema follows JSON Schema format:
Parameter Types¶
String¶
Integer¶
Number (Float)¶
Boolean¶
Enum¶
"status": {
"type": "string",
"enum": ["pending", "active", "completed"],
"description": "Current status"
}
Array¶
Nested Object¶
"config": {
"type": "object",
"description": "Configuration options",
"properties": {
"timeout": {"type": "integer"},
"retries": {"type": "integer"}
}
}
Complete Example¶
@tool(
schema={
"name": "create_user",
"description": "Create a new user account",
"input_schema": {
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "Unique username",
"minLength": 3,
"maxLength": 30
},
"email": {
"type": "string",
"description": "Email address",
"format": "email"
},
"role": {
"type": "string",
"enum": ["user", "admin", "moderator"],
"description": "User role",
"default": "user"
},
"permissions": {
"type": "array",
"items": {"type": "string"},
"description": "List of permissions"
},
"profile": {
"type": "object",
"description": "User profile data",
"properties": {
"display_name": {"type": "string"},
"bio": {"type": "string"}
}
}
},
"required": ["username", "email"]
}
},
requires_approval=True
)
def create_user(
username: str,
email: str,
role: str = "user",
permissions: list = None,
profile: dict = None
) -> str:
"""Create a new user account."""
return f"Created user {username} with role {role}"
Platform Context¶
Platform context allows tools to access runtime information like user identity, tenant, and credentials.
How It Works¶
- If your function has a
platform_contextparameter, DCAF setsrequires_platform_context=True - When the tool is executed, the agent passes context from the request
- Your function receives the context and can use it
Available Context Fields¶
| Field | Type | Description |
|---|---|---|
user_id |
str |
Current user identifier |
tenant_name |
str |
DuploCloud tenant name |
k8s_namespace |
str |
Kubernetes namespace |
duplo_base_url |
str |
DuploCloud API URL |
duplo_token |
str |
DuploCloud API token |
kubeconfig |
str |
Base64-encoded kubeconfig |
aws_credentials |
Dict |
AWS credential information |
Example with Context¶
@tool(
schema={
"name": "deploy_service",
"description": "Deploy a service to the current tenant",
"input_schema": {
"type": "object",
"properties": {
"service_name": {
"type": "string",
"description": "Name of the service to deploy"
},
"image": {
"type": "string",
"description": "Docker image to deploy"
}
},
"required": ["service_name", "image"]
}
},
requires_approval=True
)
def deploy_service(
service_name: str,
image: str,
platform_context: dict
) -> str:
"""Deploy a service using platform context."""
tenant = platform_context.get("tenant_name", "unknown")
namespace = platform_context.get("k8s_namespace", "default")
user = platform_context.get("user_id", "system")
# Use context for deployment
return f"Deployed {service_name} ({image}) to {tenant}/{namespace} by {user}"
Optional Platform Context¶
You can make platform context optional with a default value:
@tool(
schema={...}
)
def flexible_tool(data: str, platform_context: dict = None) -> str:
"""Tool that works with or without context."""
if platform_context:
user = platform_context.get("user_id", "unknown")
return f"User {user} processed: {data}"
return f"Processed: {data}"
# Works both ways
result1 = flexible_tool.execute({"data": "test"})
result2 = flexible_tool.execute({"data": "test"}, {"user_id": "alice"})
Approval Workflows¶
Tools that modify state or perform sensitive operations should require approval.
How Approval Works¶
- Agent calls tool with
requires_approval=True - Instead of executing, the agent returns a
ToolCallobject - Client presents the tool call to the user for approval
- User approves or rejects (with optional reason)
- Client sends the decision back to the agent
- If approved, agent executes the tool
Marking Tools for Approval¶
@tool(
schema={
"name": "terminate_instance",
"description": "Terminate an EC2 instance",
"input_schema": {
"type": "object",
"properties": {
"instance_id": {
"type": "string",
"description": "EC2 instance ID"
}
},
"required": ["instance_id"]
}
},
requires_approval=True # This is the key!
)
def terminate_instance(instance_id: str) -> str:
"""Terminate an EC2 instance."""
# This only runs after user approval
return f"Instance {instance_id} terminated"
Approval Response Format¶
When a tool requires approval, the agent returns:
AgentMessage(
content="I need your approval to execute the following tools:",
data=Data(
tool_calls=[
ToolCall(
id="unique-tool-use-id",
name="terminate_instance",
input={"instance_id": "i-1234567890abcdef0"},
tool_description="Terminate an EC2 instance",
input_description={
"instance_id": {
"type": "string",
"description": "EC2 instance ID"
}
},
execute=False # Not yet approved
)
]
)
)
Sending Approval¶
# Client sends back with execute=True or rejection_reason
messages = {
"messages": [
{
"role": "user",
"content": "Terminate the instance",
"data": {
"tool_calls": [
{
"id": "unique-tool-use-id",
"name": "terminate_instance",
"input": {"instance_id": "i-1234567890abcdef0"},
"execute": True # Approved!
# OR
# "rejection_reason": "Wrong instance"
}
]
}
}
]
}
Examples¶
Example 1: Simple Calculator¶
from dcaf.tools import tool
@tool(
schema={
"name": "calculate",
"description": "Perform arithmetic operations",
"input_schema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {"type": "number", "description": "First operand"},
"b": {"type": "number", "description": "Second operand"}
},
"required": ["operation", "a", "b"]
}
},
requires_approval=False
)
def calculate(operation: str, a: float, b: float) -> str:
ops = {
"add": lambda x, y: x + y,
"subtract": lambda x, y: x - y,
"multiply": lambda x, y: x * y,
"divide": lambda x, y: x / y if y != 0 else "undefined"
}
result = ops[operation](a, b)
return f"{a} {operation} {b} = {result}"
# Test
print(calculate.execute({"operation": "multiply", "a": 7, "b": 8}))
# "7 multiply 8 = 56"
Example 2: Database Query Tool¶
@tool(
schema={
"name": "query_database",
"description": "Execute a read-only database query",
"input_schema": {
"type": "object",
"properties": {
"table": {
"type": "string",
"enum": ["users", "orders", "products"],
"description": "Table to query"
},
"limit": {
"type": "integer",
"description": "Maximum rows to return",
"default": 10,
"minimum": 1,
"maximum": 100
},
"filters": {
"type": "object",
"description": "Filter conditions"
}
},
"required": ["table"]
}
},
requires_approval=False
)
def query_database(
table: str,
limit: int = 10,
filters: dict = None,
platform_context: dict = None
) -> str:
"""Query the database with optional filters."""
tenant = platform_context.get("tenant_name", "default") if platform_context else "default"
# Simulated query
return f"Queried {table} in {tenant}, limit={limit}, filters={filters}"
Example 3: Tool Registry¶
from dcaf.tools import tool, Tool
from typing import List
# Create multiple tools
@tool(schema={...}, requires_approval=False)
def tool_a(x: str) -> str:
return f"A: {x}"
@tool(schema={...}, requires_approval=True)
def tool_b(y: str, platform_context: dict) -> str:
return f"B: {y}"
@tool(schema={...}, requires_approval=False)
def tool_c(z: int) -> str:
return f"C: {z}"
# Create a registry
tools: List[Tool] = [tool_a, tool_b, tool_c]
# Analyze tools
print("Tool Analysis:")
print("-" * 50)
for t in tools:
ctx = "✓" if t.requires_platform_context else "✗"
app = "✓" if t.requires_approval else "✗"
print(f" {t.name:20} Context: {ctx} Approval: {app}")
# Filter by requirements
approval_tools = [t for t in tools if t.requires_approval]
context_tools = [t for t in tools if t.requires_platform_context]
Best Practices¶
1. Clear Descriptions¶
# Good: Specific and actionable
"description": "Delete a user account and all associated data permanently"
# Bad: Vague
"description": "Delete user"
2. Appropriate Approval¶
# Require approval for:
# - Destructive operations (delete, terminate, drop)
# - State changes (create, update, modify)
# - Cost-incurring actions (deploy, scale up)
# - Security-sensitive operations
# No approval needed for:
# - Read-only operations (get, list, describe)
# - Calculations
# - Information retrieval
3. Parameter Validation¶
# Use constraints in schema
"count": {
"type": "integer",
"minimum": 1,
"maximum": 1000,
"description": "Number of items (1-1000)"
}
4. Helpful Error Messages¶
def my_tool(param: str) -> str:
if not param:
return "Error: Parameter cannot be empty"
if len(param) > 100:
return f"Error: Parameter too long ({len(param)} > 100)"
# ...
5. Use Enums for Fixed Options¶
"status": {
"type": "string",
"enum": ["pending", "active", "completed"],
"description": "One of: pending, active, completed"
}