diff --git a/Makefile b/Makefile index 36d6918..5727abe 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ build: build-go build-docs ## Build the project # Run the Go project .PHONY: run -run: build ## Build and run the Go project +run: build-go ## Build and run the Go project ./$(BUILD_DIR)/$(BINARY_NAME) # Clean up the build artifacts diff --git a/cmd/ayo/chat.go b/cmd/ayo/chat.go index 0506765..7e911a2 100644 --- a/cmd/ayo/chat.go +++ b/cmd/ayo/chat.go @@ -18,7 +18,7 @@ func (c *Chat) Run(ctx context.Context, cli *CLI) error { log := log.FromContext(ctx).With("caller", logPackagePrefix+"Chat:Run") log.Debug("loading tools", "dir", cli.Toolbox) - tools, err := tool.LoadTools(cli.Toolbox) + tools, err := tool.LoadAll(cli.Toolbox) if err != nil { return err } diff --git a/cmd/ayo/main.go b/cmd/ayo/main.go index 1b402d6..ae0d598 100644 --- a/cmd/ayo/main.go +++ b/cmd/ayo/main.go @@ -17,6 +17,7 @@ type CLI struct { Chat Chat `cmd:"" help:"Send a message to the chatbot" default:"withargs"` Version Version `cmd:"" help:"Show the version information"` + Tool Tool `cmd:"" help:"Get information about a tool"` } func main() { diff --git a/cmd/ayo/tool.go b/cmd/ayo/tool.go new file mode 100644 index 0000000..e5b6d5b --- /dev/null +++ b/cmd/ayo/tool.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "strings" + "text/tabwriter" + + "github.com/alecthomas/kong" + + "github.com/banst/ayo/pkg/tool" +) + +type Tool struct { + Tools []string `arg:"" optional:"" help:"Filename of the tool to get information about. If empty, all tools from the toolbox are listed." type:"existingfile"` //nolint:lll // Struct tags are long. +} + +func (t *Tool) Run(context *kong.Context, cli *CLI) error { + tw := tabwriter.NewWriter(context.Stdout, 0, 0, 3, ' ', 0) + + if len(t.Tools) == 0 { + var err error + t.Tools, err = tool.ListFiles(cli.Toolbox) + if err != nil { + return err + } + } + + for i, tf := range t.Tools { + if i > 0 { + if _, err := fmt.Fprintln(tw); err != nil { + return err + } + } + + if _, err := fmt.Fprintf(tw, "File:\t%s\n", tf); err != nil { + return err + } + + if t, err := tool.Load(tf); err != nil { + if _, err := fmt.Fprintf(tw, "Error:\t%s\n", err); err != nil { + return err + } + } else { + params := []string{} + for name, p := range t.Function.Parameters.Properties { + params = append(params, fmt.Sprintf("%s %s", name, p.Type)) + } + function := fmt.Sprintf("%s(%s)", t.Function.Name, strings.Join(params, ", ")) + + cmd := strings.Join(append([]string{t.Cmd}, t.Args...), " ") + + format := "Function:\t%s\nDescription:\t%s\nCommand:\t%s\n" + if _, err := fmt.Fprintf(tw, format, function, t.Function.Description, cmd); err != nil { + return err + } + } + + if err := tw.Flush(); err != nil { + return err + } + } + + return nil +} diff --git a/go.mod b/go.mod index d777084..705630e 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,20 @@ toolchain go1.22.6 require ( github.com/alecthomas/kong v0.9.0 github.com/lmittmann/tint v1.0.5 + github.com/muesli/termenv v0.15.2 github.com/ollama/ollama v0.3.6 github.com/stretchr/testify v1.9.0 golang.org/x/sync v0.3.0 ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/sys v0.20.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 17d0586..f5f59ae 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -12,14 +14,27 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw= github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/ollama/ollama v0.3.6 h1:nA/N0AmjP327po5cZDGLqI40nl+aeei0pD0dLa92ypE= github.com/ollama/ollama v0.3.6/go.mod h1:YrWoNkFnPOYsnDvsf/Ztb1wxU9/IXrNsQHqcxbY2r94= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/tool/tool.go b/pkg/tool/tool.go index 2b4697d..2a9e072 100644 --- a/pkg/tool/tool.go +++ b/pkg/tool/tool.go @@ -11,6 +11,8 @@ import ( "golang.org/x/sync/errgroup" ) +const Ext = ".json" + type Tool struct { api.Tool Cmd string `json:"cmd"` @@ -26,8 +28,8 @@ func (t *Tool) Run(values map[string]any) ([]byte, error) { return exec.Command(t.Cmd, args...).CombinedOutput() //nolint:gosec // This is a tool runner. } -// LoadTool loads a tool from a json file. -func LoadTool(file string) (*Tool, error) { +// Load loads a tool from a json file. +func Load(file string) (*Tool, error) { f, err := os.Open(file) if err != nil { return nil, err @@ -43,22 +45,10 @@ func LoadTool(file string) (*Tool, error) { return &tool, nil } -// LoadTools loads a list of tools from a directory. -func LoadTools(dir string) (map[string]*Tool, error) { +// LoadAll loads a list of tools from a directory. +func LoadAll(dir string) (map[string]*Tool, error) { // Get the list of json files in the directory - var files []string - err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() && filepath.Ext(path) == ".json" { - files = append(files, path) - } - return nil - }) - if err != nil { - return nil, err - } + files, err := ListFiles(dir) // Load the tools concurrently, pool of 10 goroutines max tools := make(map[string]*Tool, len(files)) @@ -68,7 +58,7 @@ func LoadTools(dir string) (map[string]*Tool, error) { for _, f := range files { file := f // capture the loop variable eg.Go(func() error { - tool, err := LoadTool(file) //nolint:govet // Shadowing err is fine here. + tool, err := Load(file) //nolint:govet // Shadowing err is fine here. if err != nil { return err } @@ -85,3 +75,22 @@ func LoadTools(dir string) (map[string]*Tool, error) { } return tools, nil } + +// ListToolFiles lists the tool files in a directory. +func ListFiles(dir string) ([]string, error) { + // Get the list of json files in the directory + var files []string + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && filepath.Ext(path) == Ext { + files = append(files, path) + } + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} diff --git a/pkg/tool/tool_test.go b/pkg/tool/tool_test.go index e30912d..9c4b59e 100644 --- a/pkg/tool/tool_test.go +++ b/pkg/tool/tool_test.go @@ -16,7 +16,7 @@ func TestLoadTool(t *testing.T) { t.Run("missing file", func(t *testing.T) { t.Parallel() - _, err := LoadTool("missing.json") + _, err := Load("missing.json") if assert.Error(t, err) { assert.Contains(t, err.Error(), "no such file or directory") } @@ -30,7 +30,7 @@ func TestLoadTool(t *testing.T) { _, err = f.WriteString("bad json") require.NoError(t, err) - _, err = LoadTool(f.Name()) + _, err = Load(f.Name()) if assert.Error(t, err) { assert.Contains(t, err.Error(), "invalid character") } @@ -55,7 +55,7 @@ func TestLoadTool(t *testing.T) { err = json.NewEncoder(f).Encode(tool) require.NoError(t, err) - actual, err := LoadTool(f.Name()) + actual, err := Load(f.Name()) assert.NoError(t, err) assert.Equal(t, tool, actual) }) @@ -67,7 +67,7 @@ func TestLoadTools(t *testing.T) { t.Run("missing directory", func(t *testing.T) { t.Parallel() - _, err := LoadTools("missing") + _, err := LoadAll("missing") if assert.Error(t, err) { assert.Contains(t, err.Error(), "no such file or directory") } @@ -77,7 +77,7 @@ func TestLoadTools(t *testing.T) { t.Parallel() dir := t.TempDir() - tools, err := LoadTools(dir) + tools, err := LoadAll(dir) assert.NoError(t, err) assert.Empty(t, tools) }) @@ -91,7 +91,7 @@ func TestLoadTools(t *testing.T) { _, err = f.WriteString("bad json") require.NoError(t, err) - _, err = LoadTools(dir) + _, err = LoadAll(dir) if assert.Error(t, err) { assert.Contains(t, err.Error(), "invalid character") } @@ -132,7 +132,7 @@ func TestLoadTools(t *testing.T) { require.NoError(t, err) } - actual, err := LoadTools(dir) + actual, err := LoadAll(dir) assert.NoError(t, err) assert.Equal(t, tools, actual) })