tool

package
v0.1.0-alpha Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2025 License: MIT Imports: 7 Imported by: 0

README

Tool System

Enable LLM agents to interact with external systems and perform actions through a flexible tool abstraction.

Overview

The Tool system allows nodes in your LangGraph-Go workflows to call external services, APIs, and functions. Tools provide a standardized interface for:

  • Web Searches - Query search engines and retrieve results
  • API Calls - Interact with REST/GraphQL APIs
  • Database Queries - Fetch and update data
  • File Operations - Read, write, and process files
  • Calculations - Perform complex computations
  • Code Execution - Run scripts and programs

Quick Start

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/dshills/langgraph-go/graph"
    "github.com/dshills/langgraph-go/graph/tool"
)

func main() {
    // Create HTTP tool
    httpTool := tool.NewHTTPTool()

    // Use tool in a node
    apiNode := graph.NodeFunc[MyState](func(ctx context.Context, state MyState) graph.NodeResult[MyState] {
        // Call external API using HTTP tool
        result, err := httpTool.Call(ctx, map[string]interface{}{
            "method": "GET",
            "url":    "https://api.example.com/data",
            "headers": map[string]interface{}{
                "Authorization": "Bearer " + state.APIKey,
            },
        })
        if err != nil {
            return graph.NodeResult[MyState]{Err: err}
        }

        // Process tool result
        statusCode := result["status_code"].(int)
        body := result["body"].(string)

        return graph.NodeResult[MyState]{
            Delta: MyState{
                Data:   body,
                Status: statusCode,
            },
            Route: graph.Stop(),
        }
    })

    // Build workflow with tool-enabled node
    // ... configure engine and run
}

Tool Interface

All tools implement the Tool interface:

type Tool interface {
    // Name returns the unique identifier for this tool
    Name() string

    // Call executes the tool with provided input
    Call(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error)
}
Method: Name()

Returns a unique identifier for the tool. Should be lowercase with underscores.

Examples: "search_web", "get_weather", "calculate", "http_request"

Method: Call(ctx, input)

Executes the tool with the provided parameters.

Parameters:

  • ctx - Context for cancellation and timeout
  • input - Tool parameters as key-value map (may be nil)

Returns:

  • map[string]interface{} - Tool execution result
  • error - Execution or validation errors

Example:

result, err := tool.Call(ctx, map[string]interface{}{
    "query": "weather in San Francisco",
    "limit": 5,
})

Built-in Tools

HTTPTool

Make HTTP requests to external APIs and services.

Create:

httpTool := tool.NewHTTPTool()

Input Parameters:

Parameter Type Required Description
method string No HTTP method (default: "GET")
url string Yes Target URL
headers map No HTTP headers
body string No Request body (for POST)

Output:

Field Type Description
status_code int HTTP status code (200, 404, etc.)
headers map Response headers
body string Response body

Example - GET Request:

result, err := httpTool.Call(ctx, map[string]interface{}{
    "method": "GET",
    "url":    "https://api.github.com/repos/golang/go",
    "headers": map[string]interface{}{
        "Accept": "application/json",
    },
})

statusCode := result["status_code"].(int)
body := result["body"].(string)

Example - POST Request:

requestBody := `{"name": "New Item", "price": 29.99}`

result, err := httpTool.Call(ctx, map[string]interface{}{
    "method": "POST",
    "url":    "https://api.example.com/items",
    "headers": map[string]interface{}{
        "Content-Type": "application/json",
    },
    "body": requestBody,
})

Supported Methods: GET, POST

Error Handling:

result, err := httpTool.Call(ctx, input)
if err != nil {
    // Handle request errors (invalid URL, network issues, etc.)
    return graph.NodeResult[State]{Err: err}
}

// Check HTTP status code
if result["status_code"].(int) >= 400 {
    // Handle HTTP errors
    log.Printf("HTTP error: %d - %s",
        result["status_code"],
        result["body"])
}

Creating Custom Tools

Implement the Tool interface to create custom tools:

type WeatherTool struct {
    apiKey string
}

func NewWeatherTool(apiKey string) *WeatherTool {
    return &WeatherTool{apiKey: apiKey}
}

func (w *WeatherTool) Name() string {
    return "get_weather"
}

func (w *WeatherTool) Call(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) {
    // 1. Validate input
    location, ok := input["location"].(string)
    if !ok || location == "" {
        return nil, fmt.Errorf("location parameter required")
    }

    // 2. Check context cancellation before expensive operations
    if err := ctx.Err(); err != nil {
        return nil, err
    }

    // 3. Perform the operation
    weather, err := w.fetchWeather(ctx, location)
    if err != nil {
        return nil, fmt.Errorf("failed to fetch weather: %w", err)
    }

    // 4. Return structured output
    return map[string]interface{}{
        "temperature": weather.Temp,
        "conditions":  weather.Conditions,
        "humidity":    weather.Humidity,
        "location":    location,
    }, nil
}

func (w *WeatherTool) fetchWeather(ctx context.Context, location string) (*WeatherData, error) {
    // Implementation details...
    return &WeatherData{}, nil
}
Best Practices for Custom Tools
  1. Validate Input:
func (t *MyTool) Call(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) {
    // Validate required parameters
    param, ok := input["param"].(string)
    if !ok {
        return nil, fmt.Errorf("param required (string)")
    }

    // Validate optional parameters
    limit := 10 // default
    if l, ok := input["limit"].(int); ok {
        limit = l
    }

    // ... execute tool
}
  1. Respect Context:
// Check before expensive operations
if err := ctx.Err(); err != nil {
    return nil, err
}

// Pass context to child operations
result, err := http.Get(ctx, url)
  1. Return Structured Output:
// ✅ Good: Structured output
return map[string]interface{}{
    "results": items,
    "count":   len(items),
    "next":    nextPageToken,
}, nil

// ❌ Bad: Raw string output
return map[string]interface{}{
    "data": jsonString,
}, nil
  1. Handle Errors Gracefully:
if err != nil {
    // Wrap errors with context
    return nil, fmt.Errorf("database query failed: %w", err)
}
  1. Be Idempotent When Possible:
// Safe to call multiple times with same input
func (t *ReadTool) Call(ctx context.Context, input map[string]interface{}) {
    // Read-only operations are naturally idempotent
}

Using Tools in Nodes

Single Tool Call
fetchNode := graph.NodeFunc[State](func(ctx context.Context, state State) graph.NodeResult[State] {
    // Call tool
    result, err := weatherTool.Call(ctx, map[string]interface{}{
        "location": state.Location,
    })
    if err != nil {
        return graph.NodeResult[State]{Err: err}
    }

    // Use result in state
    temp := result["temperature"].(int)
    return graph.NodeResult[State]{
        Delta: State{Temperature: temp},
        Route: graph.Goto("display"),
    }
})
Multiple Tool Calls
processNode := graph.NodeFunc[State](func(ctx context.Context, state State) graph.NodeResult[State] {
    // First tool: fetch user data
    userData, err := userTool.Call(ctx, map[string]interface{}{
        "user_id": state.UserID,
    })
    if err != nil {
        return graph.NodeResult[State]{Err: err}
    }

    // Second tool: enrich data
    enrichedData, err := enrichTool.Call(ctx, map[string]interface{}{
        "data": userData["profile"],
    })
    if err != nil {
        return graph.NodeResult[State]{Err: err}
    }

    // Use combined results
    return graph.NodeResult[State]{
        Delta: State{Profile: enrichedData},
        Route: graph.Stop(),
    }
})
Conditional Tool Execution
smartNode := graph.NodeFunc[State](func(ctx context.Context, state State) graph.NodeResult[State] {
    var result map[string]interface{}
    var err error

    // Choose tool based on state
    if state.NeedsWeather {
        result, err = weatherTool.Call(ctx, map[string]interface{}{
            "location": state.Location,
        })
    } else {
        result, err = newsTool.Call(ctx, map[string]interface{}{
            "topic": state.Topic,
        })
    }

    if err != nil {
        return graph.NodeResult[State]{Err: err}
    }

    return graph.NodeResult[State]{
        Delta: State{Data: result},
        Route: graph.Stop(),
    }
})
Error Handling Patterns
robustNode := graph.NodeFunc[State](func(ctx context.Context, state State) graph.NodeResult[State] {
    result, err := externalTool.Call(ctx, state.Input)
    if err != nil {
        // Option 1: Return error (workflow stops)
        return graph.NodeResult[State]{Err: err}

        // Option 2: Handle error gracefully
        return graph.NodeResult[State]{
            Delta: State{
                Error:  err.Error(),
                Status: "failed",
            },
            Route: graph.Goto("error_handler"),
        }

        // Option 3: Retry logic
        for attempt := 0; attempt < 3; attempt++ {
            result, err = externalTool.Call(ctx, state.Input)
            if err == nil {
                break
            }
            time.Sleep(time.Second * time.Duration(attempt+1))
        }
        if err != nil {
            return graph.NodeResult[State]{Err: err}
        }
    }

    // Success path
    return graph.NodeResult[State]{
        Delta: State{Result: result},
        Route: graph.Stop(),
    }
})

Tool Registry Pattern

For workflows with many tools, use a registry:

type ToolRegistry struct {
    tools map[string]tool.Tool
}

func NewToolRegistry() *ToolRegistry {
    return &ToolRegistry{
        tools: make(map[string]tool.Tool),
    }
}

func (r *ToolRegistry) Register(t tool.Tool) {
    r.tools[t.Name()] = t
}

func (r *ToolRegistry) Get(name string) (tool.Tool, error) {
    t, ok := r.tools[name]
    if !ok {
        return nil, fmt.Errorf("tool not found: %s", name)
    }
    return t, nil
}

// Usage in workflow
registry := NewToolRegistry()
registry.Register(tool.NewHTTPTool())
registry.Register(NewWeatherTool(apiKey))

dynamicNode := graph.NodeFunc[State](func(ctx context.Context, state State) graph.NodeResult[State] {
    // Get tool by name from state
    t, err := registry.Get(state.ToolName)
    if err != nil {
        return graph.NodeResult[State]{Err: err}
    }

    result, err := t.Call(ctx, state.ToolInput)
    // ... handle result
})

Integration with LLM Tool Calling

LangGraph-Go tools work seamlessly with LLM tool calling:

// Define tools for LLM
weatherSpec := model.ToolSpec{
    Name:        "get_weather",
    Description: "Get current weather for a location",
    Schema: map[string]interface{}{
        "type": "object",
        "properties": map[string]interface{}{
            "location": map[string]interface{}{
                "type":        "string",
                "description": "City name or zip code",
            },
        },
        "required": []string{"location"},
    },
}

// LLM decides which tool to call
llmNode := graph.NodeFunc[State](func(ctx context.Context, state State) graph.NodeResult[State] {
    chatOut, err := llm.Chat(ctx, state.Messages, []model.ToolSpec{weatherSpec})
    if err != nil {
        return graph.NodeResult[State]{Err: err}
    }

    // Execute tool if LLM requested it
    if chatOut.ToolCall != nil {
        result, err := weatherTool.Call(ctx, chatOut.ToolCall.Arguments)
        if err != nil {
            return graph.NodeResult[State]{Err: err}
        }

        // Add tool result to conversation
        return graph.NodeResult[State]{
            Delta: State{
                Messages: append(state.Messages, model.Message{
                    Role:    "tool",
                    Content: fmt.Sprintf("%v", result),
                }),
            },
            Route: graph.Goto("llm"), // Continue conversation
        }
    }

    // No tool call - final answer
    return graph.NodeResult[State]{
        Delta: State{Answer: chatOut.Message.Content},
        Route: graph.Stop(),
    }
})

Testing Tools

Unit Testing Custom Tools
func TestWeatherTool_Call(t *testing.T) {
    tool := NewWeatherTool("test-api-key")

    ctx := context.Background()
    input := map[string]interface{}{
        "location": "San Francisco",
    }

    result, err := tool.Call(ctx, input)
    if err != nil {
        t.Fatalf("Call() error = %v", err)
    }

    // Verify output structure
    if _, ok := result["temperature"]; !ok {
        t.Error("result missing temperature field")
    }

    if loc := result["location"]; loc != "San Francisco" {
        t.Errorf("location = %v, want San Francisco", loc)
    }
}
Testing with Mock Tools
type mockTool struct {
    name   string
    output map[string]interface{}
    err    error
}

func (m *mockTool) Name() string { return m.name }
func (m *mockTool) Call(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) {
    if m.err != nil {
        return nil, m.err
    }
    return m.output, nil
}

func TestNodeWithMockTool(t *testing.T) {
    mock := &mockTool{
        name: "test_tool",
        output: map[string]interface{}{
            "result": "success",
        },
    }

    // Test node with mock tool
    // ...
}

Performance Considerations

Tool Timeouts
// Set timeout for tool execution
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

result, err := tool.Call(ctx, input)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        log.Println("Tool execution timed out")
    }
}
Caching Tool Results
type CachedTool struct {
    tool  tool.Tool
    cache map[string]map[string]interface{}
    mu    sync.RWMutex
}

func (c *CachedTool) Call(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) {
    key := computeCacheKey(input)

    // Check cache
    c.mu.RLock()
    if cached, ok := c.cache[key]; ok {
        c.mu.RUnlock()
        return cached, nil
    }
    c.mu.RUnlock()

    // Call underlying tool
    result, err := c.tool.Call(ctx, input)
    if err != nil {
        return nil, err
    }

    // Store in cache
    c.mu.Lock()
    c.cache[key] = result
    c.mu.Unlock()

    return result, nil
}

Troubleshooting

Tool Not Found
Error: tool not found: search_web

Solution: Ensure tool is registered before use:

registry.Register(searchTool)
Invalid Input Type
Error: location parameter required (string)

Solution: Check input parameter types:

location, ok := input["location"].(string)
if !ok {
    return nil, fmt.Errorf("location must be string, got %T", input["location"])
}
Context Timeout
Error: context deadline exceeded

Solution: Increase timeout or optimize tool:

ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
Network Errors
Error: failed to execute request: dial tcp: lookup failed

Solution: Add retry logic and error handling:

for i := 0; i < 3; i++ {
    result, err = httpTool.Call(ctx, input)
    if err == nil {
        break
    }
    time.Sleep(time.Second * time.Duration(i+1))
}

Examples

See examples/tools/ for complete working examples:

  • Basic Tool Usage - Simple tool invocation in nodes
  • Multi-Tool Workflow - Orchestrating multiple tools
  • LLM Tool Calling - Integration with LLM-driven tool selection
  • Custom Tool Implementation - Building domain-specific tools

API Reference

Support

For issues or questions:

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type HTTPTool

type HTTPTool struct {
	// contains filtered or unexported fields
}

HTTPTool is a tool for making HTTP requests.

It supports GET and POST methods and returns the HTTP response including status code, headers, and body. Useful for LLM agents that need to:

  • Fetch data from REST APIs
  • Send data to webhooks
  • Scrape web pages
  • Interact with external services

Input Parameters:

  • method: HTTP method ("GET" or "POST", defaults to "GET")
  • url: Target URL (required)
  • headers: Optional map of HTTP headers
  • body: Optional request body (for POST requests)

Output:

  • status_code: HTTP status code (e.g., 200, 404)
  • headers: Response headers as map
  • body: Response body as string

Example usage:

tool := NewHTTPTool()
result, err := tool.Call(ctx, map[string]interface{}{
    "method": "GET",
    "url": "https://api.example.com/data",
    "headers": map[string]interface{}{
        "Authorization": "Bearer token",
    },
})
fmt.Printf("Status: %d, Body: %s\n", result["status_code"], result["body"])

func NewHTTPTool

func NewHTTPTool() *HTTPTool

NewHTTPTool creates a new HTTP tool with default settings.

func (*HTTPTool) Call

func (h *HTTPTool) Call(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error)

Call executes an HTTP request with the provided parameters.

func (*HTTPTool) Name

func (h *HTTPTool) Name() string

Name returns the tool identifier.

type MockTool

type MockTool struct {
	// ToolName is the identifier returned by Name().
	// Must be set for the mock to function properly.
	ToolName string

	// Responses contains the sequence of outputs to return.
	// Each call to Call() returns the next response in order.
	// If all responses are consumed, the last response repeats.
	Responses []map[string]interface{}

	// Err, if set, will be returned by Call() instead of a response.
	Err error

	// Calls tracks the history of all Call() invocations.
	// Useful for verifying that tools were called with expected inputs.
	// Each recorded input is a defensive copy, preventing external mutations
	// from affecting test assertions on call history.
	Calls []MockToolCall
	// contains filtered or unexported fields
}

MockTool is a test implementation of Tool.

Use MockTool in tests to verify workflow behavior without executing actual tool logic. It provides:

  • Configurable tool name
  • Configurable response sequences
  • Call history tracking
  • Error injection
  • Thread-safe operation

Example usage:

mock := &MockTool{
    ToolName: "search_web",
    Responses: []map[string]interface{}{
        {"results": []string{"result1", "result2"}},
    },
}
output, err := mock.Call(ctx, map[string]interface{}{"query": "test"})
// Returns {"results": ["result1", "result2"]}

Example with error injection:

mock := &MockTool{
    ToolName: "api_call",
    Err:      errors.New("API timeout"),
}
_, err := mock.Call(ctx, input)
// Returns the configured error

func (*MockTool) Call

func (m *MockTool) Call(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error)

Call implements the Tool interface.

Returns:

  • The next response from Responses (or repeats the last response)
  • Or Err if configured

Always records the call in Calls history regardless of success/failure.

func (*MockTool) CallCount

func (m *MockTool) CallCount() int

CallCount returns the number of times Call() has been called.

Thread-safe convenience method:

if mock.CallCount() != 2 {
    t.Errorf("expected 2 calls, got %d", mock.CallCount())
}

func (*MockTool) Name

func (m *MockTool) Name() string

Name implements the Tool interface.

func (*MockTool) Reset

func (m *MockTool) Reset()

Reset clears the call history and resets the response index.

Useful when reusing the same mock across multiple test cases:

mock := &MockTool{ToolName: "test", Responses: []map[string]interface{}{{"ok": true}}}
// ... run test 1 ...
mock.Reset()
// ... run test 2 with clean state ...

type MockToolCall

type MockToolCall struct {
	Input map[string]interface{}
}

MockToolCall records a single invocation of Call().

type Tool

type Tool interface {
	// Name returns the unique identifier for this tool.
	//
	// The name must match the tool name in ToolSpec used by the LLM.
	// Names should be lowercase with underscores, following function naming conventions.
	//
	// Examples: "search_web", "get_weather", "calculate", "send_email"
	Name() string

	// Call executes the tool with the provided input and returns the result.
	//
	// Parameters:
	//   - ctx: Context for cancellation, timeout, and metadata propagation
	//   - input: Tool parameters as key-value pairs (may be nil for parameterless tools)
	//
	// Returns:
	//   - map[string]interface{}: Tool execution result
	//   - error: Execution errors, validation errors, or context cancellation
	//
	// The input structure should match the Schema defined in the corresponding ToolSpec.
	// The output can be any structured data that the LLM can process.
	//
	// Implementations should:
	//   - Check ctx.Err() before expensive operations
	//   - Validate required input parameters
	//   - Return descriptive errors for invalid inputs
	//   - Include relevant metadata in the output
	Call(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error)
}

Tool defines the interface for executable tools that LLMs can invoke.

Tools enable LLMs to interact with external systems and perform actions:

  • Web searches
  • Database queries
  • API calls
  • File operations
  • Calculations
  • Code execution

Implementations should:

  • Validate input parameters
  • Respect context cancellation and timeouts
  • Return structured output as map[string]interface{}
  • Handle errors gracefully with clear error messages
  • Be idempotent when possible

Example implementation:

type WeatherTool struct{}

func (w *WeatherTool) Name() string {
    return "get_weather"
}

func (w *WeatherTool) Call(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) {
    location, ok := input["location"].(string)
    if !ok {
        return nil, errors.New("location parameter required")
    }

    // Fetch weather data...
    temp := 72.5

    return map[string]interface{}{
        "temperature": temp,
        "conditions":  "sunny",
        "location":    location,
    }, nil
}

Example usage in a workflow:

weatherTool := &WeatherTool{}
input := map[string]interface{}{"location": "San Francisco"}
output, err := weatherTool.Call(ctx, input)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Temperature: %v\n", output["temperature"])

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL