tool.go

  1package fantasy
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"fmt"
  7	"reflect"
  8
  9	"charm.land/fantasy/schema"
 10)
 11
 12// Schema represents a JSON schema for tool input validation.
 13type Schema = schema.Schema
 14
 15// ToolInfo represents tool metadata, matching the existing pattern.
 16type ToolInfo struct {
 17	Name        string         `json:"name"`
 18	Description string         `json:"description"`
 19	Parameters  map[string]any `json:"parameters"`
 20	Required    []string       `json:"required"`
 21	Parallel    bool           `json:"parallel"` // Whether this tool can run in parallel with other tools
 22}
 23
 24// ToolCall represents a tool invocation, matching the existing pattern.
 25type ToolCall struct {
 26	ID    string `json:"id"`
 27	Name  string `json:"name"`
 28	Input string `json:"input"`
 29}
 30
 31// ToolResponse represents the response from a tool execution, matching the existing pattern.
 32type ToolResponse struct {
 33	Type     string `json:"type"`
 34	Content  string `json:"content"`
 35	Metadata string `json:"metadata,omitempty"`
 36	IsError  bool   `json:"is_error"`
 37}
 38
 39// NewTextResponse creates a text response.
 40func NewTextResponse(content string) ToolResponse {
 41	return ToolResponse{
 42		Type:    "text",
 43		Content: content,
 44	}
 45}
 46
 47// NewTextErrorResponse creates an error response.
 48func NewTextErrorResponse(content string) ToolResponse {
 49	return ToolResponse{
 50		Type:    "text",
 51		Content: content,
 52		IsError: true,
 53	}
 54}
 55
 56// WithResponseMetadata adds metadata to a response.
 57func WithResponseMetadata(response ToolResponse, metadata any) ToolResponse {
 58	if metadata != nil {
 59		metadataBytes, err := json.Marshal(metadata)
 60		if err != nil {
 61			return response
 62		}
 63		response.Metadata = string(metadataBytes)
 64	}
 65	return response
 66}
 67
 68// AgentTool represents a tool that can be called by a language model.
 69// This matches the existing BaseTool interface pattern.
 70type AgentTool interface {
 71	Info() ToolInfo
 72	Run(ctx context.Context, params ToolCall) (ToolResponse, error)
 73	ProviderOptions() ProviderOptions
 74	SetProviderOptions(opts ProviderOptions)
 75}
 76
 77// NewAgentTool creates a typed tool from a function with automatic schema generation.
 78// This is the recommended way to create tools.
 79func NewAgentTool[TInput any](
 80	name string,
 81	description string,
 82	fn func(ctx context.Context, input TInput, call ToolCall) (ToolResponse, error),
 83) AgentTool {
 84	var input TInput
 85	schema := schema.Generate(reflect.TypeOf(input))
 86
 87	return &funcToolWrapper[TInput]{
 88		name:        name,
 89		description: description,
 90		fn:          fn,
 91		schema:      schema,
 92		parallel:    false, // Default to sequential execution
 93	}
 94}
 95
 96// NewParallelAgentTool creates a typed tool from a function with automatic schema generation.
 97// This also marks a tool as safe to run in parallel with other tools.
 98func NewParallelAgentTool[TInput any](
 99	name string,
100	description string,
101	fn func(ctx context.Context, input TInput, call ToolCall) (ToolResponse, error),
102) AgentTool {
103	tool := NewAgentTool(name, description, fn)
104	// Try to use the SetParallel method if available
105	if setter, ok := tool.(interface{ SetParallel(bool) }); ok {
106		setter.SetParallel(true)
107	}
108	return tool
109}
110
111// funcToolWrapper wraps a function to implement the AgentTool interface.
112type funcToolWrapper[TInput any] struct {
113	name            string
114	description     string
115	fn              func(ctx context.Context, input TInput, call ToolCall) (ToolResponse, error)
116	schema          Schema
117	providerOptions ProviderOptions
118	parallel        bool
119}
120
121func (w *funcToolWrapper[TInput]) SetProviderOptions(opts ProviderOptions) {
122	w.providerOptions = opts
123}
124
125func (w *funcToolWrapper[TInput]) ProviderOptions() ProviderOptions {
126	return w.providerOptions
127}
128
129func (w *funcToolWrapper[TInput]) SetParallel(parallel bool) {
130	w.parallel = parallel
131}
132
133func (w *funcToolWrapper[TInput]) Info() ToolInfo {
134	if w.schema.Required == nil {
135		w.schema.Required = []string{}
136	}
137	return ToolInfo{
138		Name:        w.name,
139		Description: w.description,
140		Parameters:  schema.ToParameters(w.schema),
141		Required:    w.schema.Required,
142		Parallel:    w.parallel,
143	}
144}
145
146func (w *funcToolWrapper[TInput]) Run(ctx context.Context, params ToolCall) (ToolResponse, error) {
147	var input TInput
148	if err := json.Unmarshal([]byte(params.Input), &input); err != nil {
149		return NewTextErrorResponse(fmt.Sprintf("invalid parameters: %s", err)), nil
150	}
151
152	return w.fn(ctx, input, params)
153}