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