Introduction
Give agents external capabilities with function calling
|
Tools extend agents beyond text generation, enabling them to interact with APIs, databases, file systems, and more. AgentKit automatically handles function calling and parameter validation.
Quick Start
// Define input structure with JSON schema tags
type CalculatorInput struct {
Operation string `json:"operation" jsonschema:"description=The operation to perform,enum=add,enum=subtract,enum=multiply,enum=divide"`
A float64 `json:"a" jsonschema:"description=First number"`
B float64 `json:"b" jsonschema:"description=Second number"`
}
// Implement tool handler
func calculateHandler(ctx common.RunContext, params CalculatorInput) (string, error) {
switch params.Operation {
case "add":
return fmt.Sprintf("%.2f", params.A + params.B), nil
case "subtract":
return fmt.Sprintf("%.2f", params.A - params.B), nil
case "multiply":
return fmt.Sprintf("%.2f", params.A * params.B), nil
case "divide":
if params.B == 0 {
return "", errors.New("division by zero")
}
return fmt.Sprintf("%.2f", params.A / params.B), nil
}
return "", errors.New("unknown operation")
}
// Create tool
calcTool := tool.NewFunctionTool(
"calculator",
"Perform basic math operations",
calculateHandler,
)
// Add to agent
agent := agent.New().
SetModel(model).
AddTool(calcTool)Schema Definition
AgentKit uses automatic schema generation from Go structs with JSON schema tags:
// Define input structure with JSON schema annotations
type ToolInput struct {
Name string `json:"name" jsonschema:"description=User's name"`
Age int `json:"age" jsonschema:"description=User's age"`
Active bool `json:"active" jsonschema:"description=Whether user is active"`
Tags []string `json:"tags" jsonschema:"description=List of tags"`
Metadata map[string]interface{} `json:"metadata" jsonschema:"description=Additional metadata"`
}
// With constraints using enum values
type TaskInput struct {
Priority string `json:"priority" jsonschema:"description=Task priority,enum=low,enum=medium,enum=high"`
Count int `json:"count" jsonschema:"description=Number of items,minimum=1,maximum=100"`
Email string `json:"email" jsonschema:"description=User email,pattern=^[^@]+@[^@]+\\.[^@]+$"`
}
// Required vs optional fields
type UserInput struct {
Name string `json:"name" jsonschema:"description=Required name"`
Description *string `json:"description,omitempty" jsonschema:"description=Optional description"`
}Using Tools with Agents
// Create an agent with multiple tools
weatherTool := createWeatherTool()
calcTool := createCalculatorTool()
agent := agent.New().
SetModel(model).
SetSystemPrompt("You are a helpful assistant with access to weather and calculator tools.").
AddTool(weatherTool).
AddTool(calcTool)
// Agent automatically uses tools when needed
session := session.New(agent)
response, _ := session.Run(ctx, []agent.ChatMessage{agent.NewUserMessage("What's the weather in Tokyo and what's 25 * 4?")}, nil)
// Agent will call both tools and provide a combined responseCommon Tool Patterns
API Tool
Integrate with external APIs:
type WeatherInput struct {
Location string `json:"location" jsonschema:"description=City name or coordinates"`
Units string `json:"units" jsonschema:"description=Temperature units,enum=celsius,enum=fahrenheit,default=celsius"`
}
func weatherHandler(ctx common.RunContext, params WeatherInput) (string, error) {
units := params.Units
if units == "" {
units = "celsius"
}
// Call external weather API
weather, err := callWeatherAPI(params.Location, units)
if err != nil {
return "", fmt.Errorf("weather API error: %w", err)
}
response := WeatherResponse{
Location: params.Location,
Temperature: weather.Temperature,
Condition: weather.Condition,
Units: units,
}
result, _ := json.Marshal(response)
return string(result), nil
}
func createWeatherTool() tool.Tool {
return tool.NewFunctionTool(
"get_weather",
"Get current weather for a location",
weatherHandler,
)
}
type WeatherResponse struct {
Location string `json:"location"`
Temperature float64 `json:"temperature"`
Condition string `json:"condition"`
Units string `json:"units"`
}Database Tool
Query databases safely:
type DatabaseQueryInput struct {
NameFilter *string `json:"name_filter,omitempty" jsonschema:"description=Filter users by name pattern"`
Limit int `json:"limit" jsonschema:"description=Maximum number of results,minimum=1,maximum=100,default=10"`
}
func databaseHandler(ctx common.RunContext, params DatabaseQueryInput, db *sql.DB) (string, error) {
limit := params.Limit
if limit == 0 {
limit = 10
}
query := "SELECT id, name, email FROM users"
var sqlParams []interface{}
if params.NameFilter != nil && *params.NameFilter != "" {
query += " WHERE name LIKE ?"
sqlParams = append(sqlParams, "%"+*params.NameFilter+"%")
}
query += " LIMIT ?"
sqlParams = append(sqlParams, limit)
rows, err := db.Query(query, sqlParams...)
if err != nil {
return "", fmt.Errorf("query failed: %w", err)
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
err := rows.Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return "", err
}
users = append(users, user)
}
result := QueryResult{
Users: users,
Count: len(users),
}
jsonResult, _ := json.Marshal(result)
return string(jsonResult), nil
}
func createDatabaseTool(db *sql.DB) tool.Tool {
return tool.NewFunctionTool(
"query_users",
"Query user database",
func(ctx common.RunContext, params DatabaseQueryInput) (string, error) {
return databaseHandler(ctx, params, db)
},
)
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type QueryResult struct {
Users []User `json:"users"`
Count int `json:"count"`
}File System Tool
Read and write files securely:
type FileInput struct {
Filepath string `json:"filepath" jsonschema:"description=Path to file to read"`
}
func fileHandler(ctx common.RunContext, params FileInput) (string, error) {
// Security: prevent directory traversal
if strings.Contains(params.Filepath, "..") {
return "", errors.New("directory traversal not allowed")
}
content, err := os.ReadFile(params.Filepath)
if err != nil {
return "", fmt.Errorf("failed to read file: %w", err)
}
result := FileContent{
Path: params.Filepath,
Content: string(content),
Size: len(content),
}
jsonResult, _ := json.Marshal(result)
return string(jsonResult), nil
}
func createFileTool() tool.Tool {
return tool.NewFunctionTool(
"read_file",
"Read contents of a text file",
fileHandler,
)
}
type FileContent struct {
Path string `json:"path"`
Content string `json:"content"`
Size int `json:"size"`
}Advanced Features
Tool Validation
Add custom validation logic:
type ValidatedTool struct {
tool.Tool
validator func(args tool.Args) error
}
func WithValidation(baseTool tool.Tool, validator func(args tool.Args) error) *ValidatedTool {
return &ValidatedTool{
Tool: baseTool,
validator: validator,
}
}
func (v *ValidatedTool) Execute(args tool.Args) (interface{}, error) {
// Run custom validation
if err := v.validator(args); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
return v.Tool.Execute(args)
}
// Usage
weatherTool := createWeatherTool()
validatedTool := WithValidation(weatherTool, func(args tool.Args) error {
location := args.String("location")
if len(location) < 2 {
return errors.New("location must be at least 2 characters")
}
return nil
})Tool Middleware
Add logging, metrics, and other cross-cutting concerns:
type LoggingTool struct {
tool.Tool
logger *slog.Logger
}
func WithLogging(baseTool tool.Tool, logger *slog.Logger) *LoggingTool {
return &LoggingTool{
Tool: baseTool,
logger: logger,
}
}
func (l *LoggingTool) Execute(args tool.Args) (interface{}, error) {
l.logger.Info("executing tool", "name", l.Name(), "args", args)
start := time.Now()
result, err := l.Tool.Execute(args)
duration := time.Since(start)
if err != nil {
l.logger.Error("tool failed", "name", l.Name(), "duration", duration, "error", err)
} else {
l.logger.Info("tool completed", "name", l.Name(), "duration", duration)
}
return result, err
}Tool Collections
Organize related tools:
type ToolSet struct {
tools []tool.Tool
}
func NewToolSet() *ToolSet {
return &ToolSet{}
}
func (ts *ToolSet) Add(tools ...tool.Tool) {
ts.tools = append(ts.tools, tools...)
}
func (ts *ToolSet) Tools() []tool.Tool {
return ts.tools
}
// Usage
toolSet := NewToolSet()
toolSet.Add(
createWeatherTool(),
createCalculatorTool(),
createDatabaseTool(db),
)
agent := agent.New().
SetModel(model)
// Add all tools from the set
for _, tool := range toolSet.Tools() {
agent = agent.AddTool(tool)
}Best Practices
Schema Design
Write clear, descriptive schemas:
// ✅ Good - Clear and specific
type GoodInput struct {
Location string `json:"location" jsonschema:"description=City name (e.g. 'New York') or coordinates (e.g. '40.7128,-74.0060')"`
Priority string `json:"priority" jsonschema:"description=Task priority level,enum=low,enum=medium,enum=high,enum=urgent"`
Format string `json:"format" jsonschema:"description=Output format,enum=json,enum=xml,enum=csv,default=json"`
}
// ❌ Avoid - Vague descriptions
type BadInput struct {
Location string `json:"location" jsonschema:"description=location"`
Priority string `json:"priority" jsonschema:"description=priority"`
}Error Handling
Provide helpful error messages:
type RobustInput struct {
Data string `json:"data" jsonschema:"description=Input data to process"`
}
func robustHandler(ctx common.RunContext, params RobustInput) (string, error) {
// Validate input
if err := validateInput(params); err != nil {
return "", fmt.Errorf("invalid input: %w", err)
}
// Perform operation with retries
var result string
var err error
for i := 0; i < 3; i++ {
result, err = performOperation(params)
if err == nil {
break
}
// Exponential backoff
time.Sleep(time.Second * time.Duration(1<<i))
}
if err != nil {
return "", fmt.Errorf("operation failed after 3 attempts: %w", err)
}
return result, nil
}
func robustTool() tool.Tool {
return tool.NewFunctionTool(
"robust_operation",
"A tool with robust error handling",
robustHandler,
)
}Security
Always validate and sanitize inputs:
type SecureFileInput struct {
Filepath string `json:"filepath" jsonschema:"description=Path to file to read"`
}
func secureHandler(ctx common.RunContext, params SecureFileInput) (string, error) {
// Security validations
if strings.Contains(params.Filepath, "..") {
return "", errors.New("directory traversal not allowed")
}
if !strings.HasPrefix(params.Filepath, "/safe/directory/") {
return "", errors.New("access denied: file outside safe directory")
}
// Proceed with operation
return readFileSecurely(params.Filepath)
}
func secureTool() tool.Tool {
return tool.NewFunctionTool(
"secure_file_read",
"Securely read file contents",
secureHandler,
)
}Testing Tools
Test your tools thoroughly:
func TestCalculatorTool(t *testing.T) {
ctx := common.RunContext{}
tests := []struct {
name string
input CalculatorInput
expected string
hasError bool
}{
{
name: "addition",
input: CalculatorInput{Operation: "add", A: 5.0, B: 3.0},
expected: "8.00",
},
{
name: "division by zero",
input: CalculatorInput{Operation: "divide", A: 5.0, B: 0.0},
hasError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := calculateHandler(ctx, tt.input)
if tt.hasError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}