mirror of
https://github.com/ollama/ollama.git
synced 2025-11-10 22:28:43 +01:00
tools: fix parsing issue when a tool name is a substring of another (#11456)
Co-authored-by: frob <rick+github@frob.com.au>
This commit is contained in:
@@ -115,21 +115,7 @@ func (p *Parser) findTag() (int, bool) {
|
|||||||
// parseToolCall finds the next complete tool call in the buffer
|
// parseToolCall finds the next complete tool call in the buffer
|
||||||
// incrementing n and advancing the buffer.
|
// incrementing n and advancing the buffer.
|
||||||
func (p *Parser) parseToolCall() *api.ToolCall {
|
func (p *Parser) parseToolCall() *api.ToolCall {
|
||||||
var tool *api.Tool
|
tool, end := findTool(p.tools, p.buffer)
|
||||||
var end int = len(p.buffer)
|
|
||||||
var i int
|
|
||||||
|
|
||||||
// find tool name
|
|
||||||
for _, t := range p.tools {
|
|
||||||
n := t.Function.Name
|
|
||||||
if i = bytes.Index(p.buffer, []byte(n)); i != -1 {
|
|
||||||
if i+len(n) < end {
|
|
||||||
tool = &t
|
|
||||||
end = i + len(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tool == nil {
|
if tool == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -139,10 +125,10 @@ func (p *Parser) parseToolCall() *api.ToolCall {
|
|||||||
// parsing arguments before the tool name, which may be needed in the future
|
// 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 {
|
||||||
|
var i int
|
||||||
if args, i = findArguments(*tool, p.buffer[end:]); args == nil {
|
if args, i = findArguments(*tool, p.buffer[end:]); args == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
end += i
|
end += i
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,9 +145,74 @@ func (p *Parser) parseToolCall() *api.ToolCall {
|
|||||||
return tc
|
return tc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findTool finds the first tool name in the list that matches the
|
||||||
|
// beginning of the buffer, returning nil if no tool is found
|
||||||
|
// or if the buffer ends with a partial tool name since we need
|
||||||
|
// to wait for more data to disambiguate.
|
||||||
|
// The second return value is the end position of the tool name
|
||||||
|
// if one is found, otherwise 0.
|
||||||
|
func findTool(tools []api.Tool, buf []byte) (*api.Tool, int) {
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if buffer ends with a partial tool name
|
||||||
|
// this prevents matching "get" when seeing "get_weather"
|
||||||
|
var longest string
|
||||||
|
for _, t := range tools {
|
||||||
|
if len(t.Function.Name) > len(longest) {
|
||||||
|
longest = t.Function.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only check up to longest characters from the end
|
||||||
|
for i := 1; i <= min(len(buf), len(longest)); i++ {
|
||||||
|
tail := buf[len(buf)-i:]
|
||||||
|
for _, t := range tools {
|
||||||
|
name := []byte(t.Function.Name)
|
||||||
|
if len(tail) < len(name) && bytes.HasPrefix(name, tail) {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find first occurrence of the longest tool name
|
||||||
|
var found *api.Tool
|
||||||
|
start := -1
|
||||||
|
end := -1
|
||||||
|
|
||||||
|
for i := range tools {
|
||||||
|
name := []byte(tools[i].Function.Name)
|
||||||
|
pos := bytes.Index(buf, name)
|
||||||
|
if pos == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if we have a better match already
|
||||||
|
if start != -1 {
|
||||||
|
if pos > start {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if pos == start && len(name) <= len(found.Function.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
found = &tools[i]
|
||||||
|
start = pos
|
||||||
|
end = pos + len(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if found != nil {
|
||||||
|
return found, end
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
// findArguments returns the first object that appears to be
|
// findArguments returns the first object that appears to be
|
||||||
// arguments for the provided tool in the provided buffer,
|
// arguments for the provided tool in the provided buffer,
|
||||||
// returning nil if no arguments are found.
|
// returning nil if no arguments are found and the end position
|
||||||
// TODO (jmorganca): this does not support parsing omitted arguments
|
// TODO (jmorganca): this does not support parsing omitted arguments
|
||||||
// objects for functions that have all-optional parameters
|
// objects for functions that have all-optional parameters
|
||||||
// e.g. `{"name": "get_conditions", "arguments": {}}` will work but
|
// e.g. `{"name": "get_conditions", "arguments": {}}` will work but
|
||||||
|
|||||||
@@ -112,6 +112,81 @@ func TestParser(t *testing.T) {
|
|||||||
Description: "Say hello",
|
Description: "Say hello",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "say_hello_world",
|
||||||
|
Description: "Say hello world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "get_address",
|
||||||
|
Description: "Get the address of a given location",
|
||||||
|
Parameters: struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Defs any `json:"$defs,omitempty"`
|
||||||
|
Items any `json:"items,omitempty"`
|
||||||
|
Required []string `json:"required"`
|
||||||
|
Properties map[string]struct {
|
||||||
|
Type api.PropertyType `json:"type"`
|
||||||
|
Items any `json:"items,omitempty"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Enum []any `json:"enum,omitempty"`
|
||||||
|
} `json:"properties"`
|
||||||
|
}{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]struct {
|
||||||
|
Type api.PropertyType `json:"type"`
|
||||||
|
Items any `json:"items,omitempty"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Enum []any `json:"enum,omitempty"`
|
||||||
|
}{
|
||||||
|
"location": {
|
||||||
|
Type: api.PropertyType{"string"},
|
||||||
|
Description: "The location to get the address for",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "add",
|
||||||
|
Description: "Add two numbers",
|
||||||
|
Parameters: struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Defs any `json:"$defs,omitempty"`
|
||||||
|
Items any `json:"items,omitempty"`
|
||||||
|
Required []string `json:"required"`
|
||||||
|
Properties map[string]struct {
|
||||||
|
Type api.PropertyType `json:"type"`
|
||||||
|
Items any `json:"items,omitempty"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Enum []any `json:"enum,omitempty"`
|
||||||
|
} `json:"properties"`
|
||||||
|
}{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]struct {
|
||||||
|
Type api.PropertyType `json:"type"`
|
||||||
|
Items any `json:"items,omitempty"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Enum []any `json:"enum,omitempty"`
|
||||||
|
}{
|
||||||
|
"a": {
|
||||||
|
Type: api.PropertyType{"string"},
|
||||||
|
Description: "The first number to add",
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
Type: api.PropertyType{"string"},
|
||||||
|
Description: "The second number to add",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -629,6 +704,173 @@ func TestParser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "tool name with collision",
|
||||||
|
inputs: []string{
|
||||||
|
"<tool_call>",
|
||||||
|
"{",
|
||||||
|
"\"name\": \"say_hello",
|
||||||
|
"_world\",",
|
||||||
|
"}",
|
||||||
|
"}",
|
||||||
|
},
|
||||||
|
content: "",
|
||||||
|
tmpl: qwen,
|
||||||
|
calls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
|
Name: "say_hello_world",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool name with collision multiple",
|
||||||
|
inputs: []string{
|
||||||
|
"<tool_call>",
|
||||||
|
"{",
|
||||||
|
"\"name\": \"say_hello",
|
||||||
|
"_world\",",
|
||||||
|
"}",
|
||||||
|
"</tool_call>",
|
||||||
|
"<tool_call>",
|
||||||
|
"{",
|
||||||
|
"\"name\": \"say_hello",
|
||||||
|
"\",",
|
||||||
|
"}",
|
||||||
|
"</tool_call>",
|
||||||
|
},
|
||||||
|
content: "",
|
||||||
|
tmpl: qwen,
|
||||||
|
calls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
|
Name: "say_hello_world",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 1,
|
||||||
|
Name: "say_hello",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool name with collision non streaming",
|
||||||
|
inputs: []string{
|
||||||
|
`<tool_call>{"name": "say_hello`,
|
||||||
|
},
|
||||||
|
content: "",
|
||||||
|
tmpl: qwen,
|
||||||
|
calls: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool name with collision non streaming multiple",
|
||||||
|
inputs: []string{
|
||||||
|
`<tool_call>{"name": "say_hello"}</tool_call><tool_call>{"name": "say_hello_world"}`,
|
||||||
|
},
|
||||||
|
content: "",
|
||||||
|
tmpl: qwen,
|
||||||
|
calls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
|
Name: "say_hello",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 1,
|
||||||
|
Name: "say_hello_world",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool name with collision non streaming shorter",
|
||||||
|
inputs: []string{
|
||||||
|
`<tool_call>{"name": "say_hello"}</tool_call>`,
|
||||||
|
},
|
||||||
|
content: "",
|
||||||
|
tmpl: qwen,
|
||||||
|
calls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
|
Name: "say_hello",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool name with collision non streaming longer",
|
||||||
|
inputs: []string{
|
||||||
|
`<tool_call>{"name": "say_hello_world"}</tool_call>`,
|
||||||
|
},
|
||||||
|
content: "",
|
||||||
|
tmpl: qwen,
|
||||||
|
calls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
|
Name: "say_hello_world",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool name with substring of another",
|
||||||
|
inputs: []string{
|
||||||
|
"{",
|
||||||
|
"\"name\": \"get_address\",",
|
||||||
|
"\"arguments\": {",
|
||||||
|
"\"location\": \"London\"",
|
||||||
|
"}",
|
||||||
|
"}",
|
||||||
|
},
|
||||||
|
content: "",
|
||||||
|
tmpl: json,
|
||||||
|
calls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
|
Name: "get_address",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"location": "London",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool name with substring of another",
|
||||||
|
inputs: []string{
|
||||||
|
`<tool_call>{"name": "get_address", "arguments": {"location": "London"}}</tool_call>`,
|
||||||
|
},
|
||||||
|
content: "",
|
||||||
|
tmpl: qwen,
|
||||||
|
calls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Index: 0,
|
||||||
|
Name: "get_address",
|
||||||
|
Arguments: api.ToolCallFunctionArguments{
|
||||||
|
"location": "London",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
Reference in New Issue
Block a user