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}