feat: Enhance tool call extraction to robustly parse JSON blocks from text, including multiple occurrences.
This commit is contained in:
parent
885ee1c8ea
commit
2c980c33a8
3 changed files with 77 additions and 77 deletions
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue