From 6f9b79afa119f2a80dd6b3b1deb778ad9c2715b5 Mon Sep 17 00:00:00 2001 From: Matt Low Date: Sun, 26 Nov 2023 10:51:06 +0000 Subject: [PATCH] Add modify_file tool --- pkg/cli/functions.go | 163 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/pkg/cli/functions.go b/pkg/cli/functions.go index a882002..14371cf 100644 --- a/pkg/cli/functions.go +++ b/pkg/cli/functions.go @@ -156,6 +156,102 @@ Result is returned as JSON in the following format: return WriteFile(path, content), nil }, }, + "modify_file": { + Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{ + Name: "modify_file", + Description: `Perform complex line-based modifications to a file. + +Line ranges are inclusive. If 'start_line' is specified but 'end_line' is not, +'end_line' gets set to the last line of the file. + +To replace or remove a single line, *set start_line and end_line to the same value* + +Examples: +* Insert the lines "helloworld" at line 10, preserving other content: + {"path": "myfile", "operation": "insert", "start_line": 10, "content": "hello\nworld"} + +* Remove lines 45 up to and including 54: + {"path": "myfile", "operation": "remove", "start_line": 45, "end_line": 54} + +* Replace content from line 10 to 25: + {"path": "myfile", "operation": "replace", "start_line": 10, "end_line": 25, "content": "i\nwas\nhere"} + +* Replace contents of entire the file: + {"path": "myfile", "operation": "replace", "start_line": 0, "content": "i\nwas\nhere"}`, + Parameters: FunctionParameters{ + Type: "object", + Properties: map[string]FunctionParameter{ + "path": { + Type: "string", + Description: "Path of the file to be modified, relative to the current working directory.", + }, + "operation": { + Type: "string", + Description: `The the type of modification to make to the file. One of: insert, remove, replace`, + }, + "start_line": { + Type: "integer", + Description: `(Optional) Where to start making a modification (insert, remove, and replace).`, + }, + "end_line": { + Type: "integer", + Description: `(Optional) Where to stop making a modification (remove or replace, end of file if omitted).`, + }, + "content": { + Type: "string", + Description: `(Optional) The content to insert, or replace with.`, + }, + }, + Required: []string{"path", "operation"}, + }, + }}, + Impl: func(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["operation"] + if !ok { + return "", fmt.Errorf("operation parameter to modify_file was not included.") + } + operation, ok := tmp.(string) + if !ok { + return "", fmt.Errorf("Invalid operation 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) + } + } + + return ModifyFile(path, operation, content, start_line, end_line), nil + }, + }, } func resultToJson(result FunctionResult) string { @@ -353,3 +449,70 @@ func WriteFile(path string, content string) string { } return resultToJson(FunctionResult{}) } + +func ModifyFile(path string, operation string, content string, startLine int, endLine int) string { + ok, res := isPathWithinCWD(path) + if !ok { + return resultToJson(*res) + } + + // Read the existing file's content + data, err := os.ReadFile(path) + if err != nil { + if !os.IsNotExist(err) { + return resultToJson(FunctionResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())}) + } + _, err = os.Create(path) + if err != nil { + return resultToJson(FunctionResult{Message: fmt.Sprintf("Could not create new file: %s", err.Error())}) + } + data = []byte{} + } + + if startLine < 0 { + return resultToJson(FunctionResult{Message: "start_line cannot be less than 0"}) + } + + // Split the content by newline to process lines + lines := strings.Split(string(data), "\n") + contentLines := strings.Split(strings.Trim(content, "\n"), "\n") + + switch operation { + case "insert": + // Insert new lines + before := lines[:startLine-1] + after := append(contentLines, lines[startLine-1:]...) + lines = append(before, after...) + case "remove": + // Remove lines + if endLine == 0 || endLine > len(lines) { + endLine = len(lines) + } + + lines = append(lines[:startLine-1], lines[endLine:]...) + case "replace": + // Replace the lines between start_line and end_line + if endLine == 0 || endLine > len(lines) { + endLine = len(lines) + } + if startLine == 0 { + // model likely trying to replace contents, must start at line 1 + startLine = 1 + } + before := lines[:startLine-1] + after := lines[endLine:] + lines = append(before, append(contentLines, after...)...) + default: + return resultToJson(FunctionResult{Message: fmt.Sprintf("Invalid operation: %s", operation)}) + } + + 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 resultToJson(FunctionResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())}) + } + + return resultToJson(FunctionResult{Result: newContent}) +}