Add modify_file tool

Removed file_insert_lines, file_replace_lines
This commit is contained in:
Matt Low 2024-07-01 08:09:04 +00:00
parent c68084f8a5
commit 4ef841e945
5 changed files with 195 additions and 148 deletions

View File

@ -1,133 +0,0 @@
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}
}

View File

@ -0,0 +1,178 @@
package toolbox
import (
"fmt"
"os"
"strings"
"git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
"git.mlow.ca/mlow/lmcli/pkg/api"
)
var MODIFY_FILE_DESCRIPTION = []string{
"Modify a file. If the file does not exist, it will be created.",
"",
"Content can be either inserted, replaced, or removed through a combination of the start, stop, and content parameters.",
"Use the start and stop line numbers to limit the range of modification to the file.",
"If both `start` and `stop` are left unset (or set to 0), the entire file's contents will be updated.",
"If `start` is set to n and `stop` to n+1, content will be inserted at line n (the content that was at line n will be shifted below the newly inserted content).",
"If only `start` is set, content from the given line and onwards will be updated.",
"If only `stop` is set, content up to but not including the given line will be updated.",
"",
"Examples:",
"1. Append to a file:",
" {\"path\": \"example.txt\", \"start\": <last_line_number + 1>, \"content\": \"New content to append\"}",
"",
"2. Insert at a specific line:",
" {\"path\": \"example.txt\", \"start\": 5, \"stop\": 5, \"content\": \"New line inserted above the previous line 5\"}",
"",
"3. Replace a range of lines:",
" {\"path\": \"example.txt\", \"start\": 3, \"stop\": 7, \"content\": \"New content replacing lines 3-7\"}",
"",
"4. Remove a range of lines:",
" {\"path\": \"example.txt\", \"start\": 2, \"stop\": 5}",
"",
"5. Replace entire file contents:",
" {\"path\": \"example.txt\", \"content\": \"New file contents\"}",
"",
"6. Update from a specific line to the end of the file:",
" {\"path\": \"example.txt\", \"start\": 10, \"content\": \"New content from line 10 onwards\"}",
"",
"7. Update from the beginning of the file to a specific line:",
" {\"path\": \"example.txt\", \"stop\": 6, \"content\": \"New content for first 5 lines\"}",
"",
"Note: Always use specific line numbers based on the current file content. Avoid using arbitrarily large numbers for start or stop.",
}
var ModifyFile = api.ToolSpec{
Name: "modify_file",
Description: strings.Join(MODIFY_FILE_DESCRIPTION, "\n"),
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",
Type: "integer",
Description: `Start line of the range to modify (inclusive). If omitted, the beginning of the file is implied.`,
},
{
Name: "stop",
Type: "integer",
Description: `End line of the range to modify (inclusive). If omitted, the end of the file is implied.`,
},
{
Name: "content",
Type: "string",
Description: "Content to insert/replace at the range defined by `start` and `stop`. If omitted, the range is removed.",
},
},
Impl: func(tool *api.ToolSpec, args map[string]interface{}) (string, error) {
tmp, ok := args["path"]
if !ok {
return "", fmt.Errorf("path parameter to modify_file was not included.")
}
path, ok := tmp.(string)
if !ok {
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp)
}
var start int
tmp, ok = args["start"]
if ok {
tmp, ok := tmp.(float64)
if !ok {
return "", fmt.Errorf("Invalid start in function arguments: %v", tmp)
}
start = int(tmp)
}
var stop int
tmp, ok = args["stop"]
if ok {
tmp, ok := tmp.(float64)
if !ok {
return "", fmt.Errorf("Invalid stop in function arguments: %v", tmp)
}
stop = 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 := fileModifyContents(path, start, stop, content)
ret, err := result.ToJson()
if err != nil {
return "", fmt.Errorf("Could not serialize result: %v", err)
}
return ret, nil
},
}
func fileModifyContents(path string, startLine int, stopLine 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{}
}
lines := strings.Split(string(data), "\n")
contentLines := strings.Split(strings.TrimSuffix(content, "\n"), "\n")
// If both start and stop are unset, update the entire file
if startLine == 0 && stopLine == 0 {
lines = contentLines
} else {
if startLine < 1 {
startLine = 1
}
if stopLine == 0 || stopLine > len(lines) {
stopLine = len(lines)
}
before := lines[:startLine-1]
after := lines[stopLine:]
// Handle insertion case
if startLine == stopLine {
lines = append(before, append(contentLines, lines[startLine-1:]...)...)
} else {
// If content is omitted, remove the specified range
if content == "" {
lines = append(before, after...)
} else {
lines = append(before, append(contentLines, after...)...)
}
}
}
newContent := strings.Join(lines, "\n")
// 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: util.AddLineNumbers(newContent)}
}

View File

@ -3,7 +3,6 @@ package toolbox
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util" toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
"git.mlow.ca/mlow/lmcli/pkg/api" "git.mlow.ca/mlow/lmcli/pkg/api"
@ -60,14 +59,7 @@ func readFile(path string) api.CallResult {
if err != nil { if err != nil {
return api.CallResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())} 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{ return api.CallResult{
Result: content.String(), Result: toolutil.AddLineNumbers(string(data)),
} }
} }

View File

@ -65,3 +65,14 @@ func IsPathWithinCWD(path string) (bool, string) {
} }
return true, "" return true, ""
} }
// AddLineNumbers takes a string of content and returns a new string with line
// numbers prefixed
func AddLineNumbers(content string) string {
lines := strings.Split(strings.TrimSuffix(content, "\n"), "\n")
result := strings.Builder{}
for i, line := range lines {
result.WriteString(fmt.Sprintf("%d\t%s\n", i+1, line))
}
return result.String()
}

View File

@ -8,12 +8,11 @@ import (
) )
var AvailableTools map[string]api.ToolSpec = map[string]api.ToolSpec{ var AvailableTools map[string]api.ToolSpec = map[string]api.ToolSpec{
"dir_tree": toolbox.DirTreeTool, "dir_tree": toolbox.DirTreeTool,
"read_dir": toolbox.ReadDirTool, "read_dir": toolbox.ReadDirTool,
"read_file": toolbox.ReadFileTool, "read_file": toolbox.ReadFileTool,
"write_file": toolbox.WriteFileTool, "modify_file": toolbox.ModifyFile,
"file_insert_lines": toolbox.FileInsertLinesTool, "write_file": toolbox.WriteFileTool,
"file_replace_lines": toolbox.FileReplaceLinesTool,
} }
func ExecuteToolCalls(calls []api.ToolCall, available []api.ToolSpec) ([]api.ToolResult, error) { func ExecuteToolCalls(calls []api.ToolCall, available []api.ToolSpec) ([]api.ToolResult, error) {