mirror of
https://github.com/ollama/ollama.git
synced 2025-08-27 18:51:19 +02:00
tools: avoid matching braces that are part of tool content (#12039)
This commit is contained in:
@@ -224,22 +224,45 @@ func findArguments(buffer []byte) (map[string]any, int) {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
start := -1
|
||||
var braces int
|
||||
var start int = -1
|
||||
var inString, escaped bool
|
||||
|
||||
for i := range buffer {
|
||||
c := buffer[i]
|
||||
|
||||
if escaped {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
if c == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
if c == '"' {
|
||||
inString = !inString
|
||||
continue
|
||||
}
|
||||
|
||||
if inString {
|
||||
continue
|
||||
}
|
||||
|
||||
for i, c := range buffer {
|
||||
if c == '{' {
|
||||
if braces == 0 {
|
||||
start = i
|
||||
}
|
||||
braces++
|
||||
} else if c == '}' && braces > 0 {
|
||||
} else if c == '}' {
|
||||
braces--
|
||||
if braces == 0 && start != -1 {
|
||||
object := buffer[start : i+1]
|
||||
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(object, &data); err != nil {
|
||||
// not a valid object, keep looking
|
||||
start = -1
|
||||
continue
|
||||
}
|
||||
@@ -282,6 +305,10 @@ func findArguments(buffer []byte) (map[string]any, int) {
|
||||
|
||||
return data, i
|
||||
}
|
||||
|
||||
if braces < 0 {
|
||||
braces = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
@@ -1140,11 +1141,163 @@ func TestFindArguments(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "deepseek",
|
||||
buffer: []byte(`", "arguments": {"location": "Tokyo"}}</tool_call>`),
|
||||
buffer: []byte(`"arguments": {"location": "Tokyo"}}</tool_call>`),
|
||||
want: map[string]any{
|
||||
"location": "Tokyo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string with braces",
|
||||
buffer: []byte(`{"name": "process_code", "arguments": {"code": "if (x > 0) { return true; }"}}`),
|
||||
want: map[string]any{
|
||||
"code": "if (x > 0) { return true; }",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string with nested json",
|
||||
buffer: []byte(`{"name": "send_data", "arguments": {"payload": "{\"nested\": {\"key\": \"value\"}}"}}`),
|
||||
want: map[string]any{
|
||||
"payload": `{"nested": {"key": "value"}}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string with escaped quotes and braces",
|
||||
buffer: []byte(`{"name": "analyze", "arguments": {"text": "The JSON is: {\"key\": \"val{ue}\"}"}}`),
|
||||
want: map[string]any{
|
||||
"text": `The JSON is: {"key": "val{ue}"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple objects with string containing braces",
|
||||
buffer: []byte(`{"name": "test", "arguments": {"query": "find } in text"}} {"name": "other"}`),
|
||||
want: map[string]any{
|
||||
"query": "find } in text",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unmatched closing brace in string",
|
||||
buffer: []byte(`{"name": "search", "arguments": {"pattern": "regex: }"}}`),
|
||||
want: map[string]any{
|
||||
"pattern": "regex: }",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complex nested with mixed braces",
|
||||
buffer: []byte(`{"name": "analyze", "arguments": {"data": "{\"items\": [{\"value\": \"}\"}, {\"code\": \"if (x) { return y; }\"}]}"}}`),
|
||||
want: map[string]any{
|
||||
"data": `{"items": [{"value": "}"}, {"code": "if (x) { return y; }"}]}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string with newline and braces",
|
||||
buffer: []byte(`{"name": "format", "arguments": {"template": "{\n \"key\": \"value\"\n}"}}`),
|
||||
want: map[string]any{
|
||||
"template": "{\n \"key\": \"value\"\n}",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string with unicode escape",
|
||||
buffer: []byte(`{"name": "test", "arguments": {"text": "Unicode: \u007B and \u007D"}}`),
|
||||
want: map[string]any{
|
||||
"text": "Unicode: { and }",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "array arguments",
|
||||
buffer: []byte(`{"name": "batch", "arguments": ["item1", "item2", "{\"nested\": true}"]}`),
|
||||
want: nil, // This should return nil because arguments is not a map
|
||||
},
|
||||
{
|
||||
name: "escaped backslash before quote",
|
||||
buffer: []byte(`{"name": "path", "arguments": {"dir": "C:\\Program Files\\{App}\\"}}`),
|
||||
want: map[string]any{
|
||||
"dir": `C:\Program Files\{App}\`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single quotes not treated as string delimiters",
|
||||
buffer: []byte(`{"name": "query", "arguments": {"sql": "SELECT * FROM users WHERE name = '{admin}'"}}`),
|
||||
want: map[string]any{
|
||||
"sql": "SELECT * FROM users WHERE name = '{admin}'",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "incomplete json at buffer end",
|
||||
buffer: []byte(`{"name": "test", "arguments": {"data": "some {"`),
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "multiple escaped quotes",
|
||||
buffer: []byte(`{"name": "echo", "arguments": {"msg": "He said \"Hello {World}\" loudly"}}`),
|
||||
want: map[string]any{
|
||||
"msg": `He said "Hello {World}" loudly`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json with comments style string",
|
||||
buffer: []byte(`{"name": "code", "arguments": {"snippet": "// This is a comment with { and }"}}`),
|
||||
want: map[string]any{
|
||||
"snippet": "// This is a comment with { and }",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "consecutive escaped backslashes",
|
||||
buffer: []byte(`{"name": "test", "arguments": {"path": "C:\\\\{folder}\\\\"}}`),
|
||||
want: map[string]any{
|
||||
"path": `C:\\{folder}\\`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty string with braces after",
|
||||
buffer: []byte(`{"name": "test", "arguments": {"a": "", "b": "{value}"}}`),
|
||||
want: map[string]any{
|
||||
"a": "",
|
||||
"b": "{value}",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unicode in key names",
|
||||
buffer: []byte(`{"name": "test", "arguments": {"key{": "value", "key}": "value2"}}`),
|
||||
want: map[string]any{
|
||||
"key{": "value",
|
||||
"key}": "value2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "very long string with braces",
|
||||
buffer: []byte(`{"name": "test", "arguments": {"data": "` + strings.Repeat("a{b}c", 100) + `"}}`),
|
||||
want: map[string]any{
|
||||
"data": strings.Repeat("a{b}c", 100),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tab characters and braces",
|
||||
buffer: []byte(`{"name": "test", "arguments": {"code": "\tif (true) {\n\t\treturn;\n\t}"}}`),
|
||||
want: map[string]any{
|
||||
"code": "\tif (true) {\n\t\treturn;\n\t}",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "null byte in string",
|
||||
buffer: []byte(`{"name": "test", "arguments": {"data": "before\u0000{after}"}}`),
|
||||
want: map[string]any{
|
||||
"data": "before\x00{after}",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "escaped quote at end of string",
|
||||
buffer: []byte(`{"name": "test", "arguments": {"data": "text with quote at end\\\""}}`),
|
||||
want: map[string]any{
|
||||
"data": `text with quote at end\"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed array and object in arguments",
|
||||
buffer: []byte(`{"name": "test", "arguments": {"items": ["{", "}", {"key": "value"}]}}`),
|
||||
want: map[string]any{
|
||||
"items": []any{"{", "}", map[string]any{"key": "value"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
Reference in New Issue
Block a user