mirror of
https://github.com/mudler/LocalAI.git
synced 2025-05-20 10:35:01 +00:00
feat(functions): parse broken JSON when we parse the raw results, use dynamic rules for grammar keys (#2912)
* feat(functions): enhance parsing with broken JSON when we parse the raw results Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * breaking: make function name by default Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(grammar): dynamically generate grammars with mutating keys Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactor: simplify condition Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Update docs Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
parent
35d55572ac
commit
bf9dd1de7f
9 changed files with 279 additions and 202 deletions
|
@ -16,7 +16,7 @@ var _ = Describe("LocalAI function parse tests", func() {
|
|||
|
||||
Context("when using grammars and single result expected", func() {
|
||||
It("should parse the function name and arguments correctly", func() {
|
||||
input := `{"function": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
input := `{"name": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
|
||||
results := ParseFunctionCall(input, functionConfig)
|
||||
Expect(results).To(HaveLen(1))
|
||||
|
@ -28,13 +28,22 @@ var _ = Describe("LocalAI function parse tests", func() {
|
|||
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<function>\w+)\s*\((?P<arguments>.*)\)`}
|
||||
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() {
|
||||
|
@ -53,7 +62,7 @@ var _ = Describe("LocalAI function parse tests", func() {
|
|||
|
||||
Context("when parallel calls are enabled", func() {
|
||||
It("should handle multiple function calls", func() {
|
||||
input := `[{"function": "add", "arguments": {"x": 5, "y": 3}}, {"function": "subtract", "arguments": {"x": 10, "y": 7}}]`
|
||||
input := `[{"name": "add", "arguments": {"x": 5, "y": 3}}, {"name": "subtract", "arguments": {"x": 10, "y": 7}}]`
|
||||
|
||||
results := ParseFunctionCall(input, functionConfig)
|
||||
Expect(results).To(HaveLen(2))
|
||||
|
@ -66,8 +75,8 @@ var _ = Describe("LocalAI function parse tests", func() {
|
|||
|
||||
Context("without grammars and without regex", func() {
|
||||
It("should parse the function name and arguments correctly with the name key", func() {
|
||||
input := `{"name": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
functionConfig.FunctionName = true
|
||||
input := `{"function": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
functionConfig.FunctionNameKey = "function"
|
||||
|
||||
results := ParseFunctionCall(input, functionConfig)
|
||||
Expect(results).To(HaveLen(1))
|
||||
|
@ -76,7 +85,7 @@ var _ = Describe("LocalAI function parse tests", func() {
|
|||
})
|
||||
|
||||
It("should parse the function name and arguments correctly with the function key", func() {
|
||||
input := `{"function": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
input := `{"name": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
|
||||
results := ParseFunctionCall(input, functionConfig)
|
||||
Expect(results).To(HaveLen(1))
|
||||
|
@ -87,7 +96,7 @@ var _ = Describe("LocalAI function parse tests", func() {
|
|||
It("should parse the result by matching the JSONRegexMatch", func() {
|
||||
input := `
|
||||
<tool_call>
|
||||
{"function": "add", "arguments": {"x": 5, "y": 3}}
|
||||
{"name": "add", "arguments": {"x": 5, "y": 3}}
|
||||
</tool_call>`
|
||||
|
||||
functionConfig.JSONRegexMatch = []string{`(?s)<tool_call>(.*?)</tool_call>`}
|
||||
|
@ -100,7 +109,7 @@ var _ = Describe("LocalAI function parse tests", func() {
|
|||
|
||||
It("should parse the result by matching the JSONRegexMatch", func() {
|
||||
input := `
|
||||
{"function": "add", "arguments": {"x": 5, "y": 3}}
|
||||
{"name": "add", "arguments": {"x": 5, "y": 3}}
|
||||
</tool_call>`
|
||||
|
||||
functionConfig.JSONRegexMatch = []string{`(?s)(.*?)</tool_call>`}
|
||||
|
@ -110,13 +119,21 @@ var _ = Describe("LocalAI function parse tests", func() {
|
|||
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
|
||||
{"function": "add", "arguments": {"x": 5, "y": 3}}
|
||||
{"name": "add", "arguments": {"x": 5, "y": 3}}
|
||||
Some text after the JSON
|
||||
`
|
||||
|
||||
|
@ -134,7 +151,7 @@ Some text after the JSON
|
|||
It("should replace text before and after array JSON blob", func() {
|
||||
input := `
|
||||
Some text before the JSON
|
||||
[{"function": "add", "arguments": {"x": 5, "y": 3}}, {"function": "subtract", "arguments": {"x": 10, "y": 7}}]
|
||||
[{"name": "add", "arguments": {"x": 5, "y": 3}}, {"name": "subtract", "arguments": {"x": 10, "y": 7}}]
|
||||
Some text after the JSON
|
||||
`
|
||||
functionConfig.ReplaceFunctionResults = []ReplaceResult{
|
||||
|
@ -153,7 +170,7 @@ Some text after the JSON
|
|||
It("should convert single-quoted key-value pairs to double-quoted and escape double quotes within values", func() {
|
||||
input := `
|
||||
Some text before the JSON
|
||||
{'function': '"add"', 'arguments': {'x': 5, 'z': '"v"', 'y': 'v"value"'}}
|
||||
{'name': '"add"', 'arguments': {'x': 5, 'z': '"v"', 'y': 'v"value"'}}
|
||||
Some text after the JSON
|
||||
`
|
||||
functionConfig.JSONRegexMatch = []string{`(?s)<tool_call>(.*?)</tool_call>`}
|
||||
|
@ -186,7 +203,7 @@ Some text after the JSON
|
|||
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>{'function': '"add"', 'arguments': {'x': 5, 'z': '"v"', 'y': 'v"value"'}}</tool_call>
|
||||
<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>`}
|
||||
|
@ -219,8 +236,8 @@ Some text after the JSON
|
|||
It("should detect multiple functions call where the JSONRegexMatch is repeated", func() {
|
||||
input := `
|
||||
Some text before the JSON
|
||||
<tool_call>{"function": "add", "arguments": {"x": 5, "y": 3}}</tool_call>
|
||||
<tool_call>{"function": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call>
|
||||
<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>`}
|
||||
|
@ -240,7 +257,7 @@ Some text after the JSON
|
|||
<sketchpad>
|
||||
roses are red
|
||||
</sketchpad>
|
||||
<tool_call>{"function": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call>
|
||||
<tool_call>{"name": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call>
|
||||
Some text after the JSON
|
||||
`
|
||||
functionConfig.CaptureLLMResult = []string{`(?s)<sketchpad>(.*?)</sketchpad>`}
|
||||
|
@ -251,7 +268,7 @@ roses are red
|
|||
It("Defaults to empty if doesn't catch any", func() {
|
||||
input := `
|
||||
Some text before the JSON
|
||||
<tool_call>{"function": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call>
|
||||
<tool_call>{"name": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call>
|
||||
Some text after the JSON
|
||||
`
|
||||
functionConfig.CaptureLLMResult = []string{`(?s)<sketchpad>(.*?)</sketchpad>`}
|
||||
|
@ -259,4 +276,74 @@ roses are red
|
|||
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))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue