mirror of
https://github.com/ollama/ollama.git
synced 2025-08-29 05:55:47 +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
|
return nil, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start := -1
|
||||||
var braces int
|
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 c == '{' {
|
||||||
if braces == 0 {
|
if braces == 0 {
|
||||||
start = i
|
start = i
|
||||||
}
|
}
|
||||||
braces++
|
braces++
|
||||||
} else if c == '}' && braces > 0 {
|
} else if c == '}' {
|
||||||
braces--
|
braces--
|
||||||
if braces == 0 && start != -1 {
|
if braces == 0 && start != -1 {
|
||||||
object := buffer[start : i+1]
|
object := buffer[start : i+1]
|
||||||
|
|
||||||
var data map[string]any
|
var data map[string]any
|
||||||
if err := json.Unmarshal(object, &data); err != nil {
|
if err := json.Unmarshal(object, &data); err != nil {
|
||||||
|
// not a valid object, keep looking
|
||||||
start = -1
|
start = -1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -282,6 +305,10 @@ func findArguments(buffer []byte) (map[string]any, int) {
|
|||||||
|
|
||||||
return data, i
|
return data, i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if braces < 0 {
|
||||||
|
braces = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package tools
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
@@ -1140,11 +1141,163 @@ func TestFindArguments(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "deepseek",
|
name: "deepseek",
|
||||||
buffer: []byte(`", "arguments": {"location": "Tokyo"}}</tool_call>`),
|
buffer: []byte(`"arguments": {"location": "Tokyo"}}</tool_call>`),
|
||||||
want: map[string]any{
|
want: map[string]any{
|
||||||
"location": "Tokyo",
|
"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 {
|
for _, tt := range tests {
|
||||||
|
Reference in New Issue
Block a user