feat: Enhance tool call extraction to robustly parse JSON blocks from text, including multiple occurrences.

This commit is contained in:
Rahul Chand 2026-02-20 02:14:40 +05:30
parent 885ee1c8ea
commit 2c980c33a8
3 changed files with 77 additions and 77 deletions

View file

@ -958,24 +958,3 @@ func TestStripToolCallsJSON_OnlyToolCalls(t *testing.T) {
// --- findMatchingBrace tests --- // --- findMatchingBrace tests ---
func TestFindMatchingBrace(t *testing.T) {
tests := []struct {
text string
pos int
want int
}{
{`{"a":1}`, 0, 7},
{`{"a":{"b":2}}`, 0, 13},
{`text {"a":1} more`, 5, 12},
{`{unclosed`, 0, 0}, // no match returns pos
{`{}`, 0, 2}, // empty object
{`{{{}}}`, 0, 6}, // deeply nested
{`{"a":"b{c}d"}`, 0, 13}, // braces in strings (simplified matcher)
}
for _, tt := range tests {
got := findMatchingBrace(tt.text, tt.pos)
if got != tt.want {
t.Errorf("findMatchingBrace(%q, %d) = %d, want %d", tt.text, tt.pos, got, tt.want)
}
}
}

View file

@ -5,22 +5,17 @@ import (
"strings" "strings"
) )
// extractToolCallsFromText parses tool call JSON from response text.
// Both ClaudeCliProvider and CodexCliProvider use this to extract
// tool calls that the model outputs in its response text.
func extractToolCallsFromText(text string) []ToolCall { func extractToolCallsFromText(text string) []ToolCall {
start := strings.Index(text, `{"tool_calls"`) for i := 0; i < len(text); i++ {
if start == -1 { if text[i] == '{' {
return nil end := findMatchingBrace(text, i)
if end > i {
jsonStr := text[i:end]
// Quick check to avoid expensive parsing if it doesn't mention tool_calls
if !strings.Contains(jsonStr, "tool_calls") {
continue
} }
end := findMatchingBrace(text, start)
if end == start {
return nil
}
jsonStr := text[start:end]
var wrapper struct { var wrapper struct {
ToolCalls []struct { ToolCalls []struct {
ID string `json:"id"` ID string `json:"id"`
@ -32,10 +27,7 @@ func extractToolCallsFromText(text string) []ToolCall {
} `json:"tool_calls"` } `json:"tool_calls"`
} }
if err := json.Unmarshal([]byte(jsonStr), &wrapper); err != nil { if err := json.Unmarshal([]byte(jsonStr), &wrapper); err == nil && len(wrapper.ToolCalls) > 0 {
return nil
}
var result []ToolCall var result []ToolCall
for _, tc := range wrapper.ToolCalls { for _, tc := range wrapper.ToolCalls {
var args map[string]interface{} var args map[string]interface{}
@ -52,23 +44,36 @@ func extractToolCallsFromText(text string) []ToolCall {
}, },
}) })
} }
return result return result
} }
}
}
}
return nil
}
// stripToolCallsFromText removes tool call JSON from response text. // stripToolCallsFromText removes tool call JSON from response text.
func stripToolCallsFromText(text string) string { func stripToolCallsFromText(text string) string {
start := strings.Index(text, `{"tool_calls"`) for i := 0; i < len(text); i++ {
if start == -1 { if text[i] == '{' {
return text end := findMatchingBrace(text, i)
if end > i {
jsonStr := text[i:end]
if !strings.Contains(jsonStr, "tool_calls") {
continue
} }
end := findMatchingBrace(text, start) var wrapper struct {
if end == start { ToolCalls interface{} `json:"tool_calls"`
return text
} }
return strings.TrimSpace(text[:start] + text[end:]) if err := json.Unmarshal([]byte(jsonStr), &wrapper); err == nil && wrapper.ToolCalls != nil {
return strings.TrimSpace(text[:i] + text[end:])
}
}
}
}
return text
} }
// findMatchingBrace finds the index after the closing brace matching the opening brace at pos. // findMatchingBrace finds the index after the closing brace matching the opening brace at pos.

View file

@ -65,6 +65,22 @@ func TestExtractToolCallsFromText(t *testing.T) {
}, },
}, },
}, },
{
name: "Multiple JSON blocks",
text: `Some config: {"debug": true}. Then the tool call: {"tool_calls":[{"id":"c1","type":"function","function":{"name":"search","arguments":"{}"}}]}.`,
want: []ToolCall{
{
ID: "c1",
Type: "function",
Name: "search",
Arguments: map[string]interface{}{},
Function: &FunctionCall{
Name: "search",
Arguments: "{}",
},
},
},
},
{ {
name: "No tool calls", name: "No tool calls",
text: "Just some normal text here.", text: "Just some normal text here.",