| | package grammars_test |
| |
|
| | import ( |
| | "strings" |
| |
|
| | . "github.com/mudler/LocalAI/pkg/functions" |
| | . "github.com/mudler/LocalAI/pkg/functions/grammars" |
| | . "github.com/onsi/ginkgo/v2" |
| | . "github.com/onsi/gomega" |
| | ) |
| |
|
| | var testFunctions = []Item{ |
| | { |
| | Type: "object", |
| | Properties: createFunction( |
| | "function", |
| | "arguments", |
| | "create_event", |
| | map[string]interface{}{ |
| | "title": map[string]string{"type": "string"}, |
| | "date": map[string]string{"type": "string"}, |
| | "time": map[string]string{"type": "string"}, |
| | }, |
| | ), |
| | }, |
| | { |
| | Type: "object", |
| | Properties: createFunction( |
| | "function", |
| | "arguments", |
| | "search", |
| | map[string]interface{}{ |
| | "query": map[string]string{"type": "string"}, |
| | }), |
| | }, |
| | } |
| |
|
| | var testFunctionsName = []Item{ |
| | { |
| | Type: "object", |
| | Properties: createFunction( |
| | "name", |
| | "arguments", |
| | "create_event", |
| | map[string]interface{}{ |
| | "title": map[string]string{"type": "string"}, |
| | "date": map[string]string{"type": "string"}, |
| | "time": map[string]string{"type": "string"}, |
| | }, |
| | ), |
| | }, |
| | { |
| | Type: "object", |
| | Properties: createFunction( |
| | "name", |
| | "arguments", |
| | "search", |
| | map[string]interface{}{ |
| | "query": map[string]string{"type": "string"}, |
| | }), |
| | }, |
| | } |
| |
|
| | func rootResult(s string) string { |
| | return `root-0-name ::= "\"create_event\"" |
| | freestring ::= ( |
| | [^"\\] | |
| | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) |
| | )* space |
| | root-0 ::= "{" space "\"arguments\"" space ":" space root-0-arguments "," space "\"name\"" space ":" space root-0-name "}" space |
| | root-1-arguments ::= "{" space "\"query\"" space ":" space string "}" space |
| | realvalue ::= root-0 | root-1 |
| | root ::= ` + s + ` |
| | space ::= " "? |
| | root-0-arguments ::= "{" space "\"date\"" space ":" space string "," space "\"time\"" space ":" space string "," space "\"title\"" space ":" space string "}" space |
| | root-1 ::= "{" space "\"arguments\"" space ":" space root-1-arguments "," space "\"name\"" space ":" space root-1-name "}" space |
| | string ::= "\"" ( |
| | [^"\\] | |
| | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) |
| | )* "\"" space |
| | arr ::= |
| | "[\n" ( |
| | realvalue |
| | (",\n" realvalue)* |
| | )? "]" |
| | root-1-name ::= "\"search\""` |
| | } |
| |
|
| | const ( |
| | testInput1 = ` |
| | { |
| | "oneOf": [ |
| | { |
| | "type": "object", |
| | "properties": { |
| | "function": {"const": "create_event"}, |
| | "arguments": { |
| | "type": "object", |
| | "properties": { |
| | "title": {"type": "string"}, |
| | "date": {"type": "string"}, |
| | "time": {"type": "string"} |
| | } |
| | } |
| | } |
| | }, |
| | { |
| | "type": "object", |
| | "properties": { |
| | "function": {"const": "search"}, |
| | "arguments": { |
| | "type": "object", |
| | "properties": { |
| | "query": {"type": "string"} |
| | } |
| | } |
| | } |
| | } |
| | ] |
| | }` |
| |
|
| | inputResult1 = `root-0-function ::= "\"create_event\"" |
| | freestring ::= ( |
| | [^"\\] | |
| | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) |
| | )* space |
| | root-0 ::= "{" space "\"arguments\"" space ":" space root-0-arguments "," space "\"function\"" space ":" space root-0-function "}" space |
| | root-1-arguments ::= "{" space "\"query\"" space ":" space string "}" space |
| | root ::= root-0 | root-1 |
| | space ::= " "? |
| | root-0-arguments ::= "{" space "\"date\"" space ":" space string "," space "\"time\"" space ":" space string "," space "\"title\"" space ":" space string "}" space |
| | root-1 ::= "{" space "\"arguments\"" space ":" space root-1-arguments "," space "\"function\"" space ":" space root-1-function "}" space |
| | string ::= "\"" ( |
| | [^"\\] | |
| | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) |
| | )* "\"" space |
| | root-1-function ::= "\"search\""` |
| |
|
| | inputResult2 = `root-0-function ::= "\"create_event\"" |
| | freestring ::= ( |
| | [^"\\] | |
| | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) |
| | )* space |
| | root-0 ::= "{" space "\"arguments\"" space ":" space root-0-arguments "," space "\"function\"" space ":" space root-0-function "}" space |
| | root-1-arguments ::= "{" space "\"query\"" space ":" space string "}" space |
| | realvalue ::= root-0 | root-1 |
| | root ::= arr | realvalue |
| | space ::= " "? |
| | root-0-arguments ::= "{" space "\"date\"" space ":" space string "," space "\"time\"" space ":" space string "," space "\"title\"" space ":" space string "}" space |
| | root-1 ::= "{" space "\"arguments\"" space ":" space root-1-arguments "," space "\"function\"" space ":" space root-1-function "}" space |
| | string ::= "\"" ( |
| | [^"\\] | |
| | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) |
| | )* "\"" space |
| | arr ::= |
| | "[\n" ( |
| | realvalue |
| | (",\n" realvalue)* |
| | )? "]" |
| | root-1-function ::= "\"search\""` |
| |
|
| | testInput2 = ` |
| | { |
| | "oneOf": [ |
| | { |
| | "type": "object", |
| | "properties": { |
| | "name": {"const": "create_event"}, |
| | "arguments": { |
| | "type": "object", |
| | "properties": { |
| | "title": {"type": "string"}, |
| | "date": {"type": "string"}, |
| | "time": {"type": "string"} |
| | } |
| | } |
| | } |
| | }, |
| | { |
| | "type": "object", |
| | "properties": { |
| | "name": {"const": "search"}, |
| | "arguments": { |
| | "type": "object", |
| | "properties": { |
| | "query": {"type": "string"} |
| | } |
| | } |
| | } |
| | } |
| | ] |
| | }` |
| |
|
| | inputResult3 = `root-0-name ::= "\"create_event\"" |
| | freestring ::= ( |
| | [^"\\] | |
| | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) |
| | )* space |
| | root-0 ::= "{" space "\"arguments\"" space ":" space root-0-arguments "," space "\"name\"" space ":" space root-0-name "}" space |
| | root-1-arguments ::= "{" space "\"query\"" space ":" space string "}" space |
| | root ::= root-0 | root-1 |
| | space ::= " "? |
| | root-0-arguments ::= "{" space "\"date\"" space ":" space string "," space "\"time\"" space ":" space string "," space "\"title\"" space ":" space string "}" space |
| | root-1 ::= "{" space "\"arguments\"" space ":" space root-1-arguments "," space "\"name\"" space ":" space root-1-name "}" space |
| | string ::= "\"" ( |
| | [^"\\] | |
| | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) |
| | )* "\"" space |
| | root-1-name ::= "\"search\""` |
| |
|
| | inputResult4 = `root-0-name ::= "\"create_event\"" |
| | freestring ::= ( |
| | [^"\\] | |
| | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) |
| | )* space |
| | root-0 ::= "{" space "\"arguments\"" space ":" space root-0-arguments "," space "\"name\"" space ":" space root-0-name "}" space |
| | root-1-arguments ::= "{" space "\"query\"" space ":" space string "}" space |
| | realvalue ::= root-0 | root-1 |
| | root ::= arr | realvalue |
| | space ::= " "? |
| | root-0-arguments ::= "{" space "\"date\"" space ":" space string "," space "\"time\"" space ":" space string "," space "\"title\"" space ":" space string "}" space |
| | root-1 ::= "{" space "\"arguments\"" space ":" space root-1-arguments "," space "\"name\"" space ":" space root-1-name "}" space |
| | string ::= "\"" ( |
| | [^"\\] | |
| | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) |
| | )* "\"" space |
| | arr ::= |
| | "[\n" ( |
| | realvalue |
| | (",\n" realvalue)* |
| | )? "]" |
| | root-1-name ::= "\"search\""` |
| | ) |
| |
|
| | var _ = Describe("JSON schema grammar tests", func() { |
| | Context("JSON", func() { |
| | It("generates a valid grammar from JSON schema", func() { |
| | grammar, err := NewJSONSchemaConverter("").GrammarFromBytes([]byte(testInput1)) |
| | Expect(err).To(BeNil()) |
| | results := strings.Split(inputResult1, "\n") |
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n")))) |
| | }) |
| | It("generates a valid grammar from JSON schema", func() { |
| | grammar, err := NewJSONSchemaConverter("").GrammarFromBytes([]byte(testInput2)) |
| | Expect(err).To(BeNil()) |
| | results := strings.Split(inputResult3, "\n") |
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n")))) |
| | }) |
| | It("generates a valid grammar from JSON Objects", func() { |
| |
|
| | structuredGrammar := JSONFunctionStructure{ |
| | OneOf: testFunctions} |
| |
|
| | grammar, err := structuredGrammar.Grammar() |
| | Expect(err).To(BeNil()) |
| | results := strings.Split(inputResult1, "\n") |
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n")))) |
| | }) |
| |
|
| | It("generates a valid grammar from JSON Objects for multiple function return", func() { |
| | structuredGrammar := JSONFunctionStructure{ |
| | OneOf: testFunctions} |
| |
|
| | grammar, err := structuredGrammar.Grammar(EnableMaybeArray) |
| | Expect(err).To(BeNil()) |
| | results := strings.Split( |
| | strings.Join([]string{ |
| | inputResult2, |
| | "mixedstring ::= freestring | freestring arr | freestring realvalue"}, "\n"), |
| | "\n") |
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar) |
| | }) |
| |
|
| | It("generates a valid grammar from JSON Objects for multiple function return", func() { |
| | structuredGrammar := JSONFunctionStructure{ |
| | OneOf: testFunctionsName} |
| |
|
| | grammar, err := structuredGrammar.Grammar(EnableMaybeArray) |
| | Expect(err).To(BeNil()) |
| | results := strings.Split( |
| | strings.Join([]string{ |
| | inputResult4, |
| | "mixedstring ::= freestring | freestring arr | freestring realvalue"}, "\n"), |
| | "\n") |
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar) |
| | }) |
| |
|
| | It("generates a valid grammar from JSON Objects for multiple function return with a suffix and array", func() { |
| | structuredGrammar := JSONFunctionStructure{ |
| | OneOf: testFunctionsName} |
| |
|
| | grammar, err := structuredGrammar.Grammar( |
| | SetPrefix("suffix"), |
| | EnableMaybeArray, |
| | ) |
| | Expect(err).To(BeNil()) |
| | results := strings.Split( |
| | strings.Join([]string{ |
| | rootResult(`"suffix" arr | realvalue`), |
| | "mixedstring ::= freestring | freestring arr | freestring realvalue"}, "\n"), |
| | "\n") |
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar) |
| | }) |
| | It("generates a valid grammar from JSON Objects with a suffix", func() { |
| | structuredGrammar := JSONFunctionStructure{ |
| | OneOf: testFunctionsName} |
| |
|
| | grammar, err := structuredGrammar.Grammar(SetPrefix("suffix")) |
| | Expect(err).To(BeNil()) |
| | results := strings.Split( |
| | strings.Join([]string{ |
| | rootResult(`"suffix" realvalue`), |
| | "mixedstring ::= freestring | freestring realvalue"}, "\n"), |
| | "\n") |
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar) |
| | }) |
| | It("generates a valid grammar from JSON Objects with a suffix and could return string", func() { |
| | structuredGrammar := JSONFunctionStructure{ |
| | OneOf: testFunctionsName} |
| |
|
| | grammar, err := structuredGrammar.Grammar(SetPrefix("suffix"), EnableMaybeString) |
| | Expect(err).To(BeNil()) |
| | results := strings.Split( |
| | strings.Join([]string{ |
| | rootResult(`( "suffix" realvalue | mixedstring )`), |
| | "mixedstring ::= freestring | freestring realvalue"}, "\n"), |
| | "\n") |
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar) |
| | }) |
| | It("generates a valid grammar from JSON Objects with a suffix that could return text or an array of tools", func() { |
| | structuredGrammar := JSONFunctionStructure{ |
| | OneOf: testFunctionsName} |
| |
|
| | grammar, err := structuredGrammar.Grammar(SetPrefix("suffix"), EnableMaybeString, EnableMaybeArray) |
| | Expect(err).To(BeNil()) |
| | results := strings.Split( |
| | strings.Join([]string{ |
| | rootResult(`( "suffix" (arr | realvalue) | mixedstring )`), |
| | "mixedstring ::= freestring | freestring arr | freestring realvalue"}, "\n"), |
| | "\n") |
| |
|
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar) |
| | }) |
| |
|
| | It("generates a valid grammar from JSON Objects without a suffix that could return text or an array of tools or just string", func() { |
| | structuredGrammar := JSONFunctionStructure{ |
| | OneOf: testFunctionsName} |
| |
|
| | grammar, err := structuredGrammar.Grammar(EnableMaybeString, EnableMaybeArray) |
| | Expect(err).To(BeNil()) |
| | results := strings.Split( |
| | strings.Join([]string{ |
| | rootResult(`mixedstring | arr | realvalue`), |
| | "mixedstring ::= freestring | freestring arr | freestring realvalue"}, "\n"), |
| | "\n") |
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar) |
| | }) |
| |
|
| | It("generates a valid grammar from JSON Objects without a suffix that could return text or an array of tools or just string. Disables mixedstring", func() { |
| | structuredGrammar := JSONFunctionStructure{ |
| | OneOf: testFunctionsName} |
| |
|
| | grammar, err := structuredGrammar.Grammar(EnableMaybeString, EnableMaybeArray, NoMixedFreeString) |
| | Expect(err).To(BeNil()) |
| | results := strings.Split( |
| | strings.Join([]string{ |
| | rootResult(`freestring | arr | realvalue`), |
| | "mixedstring ::= freestring | freestring arr | freestring realvalue"}, "\n"), |
| | "\n") |
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar) |
| | }) |
| |
|
| | It("generates parallel tools without newlines in JSON", func() { |
| | structuredGrammar := JSONFunctionStructure{ |
| | OneOf: testFunctionsName} |
| | content := `arr ::= |
| | "[" ( |
| | realvalue |
| | ("," realvalue)* |
| | )? "]"` |
| | grammar, err := structuredGrammar.Grammar(EnableMaybeString, EnableMaybeArray, DisableParallelNewLines) |
| | Expect(err).To(BeNil()) |
| | results := strings.Split(content, "\n") |
| | for _, r := range results { |
| | if r != "" { |
| | Expect(grammar).To(ContainSubstring(r)) |
| | } |
| | } |
| | }) |
| |
|
| | It("handles empty object schema without properties", func() { |
| | |
| | emptyObjectSchema := `{ |
| | "type": "object", |
| | "properties": {} |
| | }` |
| |
|
| | grammar, err := NewJSONSchemaConverter("").GrammarFromBytes([]byte(emptyObjectSchema)) |
| | Expect(err).To(BeNil()) |
| | Expect(grammar).To(ContainSubstring(`root ::= "{" space "}" space`)) |
| | }) |
| |
|
| | It("handles object schema without properties field", func() { |
| | |
| | objectWithoutProperties := `{ |
| | "type": "object" |
| | }` |
| |
|
| | grammar, err := NewJSONSchemaConverter("").GrammarFromBytes([]byte(objectWithoutProperties)) |
| | Expect(err).To(BeNil()) |
| | Expect(grammar).To(ContainSubstring(`root ::= "{" space "}" space`)) |
| | }) |
| |
|
| | It("handles schema with properties but no type field", func() { |
| | |
| | schemaWithPropertiesNoType := `{ |
| | "properties": {} |
| | }` |
| |
|
| | grammar, err := NewJSONSchemaConverter("").GrammarFromBytes([]byte(schemaWithPropertiesNoType)) |
| | Expect(err).To(BeNil()) |
| | Expect(grammar).To(ContainSubstring(`root ::= "{" space "}" space`)) |
| | }) |
| |
|
| | It("handles multi-type array definitions like [string, null]", func() { |
| | |
| | multiTypeSchema := `{ |
| | "type": "object", |
| | "properties": { |
| | "street": { |
| | "description": "The given street name where the company resides.", |
| | "type": ["string", "null"] |
| | }, |
| | "city": { |
| | "description": "The given city where the company resides.", |
| | "type": ["string", "null"] |
| | } |
| | } |
| | }` |
| |
|
| | grammar, err := NewJSONSchemaConverter("").GrammarFromBytes([]byte(multiTypeSchema)) |
| | Expect(err).To(BeNil()) |
| | |
| | Expect(grammar).To(ContainSubstring("string")) |
| | Expect(grammar).To(ContainSubstring("null")) |
| | |
| | Expect(grammar).ToNot(BeEmpty()) |
| | }) |
| |
|
| | It("handles complex nested schema with multi-type arrays (issue #5572)", func() { |
| | complexSchema := `{ |
| | "type": "object", |
| | "properties": { |
| | "companylist": { |
| | "type": "array", |
| | "items": { |
| | "type": "object", |
| | "properties": { |
| | "companyname": { |
| | "description": "The given name of the company.", |
| | "type": "string" |
| | }, |
| | "street": { |
| | "description": "The given street name where the company resides.", |
| | "type": ["string", "null"] |
| | }, |
| | "city": { |
| | "description": "The given city where the company resides.", |
| | "type": ["string", "null"] |
| | } |
| | }, |
| | "additionalProperties": false, |
| | "required": ["companyname", "street", "city"] |
| | } |
| | }, |
| | "filter": { |
| | "description": "The type we should filter the list of companies by.", |
| | "type": "string" |
| | } |
| | }, |
| | "required": ["companylist", "filter"], |
| | "additionalProperties": false |
| | }` |
| |
|
| | grammar, err := NewJSONSchemaConverter("").GrammarFromBytes([]byte(complexSchema)) |
| | Expect(err).To(BeNil()) |
| | |
| | Expect(grammar).ToNot(BeEmpty()) |
| | |
| | Expect(grammar).To(ContainSubstring("{")) |
| | Expect(grammar).To(ContainSubstring("[")) |
| | }) |
| | }) |
| | }) |
| |
|