mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
Merge pull request #267 from CJackHwang/codex/fix-xml-parsing-for-tool-calls
Strip fenced code blocks before XML tool-call parsing to avoid executing examples
This commit is contained in:
@@ -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,76 @@ 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 len(line) < 3 {
|
||||
return "", false
|
||||
}
|
||||
ch := line[0]
|
||||
if ch != '`' && ch != '~' {
|
||||
return "", false
|
||||
}
|
||||
count := countLeadingFenceChars(line, ch)
|
||||
if count < 3 {
|
||||
return "", false
|
||||
}
|
||||
return strings.Repeat(string(ch), count), true
|
||||
}
|
||||
|
||||
func isFenceClose(line, marker string) bool {
|
||||
if marker == "" {
|
||||
return false
|
||||
}
|
||||
ch := marker[0]
|
||||
if line == "" || line[0] != ch {
|
||||
return false
|
||||
}
|
||||
count := countLeadingFenceChars(line, ch)
|
||||
if count < len(marker) {
|
||||
return false
|
||||
}
|
||||
rest := strings.TrimSpace(line[count:])
|
||||
return rest == ""
|
||||
}
|
||||
|
||||
func countLeadingFenceChars(line string, ch byte) int {
|
||||
count := 0
|
||||
for count < len(line) && line[count] == ch {
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -455,3 +455,33 @@ 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])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolCallsParsesAfterFourBacktickFence(t *testing.T) {
|
||||
text := "````markdown\n```xml\n<tool_call><tool_name>read_file</tool_name><parameters>{\"path\":\"README.md\"}</parameters></tool_call>\n```\n````\n<tool_call><tool_name>search</tool_name><parameters>{\"q\":\"outside\"}</parameters></tool_call>"
|
||||
res := ParseToolCallsDetailed(text, []string{"read_file", "search"})
|
||||
if len(res.Calls) != 1 {
|
||||
t.Fatalf("expected exactly one parsed call outside four-backtick 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])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user