tools: fix parsing tool calls with empty arguments, missing required fields (#11233)

This commit is contained in:
Jeffrey Morgan
2025-06-30 08:59:03 -07:00
committed by GitHub
parent 3b8b692218
commit 44b17d2bfa
2 changed files with 86 additions and 32 deletions

View File

@@ -134,16 +134,16 @@ func (p *Parser) parseToolCall() *api.ToolCall {
return nil return nil
} }
// only look for arguments if the tool has parameters // only look for arguments after the tool name if the tool has parameters
// TODO (jmorganca): while probably uncommon, this doesn't support
// parsing arguments before the tool name, which may be needed in the future
args := map[string]any{} args := map[string]any{}
if len(tool.Function.Parameters.Properties) > 0 { if len(tool.Function.Parameters.Properties) > 0 {
if args, i = p.findArguments(*tool); args == nil { if args, i = findArguments(*tool, p.buffer[end:]); args == nil {
return nil return nil
} }
if i > end { end += i
end = i
}
} }
tc := &api.ToolCall{ tc := &api.ToolCall{
@@ -160,14 +160,14 @@ func (p *Parser) parseToolCall() *api.ToolCall {
} }
// findArguments returns the first object that appears to be // findArguments returns the first object that appears to be
// arguments for the provided tool, returning nil // arguments for the provided tool in the provided buffer,
func (p *Parser) findArguments(tool api.Tool) (map[string]any, int) { // returning nil if no arguments are found.
if len(p.buffer) == 0 { // TODO (jmorganca): this does not support parsing omitted arguments
return nil, 0 // objects for functions that have all-optional parameters
} // e.g. `{"name": "get_conditions", "arguments": {}}` will work but
// `{"name": "get_conditions"}` will not currently work
// no arguments to parse func findArguments(tool api.Tool, buffer []byte) (map[string]any, int) {
if len(tool.Function.Parameters.Properties) == 0 { if len(buffer) == 0 {
return nil, 0 return nil, 0
} }
@@ -177,7 +177,7 @@ func (p *Parser) findArguments(tool api.Tool) (map[string]any, int) {
var object []byte var object []byte
// find any outer json object // find any outer json object
for i, c := range p.buffer { for i, c := range buffer {
if c == '{' { if c == '{' {
braces++ braces++
if start == -1 { if start == -1 {
@@ -190,7 +190,7 @@ func (p *Parser) findArguments(tool api.Tool) (map[string]any, int) {
braces-- braces--
if braces == 0 { if braces == 0 {
end = i + 1 end = i + 1
object = p.buffer[start:end] object = buffer[start:end]
break break
} }
} }
@@ -202,8 +202,6 @@ func (p *Parser) findArguments(tool api.Tool) (map[string]any, int) {
} }
var data map[string]any var data map[string]any
// not valid json
if err := json.Unmarshal(object, &data); err != nil { if err := json.Unmarshal(object, &data); err != nil {
return nil, 0 return nil, 0
} }
@@ -212,15 +210,27 @@ func (p *Parser) findArguments(tool api.Tool) (map[string]any, int) {
find = func(obj any) map[string]any { find = func(obj any) map[string]any {
switch obj := obj.(type) { switch obj := obj.(type) {
case map[string]any: case map[string]any:
found := true valid := true
// check if all keys in the object exist in the tool's parameters
for key := range obj { for key := range obj {
if _, exists := tool.Function.Parameters.Properties[key]; !exists { if _, exists := tool.Function.Parameters.Properties[key]; !exists {
found = false valid = false
break break
} }
} }
if found { // check for required parameters
// TODO (jmorganca): this should error instead of silently failing
if valid {
for _, required := range tool.Function.Parameters.Required {
if _, exists := obj[required]; !exists {
valid = false
break
}
}
}
if valid {
return obj return obj
} }

View File

@@ -53,6 +53,7 @@ func TestParser(t *testing.T) {
} `json:"properties"` } `json:"properties"`
}{ }{
Type: "object", Type: "object",
Required: []string{"city"},
Properties: map[string]struct { Properties: map[string]struct {
Type api.PropertyType `json:"type"` Type api.PropertyType `json:"type"`
Items any `json:"items,omitempty"` Items any `json:"items,omitempty"`
@@ -159,8 +160,23 @@ func TestParser(t *testing.T) {
calls: nil, calls: nil,
}, },
{ {
name: "missing args", name: "empty args",
inputs: []string{`<tool_call>{"name": "get_conditions"}</tool_call>`}, inputs: []string{`<tool_call>{"name": "get_conditions", "arguments": {}}</tool_call>`},
content: "",
tmpl: qwen,
calls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Index: 0,
Name: "get_conditions",
Arguments: api.ToolCallFunctionArguments{},
},
},
},
},
{
name: "missing required args",
inputs: []string{`<tool_call>{"name": "get_temperature", "arguments": {}}</tool_call>`},
content: "", content: "",
tmpl: qwen, tmpl: qwen,
calls: nil, calls: nil,
@@ -259,9 +275,9 @@ func TestParser(t *testing.T) {
}, },
}, },
{ {
name: "qwen two tool calls one with no args", name: "empty args followed by args",
inputs: []string{`Let me check the weather. <tool_call>{"name": "say_hello"}</tool_call><tool_call>{"name": "get_conditions", "arguments": {"location": "Tokyo"}}`}, inputs: []string{`Let me say hello and check the weather. <tool_call>{"name": "say_hello", "arguments": {}}</tool_call><tool_call>{"name": "get_temperature", "arguments": {"city": "London", "format": "fahrenheit"}}</tool_call>`},
content: "Let me check the weather. ", content: "Let me say hello and check the weather. ",
tmpl: qwen, tmpl: qwen,
calls: []api.ToolCall{ calls: []api.ToolCall{
{ {
@@ -271,6 +287,31 @@ func TestParser(t *testing.T) {
Arguments: api.ToolCallFunctionArguments{}, Arguments: api.ToolCallFunctionArguments{},
}, },
}, },
{
Function: api.ToolCallFunction{
Index: 1,
Name: "get_temperature",
Arguments: api.ToolCallFunctionArguments{
"city": "London",
"format": "fahrenheit",
},
},
},
},
},
{
name: "qwen empty followed by args",
inputs: []string{`Let me check the weather. <tool_call>{"name": "get_conditions", "arguments": {}}</tool_call><tool_call>{"name": "get_conditions", "arguments": {"location": "Tokyo"}}`},
content: "Let me check the weather. ",
tmpl: qwen,
calls: []api.ToolCall{
{
Function: api.ToolCallFunction{
Index: 0,
Name: "get_conditions",
Arguments: api.ToolCallFunctionArguments{},
},
},
{ {
Function: api.ToolCallFunction{ Function: api.ToolCallFunction{
Index: 1, Index: 1,
@@ -1035,16 +1076,19 @@ func TestFindArguments(t *testing.T) {
}, },
tool: tool, tool: tool,
}, },
{
name: "deepseek",
buffer: []byte(`", "arguments": {"location": "Tokyo"}}</tool_call>`),
want: map[string]any{
"location": "Tokyo",
},
tool: tool,
},
} }
for _, tt := range tests { for _, tt := range tests {
parser := &Parser{
buffer: tt.buffer,
tools: []api.Tool{tool, tool2},
}
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, _ := parser.findArguments(tool) got, _ := findArguments(tt.tool, tt.buffer)
if diff := cmp.Diff(got, tt.want); diff != "" { if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("scanArguments() args mismatch (-got +want):\n%s", diff) t.Errorf("scanArguments() args mismatch (-got +want):\n%s", diff)