Skip to content

Commit beb598e

Browse files
authored
feat(functions): mixed JSON BNF grammars (#2328)
feat(functions): support mixed JSON BNF grammar This PR provides new options to control how functions are extracted from the LLM, and also provides more control on how JSON grammars can be used (also in conjunction). New YAML settings introduced: - `grammar_message`: when enabled, the generated grammar can also decide to push strings and not only JSON objects. This allows the LLM to pick to either respond freely or using JSON. - `grammar_prefix`: Allows to prefix a string to the JSON grammar definition. - `replace_results`: Is a map that allows to replace strings in the LLM result. As an example, consider the following settings for Hermes-2-Pro-Mistral, which allow extracting both JSON results coming from the model, and the ones coming from the grammar: ```yaml function: # disable injecting the "answer" tool disable_no_action: true # This allows the grammar to also return messages grammar_message: true # Suffix to add to the grammar grammar_prefix: '<tool_call>\n' return_name_in_function_response: true # Without grammar uncomment the lines below # Warning: this is relying only on the capability of the # LLM model to generate the correct function call. # no_grammar: true # json_regex_match: "(?s)<tool_call>(.*?)</tool_call>" replace_results: "<tool_call>": "" "\'": "\"" ``` Note: To disable entirely grammars usage in the example above, uncomment the `no_grammar` and `json_regex_match`. Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent c89271b commit beb598e

File tree

4 files changed

+281
-124
lines changed

4 files changed

+281
-124
lines changed

core/http/endpoints/openai/chat.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,15 +219,15 @@ func ChatEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, startup
219219
// Handle if we should return "name" instead of "functions"
220220
if config.FunctionsConfig.FunctionName {
221221
jsStruct := funcs.ToJSONNameStructure()
222-
config.Grammar = jsStruct.Grammar("", config.FunctionsConfig.ParallelCalls)
222+
config.Grammar = jsStruct.Grammar(config.FunctionsConfig.GrammarPrefix, "", config.FunctionsConfig.ParallelCalls, config.FunctionsConfig.GrammarMessage)
223223
} else {
224224
jsStruct := funcs.ToJSONFunctionStructure()
225-
config.Grammar = jsStruct.Grammar("", config.FunctionsConfig.ParallelCalls)
225+
config.Grammar = jsStruct.Grammar(config.FunctionsConfig.GrammarPrefix, "", config.FunctionsConfig.ParallelCalls, config.FunctionsConfig.GrammarMessage)
226226
}
227227
case input.JSONFunctionGrammarObject != nil:
228-
config.Grammar = input.JSONFunctionGrammarObject.Grammar("", config.FunctionsConfig.ParallelCalls)
228+
config.Grammar = input.JSONFunctionGrammarObject.Grammar(config.FunctionsConfig.GrammarPrefix, "", config.FunctionsConfig.ParallelCalls, config.FunctionsConfig.GrammarMessage)
229229
case input.JSONFunctionGrammarObjectName != nil:
230-
config.Grammar = input.JSONFunctionGrammarObjectName.Grammar("", config.FunctionsConfig.ParallelCalls)
230+
config.Grammar = input.JSONFunctionGrammarObjectName.Grammar(config.FunctionsConfig.GrammarPrefix, "", config.FunctionsConfig.ParallelCalls, config.FunctionsConfig.GrammarMessage)
231231
default:
232232
// Force picking one of the functions by the request
233233
if config.FunctionToCall() != "" {

pkg/functions/grammar_json_schema.go

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"regexp"
99
"sort"
1010
"strings"
11+
12+
"github.com/go-skynet/LocalAI/pkg/utils"
1113
)
1214

1315
const (
@@ -48,6 +50,10 @@ var (
4850
[^"\\] |
4951
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
5052
)* "\"" space`,
53+
"freestring": `(
54+
[^"\\] |
55+
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
56+
)* space`,
5157
"null": `"null" space`,
5258
}
5359

@@ -111,22 +117,54 @@ const array = `arr ::=
111117
(",\n" realvalue)*
112118
)? "]"`
113119

114-
func (sc *JSONSchemaConverter) finalizeGrammar(maybeArray bool) string {
120+
func (sc *JSONSchemaConverter) finalizeGrammar(suffix string, maybeArray, maybeString bool) string {
115121
var lines []string
122+
123+
swapRoot := maybeArray || maybeString || suffix != ""
124+
116125
// write down the computed rules.
117126
// if maybeArray is true, we need to add the array rule and slightly tweak the root rule
118127
for name, rule := range sc.rules {
119-
if maybeArray && name == "root" {
128+
if swapRoot && name == "root" {
120129
name = "realvalue"
121130
}
122131
lines = append(lines, fmt.Sprintf("%s ::= %s", name, rule))
123132
}
124133

134+
if !swapRoot {
135+
return strings.Join(lines, "\n")
136+
}
137+
138+
newRoot := "realvalue"
125139
if maybeArray {
126-
lines = append(lines, fmt.Sprintf("%s ::= %s", "root", "arr | realvalue"))
127-
lines = append(lines, array)
140+
newRoot = "arr | realvalue"
141+
}
142+
143+
if suffix != "" {
144+
// quote newlines in suffix
145+
suffix = utils.EscapeNewLines(suffix)
146+
147+
if maybeArray && maybeString {
148+
newRoot = "(" + newRoot + ")"
149+
}
150+
151+
if maybeString {
152+
//newRoot = "( (\"" + suffix + "\" " + newRoot + ") | freestring ) "
153+
newRoot = "( \"" + suffix + "\" " + newRoot + " | freestring ) "
154+
} else {
155+
newRoot = "\"" + suffix + "\" " + "" + newRoot + ""
156+
}
157+
} else if maybeString {
158+
if maybeArray {
159+
// newRoot = "(" + newRoot + ")"
160+
}
161+
162+
newRoot = "freestring | " + newRoot
128163
}
129164

165+
lines = append(lines, fmt.Sprintf("%s ::= %s", "root", newRoot))
166+
lines = append(lines, array)
167+
130168
return strings.Join(lines, "\n")
131169
}
132170

@@ -251,15 +289,16 @@ func (sc *JSONSchemaConverter) resolveReference(ref string, rootSchema map[strin
251289

252290
return def
253291
}
254-
func (sc *JSONSchemaConverter) Grammar(schema map[string]interface{}, maybeArray bool) string {
292+
func (sc *JSONSchemaConverter) Grammar(suffix string, schema map[string]interface{}, maybeArray, maybeString bool) string {
293+
sc.addRule("freestring", PRIMITIVE_RULES["freestring"])
255294
sc.visit(schema, "", schema)
256-
return sc.finalizeGrammar(maybeArray)
295+
return sc.finalizeGrammar(suffix, maybeArray, maybeString)
257296
}
258297

259-
func (sc *JSONSchemaConverter) GrammarFromBytes(b []byte, maybeArray bool) string {
298+
func (sc *JSONSchemaConverter) GrammarFromBytes(suffix string, b []byte, maybeArray, maybeString bool) string {
260299
var schema map[string]interface{}
261300
_ = json.Unmarshal(b, &schema)
262-
return sc.Grammar(schema, maybeArray)
301+
return sc.Grammar(suffix, schema, maybeArray, maybeString)
263302
}
264303

265304
func jsonString(v interface{}) string {
@@ -302,9 +341,9 @@ type JSONFunctionStructureName struct {
302341
Defs map[string]interface{} `json:"$defs,omitempty"`
303342
}
304343

305-
func (j JSONFunctionStructureName) Grammar(propOrder string, maybeArray bool) string {
344+
func (j JSONFunctionStructureName) Grammar(suffix string, propOrder string, maybeArray, maybeString bool) string {
306345
dat, _ := json.Marshal(j)
307-
return NewJSONSchemaConverter(propOrder).GrammarFromBytes(dat, maybeArray)
346+
return NewJSONSchemaConverter(propOrder).GrammarFromBytes(suffix, dat, maybeArray, maybeString)
308347
}
309348

310349
type JSONFunctionStructureFunction struct {
@@ -313,7 +352,7 @@ type JSONFunctionStructureFunction struct {
313352
Defs map[string]interface{} `json:"$defs,omitempty"`
314353
}
315354

316-
func (j JSONFunctionStructureFunction) Grammar(propOrder string, maybeArray bool) string {
355+
func (j JSONFunctionStructureFunction) Grammar(suffix string, propOrder string, maybeArray, maybeString bool) string {
317356
dat, _ := json.Marshal(j)
318-
return NewJSONSchemaConverter(propOrder).GrammarFromBytes(dat, maybeArray)
357+
return NewJSONSchemaConverter(propOrder).GrammarFromBytes(suffix, dat, maybeArray, maybeString)
319358
}

0 commit comments

Comments
 (0)