ModelBox Open Source

/

AgentKit
Core Concepts/Tools/Introduction

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 response

Common 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)
            }
        })
    }
}

Next Steps

  • Agents - Create agents that use your tools
  • Models - Configure models for function calling
  • Examples - See complete tool implementations
Edit on GitHub