117 lines
3.6 KiB
Go
117 lines
3.6 KiB
Go
package pluginsdk
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
// Tool is a chat tool: a function Cletus's AI can call. Name, Mutating and
|
|
// RequiresApproval must match the plugin's entry in plugin.json "tools".
|
|
type Tool struct {
|
|
Name string
|
|
Description string
|
|
// Parameters is the JSON schema of the arguments (an object schema).
|
|
Parameters map[string]any
|
|
// Mutating is true if the tool writes anything anywhere.
|
|
Mutating bool
|
|
// RequiresApproval makes Cletus ask the user before every call.
|
|
RequiresApproval bool
|
|
Run func(ctx context.Context, h *Host, call ToolCall) ToolResult
|
|
}
|
|
|
|
// ToolCall is one call of a chat tool.
|
|
type ToolCall struct {
|
|
// Args is the JSON object of arguments the AI sent.
|
|
Args json.RawMessage
|
|
// WorkingDir is the user's working folder ("" when none is open).
|
|
WorkingDir string
|
|
// EditMode is false when the user has Cletus read-only: a mutating tool
|
|
// must check Host.CanWrite before each write and refuse when it's false.
|
|
EditMode bool
|
|
}
|
|
|
|
// Decode decodes the arguments into v.
|
|
func (c ToolCall) Decode(v any) error {
|
|
if len(c.Args) == 0 {
|
|
return json.Unmarshal([]byte("{}"), v)
|
|
}
|
|
if err := json.Unmarshal(c.Args, v); err != nil {
|
|
return fmt.Errorf("the arguments don't match the tool's parameters: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ToolResult is what the AI reads back. Set Text on success, Error on
|
|
// failure (a plain sentence with the next step). Images are optional
|
|
// (PNG or JPEG bytes).
|
|
type ToolResult struct {
|
|
Text string `json:"Text,omitempty"`
|
|
Error string `json:"Error,omitempty"`
|
|
Images [][]byte `json:"Images,omitempty"`
|
|
}
|
|
|
|
// Text returns a successful result.
|
|
func Text(format string, args ...any) ToolResult {
|
|
return ToolResult{Text: fmt.Sprintf(format, args...)}
|
|
}
|
|
|
|
// Fail returns a failed result.
|
|
func Fail(format string, args ...any) ToolResult {
|
|
return ToolResult{Error: fmt.Sprintf(format, args...)}
|
|
}
|
|
|
|
type toolDescriptor struct {
|
|
Def toolDef `json:"def"`
|
|
Mutating bool `json:"mutating"`
|
|
RequiresApproval bool `json:"requires_approval"`
|
|
}
|
|
|
|
type toolDef struct {
|
|
Type string `json:"type"`
|
|
Function functionDef `json:"function"`
|
|
}
|
|
|
|
type functionDef struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Parameters map[string]any `json:"parameters"`
|
|
}
|
|
|
|
func (s *server) listTools() []toolDescriptor {
|
|
out := make([]toolDescriptor, 0, len(s.tools))
|
|
for _, t := range s.tools {
|
|
params := t.Parameters
|
|
if params == nil {
|
|
params = map[string]any{"type": "object", "properties": map[string]any{}}
|
|
}
|
|
out = append(out, toolDescriptor{
|
|
Def: toolDef{Type: "function", Function: functionDef{Name: t.Name, Description: t.Description, Parameters: params}},
|
|
Mutating: t.Mutating,
|
|
RequiresApproval: t.RequiresApproval,
|
|
})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Def.Function.Name < out[j].Def.Function.Name })
|
|
return out
|
|
}
|
|
|
|
type toolInvocation struct {
|
|
Name string `json:"name"`
|
|
Args string `json:"args"`
|
|
WorkingDir string `json:"working_dir"`
|
|
EditMode bool `json:"edit_mode"`
|
|
}
|
|
|
|
func (s *server) invokeTool(ctx context.Context, h *Host, params json.RawMessage) (ToolResult, error) {
|
|
var inv toolInvocation
|
|
if err := json.Unmarshal(params, &inv); err != nil {
|
|
return ToolResult{}, fmt.Errorf("invalid tool call: %w", err)
|
|
}
|
|
t, ok := s.tools[inv.Name]
|
|
if !ok || t.Run == nil {
|
|
return Fail("This plugin has no tool named %q.", inv.Name), nil
|
|
}
|
|
return t.Run(ctx, h, ToolCall{Args: json.RawMessage(inv.Args), WorkingDir: inv.WorkingDir, EditMode: inv.EditMode}), nil
|
|
}
|