Introduce "agents"
An agent is currently a name given to a system prompt and a set of tools which the agent has access to. This resolves the previous issue of the set of configured tools being available in *all* contexts, which wasn't always desired. Tools are now only available when an agent is explicitly requested using the `-a/--agent` flag. Agents are expected to be expanded on: the concept of task-specilized agents (e.g. coding), the ability to define a set of files an agent should always have access to for RAG purposes, etc. Other changes: - Removes the "tools" top-level config structure (though this is expected to come back along with the abillity to define custom tools). - Renamed `pkg/agent` to `pkg/agents`
This commit is contained in:
142
pkg/agents/toolbox/dir_tree.go
Normal file
142
pkg/agents/toolbox/dir_tree.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package toolbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||
)
|
||||
|
||||
const TREE_DESCRIPTION = `Retrieve a tree-like view of a directory's contents.
|
||||
|
||||
Use these results for your own reference in completing your task, they do not need to be shown to the user.
|
||||
|
||||
Example result:
|
||||
{
|
||||
"message": "success",
|
||||
"result": ".
|
||||
├── a_directory/
|
||||
│ ├── file1.txt (100 bytes)
|
||||
│ └── file2.txt (200 bytes)
|
||||
├── a_file.txt (123 bytes)
|
||||
└── another_file.txt (456 bytes)"
|
||||
}
|
||||
`
|
||||
|
||||
var DirTreeTool = api.ToolSpec{
|
||||
Name: "dir_tree",
|
||||
Description: TREE_DESCRIPTION,
|
||||
Parameters: []api.ToolParameter{
|
||||
{
|
||||
Name: "relative_path",
|
||||
Type: "string",
|
||||
Description: "If set, display the tree starting from this path relative to the current one.",
|
||||
},
|
||||
{
|
||||
Name: "depth",
|
||||
Type: "integer",
|
||||
Description: "Depth of directory recursion. Defaults to 0 (no recursion), maximum of 5.",
|
||||
},
|
||||
},
|
||||
Impl: func(tool *api.ToolSpec, args map[string]interface{}) (string, error) {
|
||||
var relativeDir string
|
||||
if tmp, ok := args["relative_path"]; ok {
|
||||
relativeDir, ok = tmp.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("expected string for relative_path, got %T", tmp)
|
||||
}
|
||||
}
|
||||
|
||||
var depth int = 0 // Default value if not provided
|
||||
if tmp, ok := args["depth"]; ok {
|
||||
switch v := tmp.(type) {
|
||||
case float64:
|
||||
depth = int(v)
|
||||
case string:
|
||||
var err error
|
||||
if depth, err = strconv.Atoi(v); err != nil {
|
||||
return "", fmt.Errorf("invalid `depth` value, expected integer but got string that cannot convert: %v", tmp)
|
||||
}
|
||||
depth = max(0, min(5, depth))
|
||||
default:
|
||||
return "", fmt.Errorf("expected int or string for max_depth, got %T", tmp)
|
||||
}
|
||||
}
|
||||
|
||||
result := tree(relativeDir, depth)
|
||||
ret, err := result.ToJson()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not serialize result: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
},
|
||||
}
|
||||
|
||||
func tree(path string, depth int) api.CallResult {
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
ok, reason := toolutil.IsPathWithinCWD(path)
|
||||
if !ok {
|
||||
return api.CallResult{Message: reason}
|
||||
}
|
||||
|
||||
var treeOutput strings.Builder
|
||||
treeOutput.WriteString(path + "\n")
|
||||
err := buildTree(&treeOutput, path, "", depth)
|
||||
if err != nil {
|
||||
return api.CallResult{
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
return api.CallResult{Result: treeOutput.String()}
|
||||
}
|
||||
|
||||
func buildTree(output *strings.Builder, path string, prefix string, depth int) error {
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, file := range files {
|
||||
if strings.HasPrefix(file.Name(), ".") {
|
||||
// Skip hidden files and directories
|
||||
continue
|
||||
}
|
||||
|
||||
isLast := i == len(files)-1
|
||||
var branch string
|
||||
if isLast {
|
||||
branch = "└── "
|
||||
} else {
|
||||
branch = "├── "
|
||||
}
|
||||
|
||||
info, _ := file.Info()
|
||||
size := info.Size()
|
||||
sizeStr := fmt.Sprintf(" (%d bytes)", size)
|
||||
|
||||
output.WriteString(prefix + branch + file.Name())
|
||||
if file.IsDir() {
|
||||
output.WriteString("/\n")
|
||||
if depth > 0 {
|
||||
var nextPrefix string
|
||||
if isLast {
|
||||
nextPrefix = prefix + " "
|
||||
} else {
|
||||
nextPrefix = prefix + "│ "
|
||||
}
|
||||
buildTree(output, filepath.Join(path, file.Name()), nextPrefix, depth-1)
|
||||
}
|
||||
} else {
|
||||
output.WriteString(sizeStr + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
114
pkg/agents/toolbox/file_insert_lines.go
Normal file
114
pkg/agents/toolbox/file_insert_lines.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package toolbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||
)
|
||||
|
||||
const FILE_INSERT_LINES_DESCRIPTION = `Insert lines into a file, must specify path.
|
||||
|
||||
Make sure your inserts match the flow and indentation of surrounding content.`
|
||||
|
||||
var FileInsertLinesTool = api.ToolSpec{
|
||||
Name: "file_insert_lines",
|
||||
Description: FILE_INSERT_LINES_DESCRIPTION,
|
||||
Parameters: []api.ToolParameter{
|
||||
{
|
||||
Name: "path",
|
||||
Type: "string",
|
||||
Description: "Path of the file to be modified, relative to the current working directory.",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "position",
|
||||
Type: "integer",
|
||||
Description: `Which line to insert content *before*.`,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "content",
|
||||
Type: "string",
|
||||
Description: `The content to insert.`,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Impl: func(tool *api.ToolSpec, args map[string]interface{}) (string, error) {
|
||||
tmp, ok := args["path"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("path parameter to write_file was not included.")
|
||||
}
|
||||
path, ok := tmp.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp)
|
||||
}
|
||||
var position int
|
||||
tmp, ok = args["position"]
|
||||
if ok {
|
||||
tmp, ok := tmp.(float64)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Invalid position in function arguments: %v", tmp)
|
||||
}
|
||||
position = int(tmp)
|
||||
}
|
||||
var content string
|
||||
tmp, ok = args["content"]
|
||||
if ok {
|
||||
content, ok = tmp.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Invalid content in function arguments: %v", tmp)
|
||||
}
|
||||
}
|
||||
|
||||
result := fileInsertLines(path, position, content)
|
||||
ret, err := result.ToJson()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not serialize result: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
},
|
||||
}
|
||||
|
||||
func fileInsertLines(path string, position int, content string) api.CallResult {
|
||||
ok, reason := toolutil.IsPathWithinCWD(path)
|
||||
if !ok {
|
||||
return api.CallResult{Message: reason}
|
||||
}
|
||||
|
||||
// Read the existing file's content
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return api.CallResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())}
|
||||
}
|
||||
_, err = os.Create(path)
|
||||
if err != nil {
|
||||
return api.CallResult{Message: fmt.Sprintf("Could not create new file: %s", err.Error())}
|
||||
}
|
||||
data = []byte{}
|
||||
}
|
||||
|
||||
if position < 1 {
|
||||
return api.CallResult{Message: "start_line cannot be less than 1"}
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
contentLines := strings.Split(strings.Trim(content, "\n"), "\n")
|
||||
|
||||
before := lines[:position-1]
|
||||
after := lines[position-1:]
|
||||
lines = append(before, append(contentLines, after...)...)
|
||||
|
||||
newContent := strings.Join(lines, "\n")
|
||||
|
||||
// Join the lines and write back to the file
|
||||
err = os.WriteFile(path, []byte(newContent), 0644)
|
||||
if err != nil {
|
||||
return api.CallResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())}
|
||||
}
|
||||
|
||||
return api.CallResult{Result: newContent}
|
||||
}
|
||||
133
pkg/agents/toolbox/file_replace_lines.go
Normal file
133
pkg/agents/toolbox/file_replace_lines.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package toolbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||
)
|
||||
|
||||
const FILE_REPLACE_LINES_DESCRIPTION = `Replace or remove a range of lines within a file, must specify path.
|
||||
|
||||
Useful for re-writing snippets/blocks of code or entire functions.
|
||||
|
||||
Plan your edits carefully and ensure any new content matches the flow and indentation of surrounding text.`
|
||||
|
||||
var FileReplaceLinesTool = api.ToolSpec{
|
||||
Name: "file_replace_lines",
|
||||
Description: FILE_REPLACE_LINES_DESCRIPTION,
|
||||
Parameters: []api.ToolParameter{
|
||||
{
|
||||
Name: "path",
|
||||
Type: "string",
|
||||
Description: "Path of the file to be modified, relative to the current working directory.",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "start_line",
|
||||
Type: "integer",
|
||||
Description: `Line number which specifies the start of the replacement range (inclusive).`,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "end_line",
|
||||
Type: "integer",
|
||||
Description: `Line number which specifies the end of the replacement range (inclusive). If unset, range extends to end of file.`,
|
||||
},
|
||||
{
|
||||
Name: "content",
|
||||
Type: "string",
|
||||
Description: `Content to replace specified range. Omit to remove the specified range.`,
|
||||
},
|
||||
},
|
||||
Impl: func(tool *api.ToolSpec, args map[string]interface{}) (string, error) {
|
||||
tmp, ok := args["path"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("path parameter to write_file was not included.")
|
||||
}
|
||||
path, ok := tmp.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp)
|
||||
}
|
||||
var start_line int
|
||||
tmp, ok = args["start_line"]
|
||||
if ok {
|
||||
tmp, ok := tmp.(float64)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Invalid start_line in function arguments: %v", tmp)
|
||||
}
|
||||
start_line = int(tmp)
|
||||
}
|
||||
var end_line int
|
||||
tmp, ok = args["end_line"]
|
||||
if ok {
|
||||
tmp, ok := tmp.(float64)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Invalid end_line in function arguments: %v", tmp)
|
||||
}
|
||||
end_line = int(tmp)
|
||||
}
|
||||
var content string
|
||||
tmp, ok = args["content"]
|
||||
if ok {
|
||||
content, ok = tmp.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Invalid content in function arguments: %v", tmp)
|
||||
}
|
||||
}
|
||||
|
||||
result := fileReplaceLines(path, start_line, end_line, content)
|
||||
ret, err := result.ToJson()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not serialize result: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
},
|
||||
}
|
||||
|
||||
func fileReplaceLines(path string, startLine int, endLine int, content string) api.CallResult {
|
||||
ok, reason := toolutil.IsPathWithinCWD(path)
|
||||
if !ok {
|
||||
return api.CallResult{Message: reason}
|
||||
}
|
||||
|
||||
// Read the existing file's content
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return api.CallResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())}
|
||||
}
|
||||
_, err = os.Create(path)
|
||||
if err != nil {
|
||||
return api.CallResult{Message: fmt.Sprintf("Could not create new file: %s", err.Error())}
|
||||
}
|
||||
data = []byte{}
|
||||
}
|
||||
|
||||
if startLine < 1 {
|
||||
return api.CallResult{Message: "start_line cannot be less than 1"}
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
contentLines := strings.Split(strings.Trim(content, "\n"), "\n")
|
||||
|
||||
if endLine == 0 || endLine > len(lines) {
|
||||
endLine = len(lines)
|
||||
}
|
||||
|
||||
before := lines[:startLine-1]
|
||||
after := lines[endLine:]
|
||||
|
||||
lines = append(before, append(contentLines, after...)...)
|
||||
newContent := strings.Join(lines, "\n")
|
||||
|
||||
// Join the lines and write back to the file
|
||||
err = os.WriteFile(path, []byte(newContent), 0644)
|
||||
if err != nil {
|
||||
return api.CallResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())}
|
||||
}
|
||||
|
||||
return api.CallResult{Result: newContent}
|
||||
}
|
||||
100
pkg/agents/toolbox/read_dir.go
Normal file
100
pkg/agents/toolbox/read_dir.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package toolbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||
)
|
||||
|
||||
const READ_DIR_DESCRIPTION = `Return the contents of the CWD (current working directory).
|
||||
|
||||
Example result:
|
||||
{
|
||||
"message": "success",
|
||||
"result": [
|
||||
{"name": "a_file.txt", "type": "file", "size": 123},
|
||||
{"name": "a_directory/", "type": "dir", "size": 11},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
For files, size represents the size of the file, in bytes.
|
||||
For directories, size represents the number of entries in that directory.`
|
||||
|
||||
var ReadDirTool = api.ToolSpec{
|
||||
Name: "read_dir",
|
||||
Description: READ_DIR_DESCRIPTION,
|
||||
Parameters: []api.ToolParameter{
|
||||
{
|
||||
Name: "relative_dir",
|
||||
Type: "string",
|
||||
Description: "If set, read the contents of a directory relative to the current one.",
|
||||
},
|
||||
},
|
||||
Impl: func(tool *api.ToolSpec, args map[string]interface{}) (string, error) {
|
||||
var relativeDir string
|
||||
tmp, ok := args["relative_dir"]
|
||||
if ok {
|
||||
relativeDir, ok = tmp.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Invalid relative_dir in function arguments: %v", tmp)
|
||||
}
|
||||
}
|
||||
result := readDir(relativeDir)
|
||||
ret, err := result.ToJson()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not serialize result: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
},
|
||||
}
|
||||
|
||||
func readDir(path string) api.CallResult {
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
ok, reason := toolutil.IsPathWithinCWD(path)
|
||||
if !ok {
|
||||
return api.CallResult{Message: reason}
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return api.CallResult{
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
var dirContents []map[string]interface{}
|
||||
for _, f := range files {
|
||||
info, _ := f.Info()
|
||||
|
||||
name := f.Name()
|
||||
if strings.HasPrefix(name, ".") {
|
||||
// skip hidden files
|
||||
continue
|
||||
}
|
||||
|
||||
entryType := "file"
|
||||
size := info.Size()
|
||||
|
||||
if info.IsDir() {
|
||||
name += "/"
|
||||
entryType = "dir"
|
||||
subdirfiles, _ := os.ReadDir(filepath.Join(".", path, info.Name()))
|
||||
size = int64(len(subdirfiles))
|
||||
}
|
||||
|
||||
dirContents = append(dirContents, map[string]interface{}{
|
||||
"name": name,
|
||||
"type": entryType,
|
||||
"size": size,
|
||||
})
|
||||
}
|
||||
|
||||
return api.CallResult{Result: dirContents}
|
||||
}
|
||||
73
pkg/agents/toolbox/read_file.go
Normal file
73
pkg/agents/toolbox/read_file.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package toolbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||
)
|
||||
|
||||
const READ_FILE_DESCRIPTION = `Retrieve the contents of a text file relative to the current working directory.
|
||||
|
||||
Use the file contents for your own reference in completing your task, they do not need to be shown to the user.
|
||||
|
||||
Each line of the returned content is prefixed with its line number and a tab (\t).
|
||||
|
||||
Example result:
|
||||
{
|
||||
"message": "success",
|
||||
"result": "1\tthe contents\n2\tof the file\n"
|
||||
}`
|
||||
|
||||
var ReadFileTool = api.ToolSpec{
|
||||
Name: "read_file",
|
||||
Description: READ_FILE_DESCRIPTION,
|
||||
Parameters: []api.ToolParameter{
|
||||
{
|
||||
Name: "path",
|
||||
Type: "string",
|
||||
Description: "Path to a file within the current working directory to read.",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
|
||||
Impl: func(tool *api.ToolSpec, args map[string]interface{}) (string, error) {
|
||||
tmp, ok := args["path"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Path parameter to read_file was not included.")
|
||||
}
|
||||
path, ok := tmp.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp)
|
||||
}
|
||||
result := readFile(path)
|
||||
ret, err := result.ToJson()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not serialize result: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
},
|
||||
}
|
||||
|
||||
func readFile(path string) api.CallResult {
|
||||
ok, reason := toolutil.IsPathWithinCWD(path)
|
||||
if !ok {
|
||||
return api.CallResult{Message: reason}
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return api.CallResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())}
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
content := strings.Builder{}
|
||||
for i, line := range lines {
|
||||
content.WriteString(fmt.Sprintf("%d\t%s\n", i+1, line))
|
||||
}
|
||||
|
||||
return api.CallResult{
|
||||
Result: content.String(),
|
||||
}
|
||||
}
|
||||
67
pkg/agents/toolbox/util/util.go
Normal file
67
pkg/agents/toolbox/util/util.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// isPathContained attempts to verify whether `path` is the same as or
|
||||
// contained within `directory`. It is overly cautious, returning false even if
|
||||
// `path` IS contained within `directory`, but the two paths use different
|
||||
// casing, and we happen to be on a case-insensitive filesystem.
|
||||
// This is ultimately to attempt to stop an LLM from going outside of where I
|
||||
// tell it to. Additional layers of security should be considered.. run in a
|
||||
// VM/container.
|
||||
func IsPathContained(directory string, path string) (bool, error) {
|
||||
// Clean and resolve symlinks for both paths
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// check if path exists
|
||||
_, err = os.Stat(path)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return false, fmt.Errorf("Could not stat path: %v", err)
|
||||
}
|
||||
} else {
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
directory, err = filepath.Abs(directory)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
directory, err = filepath.EvalSymlinks(directory)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Case insensitive checks
|
||||
if !strings.EqualFold(path, directory) &&
|
||||
!strings.HasPrefix(strings.ToLower(path), strings.ToLower(directory)+string(os.PathSeparator)) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func IsPathWithinCWD(path string) (bool, string) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return false, "Failed to determine current working directory"
|
||||
}
|
||||
if ok, err := IsPathContained(cwd, path); !ok {
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("Could not determine whether path '%s' is within the current working directory: %s", path, err.Error())
|
||||
}
|
||||
return false, fmt.Sprintf("Path '%s' is not within the current working directory", path)
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
71
pkg/agents/toolbox/write_file.go
Normal file
71
pkg/agents/toolbox/write_file.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package toolbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||
)
|
||||
|
||||
const WRITE_FILE_DESCRIPTION = `Write the provided contents to a file relative to the current working directory.
|
||||
|
||||
Example result:
|
||||
{
|
||||
"message": "success"
|
||||
}`
|
||||
|
||||
var WriteFileTool = api.ToolSpec{
|
||||
Name: "write_file",
|
||||
Description: WRITE_FILE_DESCRIPTION,
|
||||
Parameters: []api.ToolParameter{
|
||||
{
|
||||
Name: "path",
|
||||
Type: "string",
|
||||
Description: "Path to a file within the current working directory to write to.",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "content",
|
||||
Type: "string",
|
||||
Description: "The content to write to the file. Overwrites any existing content!",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Impl: func(t *api.ToolSpec, args map[string]interface{}) (string, error) {
|
||||
tmp, ok := args["path"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Path parameter to write_file was not included.")
|
||||
}
|
||||
path, ok := tmp.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp)
|
||||
}
|
||||
tmp, ok = args["content"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Content parameter to write_file was not included.")
|
||||
}
|
||||
content, ok := tmp.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Invalid content in function arguments: %v", tmp)
|
||||
}
|
||||
result := writeFile(path, content)
|
||||
ret, err := result.ToJson()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not serialize result: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
},
|
||||
}
|
||||
|
||||
func writeFile(path string, content string) api.CallResult {
|
||||
ok, reason := toolutil.IsPathWithinCWD(path)
|
||||
if !ok {
|
||||
return api.CallResult{Message: reason}
|
||||
}
|
||||
err := os.WriteFile(path, []byte(content), 0644)
|
||||
if err != nil {
|
||||
return api.CallResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())}
|
||||
}
|
||||
return api.CallResult{}
|
||||
}
|
||||
48
pkg/agents/tools.go
Normal file
48
pkg/agents/tools.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.mlow.ca/mlow/lmcli/pkg/agents/toolbox"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||
)
|
||||
|
||||
var AvailableTools map[string]api.ToolSpec = map[string]api.ToolSpec{
|
||||
"dir_tree": toolbox.DirTreeTool,
|
||||
"read_dir": toolbox.ReadDirTool,
|
||||
"read_file": toolbox.ReadFileTool,
|
||||
"write_file": toolbox.WriteFileTool,
|
||||
"file_insert_lines": toolbox.FileInsertLinesTool,
|
||||
"file_replace_lines": toolbox.FileReplaceLinesTool,
|
||||
}
|
||||
|
||||
func ExecuteToolCalls(calls []api.ToolCall, available []api.ToolSpec) ([]api.ToolResult, error) {
|
||||
var toolResults []api.ToolResult
|
||||
for _, call := range calls {
|
||||
var tool *api.ToolSpec
|
||||
for i := range available {
|
||||
if available[i].Name == call.Name {
|
||||
tool = &available[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if tool == nil {
|
||||
return nil, fmt.Errorf("Requested tool '%s' is not available. Hallucination?", call.Name)
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
result, err := tool.Impl(tool, call.Parameters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Tool '%s' error: %v\n", call.Name, err)
|
||||
}
|
||||
|
||||
toolResult := api.ToolResult{
|
||||
ToolCallID: call.ID,
|
||||
ToolName: call.Name,
|
||||
Result: result,
|
||||
}
|
||||
|
||||
toolResults = append(toolResults, toolResult)
|
||||
}
|
||||
return toolResults, nil
|
||||
}
|
||||
Reference in New Issue
Block a user