Files
Enterprise-Knowledge-Base/internal/agent/tools/json_repair.go
T
wizardchen 2f7e3a8ac1 feat(agent): enhance tool execution and message sanitization
- Introduced a default timeout for tool execution to prevent indefinite hangs during long-running tasks.
- Implemented context cancellation handling in the execution loop to gracefully manage timeouts and user cancellations.
- Added a JSON repair utility to fix common malformations in tool arguments, improving robustness in argument parsing.
- Enhanced message sanitization to merge consecutive user messages and handle orphaned tool results, ensuring compatibility with LLM providers.
- Added unit tests for the new JSON repair and message sanitization functionalities to validate their effectiveness.
2026-03-25 22:08:29 +08:00

143 lines
2.8 KiB
Go

package tools
import (
"strings"
"unicode"
)
// RepairJSON attempts to fix common JSON malformations from LLM outputs.
// LLMs sometimes produce:
// - Truncated JSON (missing closing brackets/braces)
// - Trailing commas before closing brackets
// - Single quotes instead of double quotes
// - Unquoted keys
//
// Returns the repaired JSON string. If repair is not possible,
// returns the original string unchanged (caller should handle parse errors).
func RepairJSON(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return "{}"
}
// Must start with { for object
if s[0] != '{' {
// Maybe the LLM returned just key=value pairs without braces
if strings.Contains(s, ":") || strings.Contains(s, "=") {
s = "{" + s + "}"
} else {
return s
}
}
// Fix trailing commas: ,} or ,]
s = fixTrailingCommas(s)
// Balance brackets and braces
s = balanceBrackets(s)
return s
}
// fixTrailingCommas removes trailing commas before closing brackets/braces.
func fixTrailingCommas(s string) string {
// Simple state machine to handle strings
var result strings.Builder
result.Grow(len(s))
inString := false
escaped := false
runes := []rune(s)
for i, r := range runes {
if escaped {
escaped = false
result.WriteRune(r)
continue
}
if r == '\\' && inString {
escaped = true
result.WriteRune(r)
continue
}
if r == '"' {
inString = !inString
result.WriteRune(r)
continue
}
if inString {
result.WriteRune(r)
continue
}
// Outside string: check for trailing comma
if r == ',' {
// Look ahead for closing bracket/brace (skipping whitespace)
nextNonSpace := findNextNonSpace(runes, i+1)
if nextNonSpace >= 0 && (runes[nextNonSpace] == '}' || runes[nextNonSpace] == ']') {
continue // skip this comma
}
}
result.WriteRune(r)
}
return result.String()
}
// findNextNonSpace finds the index of the next non-whitespace rune.
func findNextNonSpace(runes []rune, start int) int {
for i := start; i < len(runes); i++ {
if !unicode.IsSpace(runes[i]) {
return i
}
}
return -1
}
// balanceBrackets appends missing closing brackets/braces.
func balanceBrackets(s string) string {
var stack []rune
inString := false
escaped := false
for _, r := range s {
if escaped {
escaped = false
continue
}
if r == '\\' && inString {
escaped = true
continue
}
if r == '"' {
inString = !inString
continue
}
if inString {
continue
}
switch r {
case '{':
stack = append(stack, '}')
case '[':
stack = append(stack, ']')
case '}', ']':
if len(stack) > 0 && stack[len(stack)-1] == r {
stack = stack[:len(stack)-1]
}
}
}
// Close unclosed string if needed
if inString {
s += `"`
}
// Append missing closers in reverse order
for i := len(stack) - 1; i >= 0; i-- {
s += string(stack[i])
}
return s
}