Fix XML tool-call parsing for fenced markdown examples

This commit is contained in:
CJACK.
2026-04-19 23:11:24 +08:00
parent 69b7bc0c1a
commit 5b7cdaa729
2 changed files with 76 additions and 0 deletions

View File

@@ -39,6 +39,11 @@ func parseToolCallsDetailedXMLOnly(text string) ToolCallParseResult {
return result
}
result.SawToolCallSyntax = looksLikeToolCallSyntax(trimmed)
trimmed = stripFencedCodeBlocks(trimmed)
trimmed = strings.TrimSpace(trimmed)
if trimmed == "" {
return result
}
parsed := parseXMLToolCalls(trimmed)
if len(parsed) == 0 {
@@ -83,3 +88,55 @@ func looksLikeToolCallSyntax(text string) bool {
strings.Contains(lower, "<new_task") ||
strings.Contains(lower, "<result")
}
func stripFencedCodeBlocks(text string) string {
if text == "" {
return ""
}
var b strings.Builder
b.Grow(len(text))
lines := strings.SplitAfter(text, "\n")
inFence := false
fenceMarker := ""
for _, line := range lines {
trimmed := strings.TrimLeft(line, " \t")
if !inFence {
if marker, ok := parseFenceOpen(trimmed); ok {
inFence = true
fenceMarker = marker
continue
}
b.WriteString(line)
continue
}
if isFenceClose(trimmed, fenceMarker) {
inFence = false
fenceMarker = ""
}
}
if inFence {
return ""
}
return b.String()
}
func parseFenceOpen(line string) (string, bool) {
if strings.HasPrefix(line, "```") {
return "```", true
}
if strings.HasPrefix(line, "~~~") {
return "~~~", true
}
return "", false
}
func isFenceClose(line, marker string) bool {
if marker == "" || !strings.HasPrefix(line, marker) {
return false
}
rest := strings.TrimSpace(strings.TrimPrefix(line, marker))
return rest == ""
}

View File

@@ -455,3 +455,22 @@ func TestParseToolCallsUnescapesHTMLEntityArguments(t *testing.T) {
t.Fatalf("expected html entities to be unescaped in command, got %q", cmd)
}
}
func TestParseToolCallsIgnoresXMLInsideFencedCodeBlock(t *testing.T) {
text := "Here is an example:\n```xml\n<tool_call><tool_name>read_file</tool_name><parameters>{\"path\":\"README.md\"}</parameters></tool_call>\n```\nDo not execute it."
res := ParseToolCallsDetailed(text, []string{"read_file"})
if len(res.Calls) != 0 {
t.Fatalf("expected no parsed calls for fenced example, got %#v", res.Calls)
}
}
func TestParseToolCallsParsesOnlyNonFencedXMLToolCall(t *testing.T) {
text := "```xml\n<tool_call><tool_name>read_file</tool_name><parameters>{\"path\":\"README.md\"}</parameters></tool_call>\n```\n<tool_call><tool_name>search</tool_name><parameters>{\"q\":\"golang\"}</parameters></tool_call>"
res := ParseToolCallsDetailed(text, []string{"read_file", "search"})
if len(res.Calls) != 1 {
t.Fatalf("expected exactly one parsed call outside fence, got %#v", res.Calls)
}
if res.Calls[0].Name != "search" {
t.Fatalf("expected non-fenced tool call to be parsed, got %#v", res.Calls[0])
}
}