|
package functions_test |
|
|
|
import ( |
|
. "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)) |
|
}) |
|
}) |
|
}) |
|
|