Add support for multiple providers
This commit is contained in:
@@ -22,6 +22,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
|
||||
"code.chimeric.al/chimerical/odidere/internal/config"
|
||||
)
|
||||
|
||||
// fn provides template functions available to argument templates.
|
||||
@@ -36,25 +38,44 @@ var fn = template.FuncMap{
|
||||
// Tools are executed as subprocesses with templated arguments.
|
||||
type Tool struct {
|
||||
// Name uniquely identifies the tool within a registry.
|
||||
Name string `yaml:"name"`
|
||||
Name string
|
||||
// Description explains the tool's purpose for the LLM.
|
||||
Description string `yaml:"description"`
|
||||
Description string
|
||||
// Command is the executable path or name.
|
||||
Command string `yaml:"command"`
|
||||
Command string
|
||||
// Arguments are Go templates expanded with LLM-provided parameters.
|
||||
// Empty results after expansion are filtered out.
|
||||
Arguments []string `yaml:"arguments"`
|
||||
Arguments []string
|
||||
// Parameters is a JSON Schema describing expected input from the LLM.
|
||||
Parameters map[string]any `yaml:"parameters"`
|
||||
// Timeout limits execution time (e.g., "30s", "5m").
|
||||
// Empty means no timeout.
|
||||
Timeout string `yaml:"timeout"`
|
||||
// timeout is the parsed duration, set during registry construction.
|
||||
timeout time.Duration `yaml:"-"`
|
||||
Parameters map[string]any
|
||||
// timeout is the parsed execution duration limit.
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewTool creates a Tool from a ToolConfig.
|
||||
func NewTool(cfg config.ToolConfig) (*Tool, error) {
|
||||
var timeout time.Duration
|
||||
if cfg.Timeout != "" {
|
||||
var err error
|
||||
timeout, err = time.ParseDuration(cfg.Timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid timeout %q: %w", cfg.Timeout, err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return &Tool{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
Command: cfg.Command,
|
||||
Arguments: cfg.Arguments,
|
||||
Parameters: cfg.Parameters,
|
||||
timeout: timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OpenAI converts the tool to an OpenAI function definition for API calls.
|
||||
func (t Tool) OpenAI() openai.Tool {
|
||||
func (t *Tool) OpenAI() openai.Tool {
|
||||
return openai.Tool{
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: &openai.FunctionDefinition{
|
||||
@@ -69,13 +90,11 @@ func (t Tool) OpenAI() openai.Tool {
|
||||
// The args parameter should be a JSON object string; empty string or "{}"
|
||||
// results in an empty data map. Templates producing empty strings are
|
||||
// filtered from the result, allowing conditional arguments.
|
||||
func (t Tool) ParseArguments(args string) ([]string, error) {
|
||||
func (t *Tool) ParseArguments(args string) ([]string, error) {
|
||||
var data = map[string]any{}
|
||||
if args != "" && args != "{}" {
|
||||
if err := json.Unmarshal([]byte(args), &data); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid arguments JSON: %w", err,
|
||||
)
|
||||
return nil, fmt.Errorf("invalid arguments JSON: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,16 +102,12 @@ func (t Tool) ParseArguments(args string) ([]string, error) {
|
||||
for _, v := range t.Arguments {
|
||||
tmpl, err := template.New("").Funcs(fn).Parse(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid template %q: %w", v, err,
|
||||
)
|
||||
return nil, fmt.Errorf("invalid template %q: %w", v, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"execute template %q: %w", v, err,
|
||||
)
|
||||
return nil, fmt.Errorf("execute template %q: %w", v, err)
|
||||
}
|
||||
|
||||
// Filter out empty strings (unused conditional arguments).
|
||||
@@ -104,35 +119,8 @@ func (t Tool) ParseArguments(args string) ([]string, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Validate checks that the tool definition is complete and valid.
|
||||
// It verifies required fields are present, the timeout (if specified)
|
||||
// is parseable, and all argument templates are syntactically valid.
|
||||
func (t Tool) Validate() error {
|
||||
if t.Name == "" {
|
||||
return fmt.Errorf("missing name")
|
||||
}
|
||||
if t.Description == "" {
|
||||
return fmt.Errorf("missing description")
|
||||
}
|
||||
if t.Command == "" {
|
||||
return fmt.Errorf("missing command")
|
||||
}
|
||||
if t.Timeout != "" {
|
||||
if _, err := time.ParseDuration(t.Timeout); err != nil {
|
||||
return fmt.Errorf("invalid timeout: %v", err)
|
||||
}
|
||||
}
|
||||
for _, arg := range t.Arguments {
|
||||
if _, err := template.New("").Funcs(fn).Parse(arg); err != nil {
|
||||
return fmt.Errorf("invalid argument template")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Registry holds tools indexed by name and handles their execution.
|
||||
// It validates all tools at construction time to fail fast on
|
||||
// It validates all tool definitions at construction time to fail fast on
|
||||
// configuration errors.
|
||||
type Registry struct {
|
||||
tools map[string]*Tool
|
||||
@@ -140,30 +128,20 @@ type Registry struct {
|
||||
|
||||
// NewRegistry creates a registry from the provided tool definitions.
|
||||
// Returns an error if any tool fails validation or if duplicate names exist.
|
||||
func NewRegistry(tools []Tool) (*Registry, error) {
|
||||
func NewRegistry(tools []config.ToolConfig) (*Registry, error) {
|
||||
var r = &Registry{
|
||||
tools: make(map[string]*Tool),
|
||||
}
|
||||
for _, t := range tools {
|
||||
if err := t.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid tool: %v", err)
|
||||
}
|
||||
if t.Timeout != "" {
|
||||
d, err := time.ParseDuration(t.Timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"parse timeout: %v", err,
|
||||
)
|
||||
}
|
||||
t.timeout = d
|
||||
for _, tc := range tools {
|
||||
t, err := NewTool(tc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid tool %q: %w", tc.Name, err)
|
||||
}
|
||||
|
||||
if _, exists := r.tools[t.Name]; exists {
|
||||
return nil, fmt.Errorf(
|
||||
"duplicate tool name: %s", t.Name,
|
||||
)
|
||||
return nil, fmt.Errorf("duplicate tool name: %s", t.Name)
|
||||
}
|
||||
r.tools[t.Name] = &t
|
||||
r.tools[t.Name] = t
|
||||
}
|
||||
|
||||
return r, nil
|
||||
@@ -185,8 +163,8 @@ func (r *Registry) List() []string {
|
||||
}
|
||||
|
||||
// Execute runs a tool by name with the provided JSON arguments.
|
||||
// It expands argument templates, executes the command as a subprocess,
|
||||
// and returns stdout on success. The context can be used for cancellation;
|
||||
// It expands argument templates, executes the command as a subprocess, and
|
||||
// returns stdout on success. The context can be used for cancellation;
|
||||
// tool-specific timeouts are applied on top of any context deadline.
|
||||
func (r *Registry) Execute(
|
||||
ctx context.Context, name string, args string,
|
||||
|
||||
Reference in New Issue
Block a user