diff --git a/pkg/cli/functions.go b/pkg/cli/functions.go index 3654b68..b4b68da 100644 --- a/pkg/cli/functions.go +++ b/pkg/cli/functions.go @@ -70,25 +70,11 @@ Result is returned as JSON in the following format: "message": "success", // if successful, or a different message indicating failure }` - MODIFY_FILE_DESCRIPTION = `Perform complex line-based modifications to a file. + FILE_INSERT_LINES = `Insert lines into 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. + FILE_REPLACE_LINES = `Replace an (inclusive) range of lines within a 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_before", "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"}` + FILE_REMOVE_LINES = `Remove an (inclusive) range of lines from a file.` ) var AvailableTools = map[string]AvailableTool{ @@ -184,10 +170,10 @@ var AvailableTools = map[string]AvailableTool{ return WriteFile(path, content), nil }, }, - "modify_file": { + "file_insert_lines": { Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{ - Name: "modify_file", - Description: MODIFY_FILE_DESCRIPTION, + Name: "file_insert_lines", + Description: FILE_INSERT_LINES, Parameters: FunctionParameters{ Type: "object", Properties: map[string]FunctionParameter{ @@ -195,24 +181,16 @@ var AvailableTools = map[string]AvailableTool{ 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_before, 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).`, + Description: `Which line to begin inserting lines at (existing line will be moved to after content).`, }, "content": { Type: "string", - Description: `(Optional) The content to insert, or replace with.`, + Description: `The content to insert.`, }, }, - Required: []string{"path", "operation"}, + Required: []string{"path", "start", "content"}, }, }}, Impl: func(args map[string]interface{}) (string, error) { @@ -224,13 +202,61 @@ var AvailableTools = map[string]AvailableTool{ 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.") + 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) } - operation, ok := tmp.(string) + 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 FileInsertLines(path, start_line, content), nil + }, + }, + "file_replace_lines": { + Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{ + Name: "file_replace_lines", + Description: FILE_REPLACE_LINES, + 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.", + }, + "start_line": { + Type: "integer", + Description: `Line number which specifies the start of the replacement range (inclusive, this line will be replaced)`, + }, + "end_line": { + Type: "integer", + Description: `Line number which specifies the end of the replacement range (inclusive, this line will be replaced). If unset, will be set to the end of the file.`, + }, + "content": { + Type: "string", + Description: `Content which will replace specified range.`, + }, + }, + Required: []string{"path", "start_line", "content"}, + }, + }}, + Impl: func(args map[string]interface{}) (string, error) { + tmp, ok := args["path"] if !ok { - return "", fmt.Errorf("Invalid operation in function arguments: %v", tmp) + 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"] @@ -259,7 +285,61 @@ var AvailableTools = map[string]AvailableTool{ } } - return ModifyFile(path, operation, content, start_line, end_line), nil + return FileReplaceLines(path, start_line, end_line, content), nil + }, + }, + "file_remove_lines": { + Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{ + Name: "file_remove_lines", + Description: FILE_REMOVE_LINES, + 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.", + }, + "start_line": { + Type: "integer", + Description: `Line number which specifies the start of the removal range (inclusive, this line will be removed)`, + }, + "end_line": { + Type: "integer", + Description: `Line number which specifies the end of the removal range (inclusive, this line will be removed).`, + }, + }, + Required: []string{"path", "start_line", "end_line"}, + }, + }}, + 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) + } + 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) + } + + return FileRemoveLines(path, start_line, end_line), nil }, }, } @@ -460,7 +540,7 @@ func WriteFile(path string, content string) string { return resultToJson(FunctionResult{}) } -func ModifyFile(path string, operation string, content string, startLine int, endLine int) string { +func FileInsertLines(path string, startLine int, content string) string { ok, res := isPathWithinCWD(path) if !ok { return resultToJson(*res) @@ -479,42 +559,16 @@ func ModifyFile(path string, operation string, content string, startLine int, en data = []byte{} } - if startLine < 0 { - return resultToJson(FunctionResult{Message: "start_line cannot be less than 0"}) + if startLine < 1 { + return resultToJson(FunctionResult{Message: "start_line cannot be less than 1"}) } - // 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_before": - // 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)}) - } + before := lines[:startLine-1] + after := lines[startLine-1:] + lines = append(before, append(contentLines, after...)...) newContent := strings.Join(lines, "\n") @@ -526,3 +580,90 @@ func ModifyFile(path string, operation string, content string, startLine int, en return resultToJson(FunctionResult{Result: newContent}) } + +func FileReplaceLines(path string, startLine int, endLine int, content string) 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 < 1 { + return resultToJson(FunctionResult{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 resultToJson(FunctionResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())}) + } + + return resultToJson(FunctionResult{Result: newContent}) + +} + +func FileRemoveLines(path 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 < 1 { + return resultToJson(FunctionResult{Message: "start_line cannot be less than 1"}) + } + + lines := strings.Split(string(data), "\n") + + if endLine == 0 || endLine > len(lines) { + endLine = len(lines) + } + + lines = append(lines[:startLine-1], lines[endLine:]...) + 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}) +}