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}