mirror of
https://github.com/dogkeeper886/ollama37.git
synced 2025-12-10 15:57:04 +00:00
tools: fix parsing tool calls with empty arguments, missing required fields (#11233)
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user