Step-by-step guide to define a tool, register it, attach the registry to context, and configure tool calling on a Turn.
This playbook walks through adding a new callable tool to your Geppetto application. By the end, your tool will be registered, advertised to the model, and executable during inference.
Create a Go function that implements your tool's logic. The function can optionally accept context.Context as its first parameter:
package main
import (
"context"
"fmt"
)
// Tool input struct - fields become JSON schema properties
type GetWeatherInput struct {
Location string `json:"location" description:"City name or coordinates"`
Units string `json:"units" description:"celsius or fahrenheit"`
}
// Tool output struct
type GetWeatherOutput struct {
Temperature float64 `json:"temperature"`
Conditions string `json:"conditions"`
}
// Tool function - can accept context as first parameter
func getWeather(ctx context.Context, input GetWeatherInput) (GetWeatherOutput, error) {
// Your implementation here
// Access context for cancellation, deadlines, or request-scoped values
select {
case <-ctx.Done():
return GetWeatherOutput{}, ctx.Err()
default:
}
return GetWeatherOutput{
Temperature: 22.0,
Conditions: "Partly cloudy",
}, nil
}
Why struct inputs? Geppetto auto-generates a JSON schema from the struct, which the model uses to produce valid arguments.
Convert your function into a ToolDefinition using NewToolFromFunc:
import "github.com/go-go-golems/geppetto/pkg/inference/tools"
toolDef, err := tools.NewToolFromFunc(
"get_weather", // Tool name (model uses this)
"Get current weather for a location", // Description (helps model decide when to use it)
getWeather, // Your function
)
if err != nil {
return fmt.Errorf("failed to create tool: %w", err)
}
Create an in-memory registry and register your tool:
import "github.com/go-go-golems/geppetto/pkg/inference/tools"
registry := tools.NewInMemoryToolRegistry()
if err := registry.RegisterTool("get_weather", *toolDef); err != nil {
return fmt.Errorf("failed to register tool: %w", err)
}
// Register additional tools as needed
// registry.RegisterTool("search_web", *searchDef)
// registry.RegisterTool("run_code", *codeDef)
The registry must be attached to context.Context so engines and middleware can access it:
import "github.com/go-go-golems/geppetto/pkg/inference/tools"
// Before calling eng.RunInference(...) directly
ctx = tools.WithRegistry(ctx, registry)
Why context, not Turn? The registry contains function pointers (not serializable). Keeping it in context separates runtime state from persistable Turn data.
When using the canonical tool loop (toolloop.Loop), you typically configure tool calling via:
toolloop.LoopConfig (loop orchestration, like max iterations)tools.ToolConfig (tool policy, like tool choice and timeouts)The loop stores provider-facing tool configuration on Turn.Data automatically.
import (
"time"
"github.com/go-go-golems/geppetto/pkg/inference/toolloop"
"github.com/go-go-golems/geppetto/pkg/turns"
"github.com/go-go-golems/geppetto/pkg/inference/tools"
)
turn := &turns.Turn{
Data: map[turns.TurnDataKey]any{},
}
loopCfg := toolloop.NewLoopConfig().
WithMaxIterations(5)
toolCfg := tools.DefaultToolConfig().
WithToolChoice(tools.ToolChoiceAuto).
WithExecutionTimeout(30 * time.Second).
WithMaxParallelTools(1)
turns.AppendBlock(turn, turns.NewSystemTextBlock("You are a helpful assistant with weather tools."))
turns.AppendBlock(turn, turns.NewUserTextBlock("What's the weather in Paris?"))
Use toolloop.Loop for automatic tool execution:
import "github.com/go-go-golems/geppetto/pkg/inference/toolloop"
loop := toolloop.New(
toolloop.WithEngine(eng),
toolloop.WithRegistry(registry),
toolloop.WithLoopConfig(loopCfg),
toolloop.WithToolConfig(toolCfg),
)
result, err := loop.RunLoop(ctx, turn)
if err != nil {
return fmt.Errorf("tool calling failed: %w", err)
}
// result contains: [system] → [user] → [llm_text] → [tool_call] → [tool_use] → [llm_text]
What happens:
tool_call block requesting get_weathergetWeather() with the model's argumentstool_use block with the resultpackage main
import (
"context"
"fmt"
"github.com/go-go-golems/geppetto/pkg/inference/engine/factory"
"github.com/go-go-golems/geppetto/pkg/inference/toolloop"
"github.com/go-go-golems/geppetto/pkg/inference/tools"
"github.com/go-go-golems/geppetto/pkg/turns"
)
type GetWeatherInput struct {
Location string `json:"location"`
Units string `json:"units"`
}
type GetWeatherOutput struct {
Temperature float64 `json:"temperature"`
Conditions string `json:"conditions"`
}
func getWeather(ctx context.Context, input GetWeatherInput) (GetWeatherOutput, error) {
return GetWeatherOutput{Temperature: 22.0, Conditions: "Sunny"}, nil
}
func main() {
ctx := context.Background()
// 1. Create engine (assumes parsed layers from CLI or config)
eng, _ := factory.NewEngineFromParsedValues(parsedValues)
// 2. Create and register tool
toolDef, _ := tools.NewToolFromFunc("get_weather", "Get weather for location", getWeather)
registry := tools.NewInMemoryToolRegistry()
_ = registry.RegisterTool("get_weather", *toolDef)
// 3. Build Turn
turn := &turns.Turn{Data: map[turns.TurnDataKey]any{}}
turns.AppendBlock(turn, turns.NewSystemTextBlock("You have access to weather tools."))
turns.AppendBlock(turn, turns.NewUserTextBlock("What's the weather in Tokyo?"))
// 4. Run tool calling loop
loop := toolloop.New(
toolloop.WithEngine(eng),
toolloop.WithRegistry(registry),
toolloop.WithLoopConfig(toolloop.NewLoopConfig().WithMaxIterations(5)),
toolloop.WithToolConfig(tools.DefaultToolConfig().WithToolChoice(tools.ToolChoiceAuto)),
)
result, err := loop.RunLoop(ctx, turn)
if err != nil {
panic(err)
}
// 6. Print final response
for _, block := range result.Blocks {
if block.Kind == turns.BlockKindLLMText {
fmt.Println(block.Payload[turns.PayloadKeyText])
}
}
}
| Problem | Cause | Solution |
|---|---|---|
| Tool not called | Tools disabled or tool choice is none | Ensure tools.ToolConfig{Enabled: true, ToolChoice: tools.ToolChoiceAuto} |
| Tool not advertised | Registry not available to the engine | Run inference via toolloop.Loop (it attaches the registry to context) |
| "tool not found" error | Name mismatch | Ensure RegisterTool name matches model's request |
| Infinite loop | No max iterations | Use WithMaxIterations(n) in config |
| Context values lost | Wrong context | Pass the same ctx to loop.RunLoop(...) (and attach sinks via events.WithEventSinks if streaming) |
geppetto/cmd/examples/advanced/generic-tool-calling/main.go