Compare commits

..

8 Commits

Author SHA1 Message Date
6f9b79afa1 Add modify_file tool 2023-11-26 10:51:06 +00:00
42f7b7aa29 Adjust read_file so it returns line numbers 2023-11-26 10:43:47 +00:00
9976c59f58 Small fixes 2023-11-26 10:43:25 +00:00
4b85b005dd Add read_file and write_file tools
Also improve `read_dir` description, and make it skip hidden files
2023-11-26 07:32:28 +00:00
59487d5721 Adjust read_dir description and return value 2023-11-26 06:37:54 +00:00
1fc0af56df Only allow read_dir (and other file access) within current working dir
Hopefully, anyway :)
2023-11-26 06:35:22 +00:00
fa27f83630 Add tool calling support to streamed requests 2023-11-26 02:46:38 +00:00
b229c42811 Add initial support for tools
So far only supported on the non-streaming endpoint.

Added the `read_dir` tool for reading contents from paths relative to the
current working directory.
2023-11-26 02:34:19 +00:00
2 changed files with 93 additions and 157 deletions

View File

@ -22,7 +22,7 @@ func (m *Message) FriendlyRole() string {
} }
func (c *Conversation) GenerateTitle() error { func (c *Conversation) GenerateTitle() error {
const header = "Generate a consise 4-5 word title for the conversation below." const header = "Generate a short title for the conversation below."
prompt := fmt.Sprintf("%s\n\n---\n\n%s", header, c.FormatForExternalPrompting()) prompt := fmt.Sprintf("%s\n\n---\n\n%s", header, c.FormatForExternalPrompting())
messages := []Message{ messages := []Message{

View File

@ -35,8 +35,11 @@ type AvailableTool struct {
Impl func(arguments map[string]interface{}) (string, error) Impl func(arguments map[string]interface{}) (string, error)
} }
const ( var AvailableTools = map[string]AvailableTool{
READ_DIR_DESCRIPTION = `Return the contents of the CWD (current working directory). "read_dir": {
Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{
Name: "read_dir",
Description: `Return the contents of the CWD (current working directory).
Results are returned as JSON in the following format: Results are returned as JSON in the following format:
{ {
@ -50,46 +53,7 @@ Results are returned as JSON in the following format:
} }
For files, size represents the size (in bytes) of the file. For files, size represents the size (in bytes) of the file.
For directories, size represents the number of entries in that directory.` For directories, size represents the number of entries in that directory.`,
READ_FILE_DESCRIPTION = `Read the contents of a text file relative to the current working directory.
Each line of the file is prefixed with its line number and a tabs (\t) to make
it make it easier to see which lines to change for other modifications.
Example result:
{
"message": "success", // if successful, or a different message indicating failure
"result": "1\tthe contents\n2\tof the file\n"
}`
WRITE_FILE_DESCRIPTION = `Write the provided contents to a file relative to the current working directory.
Note: only use this tool when you've been explicitly asked to create or write to a file.
When using this function, you do not need to share the content you intend to write with the user first.
Example result:
{
"message": "success", // if successful, or a different message indicating failure
}`
FILE_INSERT_LINES_DESCRIPTION = `Insert lines into a file, must specify path.
Make sure your inserts match the flow and indentation of surrounding content.`
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.
Be cautious with your edits. When replacing, ensure the replacement content matches the flow and indentation of surrounding content.`
)
var AvailableTools = map[string]AvailableTool{
"read_dir": {
Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{
Name: "read_dir",
Description: READ_DIR_DESCRIPTION,
Parameters: FunctionParameters{ Parameters: FunctionParameters{
Type: "object", Type: "object",
Properties: map[string]FunctionParameter{ Properties: map[string]FunctionParameter{
@ -115,7 +79,16 @@ var AvailableTools = map[string]AvailableTool{
"read_file": { "read_file": {
Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{ Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{
Name: "read_file", Name: "read_file",
Description: READ_FILE_DESCRIPTION, Description: `Read the contents of a text file relative to the current working directory.
Each line of the file is prefixed with its line number and a tabs (\t) to make
it make it easier to see which lines to change for other modifications.
Example:
{
"message": "success", // if successful, or a different message indicating failure
"result": "1\tthe contents\n2\tof the file\n"
}`,
Parameters: FunctionParameters{ Parameters: FunctionParameters{
Type: "object", Type: "object",
Properties: map[string]FunctionParameter{ Properties: map[string]FunctionParameter{
@ -142,7 +115,12 @@ var AvailableTools = map[string]AvailableTool{
"write_file": { "write_file": {
Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{ Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{
Name: "write_file", Name: "write_file",
Description: WRITE_FILE_DESCRIPTION, Description: `Write the provided contents to a file relative to the current working directory.
Result is returned as JSON in the following format:
{
"message": "success", // if successful, or a different message indicating failure
}`,
Parameters: FunctionParameters{ Parameters: FunctionParameters{
Type: "object", Type: "object",
Properties: map[string]FunctionParameter{ Properties: map[string]FunctionParameter{
@ -178,10 +156,28 @@ var AvailableTools = map[string]AvailableTool{
return WriteFile(path, content), nil return WriteFile(path, content), nil
}, },
}, },
"file_insert_lines": { "modify_file": {
Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{ Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{
Name: "file_insert_lines", Name: "modify_file",
Description: FILE_INSERT_LINES_DESCRIPTION, 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 "hello<new line>world" 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{ Parameters: FunctionParameters{
Type: "object", Type: "object",
Properties: map[string]FunctionParameter{ Properties: map[string]FunctionParameter{
@ -189,72 +185,24 @@ var AvailableTools = map[string]AvailableTool{
Type: "string", Type: "string",
Description: "Path of the file to be modified, relative to the current working directory.", Description: "Path of the file to be modified, relative to the current working directory.",
}, },
"position": { "operation": {
Type: "integer",
Description: `Which line to insert content *before*.`,
},
"content": {
Type: "string", Type: "string",
Description: `The content to insert.`, Description: `The the type of modification to make to the file. One of: insert, remove, replace`,
},
},
Required: []string{"path", "position", "content"},
},
}},
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 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)
}
}
return FileInsertLines(path, position, content), nil
},
},
"file_replace_lines": {
Tool: openai.Tool{Type: "function", Function: openai.FunctionDefinition{
Name: "file_replace_lines",
Description: FILE_REPLACE_LINES_DESCRIPTION,
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": { "start_line": {
Type: "integer", Type: "integer",
Description: `Line number which specifies the start of the replacement range (inclusive).`, Description: `(Optional) Where to start making a modification (insert, remove, and replace).`,
}, },
"end_line": { "end_line": {
Type: "integer", Type: "integer",
Description: `Line number which specifies the end of the replacement range (inclusive). If unset, range extends to end of file.`, Description: `(Optional) Where to stop making a modification (remove or replace, end of file if omitted).`,
}, },
"content": { "content": {
Type: "string", Type: "string",
Description: `Content to replace specified range. Omit to remove the specified range.`, Description: `(Optional) The content to insert, or replace with.`,
}, },
}, },
Required: []string{"path", "start_line"}, Required: []string{"path", "operation"},
}, },
}}, }},
Impl: func(args map[string]interface{}) (string, error) { Impl: func(args map[string]interface{}) (string, error) {
@ -266,6 +214,14 @@ var AvailableTools = map[string]AvailableTool{
if !ok { if !ok {
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp) 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 var start_line int
tmp, ok = args["start_line"] tmp, ok = args["start_line"]
if ok { if ok {
@ -293,7 +249,7 @@ var AvailableTools = map[string]AvailableTool{
} }
} }
return FileReplaceLines(path, start_line, end_line, content), nil return ModifyFile(path, operation, content, start_line, end_line), nil
}, },
}, },
} }
@ -494,7 +450,7 @@ func WriteFile(path string, content string) string {
return resultToJson(FunctionResult{}) return resultToJson(FunctionResult{})
} }
func FileInsertLines(path string, position int, content string) string { func ModifyFile(path string, operation string, content string, startLine int, endLine int) string {
ok, res := isPathWithinCWD(path) ok, res := isPathWithinCWD(path)
if !ok { if !ok {
return resultToJson(*res) return resultToJson(*res)
@ -513,62 +469,43 @@ func FileInsertLines(path string, position int, content string) string {
data = []byte{} data = []byte{}
} }
if position < 1 { if startLine < 0 {
return resultToJson(FunctionResult{Message: "start_line cannot be less than 1"}) return resultToJson(FunctionResult{Message: "start_line cannot be less than 0"})
}
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 resultToJson(FunctionResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())})
}
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"})
} }
// Split the content by newline to process lines
lines := strings.Split(string(data), "\n") lines := strings.Split(string(data), "\n")
contentLines := strings.Split(strings.Trim(content, "\n"), "\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) { if endLine == 0 || endLine > len(lines) {
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] before := lines[:startLine-1]
after := lines[endLine:] after := lines[endLine:]
lines = append(before, append(contentLines, after...)...) lines = append(before, append(contentLines, after...)...)
default:
return resultToJson(FunctionResult{Message: fmt.Sprintf("Invalid operation: %s", operation)})
}
newContent := strings.Join(lines, "\n") newContent := strings.Join(lines, "\n")
// Join the lines and write back to the file // Join the lines and write back to the file
@ -578,5 +515,4 @@ func FileReplaceLines(path string, startLine int, endLine int, content string) s
} }
return resultToJson(FunctionResult{Result: newContent}) return resultToJson(FunctionResult{Result: newContent})
} }