Feat (tools): file_edit
This commit is contained in:
31
bot.go
31
bot.go
@@ -66,6 +66,8 @@ var (
|
|||||||
LocalModels = []string{}
|
LocalModels = []string{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var thinkBlockRE = regexp.MustCompile(`(?s)<think>.*?</think>`)
|
||||||
|
|
||||||
// parseKnownToTag extracts known_to list from content using configured tag.
|
// parseKnownToTag extracts known_to list from content using configured tag.
|
||||||
// Returns cleaned content and list of character names.
|
// Returns cleaned content and list of character names.
|
||||||
func parseKnownToTag(content string) []string {
|
func parseKnownToTag(content string) []string {
|
||||||
@@ -933,7 +935,9 @@ out:
|
|||||||
if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil {
|
if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil {
|
||||||
logger.Warn("failed to update storage", "error", err, "name", activeChatName)
|
logger.Warn("failed to update storage", "error", err, "name", activeChatName)
|
||||||
}
|
}
|
||||||
if findCall(respText.String(), toolResp.String()) {
|
// Strip think blocks before parsing for tool calls
|
||||||
|
respTextNoThink := thinkBlockRE.ReplaceAllString(respText.String(), "")
|
||||||
|
if findCall(respTextNoThink, toolResp.String()) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Check if this message was sent privately to specific characters
|
// Check if this message was sent privately to specific characters
|
||||||
@@ -1077,11 +1081,30 @@ func findCall(msg, toolCall string) bool {
|
|||||||
if jsStr == "" { // no tool call case
|
if jsStr == "" { // no tool call case
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
prefix := "__tool_call__\n"
|
// Remove prefix/suffix with flexible whitespace handling
|
||||||
suffix := "\n__tool_call__"
|
jsStr = strings.TrimSpace(jsStr)
|
||||||
jsStr = strings.TrimSuffix(strings.TrimPrefix(jsStr, prefix), suffix)
|
jsStr = strings.TrimPrefix(jsStr, "__tool_call__")
|
||||||
|
jsStr = strings.TrimSuffix(jsStr, "__tool_call__")
|
||||||
|
jsStr = strings.TrimSpace(jsStr)
|
||||||
// HTML-decode the JSON string to handle encoded characters like < -> <=
|
// HTML-decode the JSON string to handle encoded characters like < -> <=
|
||||||
decodedJsStr := html.UnescapeString(jsStr)
|
decodedJsStr := html.UnescapeString(jsStr)
|
||||||
|
// Try to find valid JSON bounds (first { to last })
|
||||||
|
start := strings.Index(decodedJsStr, "{")
|
||||||
|
end := strings.LastIndex(decodedJsStr, "}")
|
||||||
|
if start == -1 || end == -1 || end <= start {
|
||||||
|
logger.Error("failed to find valid JSON in tool call", "json_string", decodedJsStr)
|
||||||
|
toolResponseMsg := models.RoleMsg{
|
||||||
|
Role: cfg.ToolRole,
|
||||||
|
Content: "Error processing tool call: no valid JSON found. Please check the JSON format.",
|
||||||
|
}
|
||||||
|
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
|
||||||
|
crr := &models.ChatRoundReq{
|
||||||
|
Role: cfg.AssistantRole,
|
||||||
|
}
|
||||||
|
chatRoundChan <- crr
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
decodedJsStr = decodedJsStr[start : end+1]
|
||||||
var err error
|
var err error
|
||||||
fc, err = unmarshalFuncCall(decodedJsStr)
|
fc, err = unmarshalFuncCall(decodedJsStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
115
tools.go
115
tools.go
@@ -95,6 +95,11 @@ Your current tools:
|
|||||||
"when_to_use": "when asked to append content to a file; use sed to edit content"
|
"when_to_use": "when asked to append content to a file; use sed to edit content"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"name":"file_edit",
|
||||||
|
"args": ["path", "oldString", "newString", "lineNumber"],
|
||||||
|
"when_to_use": "when you need to make targeted changes to a specific section of a file without rewriting the entire file; lineNumber is optional - if provided, only edits that specific line; if not provided, replaces all occurrences of oldString"
|
||||||
|
},
|
||||||
|
{
|
||||||
"name":"file_delete",
|
"name":"file_delete",
|
||||||
"args": ["path"],
|
"args": ["path"],
|
||||||
"when_to_use": "when asked to delete a file"
|
"when_to_use": "when asked to delete a file"
|
||||||
@@ -506,6 +511,85 @@ func fileWriteAppend(args map[string]string) []byte {
|
|||||||
return []byte(msg)
|
return []byte(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fileEdit(args map[string]string) []byte {
|
||||||
|
path, ok := args["path"]
|
||||||
|
if !ok || path == "" {
|
||||||
|
msg := "path not provided to file_edit tool"
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
path = resolvePath(path)
|
||||||
|
|
||||||
|
oldString, ok := args["oldString"]
|
||||||
|
if !ok || oldString == "" {
|
||||||
|
msg := "oldString not provided to file_edit tool"
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
newString, ok := args["newString"]
|
||||||
|
if !ok {
|
||||||
|
newString = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
lineNumberStr, hasLineNumber := args["lineNumber"]
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
msg := "failed to read file: " + err.Error()
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContent := string(content)
|
||||||
|
var replacementCount int
|
||||||
|
|
||||||
|
if hasLineNumber && lineNumberStr != "" {
|
||||||
|
// Line-number based edit
|
||||||
|
lineNum, err := strconv.Atoi(lineNumberStr)
|
||||||
|
if err != nil {
|
||||||
|
msg := "invalid lineNumber: must be a valid integer"
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
lines := strings.Split(fileContent, "\n")
|
||||||
|
if lineNum < 1 || lineNum > len(lines) {
|
||||||
|
msg := fmt.Sprintf("lineNumber %d out of range (file has %d lines)", lineNum, len(lines))
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
// Find oldString in the specific line
|
||||||
|
targetLine := lines[lineNum-1]
|
||||||
|
if !strings.Contains(targetLine, oldString) {
|
||||||
|
msg := fmt.Sprintf("oldString not found on line %d", lineNum)
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
lines[lineNum-1] = strings.Replace(targetLine, oldString, newString, 1)
|
||||||
|
replacementCount = 1
|
||||||
|
fileContent = strings.Join(lines, "\n")
|
||||||
|
} else {
|
||||||
|
// Replace all occurrences
|
||||||
|
if !strings.Contains(fileContent, oldString) {
|
||||||
|
msg := "oldString not found in file"
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
fileContent = strings.ReplaceAll(fileContent, oldString, newString)
|
||||||
|
replacementCount = strings.Count(fileContent, newString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, []byte(fileContent), 0644); err != nil {
|
||||||
|
msg := "failed to write file: " + err.Error()
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("file edited successfully at %s (%d replacement(s))", path, replacementCount)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
|
||||||
func fileDelete(args map[string]string) []byte {
|
func fileDelete(args map[string]string) []byte {
|
||||||
path, ok := args["path"]
|
path, ok := args["path"]
|
||||||
if !ok || path == "" {
|
if !ok || path == "" {
|
||||||
@@ -998,6 +1082,7 @@ var fnMap = map[string]fnSig{
|
|||||||
"file_read": fileRead,
|
"file_read": fileRead,
|
||||||
"file_write": fileWrite,
|
"file_write": fileWrite,
|
||||||
"file_write_append": fileWriteAppend,
|
"file_write_append": fileWriteAppend,
|
||||||
|
"file_edit": fileEdit,
|
||||||
"file_delete": fileDelete,
|
"file_delete": fileDelete,
|
||||||
"file_move": fileMove,
|
"file_move": fileMove,
|
||||||
"file_copy": fileCopy,
|
"file_copy": fileCopy,
|
||||||
@@ -1265,6 +1350,36 @@ var baseTools = []models.Tool{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// file_edit
|
||||||
|
models.Tool{
|
||||||
|
Type: "function",
|
||||||
|
Function: models.ToolFunc{
|
||||||
|
Name: "file_edit",
|
||||||
|
Description: "Edit a specific section of a file by replacing oldString with newString. Use for targeted changes without rewriting the entire file.",
|
||||||
|
Parameters: models.ToolFuncParams{
|
||||||
|
Type: "object",
|
||||||
|
Required: []string{"path", "oldString", "newString"},
|
||||||
|
Properties: map[string]models.ToolArgProps{
|
||||||
|
"path": models.ToolArgProps{
|
||||||
|
Type: "string",
|
||||||
|
Description: "path of the file to edit",
|
||||||
|
},
|
||||||
|
"oldString": models.ToolArgProps{
|
||||||
|
Type: "string",
|
||||||
|
Description: "the exact string to find and replace",
|
||||||
|
},
|
||||||
|
"newString": models.ToolArgProps{
|
||||||
|
Type: "string",
|
||||||
|
Description: "the string to replace oldString with",
|
||||||
|
},
|
||||||
|
"lineNumber": models.ToolArgProps{
|
||||||
|
Type: "string",
|
||||||
|
Description: "optional line number (1-indexed) to edit - if provided, only that line is edited",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// file_delete
|
// file_delete
|
||||||
models.Tool{
|
models.Tool{
|
||||||
Type: "function",
|
Type: "function",
|
||||||
|
|||||||
Reference in New Issue
Block a user