tool

package
v0.3.0-alpha Latest Latest
Warning

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

Go to latest
Published: Nov 17, 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

Overview

Package tool provides tool interfaces for graph nodes.

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