|
|
package functions_test |
|
|
|
|
|
import ( |
|
|
"encoding/json" |
|
|
"regexp" |
|
|
"strings" |
|
|
|
|
|
. "github.com/mudler/LocalAI/pkg/functions" |
|
|
. "github.com/onsi/ginkgo/v2" |
|
|
. "github.com/onsi/gomega" |
|
|
) |
|
|
|
|
|
var _ = Describe("LocalAI function parse tests", func() { |
|
|
var functionConfig FunctionsConfig |
|
|
|
|
|
BeforeEach(func() { |
|
|
|
|
|
functionConfig = FunctionsConfig{} |
|
|
}) |
|
|
|
|
|
Context("when using grammars and single result expected", func() { |
|
|
It("should parse the function name and arguments correctly", func() { |
|
|
input := `{"name": "add", "arguments": {"x": 5, "y": 3}}` |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Context("when not using grammars and regex is needed", func() { |
|
|
It("should extract function name and arguments from the regex", func() { |
|
|
input := `add({"x":5,"y":3})` |
|
|
functionConfig.ResponseRegex = []string{`(?P<name>\w+)\s*\((?P<arguments>.*)\)`} |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
}) |
|
|
It("should extract function name and arguments from the regex", func() { |
|
|
input := `add({"x":5,"y":3})` |
|
|
functionConfig.ResponseRegex = []string{`(?P<function>\w+)\s*\((?P<arguments>.*)\)`} |
|
|
functionConfig.FunctionNameKey = "function" |
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Context("when having invalid input", func() { |
|
|
It("returns no results when there is no input", func() { |
|
|
input := "" |
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(0)) |
|
|
}) |
|
|
It("returns no results when is invalid", func() { |
|
|
input := "invalid input" |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(0)) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Context("when parallel calls are enabled", func() { |
|
|
It("should handle multiple function calls", func() { |
|
|
input := `[{"name": "add", "arguments": {"x": 5, "y": 3}}, {"name": "subtract", "arguments": {"x": 10, "y": 7}}]` |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(2)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
Expect(results[1].Name).To(Equal("subtract")) |
|
|
Expect(results[1].Arguments).To(Equal(`{"x":10,"y":7}`)) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Context("without grammars and without regex", func() { |
|
|
It("should parse the function name and arguments correctly with the name key", func() { |
|
|
input := `{"function": "add", "arguments": {"x": 5, "y": 3}}` |
|
|
functionConfig.FunctionNameKey = "function" |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
}) |
|
|
|
|
|
It("should parse the function name and arguments correctly with the function key", func() { |
|
|
input := `{"name": "add", "arguments": {"x": 5, "y": 3}}` |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
}) |
|
|
|
|
|
It("should parse the result by matching the JSONRegexMatch", func() { |
|
|
input := ` |
|
|
<tool_call> |
|
|
{"name": "add", "arguments": {"x": 5, "y": 3}} |
|
|
</tool_call>` |
|
|
|
|
|
functionConfig.JSONRegexMatch = []string{`(?s)<tool_call>(.*?)</tool_call>`} |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
}) |
|
|
|
|
|
It("should parse the result by matching the JSONRegexMatch", func() { |
|
|
input := ` |
|
|
{"name": "add", "arguments": {"x": 5, "y": 3}} |
|
|
</tool_call>` |
|
|
|
|
|
functionConfig.JSONRegexMatch = []string{`(?s)(.*?)</tool_call>`} |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
}) |
|
|
|
|
|
It("should parse the result even with invalid JSON", func() { |
|
|
input := `{"name": "add", "arguments": {"x": 5, "y": 3}} invalid {"name": "add", "arguments": {"x": 5, "y": 3}}` |
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(2)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Context("when using ReplaceResults to clean up input", func() { |
|
|
It("should replace text before and after JSON blob", func() { |
|
|
input := ` |
|
|
Some text before the JSON |
|
|
{"name": "add", "arguments": {"x": 5, "y": 3}} |
|
|
Some text after the JSON |
|
|
` |
|
|
|
|
|
functionConfig.ReplaceFunctionResults = []ReplaceResult{ |
|
|
{Key: `(?s)^[^{\[]*`, Value: ""}, |
|
|
{Key: `(?s)[^}\]]*$`, Value: ""}, |
|
|
} |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
}) |
|
|
|
|
|
It("should replace text before and after array JSON blob", func() { |
|
|
input := ` |
|
|
Some text before the JSON |
|
|
[{"name": "add", "arguments": {"x": 5, "y": 3}}, {"name": "subtract", "arguments": {"x": 10, "y": 7}}] |
|
|
Some text after the JSON |
|
|
` |
|
|
functionConfig.ReplaceFunctionResults = []ReplaceResult{ |
|
|
{Key: `(?s)^[^{\[]*`, Value: ""}, |
|
|
{Key: `(?s)[^}\]]*$`, Value: ""}, |
|
|
} |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(2)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
Expect(results[1].Name).To(Equal("subtract")) |
|
|
Expect(results[1].Arguments).To(Equal(`{"x":10,"y":7}`)) |
|
|
}) |
|
|
|
|
|
It("should convert single-quoted key-value pairs to double-quoted and escape double quotes within values", func() { |
|
|
input := ` |
|
|
Some text before the JSON |
|
|
{'name': '"add"', 'arguments': {'x': 5, 'z': '"v"', 'y': 'v"value"'}} |
|
|
Some text after the JSON |
|
|
` |
|
|
functionConfig.JSONRegexMatch = []string{`(?s)<tool_call>(.*?)</tool_call>`} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
functionConfig.ReplaceFunctionResults = []ReplaceResult{ |
|
|
{Key: `(?s)^[^{\[]*`, Value: ""}, |
|
|
{Key: `(?s)[^}\]]*$`, Value: ""}, |
|
|
|
|
|
|
|
|
{Key: `'([^']*?)'`, Value: `_DQUOTE_${1}_DQUOTE_`}, |
|
|
|
|
|
{Key: `\\"`, Value: `__TEMP_QUOTE__`}, |
|
|
{Key: `"`, Value: `\"`}, |
|
|
{Key: `\'`, Value: `'`}, |
|
|
{Key: `_DQUOTE_`, Value: `"`}, |
|
|
{Key: `__TEMP_QUOTE__`, Value: `"`}, |
|
|
} |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("\"add\"")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":"v\"value\"","z":"\"v\""}`)) |
|
|
}) |
|
|
|
|
|
It("should convert single-quoted key-value pairs to double-quoted and escape double quotes within values", func() { |
|
|
input := ` |
|
|
Some text before the JSON |
|
|
<tool_call>{'name': '"add"', 'arguments': {'x': 5, 'z': '"v"', 'y': 'v"value"'}}</tool_call> |
|
|
Some text after the JSON |
|
|
` |
|
|
functionConfig.JSONRegexMatch = []string{`(?s)<tool_call>(.*?)</tool_call>`} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
functionConfig.ReplaceFunctionResults = []ReplaceResult{ |
|
|
{Key: `(?s)^[^{\[]*`, Value: ""}, |
|
|
{Key: `(?s)[^}\]]*$`, Value: ""}, |
|
|
|
|
|
|
|
|
{Key: `'([^']*?)'`, Value: `_DQUOTE_${1}_DQUOTE_`}, |
|
|
|
|
|
{Key: `\\"`, Value: `__TEMP_QUOTE__`}, |
|
|
{Key: `"`, Value: `\"`}, |
|
|
{Key: `\'`, Value: `'`}, |
|
|
{Key: `_DQUOTE_`, Value: `"`}, |
|
|
{Key: `__TEMP_QUOTE__`, Value: `"`}, |
|
|
} |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("\"add\"")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":"v\"value\"","z":"\"v\""}`)) |
|
|
}) |
|
|
|
|
|
It("should detect multiple functions call where the JSONRegexMatch is repeated", func() { |
|
|
input := ` |
|
|
Some text before the JSON |
|
|
<tool_call>{"name": "add", "arguments": {"x": 5, "y": 3}}</tool_call> |
|
|
<tool_call>{"name": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call> |
|
|
Some text after the JSON |
|
|
` |
|
|
functionConfig.JSONRegexMatch = []string{`(?s)<tool_call>(.*?)</tool_call>`} |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(2)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
Expect(results[1].Name).To(Equal("subtract")) |
|
|
Expect(results[1].Arguments).To(Equal(`{"x":10,"y":7}`)) |
|
|
}) |
|
|
}) |
|
|
Context("ParseTextContent", func() { |
|
|
It("Can extract notes from the LLM result", func() { |
|
|
input := ` |
|
|
Some text before the JSON |
|
|
<sketchpad> |
|
|
roses are red |
|
|
</sketchpad> |
|
|
<tool_call>{"name": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call> |
|
|
Some text after the JSON |
|
|
` |
|
|
functionConfig.CaptureLLMResult = []string{`(?s)<sketchpad>(.*?)</sketchpad>`} |
|
|
results := ParseTextContent(input, functionConfig) |
|
|
Expect(results).To(Equal("roses are red")) |
|
|
}) |
|
|
|
|
|
It("Defaults to empty if doesn't catch any", func() { |
|
|
input := ` |
|
|
Some text before the JSON |
|
|
<tool_call>{"name": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call> |
|
|
Some text after the JSON |
|
|
` |
|
|
functionConfig.CaptureLLMResult = []string{`(?s)<sketchpad>(.*?)</sketchpad>`} |
|
|
results := ParseTextContent(input, functionConfig) |
|
|
Expect(results).To(Equal("")) |
|
|
}) |
|
|
}) |
|
|
Context("ParseJSON - when given valid JSON strings", func() { |
|
|
It("should parse multiple JSON objects", func() { |
|
|
input := `{"key1": "value1"} {"key2": "value2"}` |
|
|
expected := []map[string]any{ |
|
|
{"key1": "value1"}, |
|
|
{"key2": "value2"}, |
|
|
} |
|
|
result, err := ParseJSON(input) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(result).To(Equal(expected)) |
|
|
}) |
|
|
|
|
|
It("should parse a single JSON object with various types", func() { |
|
|
input := `{"key1": "value1", "key2": 2}` |
|
|
expected := []map[string]any{ |
|
|
{"key1": "value1", "key2": float64(2)}, |
|
|
} |
|
|
result, err := ParseJSON(input) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(result).To(Equal(expected)) |
|
|
}) |
|
|
It("should handle JSON without syntax errors gracefully", func() { |
|
|
input := `{"key1": "value1"}` |
|
|
expected := []map[string]any{ |
|
|
{"key1": "value1"}, |
|
|
} |
|
|
result, err := ParseJSON(input) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(result).To(Equal(expected)) |
|
|
}) |
|
|
It("should handle JSON without syntax errors gracefully", func() { |
|
|
input := `[{"key1": "value1"}]` |
|
|
expected := []map[string]any{ |
|
|
{"key1": "value1"}, |
|
|
} |
|
|
result, err := ParseJSON(input) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(result).To(Equal(expected)) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Context("ParseJSON - when given invalid JSON strings", func() { |
|
|
It("should return an error for completely invalid JSON", func() { |
|
|
input := `invalid json` |
|
|
result, err := ParseJSON(input) |
|
|
Expect(err).To(HaveOccurred()) |
|
|
Expect(result).To(BeNil()) |
|
|
}) |
|
|
|
|
|
It("should skip invalid JSON parts and parse valid parts", func() { |
|
|
input := `{"key1": "value1"} invalid {"key2": "value2"}` |
|
|
expected := []map[string]any{ |
|
|
{"key1": "value1"}, |
|
|
{"key2": "value2"}, |
|
|
} |
|
|
result, err := ParseJSON(input) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(result).To(Equal(expected)) |
|
|
}) |
|
|
|
|
|
PIt("should handle JSON with syntax errors gracefully", func() { |
|
|
input := `{"key1": "value1", "key2": }` |
|
|
expected := []map[string]any{ |
|
|
{"key1": "value1"}, |
|
|
} |
|
|
result, err := ParseJSON(input) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(result).To(Equal(expected)) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Context("ParseXML - when given XML tool call strings", func() { |
|
|
It("should parse a basic XML tool call with tool_call wrapper", func() { |
|
|
input := `<tool_call> |
|
|
<function=glob> |
|
|
<parameter=pattern> |
|
|
**/package.json |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("glob")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"pattern":"**/package.json"}`)) |
|
|
}) |
|
|
|
|
|
It("should parse XML tool call without tool_call wrapper", func() { |
|
|
input := `<function=add> |
|
|
<parameter=x> |
|
|
5 |
|
|
</parameter> |
|
|
<parameter=y> |
|
|
3 |
|
|
</parameter> |
|
|
</function>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
|
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
}) |
|
|
|
|
|
It("should parse XML tool call with multiple parameters", func() { |
|
|
input := `<tool_call> |
|
|
<function=function_name> |
|
|
<parameter=param_1> |
|
|
param_1_Value |
|
|
</parameter> |
|
|
<parameter=param_2> |
|
|
param_2_Value |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("function_name")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"param_1":"param_1_Value","param_2":"param_2_Value"}`)) |
|
|
}) |
|
|
|
|
|
It("should parse multiple XML tool calls", func() { |
|
|
input := `<tool_call> |
|
|
<function=add> |
|
|
<parameter=x> |
|
|
5 |
|
|
</parameter> |
|
|
<parameter=y> |
|
|
3 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call> |
|
|
<tool_call> |
|
|
<function=subtract> |
|
|
<parameter=x> |
|
|
10 |
|
|
</parameter> |
|
|
<parameter=y> |
|
|
7 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(2)) |
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
|
|
|
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`)) |
|
|
Expect(results[1].Name).To(Equal("subtract")) |
|
|
Expect(results[1].Arguments).To(Equal(`{"x":10,"y":7}`)) |
|
|
}) |
|
|
|
|
|
It("should handle mixed text and XML tool calls", func() { |
|
|
input := `A message from the LLM |
|
|
<tool_call> |
|
|
<function=glob> |
|
|
<parameter=pattern> |
|
|
**/package.json |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call> |
|
|
Some text after the tool call` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("glob")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"pattern":"**/package.json"}`)) |
|
|
}) |
|
|
|
|
|
It("should handle parameter values with newlines and whitespace", func() { |
|
|
input := `<tool_call> |
|
|
<function=search> |
|
|
<parameter=query> |
|
|
This is a multi-line |
|
|
parameter value |
|
|
with whitespace |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("search")) |
|
|
|
|
|
args := results[0].Arguments |
|
|
Expect(args).To(ContainSubstring("query")) |
|
|
Expect(args).To(ContainSubstring("multi-line")) |
|
|
}) |
|
|
|
|
|
It("should return empty results for invalid XML", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=x> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(results).NotTo(BeNil()) |
|
|
|
|
|
}) |
|
|
|
|
|
It("should return empty results when no XML tool calls found", func() { |
|
|
input := `Just some regular text without any XML tool calls` |
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(0)) |
|
|
}) |
|
|
|
|
|
It("should handle parameter values that are JSON", func() { |
|
|
input := `<tool_call> |
|
|
<function=process> |
|
|
<parameter=config> |
|
|
{"key": "value", "number": 42} |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("process")) |
|
|
|
|
|
Expect(results[0].Arguments).To(ContainSubstring("key")) |
|
|
Expect(results[0].Arguments).To(ContainSubstring("value")) |
|
|
}) |
|
|
|
|
|
It("should auto-detect Qwen3-Coder format", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
value |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("test")) |
|
|
}) |
|
|
|
|
|
It("should auto-detect GLM 4.5 format", func() { |
|
|
input := `<tool_call> |
|
|
test_function |
|
|
<arg_key>key1</arg_key> |
|
|
<arg_value>value1</arg_value> |
|
|
<arg_key>key2</arg_key> |
|
|
<arg_value>value2</arg_value> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("test_function")) |
|
|
Expect(results[0].Arguments).To(ContainSubstring("key1")) |
|
|
Expect(results[0].Arguments).To(ContainSubstring("value1")) |
|
|
}) |
|
|
|
|
|
It("should auto-detect MiniMax-M2 format", func() { |
|
|
input := `<minimax:tool_call> |
|
|
<invoke name="test_function"> |
|
|
<parameter name="key1">value1</parameter> |
|
|
<parameter name="key2">value2</parameter> |
|
|
</invoke> |
|
|
</minimax:tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("test_function")) |
|
|
Expect(results[0].Arguments).To(ContainSubstring("key1")) |
|
|
}) |
|
|
|
|
|
It("should auto-detect Functionary format", func() { |
|
|
input := `<function=test_function>{"key1": "value1", "key2": "value2"}</function>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("test_function")) |
|
|
Expect(results[0].Arguments).To(ContainSubstring("key1")) |
|
|
}) |
|
|
|
|
|
It("should use forced format when preset is specified via config", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
value |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
functionConfig.XMLFormatPreset = "qwen3-coder" |
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("test")) |
|
|
}) |
|
|
|
|
|
It("should handle GLM 4.5 format with arg_key/arg_value pairs", func() { |
|
|
input := `<tool_call> |
|
|
search_function |
|
|
<arg_key>query</arg_key> |
|
|
<arg_value>test search</arg_value> |
|
|
<arg_key>limit</arg_key> |
|
|
<arg_value>10</arg_value> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("search_function")) |
|
|
Expect(results[0].Arguments).To(ContainSubstring("query")) |
|
|
Expect(results[0].Arguments).To(ContainSubstring("test search")) |
|
|
}) |
|
|
|
|
|
It("should strip Kimi-K2 function name prefixes", func() { |
|
|
|
|
|
|
|
|
input := `<|tool_calls_section_begin|> |
|
|
<|tool_call_begin|> |
|
|
functions.search:0<|tool_call_argument_begin|>{"query": "test", "limit": 10}<|tool_call_end|> |
|
|
<|tool_calls_section_end|>` |
|
|
|
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("search")) |
|
|
Expect(results[0].Arguments).To(ContainSubstring("query")) |
|
|
}) |
|
|
|
|
|
It("should handle formats with last_val_end for last parameter", func() { |
|
|
|
|
|
input := `<tool_calls>[ |
|
|
{"name": "test_function", "arguments": {"key1": "value1", "key2": "value2"}} |
|
|
]</tool_calls>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("test_function")) |
|
|
}) |
|
|
|
|
|
It("should validate scope_start has only whitespace before it", func() { |
|
|
|
|
|
input := `text<minimax:tool_call> |
|
|
<invoke name="test"> |
|
|
<parameter name="key">value</parameter> |
|
|
</invoke> |
|
|
</minimax:tool_call>` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
|
|
|
|
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(results).NotTo(BeNil()) |
|
|
}) |
|
|
|
|
|
It("should handle empty tool calls with no arguments", func() { |
|
|
|
|
|
input := `<tool_call> |
|
|
<function=test_function> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("test_function")) |
|
|
Expect(results[0].Arguments).To(Equal("{}")) |
|
|
}) |
|
|
|
|
|
It("should support partial parsing for streaming", func() { |
|
|
|
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
value |
|
|
</parameter>` |
|
|
|
|
|
partialResult, err := ParseXMLPartial(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(partialResult).NotTo(BeNil()) |
|
|
|
|
|
Expect(partialResult).NotTo(BeNil()) |
|
|
Expect(partialResult.IsPartial).To(BeTrue()) |
|
|
}) |
|
|
|
|
|
It("should parse JSON values correctly in all formats", func() { |
|
|
|
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=count> |
|
|
42 |
|
|
</parameter> |
|
|
<parameter=enabled> |
|
|
true |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
|
|
|
Expect(results[0].Arguments).To(ContainSubstring(`"count":42`)) |
|
|
Expect(results[0].Arguments).To(ContainSubstring(`"enabled":true`)) |
|
|
}) |
|
|
|
|
|
It("should handle reasoning blocks with tool calls", func() { |
|
|
|
|
|
|
|
|
|
|
|
input := `<think> |
|
|
I need to search for information. |
|
|
</think> |
|
|
<tool_call> |
|
|
<function=search> |
|
|
<parameter=query> |
|
|
test query |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
|
|
|
results, err := ParseXML(input, nil) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("search")) |
|
|
}) |
|
|
|
|
|
It("should use iterative parser for streaming scenarios", func() { |
|
|
|
|
|
input := `<tool_call> |
|
|
<function=test_function> |
|
|
<parameter=key1> |
|
|
value1 |
|
|
</parameter> |
|
|
<parameter=key2> |
|
|
value2 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("test_function")) |
|
|
Expect(results[0].Arguments).To(ContainSubstring("key1")) |
|
|
Expect(results[0].Arguments).To(ContainSubstring("value1")) |
|
|
}) |
|
|
|
|
|
It("should handle partial parsing with iterative parser", func() { |
|
|
|
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
value |
|
|
</parameter>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, true) |
|
|
|
|
|
|
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(results).NotTo(BeNil()) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Context("ParseFunctionCall with XML tool calls", func() { |
|
|
It("should parse XML tool calls when JSON parsing fails", func() { |
|
|
input := `A message from the LLM |
|
|
<tool_call> |
|
|
<function=glob> |
|
|
<parameter=pattern> |
|
|
**/package.json |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("glob")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"pattern":"**/package.json"}`)) |
|
|
}) |
|
|
|
|
|
It("should parse XML tool calls alongside JSON tool calls", func() { |
|
|
input := `{"name": "add", "arguments": {"x": 5, "y": 3}} |
|
|
<tool_call> |
|
|
<function=subtract> |
|
|
<parameter=x> |
|
|
10 |
|
|
</parameter> |
|
|
<parameter=y> |
|
|
7 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
|
|
|
Expect(results).To(HaveLen(2)) |
|
|
|
|
|
Expect(results[0].Name).To(Equal("add")) |
|
|
|
|
|
Expect(results[1].Name).To(Equal("subtract")) |
|
|
}) |
|
|
|
|
|
It("should handle mixed content with text, JSON, and XML", func() { |
|
|
input := `Some introductory text |
|
|
{"name": "first", "arguments": {"a": 1}} |
|
|
More text in between |
|
|
<tool_call> |
|
|
<function=second> |
|
|
<parameter=b> |
|
|
2 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call> |
|
|
Final text` |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
Expect(results).To(HaveLen(2)) |
|
|
Expect(results[0].Name).To(Equal("first")) |
|
|
Expect(results[1].Name).To(Equal("second")) |
|
|
}) |
|
|
|
|
|
It("should not duplicate parse JSON inside tool_call tags", func() { |
|
|
|
|
|
|
|
|
|
|
|
input := `<tool_call> |
|
|
{"name": "get_current_weather", "arguments": {"location": "Beijing", "unit": "celsius"}} |
|
|
</tool_call>` |
|
|
|
|
|
results := ParseFunctionCall(input, functionConfig) |
|
|
|
|
|
Expect(results).To(HaveLen(1), "Should not create duplicate entries when JSON is inside XML tags") |
|
|
Expect(results[0].Name).To(Equal("get_current_weather")) |
|
|
Expect(results[0].Arguments).To(Equal(`{"location":"Beijing","unit":"celsius"}`)) |
|
|
|
|
|
Expect(results[0].Name).NotTo(ContainSubstring(`{"name"`), "Function name should not contain JSON object") |
|
|
}) |
|
|
}) |
|
|
|
|
|
Context("Iterative Parser (ChatMsgParser)", func() { |
|
|
Describe("Basic functionality", func() { |
|
|
It("should track position correctly", func() { |
|
|
parser := NewChatMsgParser("hello world", false) |
|
|
Expect(parser.Pos()).To(Equal(0)) |
|
|
Expect(parser.Input()).To(Equal("hello world")) |
|
|
Expect(parser.IsPartial()).To(BeFalse()) |
|
|
|
|
|
err := parser.MoveTo(5) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(parser.Pos()).To(Equal(5)) |
|
|
|
|
|
err = parser.MoveBack(2) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(parser.Pos()).To(Equal(3)) |
|
|
}) |
|
|
|
|
|
It("should handle position errors", func() { |
|
|
parser := NewChatMsgParser("test", false) |
|
|
err := parser.MoveTo(10) |
|
|
Expect(err).To(HaveOccurred()) |
|
|
|
|
|
err = parser.MoveBack(10) |
|
|
Expect(err).To(HaveOccurred()) |
|
|
}) |
|
|
|
|
|
It("should find literals correctly", func() { |
|
|
parser := NewChatMsgParser("hello world test", false) |
|
|
result := parser.TryFindLiteral("world") |
|
|
Expect(result).NotTo(BeNil()) |
|
|
Expect(result.Prelude).To(Equal("hello ")) |
|
|
Expect(parser.Pos()).To(Equal(11)) |
|
|
}) |
|
|
|
|
|
It("should consume literals correctly", func() { |
|
|
parser := NewChatMsgParser("hello world", false) |
|
|
success := parser.TryConsumeLiteral("hello") |
|
|
Expect(success).To(BeTrue()) |
|
|
Expect(parser.Pos()).To(Equal(5)) |
|
|
|
|
|
success = parser.TryConsumeLiteral("invalid") |
|
|
Expect(success).To(BeFalse()) |
|
|
}) |
|
|
|
|
|
It("should consume spaces", func() { |
|
|
parser := NewChatMsgParser(" hello", false) |
|
|
consumed := parser.ConsumeSpaces() |
|
|
Expect(consumed).To(BeTrue()) |
|
|
Expect(parser.Pos()).To(Equal(3)) |
|
|
}) |
|
|
|
|
|
It("should add content and tool calls", func() { |
|
|
parser := NewChatMsgParser("test", false) |
|
|
parser.AddContent("hello") |
|
|
parser.AddReasoningContent("thinking") |
|
|
parser.AddToolCall("test_func", "", `{"arg":"value"}`) |
|
|
|
|
|
Expect(parser.Content()).To(Equal("hello")) |
|
|
Expect(parser.Reasoning()).To(Equal("thinking")) |
|
|
Expect(parser.ToolCalls()).To(HaveLen(1)) |
|
|
Expect(parser.ToolCalls()[0].Name).To(Equal("test_func")) |
|
|
}) |
|
|
|
|
|
It("should not add tool call with empty name", func() { |
|
|
parser := NewChatMsgParser("test", false) |
|
|
success := parser.AddToolCall("", "", `{}`) |
|
|
Expect(success).To(BeFalse()) |
|
|
Expect(parser.ToolCalls()).To(HaveLen(0)) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("JSON parsing", func() { |
|
|
It("should parse complete JSON objects", func() { |
|
|
parser := NewChatMsgParser(`{"name":"test","value":42}`, false) |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(isPartial).To(BeFalse()) |
|
|
Expect(jsonDumpMarker).To(Equal(""), "Complete JSON should have empty jsonDumpMarker") |
|
|
Expect(jsonValue).NotTo(BeNil()) |
|
|
|
|
|
obj, ok := jsonValue.(map[string]any) |
|
|
Expect(ok).To(BeTrue()) |
|
|
Expect(obj["name"]).To(Equal("test")) |
|
|
Expect(obj["value"]).To(Equal(float64(42))) |
|
|
}) |
|
|
|
|
|
It("should parse JSON arrays (matching llama.cpp behavior)", func() { |
|
|
parser := NewChatMsgParser(`[{"a":1},{"b":2}]`, false) |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
|
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(isPartial).To(BeFalse()) |
|
|
Expect(jsonDumpMarker).To(Equal(""), "Complete JSON should have empty jsonDumpMarker") |
|
|
Expect(jsonValue).NotTo(BeNil()) |
|
|
|
|
|
arr, ok := jsonValue.([]any) |
|
|
Expect(ok).To(BeTrue()) |
|
|
Expect(arr).To(HaveLen(2)) |
|
|
|
|
|
obj1, ok := arr[0].(map[string]any) |
|
|
Expect(ok).To(BeTrue()) |
|
|
Expect(obj1["a"]).To(Equal(float64(1))) |
|
|
}) |
|
|
|
|
|
It("should heal incomplete JSON in partial mode", func() { |
|
|
parser := NewChatMsgParser(`{"name":"test","value":`, true) |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
|
|
|
|
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
Expect(jsonDumpMarker).NotTo(Equal(""), "Healed JSON should have non-empty jsonDumpMarker") |
|
|
Expect(jsonValue).NotTo(BeNil()) |
|
|
|
|
|
obj, ok := jsonValue.(map[string]any) |
|
|
Expect(ok).To(BeTrue()) |
|
|
Expect(obj["name"]).To(Equal("test")) |
|
|
}) |
|
|
|
|
|
It("should reject non-JSON input", func() { |
|
|
parser := NewChatMsgParser("not json", false) |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
Expect(err).To(HaveOccurred()) |
|
|
Expect(isPartial).To(BeFalse()) |
|
|
Expect(jsonDumpMarker).To(Equal(""), "Error case should have empty jsonDumpMarker") |
|
|
Expect(jsonValue).To(BeNil()) |
|
|
}) |
|
|
|
|
|
It("should parse multiple JSON objects", func() { |
|
|
input := `{"a":1} {"b":2}` |
|
|
results, err := ParseJSONIterative(input, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(2)) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("XML parsing", func() { |
|
|
It("should parse XML tool calls with iterative parser", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
value |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
Expect(parser.ToolCalls()).To(HaveLen(1)) |
|
|
Expect(parser.ToolCalls()[0].Name).To(Equal("test")) |
|
|
}) |
|
|
|
|
|
It("should return partial exception for incomplete XML tool calls", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
value |
|
|
</parameter>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, true) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
|
|
|
Expect(err).To(HaveOccurred()) |
|
|
_, isPartialErr := err.(*ChatMsgPartialException) |
|
|
Expect(isPartialErr).To(BeTrue(), "Should return ChatMsgPartialException for incomplete XML") |
|
|
Expect(success).To(BeFalse()) |
|
|
}) |
|
|
|
|
|
It("should return partial exception for incomplete literals", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, true) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
|
|
|
Expect(err).To(HaveOccurred()) |
|
|
_, isPartial := err.(*ChatMsgPartialException) |
|
|
Expect(isPartial).To(BeTrue(), "Should return ChatMsgPartialException for incomplete literal") |
|
|
Expect(success).To(BeFalse()) |
|
|
}) |
|
|
|
|
|
It("should handle empty tool calls", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
Expect(parser.ToolCalls()).To(HaveLen(1)) |
|
|
Expect(parser.ToolCalls()[0].Arguments).To(Equal("{}")) |
|
|
}) |
|
|
|
|
|
It("should handle Kimi-K2 function name stripping", func() { |
|
|
input := `<|tool_calls_section_begin|> |
|
|
<|tool_call_begin|> |
|
|
functions.search:0 |
|
|
<|tool_call_argument_begin|>{"query":"test"} |
|
|
<|tool_call_end|> |
|
|
<|tool_calls_section_end|>` |
|
|
format := GetXMLFormatPreset("kimi-k2") |
|
|
Expect(format).NotTo(BeNil()) |
|
|
|
|
|
results, err := ParseXML(input, format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("search")) |
|
|
}) |
|
|
|
|
|
It("should validate scope_start has only whitespace before it", func() { |
|
|
input := `text<minimax:tool_call><invoke name="test"><parameter name="key">value</parameter></invoke></minimax:tool_call>` |
|
|
format := GetXMLFormatPreset("minimax-m2") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeFalse()) |
|
|
}) |
|
|
|
|
|
It("should handle GLM 4.5 format", func() { |
|
|
input := `<tool_call> |
|
|
test_function |
|
|
<arg_key>key1</arg_key> |
|
|
<arg_value>value1</arg_value> |
|
|
<arg_key>key2</arg_key> |
|
|
<arg_value>value2</arg_value> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("glm-4.5") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
Expect(parser.ToolCalls()).To(HaveLen(1)) |
|
|
Expect(parser.ToolCalls()[0].Name).To(Equal("test_function")) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("Partial parsing and streaming", func() { |
|
|
It("should heal incomplete JSON in partial mode", func() { |
|
|
parser := NewChatMsgParser(`{"name":"test","value":`, true) |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
|
|
|
|
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
Expect(jsonDumpMarker).NotTo(Equal(""), "Healed JSON should have non-empty jsonDumpMarker") |
|
|
Expect(jsonValue).NotTo(BeNil()) |
|
|
|
|
|
obj, ok := jsonValue.(map[string]any) |
|
|
Expect(ok).To(BeTrue()) |
|
|
Expect(obj["name"]).To(Equal("test")) |
|
|
}) |
|
|
|
|
|
It("should return partial exception for incomplete XML", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, true) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
|
|
|
Expect(err).To(HaveOccurred()) |
|
|
_, isPartial := err.(*ChatMsgPartialException) |
|
|
Expect(isPartial).To(BeTrue(), "Should return ChatMsgPartialException for incomplete XML") |
|
|
Expect(success).To(BeFalse()) |
|
|
}) |
|
|
|
|
|
It("should return partial exception for incomplete tool call", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
partial_value` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, true) |
|
|
_, err := parser.TryConsumeXMLToolCalls(format) |
|
|
|
|
|
Expect(err).To(HaveOccurred()) |
|
|
_, ok := err.(*ChatMsgPartialException) |
|
|
Expect(ok).To(BeTrue(), "Should return ChatMsgPartialException for incomplete tool call") |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("JSON parsing order and primitive fallback", func() { |
|
|
It("should parse JSON object before val_end", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
{"nested":"value"} |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
Expect(parser.ToolCalls()).To(HaveLen(1)) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
value, ok := args["key"] |
|
|
Expect(ok).To(BeTrue()) |
|
|
nested, ok := value.(map[string]any) |
|
|
Expect(ok).To(BeTrue()) |
|
|
Expect(nested["nested"]).To(Equal("value")) |
|
|
}) |
|
|
|
|
|
It("should parse JSON primitive null", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
null |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["key"]).To(BeNil()) |
|
|
}) |
|
|
|
|
|
It("should parse JSON primitive true", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
true |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["key"]).To(Equal(true)) |
|
|
}) |
|
|
|
|
|
It("should parse JSON primitive false", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
false |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["key"]).To(Equal(false)) |
|
|
}) |
|
|
|
|
|
It("should parse JSON primitive number", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
42 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["key"]).To(Equal(float64(42))) |
|
|
}) |
|
|
|
|
|
It("should parse JSON primitive negative number", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
-123.45 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(args["key"]).To(Equal(float64(-123.45))) |
|
|
}) |
|
|
|
|
|
It("should fallback to text when JSON not found", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
plain text value |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["key"]).To(Equal("plain text value")) |
|
|
}) |
|
|
|
|
|
It("should handle JSON array in parameter value", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
[1,2,3] |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
arr, ok := args["key"].([]any) |
|
|
Expect(ok).To(BeTrue()) |
|
|
Expect(arr).To(HaveLen(3)) |
|
|
Expect(arr[0]).To(Equal(float64(1))) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("Error recovery", func() { |
|
|
It("should recover from recoverable errors", func() { |
|
|
parser := NewChatMsgParser("test", false) |
|
|
|
|
|
err := parser.MoveTo(100) |
|
|
Expect(err).To(HaveOccurred()) |
|
|
|
|
|
Expect(parser.Pos()).To(Equal(0)) |
|
|
}) |
|
|
|
|
|
It("should handle ChatMsgPartialException", func() { |
|
|
err := &ChatMsgPartialException{Message: "test partial"} |
|
|
Expect(err.Error()).To(Equal("test partial")) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("Reasoning block handling", func() { |
|
|
It("should extract reasoning blocks from content", func() { |
|
|
input := `Some text <think>This is reasoning</think> More text` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
err := parser.ParseMsgWithXMLToolCalls(format, "<think>", "</think>") |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(parser.Reasoning()).To(Equal("This is reasoning")) |
|
|
Expect(parser.Content()).To(ContainSubstring("Some text")) |
|
|
Expect(parser.Content()).To(ContainSubstring("More text")) |
|
|
}) |
|
|
|
|
|
It("should handle unclosed reasoning blocks", func() { |
|
|
input := `Some text <think>This is unclosed reasoning` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, true) |
|
|
err := parser.ParseMsgWithXMLToolCalls(format, "<think>", "</think>") |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(parser.Reasoning()).To(ContainSubstring("This is unclosed reasoning")) |
|
|
}) |
|
|
|
|
|
It("should handle tool calls inside reasoning blocks when allowed", func() { |
|
|
input := `<think>Reasoning <tool_call><function=test></function></tool_call></think>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
format.AllowToolcallInThink = true |
|
|
parser := NewChatMsgParser(input, false) |
|
|
err := parser.ParseMsgWithXMLToolCalls(format, "<think>", "</think>") |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(parser.ToolCalls()).To(HaveLen(1)) |
|
|
Expect(parser.ToolCalls()[0].Name).To(Equal("test")) |
|
|
}) |
|
|
|
|
|
It("should skip tool calls inside reasoning blocks when not allowed", func() { |
|
|
input := `<think>Reasoning <tool_call><function=test></function></tool_call></think>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
format.AllowToolcallInThink = false |
|
|
parser := NewChatMsgParser(input, false) |
|
|
err := parser.ParseMsgWithXMLToolCalls(format, "<think>", "</think>") |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(parser.ToolCalls()).To(HaveLen(0)) |
|
|
}) |
|
|
|
|
|
It("should handle multiple reasoning blocks", func() { |
|
|
input := `<think>First</think> Text <think>Second</think> More text` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
err := parser.ParseMsgWithXMLToolCalls(format, "<think>", "</think>") |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(parser.Reasoning()).To(ContainSubstring("First")) |
|
|
Expect(parser.Reasoning()).To(ContainSubstring("Second")) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("JSON healing marker behavior", func() { |
|
|
It("should return empty jsonDumpMarker for complete JSON", func() { |
|
|
parser := NewChatMsgParser(`{"key":"value"}`, false) |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(isPartial).To(BeFalse()) |
|
|
Expect(jsonDumpMarker).To(Equal(""), "Complete JSON should have empty jsonDumpMarker") |
|
|
Expect(jsonValue).NotTo(BeNil()) |
|
|
}) |
|
|
|
|
|
It("should return non-empty jsonDumpMarker for healed JSON", func() { |
|
|
parser := NewChatMsgParser(`{"key":"value`, true) |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
Expect(jsonDumpMarker).NotTo(Equal(""), "Healed JSON should have non-empty jsonDumpMarker") |
|
|
Expect(jsonValue).NotTo(BeNil()) |
|
|
}) |
|
|
|
|
|
It("should reject healed JSON when val_end doesn't follow", func() { |
|
|
|
|
|
|
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
{"nested":"value` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, true) |
|
|
_, err := parser.TryConsumeXMLToolCalls(format) |
|
|
|
|
|
Expect(err).To(HaveOccurred()) |
|
|
_, isPartial := err.(*ChatMsgPartialException) |
|
|
Expect(isPartial).To(BeTrue(), "Should return ChatMsgPartialException for partial XML") |
|
|
|
|
|
|
|
|
}) |
|
|
|
|
|
It("should accept non-healed JSON when val_end follows", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
{"nested":"value"} |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
Expect(parser.ToolCalls()).To(HaveLen(1)) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
value, ok := args["key"] |
|
|
Expect(ok).To(BeTrue()) |
|
|
nested, ok := value.(map[string]any) |
|
|
Expect(ok).To(BeTrue()) |
|
|
Expect(nested["nested"]).To(Equal("value")) |
|
|
}) |
|
|
|
|
|
It("should cut JSON string at jsonDumpMarker position for partial tool calls", func() { |
|
|
|
|
|
|
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
{"nested":"value` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, true) |
|
|
_, err := parser.TryConsumeXMLToolCalls(format) |
|
|
|
|
|
Expect(err).To(HaveOccurred()) |
|
|
_, isPartial := err.(*ChatMsgPartialException) |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
|
|
|
Expect(parser.ToolCalls()).To(HaveLen(1), "Should emit partial tool call") |
|
|
|
|
|
|
|
|
argsStr := parser.ToolCalls()[0].Arguments |
|
|
|
|
|
|
|
|
Expect(argsStr).NotTo(HaveSuffix("}"), "Partial JSON should be cut and not end with }") |
|
|
|
|
|
Expect(argsStr).To(ContainSubstring(`"key"`)) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("JSON parsing order and primitive fallback", func() { |
|
|
It("should parse JSON object before val_end", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
{"nested":"value"} |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
Expect(parser.ToolCalls()).To(HaveLen(1)) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
value, ok := args["key"] |
|
|
Expect(ok).To(BeTrue()) |
|
|
nested, ok := value.(map[string]any) |
|
|
Expect(ok).To(BeTrue()) |
|
|
Expect(nested["nested"]).To(Equal("value")) |
|
|
}) |
|
|
|
|
|
It("should parse JSON primitive null", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
null |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["key"]).To(BeNil()) |
|
|
}) |
|
|
|
|
|
It("should parse JSON primitive true", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
true |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["key"]).To(Equal(true)) |
|
|
}) |
|
|
|
|
|
It("should parse JSON primitive false", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
false |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["key"]).To(Equal(false)) |
|
|
}) |
|
|
|
|
|
It("should parse JSON primitive number", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
42 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["key"]).To(Equal(float64(42))) |
|
|
}) |
|
|
|
|
|
It("should parse JSON primitive negative number", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
-123.45 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(args["key"]).To(Equal(float64(-123.45))) |
|
|
}) |
|
|
|
|
|
It("should fallback to text when JSON not found", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
plain text value |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["key"]).To(Equal("plain text value")) |
|
|
}) |
|
|
|
|
|
It("should handle JSON array in parameter value", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
[1,2,3] |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
parser := NewChatMsgParser(input, false) |
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(success).To(BeTrue()) |
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(parser.ToolCalls()[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
arr, ok := args["key"].([]any) |
|
|
Expect(ok).To(BeTrue()) |
|
|
Expect(arr).To(HaveLen(3)) |
|
|
Expect(arr[0]).To(Equal(float64(1))) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("Healing markers", func() { |
|
|
It("should generate unique healing markers", func() { |
|
|
parser1 := NewChatMsgParser("test", false) |
|
|
parser2 := NewChatMsgParser("test", false) |
|
|
|
|
|
marker1 := parser1.HealingMarker() |
|
|
marker2 := parser2.HealingMarker() |
|
|
|
|
|
|
|
|
Expect(marker1).NotTo(BeEmpty()) |
|
|
Expect(marker2).NotTo(BeEmpty()) |
|
|
|
|
|
|
|
|
}) |
|
|
|
|
|
It("should not include healing marker in input", func() { |
|
|
input := "test input" |
|
|
parser := NewChatMsgParser(input, false) |
|
|
marker := parser.HealingMarker() |
|
|
Expect(strings.Contains(input, marker)).To(BeFalse()) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("ParseXMLIterative", func() { |
|
|
It("should parse XML with auto-detection", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
value |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("test")) |
|
|
}) |
|
|
|
|
|
It("should parse XML with specific format", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key> |
|
|
value |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
format := GetXMLFormatPreset("qwen3-coder") |
|
|
results, err := ParseXMLIterative(input, format, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
}) |
|
|
|
|
|
It("should return partial tool call for incomplete XML", func() { |
|
|
input := `<tool_call> |
|
|
<function=test> |
|
|
<parameter=key>` |
|
|
results, err := ParseXMLIterative(input, nil, true) |
|
|
|
|
|
|
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).NotTo(BeNil()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0].Name).To(Equal("test")) |
|
|
|
|
|
Expect(results[0].Arguments).To(ContainSubstring("key")) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("ParseJSONIterative", func() { |
|
|
It("should parse complete JSON", func() { |
|
|
input := `{"name":"test","value":42}` |
|
|
results, err := ParseJSONIterative(input, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(1)) |
|
|
Expect(results[0]["name"]).To(Equal("test")) |
|
|
}) |
|
|
|
|
|
It("should parse multiple JSON objects", func() { |
|
|
input := `{"a":1} {"b":2} {"c":3}` |
|
|
results, err := ParseJSONIterative(input, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).To(HaveLen(3)) |
|
|
}) |
|
|
|
|
|
It("should handle partial JSON gracefully (may fall back to legacy parser)", func() { |
|
|
input := `{"name":"test","value":` |
|
|
results, err := ParseJSONIterative(input, true) |
|
|
|
|
|
|
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(results).NotTo(BeNil()) |
|
|
|
|
|
Expect(len(results)).To(BeNumerically(">=", 0)) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("Comprehensive JSON partial parsing tests (matching llama.cpp)", func() { |
|
|
|
|
|
testJSONHealing := func(input, expectedJSON, expectedMarker string) { |
|
|
parser := NewChatMsgParser(input, true) |
|
|
parser.SetHealingMarker("$foo") |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
Expect(err).NotTo(HaveOccurred(), "Should parse successfully: %s", input) |
|
|
Expect(isPartial).To(BeTrue(), "Should be partial: %s", input) |
|
|
|
|
|
if expectedMarker != "" { |
|
|
|
|
|
markerRegex := regexp.QuoteMeta(expectedMarker) |
|
|
if strings.HasPrefix(expectedMarker, ",") { |
|
|
|
|
|
Expect(jsonDumpMarker).To(MatchRegexp(`^,?`+markerRegex+`$`), "jsonDumpMarker mismatch for input: %s (got %q, expected %q)", input, jsonDumpMarker, expectedMarker) |
|
|
} else { |
|
|
|
|
|
Expect(jsonDumpMarker).To(MatchRegexp(`^,?`+markerRegex+`$`), "jsonDumpMarker mismatch for input: %s (got %q, expected %q)", input, jsonDumpMarker, expectedMarker) |
|
|
} |
|
|
} else { |
|
|
Expect(jsonDumpMarker).To(Equal(expectedMarker), "jsonDumpMarker mismatch for input: %s", input) |
|
|
} |
|
|
|
|
|
|
|
|
jsonBytes, err := json.Marshal(jsonValue) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
actualJSON := string(jsonBytes) |
|
|
|
|
|
if strings.HasPrefix(expectedJSON, "[") && strings.HasPrefix(actualJSON, "[") { |
|
|
|
|
|
|
|
|
Expect(actualJSON).To(MatchRegexp(`^\[.*\]$`), "Should be valid JSON array for input: %s (got %q, expected %q)", input, actualJSON, expectedJSON) |
|
|
} else { |
|
|
Expect(actualJSON).To(Equal(expectedJSON), "JSON mismatch for input: %s (got %q, expected %q)", input, actualJSON, expectedJSON) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
testIncrementalParsing := func(input string) { |
|
|
|
|
|
|
|
|
for i := 1; i < len(input); i++ { |
|
|
prefix := input[:i] |
|
|
parser := NewChatMsgParser(prefix, true) |
|
|
parser.SetHealingMarker("$llama.cpp.json$") |
|
|
jsonValue, _, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
_, isPartialErr := err.(*ChatMsgPartialException) |
|
|
if !isPartialErr { |
|
|
|
|
|
|
|
|
|
|
|
continue |
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
Expect(jsonValue).NotTo(BeNil(), "Should parse prefix: %s", prefix) |
|
|
if jsonDumpMarker != "" { |
|
|
|
|
|
jsonBytes, _ := json.Marshal(jsonValue) |
|
|
Expect(len(jsonBytes)).To(BeNumerically(">", 0), "Should have non-empty JSON for prefix: %s", prefix) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
It("should handle incremental prefix parsing", func() { |
|
|
testIncrementalParsing(`{"a": "b"}`) |
|
|
testIncrementalParsing(`{"hey": 1, "ho\"ha": [1]}`) |
|
|
testIncrementalParsing(`[{"a": "b"}]`) |
|
|
}) |
|
|
|
|
|
It("should parse complete JSON without healing", func() { |
|
|
parser := NewChatMsgParser(`[{"a":"b"}, "y"]`, false) |
|
|
parser.SetHealingMarker("$foo") |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(isPartial).To(BeFalse()) |
|
|
Expect(jsonDumpMarker).To(Equal(""), "Complete JSON should have empty marker") |
|
|
|
|
|
jsonBytes, _ := json.Marshal(jsonValue) |
|
|
jsonStr := string(jsonBytes) |
|
|
Expect(jsonStr).To(Equal(`[{"a":"b"},"y"]`), "Should produce compact JSON") |
|
|
}) |
|
|
|
|
|
It("should heal partial literals in arrays", func() { |
|
|
|
|
|
|
|
|
testJSONHealing(`[1)`, `[""]`, `"$foo`) |
|
|
testJSONHealing(`[tru)`, `[""]`, `"$foo`) |
|
|
testJSONHealing(`[n)`, `[""]`, `"$foo`) |
|
|
testJSONHealing(`[nul)`, `[""]`, `"$foo`) |
|
|
testJSONHealing(`[23.2)`, `[""]`, `"$foo`) |
|
|
}) |
|
|
|
|
|
It("should heal partial literals in objects", func() { |
|
|
|
|
|
|
|
|
testJSONHealing(`{"a": 1)`, `{"a":""}`, `"$foo`) |
|
|
testJSONHealing(`{"a": tru)`, `{"a":""}`, `"$foo`) |
|
|
testJSONHealing(`{"a": n)`, `{"a":""}`, `"$foo`) |
|
|
testJSONHealing(`{"a": nul)`, `{"a":""}`, `"$foo`) |
|
|
testJSONHealing(`{"a": 23.2)`, `{"a":""}`, `"$foo`) |
|
|
}) |
|
|
|
|
|
It("should heal empty structures", func() { |
|
|
|
|
|
|
|
|
parser := NewChatMsgParser(`{`, true) |
|
|
parser.SetHealingMarker("$foo") |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
Expect(err).NotTo(HaveOccurred(), "Should parse successfully: {") |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
Expect(jsonDumpMarker).To(Equal(`"$foo`), "Marker should be \"$foo") |
|
|
jsonBytes, _ := json.Marshal(jsonValue) |
|
|
|
|
|
|
|
|
obj, ok := jsonValue.(map[string]any) |
|
|
Expect(ok).To(BeTrue(), "Should be an object") |
|
|
|
|
|
Expect(len(obj)).To(BeNumerically(">=", 0), "Object should exist (may be empty after marker removal)") |
|
|
|
|
|
parser = NewChatMsgParser(`[`, true) |
|
|
parser.SetHealingMarker("$foo") |
|
|
jsonValue, isPartial, jsonDumpMarker, err = parser.TryConsumeJSON() |
|
|
Expect(err).NotTo(HaveOccurred(), "Should parse successfully: [") |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
Expect(jsonDumpMarker).To(Equal(`"$foo`), "Marker should be \"$foo") |
|
|
jsonBytes, _ = json.Marshal(jsonValue) |
|
|
|
|
|
|
|
|
actualJSON := string(jsonBytes) |
|
|
Expect(actualJSON).To(Equal(`[""]`), "After marker removal, should be [\"\"]") |
|
|
}) |
|
|
|
|
|
It("should handle healing after complete literals", func() { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parser1 := NewChatMsgParser(`[1 )`, true) |
|
|
parser1.SetHealingMarker("$foo") |
|
|
jsonValue1, isPartial1, jsonDumpMarker1, err1 := parser1.TryConsumeJSON() |
|
|
Expect(err1).NotTo(HaveOccurred()) |
|
|
Expect(isPartial1).To(BeTrue()) |
|
|
|
|
|
Expect(jsonDumpMarker1).To(MatchRegexp(`^,?"\$foo`), "Marker should be ,\"$foo or \"$foo") |
|
|
jsonBytes1, _ := json.Marshal(jsonValue1) |
|
|
|
|
|
|
|
|
actualJSON1 := string(jsonBytes1) |
|
|
Expect(actualJSON1).To(MatchRegexp(`^\[.*\]$`), "Should be a valid JSON array") |
|
|
|
|
|
testJSONHealing(`[{})`, `[{},""]`, `"$foo`) |
|
|
testJSONHealing(`[{} )`, `[{},""]`, `"$foo`) |
|
|
testJSONHealing(`[true)`, `[""]`, `"$foo`) |
|
|
testJSONHealing(`[true )`, `[true,""]`, `"$foo`) |
|
|
testJSONHealing(`[true,)`, `[true,""]`, `"$foo`) |
|
|
}) |
|
|
|
|
|
It("should heal nested structures", func() { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parser := NewChatMsgParser(`[{"a": [)`, true) |
|
|
parser.SetHealingMarker("$foo") |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
if err == nil { |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
Expect(jsonDumpMarker).NotTo(Equal("")) |
|
|
jsonBytes, _ := json.Marshal(jsonValue) |
|
|
Expect(string(jsonBytes)).To(ContainSubstring("a"), "Should contain 'a' key") |
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
It("should heal partial strings", func() { |
|
|
|
|
|
|
|
|
|
|
|
parser := NewChatMsgParser(`[{"a": "b"})`, true) |
|
|
parser.SetHealingMarker("$foo") |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
|
|
|
Expect(jsonDumpMarker).To(Equal(`"$foo`), "Marker should be \"$foo") |
|
|
jsonBytes, _ := json.Marshal(jsonValue) |
|
|
|
|
|
actualJSON := string(jsonBytes) |
|
|
|
|
|
Expect(actualJSON).To(Equal(`[""]`), "After marker removal should be [\"\"]") |
|
|
|
|
|
|
|
|
|
|
|
parser3 := NewChatMsgParser(`[{"a": "b"} )`, true) |
|
|
parser3.SetHealingMarker("$foo") |
|
|
jsonValue3, isPartial3, jsonDumpMarker3, err3 := parser3.TryConsumeJSON() |
|
|
Expect(err3).NotTo(HaveOccurred()) |
|
|
Expect(isPartial3).To(BeTrue()) |
|
|
|
|
|
Expect(jsonDumpMarker3).To(MatchRegexp(`^,?"\$foo`), "Marker should be ,\"$foo or \"$foo") |
|
|
jsonBytes3, _ := json.Marshal(jsonValue3) |
|
|
|
|
|
|
|
|
actualJSON3 := string(jsonBytes3) |
|
|
Expect(actualJSON3).To(MatchRegexp(`^\[.*\]$`), "Should be a valid JSON array") |
|
|
testJSONHealing(`[{"a": "b"},)`, `[{"a":"b"},""]`, `"$foo`) |
|
|
testJSONHealing(`[{"a": "b"}, )`, `[{"a":"b"},""]`, `"$foo`) |
|
|
|
|
|
|
|
|
|
|
|
parser1 := NewChatMsgParser(`{ "code)`, true) |
|
|
parser1.SetHealingMarker("$foo") |
|
|
jsonValue1, isPartial1, jsonDumpMarker1, err1 := parser1.TryConsumeJSON() |
|
|
Expect(err1).NotTo(HaveOccurred()) |
|
|
Expect(isPartial1).To(BeTrue()) |
|
|
Expect(jsonDumpMarker1).To(Equal(`$foo`), "Marker should be $foo") |
|
|
jsonBytes1, _ := json.Marshal(jsonValue1) |
|
|
|
|
|
Expect(string(jsonBytes1)).To(ContainSubstring("code"), "Should contain 'code'") |
|
|
|
|
|
|
|
|
|
|
|
parser2 := NewChatMsgParser(`{ "code\)`, true) |
|
|
parser2.SetHealingMarker("$foo") |
|
|
jsonValue2, isPartial2, jsonDumpMarker2, err2 := parser2.TryConsumeJSON() |
|
|
if err2 == nil { |
|
|
|
|
|
Expect(isPartial2).To(BeTrue()) |
|
|
Expect(jsonDumpMarker2).NotTo(Equal(""), "Marker should not be empty") |
|
|
jsonBytes2, _ := json.Marshal(jsonValue2) |
|
|
Expect(string(jsonBytes2)).To(ContainSubstring("code"), "Should contain 'code'") |
|
|
} else { |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
parserCode := NewChatMsgParser(`{ "code")`, true) |
|
|
parserCode.SetHealingMarker("$foo") |
|
|
jsonValueCode, isPartialCode, jsonDumpMarkerCode, errCode := parserCode.TryConsumeJSON() |
|
|
if errCode == nil { |
|
|
|
|
|
Expect(isPartialCode).To(BeTrue()) |
|
|
Expect(jsonDumpMarkerCode).NotTo(Equal(""), "Marker should not be empty") |
|
|
jsonBytesCode, _ := json.Marshal(jsonValueCode) |
|
|
Expect(string(jsonBytesCode)).To(ContainSubstring("code"), "Should contain 'code'") |
|
|
} else { |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
parserKey := NewChatMsgParser(`{ "key")`, true) |
|
|
parserKey.SetHealingMarker("$foo") |
|
|
jsonValueKey, isPartialKey, jsonDumpMarkerKey, errKey := parserKey.TryConsumeJSON() |
|
|
if errKey == nil { |
|
|
Expect(isPartialKey).To(BeTrue()) |
|
|
Expect(jsonDumpMarkerKey).NotTo(Equal(""), "Marker should not be empty") |
|
|
jsonBytesKey, _ := json.Marshal(jsonValueKey) |
|
|
Expect(string(jsonBytesKey)).To(ContainSubstring("key"), "Should contain 'key'") |
|
|
} |
|
|
_ = jsonValue2 |
|
|
_ = jsonValueCode |
|
|
_ = jsonValueKey |
|
|
|
|
|
_ = jsonValue1 |
|
|
_ = jsonValue2 |
|
|
}) |
|
|
|
|
|
It("should heal unicode escape sequences", func() { |
|
|
|
|
|
|
|
|
parser := NewChatMsgParser(`{"a":"\u)`, true) |
|
|
parser.SetHealingMarker("$foo") |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
|
|
|
Expect(jsonDumpMarker).NotTo(Equal(""), "Marker should not be empty") |
|
|
Expect(jsonDumpMarker).To(ContainSubstring("$foo"), "Marker should contain $foo") |
|
|
jsonBytes, _ := json.Marshal(jsonValue) |
|
|
|
|
|
Expect(string(jsonBytes)).To(ContainSubstring(`"a"`), "Should contain 'a' key") |
|
|
|
|
|
parser = NewChatMsgParser(`{"a":"\u00)`, true) |
|
|
parser.SetHealingMarker("$foo") |
|
|
jsonValue, isPartial, jsonDumpMarker, err = parser.TryConsumeJSON() |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
|
|
|
Expect(jsonDumpMarker).NotTo(Equal(""), "Marker should not be empty") |
|
|
Expect(jsonDumpMarker).To(ContainSubstring("$foo"), "Marker should contain $foo") |
|
|
|
|
|
|
|
|
parser = NewChatMsgParser(`{"a":"\ud300)`, true) |
|
|
parser.SetHealingMarker("$foo") |
|
|
jsonValue, isPartial, jsonDumpMarker, err = parser.TryConsumeJSON() |
|
|
if err == nil { |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
Expect(jsonDumpMarker).NotTo(Equal("")) |
|
|
} |
|
|
|
|
|
parser = NewChatMsgParser(`{"a":"\ud800)`, true) |
|
|
parser.SetHealingMarker("$foo") |
|
|
jsonValue, isPartial, jsonDumpMarker, err = parser.TryConsumeJSON() |
|
|
if err == nil { |
|
|
Expect(isPartial).To(BeTrue()) |
|
|
|
|
|
Expect(jsonDumpMarker).To(MatchRegexp(`.*\\udc00.*\$foo|.*\$foo`), "Marker should include surrogate padding or $foo") |
|
|
} |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("Incremental streaming test infrastructure (matching llama.cpp)", func() { |
|
|
|
|
|
utf8TruncateSafe := func(s string, maxLen int) string { |
|
|
if maxLen >= len(s) { |
|
|
return s |
|
|
} |
|
|
if maxLen <= 0 { |
|
|
return "" |
|
|
} |
|
|
|
|
|
for maxLen > 0 && (s[maxLen]&0xC0) == 0x80 { |
|
|
maxLen-- |
|
|
} |
|
|
return s[:maxLen] |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
testParserWithStreaming := func(expected []FuncCallResults, input string, parseFunc func(string, bool) ([]FuncCallResults, error)) { |
|
|
var merged []FuncCallResults |
|
|
var lastResults []FuncCallResults |
|
|
|
|
|
|
|
|
for i := 1; i <= len(input); i++ { |
|
|
prefix := utf8TruncateSafe(input, i) |
|
|
if len(prefix) == 0 { |
|
|
continue |
|
|
} |
|
|
|
|
|
results, err := parseFunc(prefix, true) |
|
|
if err != nil { |
|
|
|
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
if len(results) == 0 { |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for _, result := range results { |
|
|
if len(merged) < len(results) { |
|
|
|
|
|
merged = append(merged, FuncCallResults{ |
|
|
Name: result.Name, |
|
|
Arguments: result.Arguments, |
|
|
}) |
|
|
} else { |
|
|
|
|
|
idx := len(merged) - 1 |
|
|
if idx >= 0 && merged[idx].Name == result.Name { |
|
|
merged[idx].Arguments += result.Arguments |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if len(results) > 0 { |
|
|
Expect(len(results)).To(BeNumerically("<=", len(merged)), "Results should not exceed merged count") |
|
|
} |
|
|
|
|
|
_ = lastResults |
|
|
lastResults = results |
|
|
} |
|
|
|
|
|
|
|
|
finalResults, err := parseFunc(input, false) |
|
|
Expect(err).NotTo(HaveOccurred(), "Should parse complete input") |
|
|
Expect(len(finalResults)).To(Equal(len(expected)), "Final results count should match expected") |
|
|
|
|
|
|
|
|
if len(merged) > 0 { |
|
|
Expect(len(merged)).To(BeNumerically(">=", len(expected)), "Merged results should have at least expected count") |
|
|
} |
|
|
} |
|
|
|
|
|
It("should handle streaming XML tool calls with multiple parameters", func() { |
|
|
expected := []FuncCallResults{ |
|
|
{ |
|
|
Name: "complex_function", |
|
|
Arguments: `{"name":"John Doe","age":30,"active":true,"score":95.5}`, |
|
|
}, |
|
|
} |
|
|
|
|
|
input := `<tool_call> |
|
|
<function=complex_function> |
|
|
<parameter=name> |
|
|
John Doe |
|
|
</parameter> |
|
|
<parameter=age> |
|
|
30 |
|
|
</parameter> |
|
|
<parameter=active> |
|
|
true |
|
|
</parameter> |
|
|
<parameter=score> |
|
|
95.5 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
testParserWithStreaming(expected, input, func(s string, isPartial bool) ([]FuncCallResults, error) { |
|
|
return ParseXMLIterative(s, nil, isPartial) |
|
|
}) |
|
|
}) |
|
|
|
|
|
It("should handle streaming with special characters and Unicode", func() { |
|
|
expected := []FuncCallResults{ |
|
|
{ |
|
|
Name: "unicode_function", |
|
|
Arguments: `{"message":"Hello 世界! 🌍 Special chars: @#$%^&*()"}`, |
|
|
}, |
|
|
} |
|
|
|
|
|
input := `<tool_call> |
|
|
<function=unicode_function> |
|
|
<parameter=message> |
|
|
Hello 世界! 🌍 Special chars: @#$%^&*() |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
testParserWithStreaming(expected, input, func(s string, isPartial bool) ([]FuncCallResults, error) { |
|
|
return ParseXMLIterative(s, nil, isPartial) |
|
|
}) |
|
|
}) |
|
|
|
|
|
It("should handle streaming with multiline content", func() { |
|
|
expected := []FuncCallResults{ |
|
|
{ |
|
|
Name: "code_function", |
|
|
Arguments: `{"code":"def hello():\n print(\"Hello, World!\")\n return True"}`, |
|
|
}, |
|
|
} |
|
|
|
|
|
input := `<tool_call> |
|
|
<function=code_function> |
|
|
<parameter=code> |
|
|
def hello(): |
|
|
print("Hello, World!") |
|
|
return True |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
testParserWithStreaming(expected, input, func(s string, isPartial bool) ([]FuncCallResults, error) { |
|
|
return ParseXMLIterative(s, nil, isPartial) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("Unicode and Special Character Tests (matching llama.cpp)", func() { |
|
|
It("should handle Unicode characters in XML parameters", func() { |
|
|
input := `<tool_call> |
|
|
<function=unicode_function> |
|
|
<parameter=message> |
|
|
Hello 世界! 🌍 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(len(results)).To(Equal(1)) |
|
|
Expect(results[0].Name).To(Equal("unicode_function")) |
|
|
|
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(results[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(args["message"]).To(ContainSubstring("世界")) |
|
|
Expect(args["message"]).To(ContainSubstring("🌍")) |
|
|
}) |
|
|
|
|
|
It("should handle special characters in XML parameters", func() { |
|
|
input := `<tool_call> |
|
|
<function=special_function> |
|
|
<parameter=chars> |
|
|
@#$%^&*() |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(len(results)).To(Equal(1)) |
|
|
Expect(results[0].Name).To(Equal("special_function")) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(results[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(args["chars"]).To(ContainSubstring("@#$%^&*()")) |
|
|
}) |
|
|
|
|
|
It("should handle scientific notation in numbers", func() { |
|
|
input := `<tool_call> |
|
|
<function=math_function> |
|
|
<parameter=value> |
|
|
1.23e-4 |
|
|
</parameter> |
|
|
<parameter=large> |
|
|
1.5e+10 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(len(results)).To(Equal(1)) |
|
|
Expect(results[0].Name).To(Equal("math_function")) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(results[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["value"]).NotTo(BeNil()) |
|
|
Expect(args["large"]).NotTo(BeNil()) |
|
|
}) |
|
|
|
|
|
It("should handle negative numbers", func() { |
|
|
input := `<tool_call> |
|
|
<function=math_function> |
|
|
<parameter=negative_int> |
|
|
-42 |
|
|
</parameter> |
|
|
<parameter=negative_float> |
|
|
-3.14 |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(len(results)).To(Equal(1)) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(results[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(args["negative_int"]).NotTo(BeNil()) |
|
|
Expect(args["negative_float"]).NotTo(BeNil()) |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("JSON Dump Format Tests (matching llama.cpp)", func() { |
|
|
It("should dump JSON arguments in compact format", func() { |
|
|
input := `<tool_call> |
|
|
<function=test_function> |
|
|
<parameter=args> |
|
|
{"key1": "value1", "key2": 42} |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(len(results)).To(Equal(1)) |
|
|
|
|
|
|
|
|
argsStr := results[0].Arguments |
|
|
|
|
|
Expect(argsStr).NotTo(ContainSubstring(`": "`), "Should not have space after colon in compact format") |
|
|
Expect(argsStr).NotTo(ContainSubstring(`", "`), "Should not have space after comma in compact format") |
|
|
|
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(argsStr), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
}) |
|
|
|
|
|
It("should handle JSON dump marker in healed JSON", func() { |
|
|
|
|
|
parser := NewChatMsgParser(`{"a": "b"}`, true) |
|
|
parser.SetHealingMarker("$test") |
|
|
jsonValue, isPartial, jsonDumpMarker, err := parser.TryConsumeJSON() |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
if isPartial && jsonDumpMarker != "" { |
|
|
|
|
|
jsonBytes, _ := json.Marshal(jsonValue) |
|
|
jsonStr := string(jsonBytes) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Expect(jsonStr).NotTo(BeEmpty(), "Healed JSON should not be empty") |
|
|
} |
|
|
}) |
|
|
}) |
|
|
|
|
|
Describe("Edge Case Tests (matching llama.cpp)", func() { |
|
|
It("should handle empty parameter values", func() { |
|
|
input := `<tool_call> |
|
|
<function=test_function> |
|
|
<parameter=empty> |
|
|
</parameter> |
|
|
<parameter=whitespace> |
|
|
|
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(len(results)).To(Equal(1)) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(results[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args).To(HaveKey("empty")) |
|
|
Expect(args).To(HaveKey("whitespace")) |
|
|
}) |
|
|
|
|
|
It("should handle XML-like content in parameters", func() { |
|
|
input := `<tool_call> |
|
|
<function=test_function> |
|
|
<parameter=xml_content> |
|
|
<tag>content</tag> |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(len(results)).To(Equal(1)) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(results[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["xml_content"]).To(ContainSubstring("<tag>")) |
|
|
}) |
|
|
|
|
|
It("should handle JSON objects as parameter values", func() { |
|
|
input := `<tool_call> |
|
|
<function=test_function> |
|
|
<parameter=nested> |
|
|
{"inner": {"key": "value"}} |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(len(results)).To(Equal(1)) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(results[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
nested, ok := args["nested"].(map[string]any) |
|
|
Expect(ok).To(BeTrue(), "Nested should be a map") |
|
|
inner, ok := nested["inner"].(map[string]any) |
|
|
Expect(ok).To(BeTrue(), "Inner should be a map") |
|
|
Expect(inner["key"]).To(Equal("value")) |
|
|
}) |
|
|
|
|
|
It("should handle JSON arrays as parameter values", func() { |
|
|
input := `<tool_call> |
|
|
<function=test_function> |
|
|
<parameter=array> |
|
|
[1, 2, 3, "four"] |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(len(results)).To(Equal(1)) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(results[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
arr, ok := args["array"].([]any) |
|
|
Expect(ok).To(BeTrue(), "Array should be a slice") |
|
|
Expect(len(arr)).To(Equal(4)) |
|
|
}) |
|
|
|
|
|
It("should handle boolean values as parameters", func() { |
|
|
input := `<tool_call> |
|
|
<function=test_function> |
|
|
<parameter=true_val> |
|
|
true |
|
|
</parameter> |
|
|
<parameter=false_val> |
|
|
false |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(len(results)).To(Equal(1)) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(results[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["true_val"]).To(Equal(true)) |
|
|
Expect(args["false_val"]).To(Equal(false)) |
|
|
}) |
|
|
|
|
|
It("should handle null values as parameters", func() { |
|
|
input := `<tool_call> |
|
|
<function=test_function> |
|
|
<parameter=null_val> |
|
|
null |
|
|
</parameter> |
|
|
</function> |
|
|
</tool_call>` |
|
|
|
|
|
results, err := ParseXMLIterative(input, nil, false) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
Expect(len(results)).To(Equal(1)) |
|
|
|
|
|
var args map[string]any |
|
|
err = json.Unmarshal([]byte(results[0].Arguments), &args) |
|
|
Expect(err).NotTo(HaveOccurred()) |
|
|
|
|
|
Expect(args["null_val"]).To(BeNil()) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
|