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	// Data contains binary data for image/media responses (e.g., image bytes, audio data).
 36	Data []byte `json:"data,omitempty"`
 37	// MediaType specifies the MIME type of the media (e.g., "image/png", "audio/wav").
 38	MediaType string `json:"media_type,omitempty"`
 39	Metadata  string `json:"metadata,omitempty"`
 40	IsError   bool   `json:"is_error"`
 41}
 42
 43// NewTextResponse creates a text response.
 44func NewTextResponse(content string) ToolResponse {
 45	return ToolResponse{
 46		Type:    "text",
 47		Content: content,
 48	}
 49}
 50
 51// NewTextErrorResponse creates an error response.
 52func NewTextErrorResponse(content string) ToolResponse {
 53	return ToolResponse{
 54		Type:    "text",
 55		Content: content,
 56		IsError: true,
 57	}
 58}
 59
 60// NewImageResponse creates an image response with binary data.
 61func NewImageResponse(data []byte, mediaType string) ToolResponse {
 62	return ToolResponse{
 63		Type:      "image",
 64		Data:      data,
 65		MediaType: mediaType,
 66	}
 67}
 68
 69// NewMediaResponse creates a media response with binary data (e.g., audio, video).
 70func NewMediaResponse(data []byte, mediaType string) ToolResponse {
 71	return ToolResponse{
 72		Type:      "media",
 73		Data:      data,
 74		MediaType: mediaType,
 75	}
 76}
 77
 78// WithResponseMetadata adds metadata to a response.
 79func WithResponseMetadata(response ToolResponse, metadata any) ToolResponse {
 80	if metadata != nil {
 81		metadataBytes, err := json.Marshal(metadata)
 82		if err != nil {
 83			return response
 84		}
 85		response.Metadata = string(metadataBytes)
 86	}
 87	return response
 88}
 89
 90// AgentTool represents a tool that can be called by a language model.
 91// This matches the existing BaseTool interface pattern.
 92type AgentTool interface {
 93	Info() ToolInfo
 94	Run(ctx context.Context, params ToolCall) (ToolResponse, error)
 95	ProviderOptions() ProviderOptions
 96	SetProviderOptions(opts ProviderOptions)
 97}
 98
 99// NewAgentTool creates a typed tool from a function with automatic schema generation.
100// This is the recommended way to create tools.
101func NewAgentTool[TInput any](
102	name string,
103	description string,
104	fn func(ctx context.Context, input TInput, call ToolCall) (ToolResponse, error),
105) AgentTool {
106	var input TInput
107	schema := schema.Generate(reflect.TypeOf(input))
108
109	return &funcToolWrapper[TInput]{
110		name:        name,
111		description: description,
112		fn:          fn,
113		schema:      schema,
114		parallel:    false, // Default to sequential execution
115	}
116}
117
118// NewParallelAgentTool creates a typed tool from a function with automatic schema generation.
119// This also marks a tool as safe to run in parallel with other tools.
120func NewParallelAgentTool[TInput any](
121	name string,
122	description string,
123	fn func(ctx context.Context, input TInput, call ToolCall) (ToolResponse, error),
124) AgentTool {
125	tool := NewAgentTool(name, description, fn)
126	// Try to use the SetParallel method if available
127	if setter, ok := tool.(interface{ SetParallel(bool) }); ok {
128		setter.SetParallel(true)
129	}
130	return tool
131}
132
133// funcToolWrapper wraps a function to implement the AgentTool interface.
134type funcToolWrapper[TInput any] struct {
135	name            string
136	description     string
137	fn              func(ctx context.Context, input TInput, call ToolCall) (ToolResponse, error)
138	schema          Schema
139	providerOptions ProviderOptions
140	parallel        bool
141}
142
143func (w *funcToolWrapper[TInput]) SetProviderOptions(opts ProviderOptions) {
144	w.providerOptions = opts
145}
146
147func (w *funcToolWrapper[TInput]) ProviderOptions() ProviderOptions {
148	return w.providerOptions
149}
150
151func (w *funcToolWrapper[TInput]) SetParallel(parallel bool) {
152	w.parallel = parallel
153}
154
155func (w *funcToolWrapper[TInput]) Info() ToolInfo {
156	if w.schema.Required == nil {
157		w.schema.Required = []string{}
158	}
159	return ToolInfo{
160		Name:        w.name,
161		Description: w.description,
162		Parameters:  schema.ToParameters(w.schema),
163		Required:    w.schema.Required,
164		Parallel:    w.parallel,
165	}
166}
167
168func (w *funcToolWrapper[TInput]) Run(ctx context.Context, params ToolCall) (ToolResponse, error) {
169	var input TInput
170	if err := json.Unmarshal([]byte(params.Input), &input); err != nil {
171		return NewTextErrorResponse(fmt.Sprintf("invalid parameters: %s", err)), nil
172	}
173
174	return w.fn(ctx, input, params)
175}