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}