mirror of
https://github.com/mudler/LocalAI.git
synced 2025-02-20 09:26:15 +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
@ -225,18 +225,10 @@ func ChatEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, startup
|
||||
}
|
||||
|
||||
// Update input grammar
|
||||
// Handle if we should return "name" instead of "functions"
|
||||
if config.FunctionsConfig.FunctionName {
|
||||
jsStruct := funcs.ToJSONNameStructure()
|
||||
config.Grammar = jsStruct.Grammar(config.FunctionsConfig.GrammarConfig.Options()...)
|
||||
} else {
|
||||
jsStruct := funcs.ToJSONFunctionStructure()
|
||||
config.Grammar = jsStruct.Grammar(config.FunctionsConfig.GrammarConfig.Options()...)
|
||||
}
|
||||
jsStruct := funcs.ToJSONStructure(config.FunctionsConfig.FunctionNameKey, config.FunctionsConfig.FunctionNameKey)
|
||||
config.Grammar = jsStruct.Grammar(config.FunctionsConfig.GrammarConfig.Options()...)
|
||||
case input.JSONFunctionGrammarObject != nil:
|
||||
config.Grammar = input.JSONFunctionGrammarObject.Grammar(config.FunctionsConfig.GrammarConfig.Options()...)
|
||||
case input.JSONFunctionGrammarObjectName != nil:
|
||||
config.Grammar = input.JSONFunctionGrammarObjectName.Grammar(config.FunctionsConfig.GrammarConfig.Options()...)
|
||||
default:
|
||||
// Force picking one of the functions by the request
|
||||
if config.FunctionToCall() != "" {
|
||||
|
@ -179,8 +179,7 @@ type OpenAIRequest struct {
|
||||
// A grammar to constrain the LLM output
|
||||
Grammar string `json:"grammar" yaml:"grammar"`
|
||||
|
||||
JSONFunctionGrammarObject *functions.JSONFunctionStructureFunction `json:"grammar_json_functions" yaml:"grammar_json_functions"`
|
||||
JSONFunctionGrammarObjectName *functions.JSONFunctionStructureName `json:"grammar_json_name" yaml:"grammar_json_name"`
|
||||
JSONFunctionGrammarObject *functions.JSONFunctionStructure `json:"grammar_json_functions" yaml:"grammar_json_functions"`
|
||||
|
||||
Backend string `json:"backend" yaml:"backend"`
|
||||
|
||||
|
@ -152,7 +152,8 @@ function:
|
||||
replace_function_results: [] # Placeholder to replace function call results with arbitrary strings or patterns.
|
||||
replace_llm_results: [] # Replace language model results with arbitrary strings or patterns.
|
||||
capture_llm_results: [] # Capture language model results as text result, among JSON, in function calls. For instance, if a model returns a block for "thinking" and a block for "response", this will allow you to capture the thinking block.
|
||||
return_name_in_function_response: false # Some models might prefer to use "name" rather then "function" when returning JSON data. This will allow to use "name" as a key in the JSON response.
|
||||
function_name_key: "name"
|
||||
function_arguments_key: "arguments"
|
||||
|
||||
# Feature gating flags to enable experimental or optional features.
|
||||
feature_flags: {}
|
||||
|
@ -6,6 +6,11 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFunctionNameKey = "name"
|
||||
defaultFunctionArgumentsKey = "arguments"
|
||||
)
|
||||
|
||||
type Function struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
@ -19,50 +24,18 @@ type Tool struct {
|
||||
}
|
||||
type Tools []Tool
|
||||
|
||||
// ToJSONFunctionStructure converts a list of functions to a JSON structure that can be parsed to a grammar
|
||||
// This allows the LLM to return a response of the type: { "function": "function_name", "arguments": { "arg1": "value1", "arg2": "value2" } }
|
||||
func (f Functions) ToJSONFunctionStructure() JSONFunctionStructureFunction {
|
||||
js := JSONFunctionStructureFunction{}
|
||||
for _, function := range f {
|
||||
// t := function.Parameters["type"]
|
||||
//tt := t.(string)
|
||||
|
||||
properties := function.Parameters["properties"]
|
||||
defs := function.Parameters["$defs"]
|
||||
dat, _ := json.Marshal(properties)
|
||||
dat2, _ := json.Marshal(defs)
|
||||
prop := map[string]interface{}{}
|
||||
defsD := map[string]interface{}{}
|
||||
|
||||
err := json.Unmarshal(dat, &prop)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error unmarshalling dat")
|
||||
}
|
||||
err = json.Unmarshal(dat2, &defsD)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error unmarshalling dat2")
|
||||
}
|
||||
if js.Defs == nil {
|
||||
js.Defs = defsD
|
||||
}
|
||||
js.OneOf = append(js.OneOf, ItemFunction{
|
||||
Type: "object",
|
||||
Properties: FunctionProperties{
|
||||
Function: FunctionName{Const: function.Name},
|
||||
Arguments: Argument{
|
||||
Type: "object",
|
||||
Properties: prop,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return js
|
||||
}
|
||||
|
||||
// ToJSONNameStructure converts a list of functions to a JSON structure that can be parsed to a grammar
|
||||
// This allows the LLM to return a response of the type: { "name": "function_name", "arguments": { "arg1": "value1", "arg2": "value2" } }
|
||||
func (f Functions) ToJSONNameStructure() JSONFunctionStructureName {
|
||||
js := JSONFunctionStructureName{}
|
||||
func (f Functions) ToJSONStructure(name, args string) JSONFunctionStructure {
|
||||
nameKey := defaultFunctionNameKey
|
||||
argsKey := defaultFunctionArgumentsKey
|
||||
if name != "" {
|
||||
nameKey = name
|
||||
}
|
||||
if args != "" {
|
||||
argsKey = args
|
||||
}
|
||||
js := JSONFunctionStructure{}
|
||||
for _, function := range f {
|
||||
// t := function.Parameters["type"]
|
||||
//tt := t.(string)
|
||||
@ -85,15 +58,16 @@ func (f Functions) ToJSONNameStructure() JSONFunctionStructureName {
|
||||
if js.Defs == nil {
|
||||
js.Defs = defsD
|
||||
}
|
||||
js.OneOf = append(js.OneOf, ItemName{
|
||||
Type: "object",
|
||||
Properties: NameProperties{
|
||||
Function: FunctionName{Const: function.Name},
|
||||
Arguments: Argument{
|
||||
Type: "object",
|
||||
Properties: prop,
|
||||
},
|
||||
},
|
||||
|
||||
property := map[string]interface{}{}
|
||||
property[nameKey] = FunctionName{Const: function.Name}
|
||||
property[argsKey] = Argument{
|
||||
Type: "object",
|
||||
Properties: prop,
|
||||
}
|
||||
js.OneOf = append(js.OneOf, Item{
|
||||
Type: "object",
|
||||
Properties: property,
|
||||
})
|
||||
}
|
||||
return js
|
||||
|
@ -35,21 +35,35 @@ var _ = Describe("LocalAI grammar functions", func() {
|
||||
},
|
||||
}
|
||||
|
||||
js := functions.ToJSONFunctionStructure()
|
||||
js := functions.ToJSONStructure("function", "arguments")
|
||||
Expect(len(js.OneOf)).To(Equal(2))
|
||||
Expect(js.OneOf[0].Properties.Function.Const).To(Equal("create_event"))
|
||||
Expect(js.OneOf[0].Properties.Arguments.Properties["event_name"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(js.OneOf[0].Properties.Arguments.Properties["event_date"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(js.OneOf[1].Properties.Function.Const).To(Equal("search"))
|
||||
Expect(js.OneOf[1].Properties.Arguments.Properties["query"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
fnName := js.OneOf[0].Properties["function"].(FunctionName)
|
||||
fnArgs := js.OneOf[0].Properties["arguments"].(Argument)
|
||||
Expect(fnName.Const).To(Equal("create_event"))
|
||||
Expect(fnArgs.Properties["event_name"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(fnArgs.Properties["event_date"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
|
||||
jsN := functions.ToJSONNameStructure()
|
||||
fnName = js.OneOf[1].Properties["function"].(FunctionName)
|
||||
fnArgs = js.OneOf[1].Properties["arguments"].(Argument)
|
||||
Expect(fnName.Const).To(Equal("search"))
|
||||
Expect(fnArgs.Properties["query"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
|
||||
// Test with custom keys
|
||||
jsN := functions.ToJSONStructure("name", "arguments")
|
||||
Expect(len(jsN.OneOf)).To(Equal(2))
|
||||
Expect(jsN.OneOf[0].Properties.Function.Const).To(Equal("create_event"))
|
||||
Expect(jsN.OneOf[0].Properties.Arguments.Properties["event_name"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(jsN.OneOf[0].Properties.Arguments.Properties["event_date"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(jsN.OneOf[1].Properties.Function.Const).To(Equal("search"))
|
||||
Expect(jsN.OneOf[1].Properties.Arguments.Properties["query"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
|
||||
fnName = jsN.OneOf[0].Properties["name"].(FunctionName)
|
||||
fnArgs = jsN.OneOf[0].Properties["arguments"].(Argument)
|
||||
|
||||
Expect(fnName.Const).To(Equal("create_event"))
|
||||
Expect(fnArgs.Properties["event_name"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(fnArgs.Properties["event_date"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
|
||||
fnName = jsN.OneOf[1].Properties["name"].(FunctionName)
|
||||
fnArgs = jsN.OneOf[1].Properties["arguments"].(Argument)
|
||||
|
||||
Expect(fnName.Const).To(Equal("search"))
|
||||
Expect(fnArgs.Properties["query"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
})
|
||||
})
|
||||
Context("Select()", func() {
|
||||
|
@ -331,6 +331,7 @@ func (sc *JSONSchemaConverter) resolveReference(ref string, rootSchema map[strin
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
func (sc *JSONSchemaConverter) Grammar(schema map[string]interface{}, options ...func(*GrammarOption)) string {
|
||||
sc.addRule("freestring", PRIMITIVE_RULES["freestring"])
|
||||
sc.visit(schema, "", schema)
|
||||
@ -352,52 +353,23 @@ type FunctionName struct {
|
||||
Const string `json:"const"`
|
||||
}
|
||||
|
||||
type FunctionProperties struct {
|
||||
Function FunctionName `json:"function"`
|
||||
Arguments Argument `json:"arguments"`
|
||||
}
|
||||
|
||||
type NameProperties struct {
|
||||
Function FunctionName `json:"name"`
|
||||
Arguments Argument `json:"arguments"`
|
||||
}
|
||||
|
||||
type Argument struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
}
|
||||
|
||||
type ItemName struct {
|
||||
Type string `json:"type"`
|
||||
Properties NameProperties `json:"properties"`
|
||||
type Item struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
}
|
||||
|
||||
type ItemFunction struct {
|
||||
Type string `json:"type"`
|
||||
Properties FunctionProperties `json:"properties"`
|
||||
}
|
||||
|
||||
type JSONFunctionStructureName struct {
|
||||
OneOf []ItemName `json:"oneOf,omitempty"`
|
||||
AnyOf []ItemName `json:"anyOf,omitempty"`
|
||||
type JSONFunctionStructure struct {
|
||||
OneOf []Item `json:"oneOf,omitempty"`
|
||||
AnyOf []Item `json:"anyOf,omitempty"`
|
||||
Defs map[string]interface{} `json:"$defs,omitempty"`
|
||||
}
|
||||
|
||||
func (j JSONFunctionStructureName) Grammar(options ...func(*GrammarOption)) string {
|
||||
grammarOpts := &GrammarOption{}
|
||||
grammarOpts.Apply(options...)
|
||||
|
||||
dat, _ := json.Marshal(j)
|
||||
return NewJSONSchemaConverter(grammarOpts.PropOrder).GrammarFromBytes(dat, options...)
|
||||
}
|
||||
|
||||
type JSONFunctionStructureFunction struct {
|
||||
OneOf []ItemFunction `json:"oneOf,omitempty"`
|
||||
AnyOf []ItemFunction `json:"anyOf,omitempty"`
|
||||
Defs map[string]interface{} `json:"$defs,omitempty"`
|
||||
}
|
||||
|
||||
func (j JSONFunctionStructureFunction) Grammar(options ...func(*GrammarOption)) string {
|
||||
func (j JSONFunctionStructure) Grammar(options ...func(*GrammarOption)) string {
|
||||
grammarOpts := &GrammarOption{}
|
||||
grammarOpts.Apply(options...)
|
||||
|
||||
|
@ -9,69 +9,65 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var testFunctions = []ItemFunction{
|
||||
func createFunction(field1 string, field2 string, name string, properties map[string]interface{}) map[string]interface{} {
|
||||
property := map[string]interface{}{}
|
||||
property[field1] = FunctionName{Const: name}
|
||||
property[field2] = Argument{
|
||||
Type: "object",
|
||||
Properties: properties,
|
||||
}
|
||||
return property
|
||||
}
|
||||
|
||||
var testFunctions = []Item{
|
||||
{
|
||||
Type: "object",
|
||||
Properties: FunctionProperties{
|
||||
Function: FunctionName{
|
||||
Const: "create_event",
|
||||
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"},
|
||||
},
|
||||
Arguments: Argument{ // this is OpenAI's parameter
|
||||
Type: "object",
|
||||
Properties: map[string]interface{}{
|
||||
"title": map[string]string{"type": "string"},
|
||||
"date": map[string]string{"type": "string"},
|
||||
"time": map[string]string{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
Type: "object",
|
||||
Properties: FunctionProperties{
|
||||
Function: FunctionName{
|
||||
Const: "search",
|
||||
},
|
||||
Arguments: Argument{
|
||||
Type: "object",
|
||||
Properties: map[string]interface{}{
|
||||
"query": map[string]string{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: createFunction(
|
||||
"function",
|
||||
"arguments",
|
||||
"search",
|
||||
map[string]interface{}{
|
||||
"query": map[string]string{"type": "string"},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
var testFunctionsName = []ItemName{
|
||||
var testFunctionsName = []Item{
|
||||
{
|
||||
Type: "object",
|
||||
Properties: NameProperties{
|
||||
Function: FunctionName{
|
||||
Const: "create_event",
|
||||
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"},
|
||||
},
|
||||
Arguments: Argument{ // this is OpenAI's parameter
|
||||
Type: "object",
|
||||
Properties: map[string]interface{}{
|
||||
"title": map[string]string{"type": "string"},
|
||||
"date": map[string]string{"type": "string"},
|
||||
"time": map[string]string{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
Type: "object",
|
||||
Properties: NameProperties{
|
||||
Function: FunctionName{
|
||||
Const: "search",
|
||||
},
|
||||
Arguments: Argument{
|
||||
Type: "object",
|
||||
Properties: map[string]interface{}{
|
||||
"query": map[string]string{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: createFunction(
|
||||
"name",
|
||||
"arguments",
|
||||
"search",
|
||||
map[string]interface{}{
|
||||
"query": map[string]string{"type": "string"},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@ -270,7 +266,7 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
It("generates a valid grammar from JSON Objects", func() {
|
||||
|
||||
structuredGrammar := JSONFunctionStructureFunction{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctions}
|
||||
|
||||
grammar := structuredGrammar.Grammar()
|
||||
@ -284,7 +280,7 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
It("generates a valid grammar from JSON Objects for multiple function return", func() {
|
||||
structuredGrammar := JSONFunctionStructureFunction{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctions}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.EnableMaybeArray)
|
||||
@ -302,7 +298,7 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
It("generates a valid grammar from JSON Objects for multiple function return", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.EnableMaybeArray)
|
||||
@ -320,7 +316,7 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
It("generates a valid grammar from JSON Objects for multiple function return with a suffix and array", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(
|
||||
@ -340,7 +336,7 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar)
|
||||
})
|
||||
It("generates a valid grammar from JSON Objects with a suffix", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.SetPrefix("suffix"))
|
||||
@ -357,7 +353,7 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
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 := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.SetPrefix("suffix"), functions.EnableMaybeString)
|
||||
@ -374,7 +370,7 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
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 := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.SetPrefix("suffix"), functions.EnableMaybeString, functions.EnableMaybeArray)
|
||||
@ -393,7 +389,7 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
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 := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.EnableMaybeString, functions.EnableMaybeArray)
|
||||
@ -411,7 +407,7 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
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 := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.EnableMaybeString, functions.EnableMaybeArray, functions.NoMixedFreeString)
|
||||
@ -429,7 +425,7 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
It("generates parallel tools without newlines in JSON", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
content := `arr ::=
|
||||
"[" (
|
||||
|
@ -2,6 +2,8 @@ package functions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@ -76,7 +78,8 @@ type FunctionsConfig struct {
|
||||
// FunctionName enable the LLM to return { "name": "function_name", "arguments": { "arg1": "value1", "arg2": "value2" } }
|
||||
// instead of { "function": "function_name", "arguments": { "arg1": "value1", "arg2": "value2" } }.
|
||||
// This might be useful for certain models trained with the function name as the first token.
|
||||
FunctionName bool `yaml:"return_name_in_function_response"`
|
||||
FunctionNameKey string `yaml:"function_name_key"`
|
||||
FunctionArgumentsKey string `yaml:"function_arguments_key"`
|
||||
}
|
||||
|
||||
type ReplaceResult struct {
|
||||
@ -145,6 +148,47 @@ func ParseTextContent(llmresult string, functionConfig FunctionsConfig) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ParseJSON is a function that parses a JSON string that might contain multiple JSON objects
|
||||
// and syntax errors in between by shifting the offset
|
||||
// This for e.g. allow to parse
|
||||
// { "foo": "bar" } invalid { "baz": "qux" }
|
||||
// into
|
||||
// [ { "foo": "bar" }, { "baz": "qux" } ]
|
||||
// Credits to Michael Yang (https://github.com/mxyng) for the original implementation
|
||||
// This is a slighly reworked version, improved for readability and error handling
|
||||
func ParseJSON(s string) ([]map[string]any, error) {
|
||||
var objs []map[string]any
|
||||
offset := 0
|
||||
|
||||
for offset < len(s) {
|
||||
var obj map[string]any
|
||||
decoder := json.NewDecoder(strings.NewReader(s[offset:]))
|
||||
|
||||
err := decoder.Decode(&obj)
|
||||
switch {
|
||||
case errors.Is(err, io.EOF):
|
||||
return objs, nil
|
||||
case err == nil:
|
||||
offset += int(decoder.InputOffset())
|
||||
objs = append(objs, obj)
|
||||
default: // handle the error type
|
||||
var syntaxErr *json.SyntaxError
|
||||
var unmarshalTypeErr *json.UnmarshalTypeError
|
||||
|
||||
switch {
|
||||
case errors.As(err, &syntaxErr):
|
||||
offset += int(syntaxErr.Offset)
|
||||
case errors.As(err, &unmarshalTypeErr):
|
||||
offset += int(unmarshalTypeErr.Offset)
|
||||
default:
|
||||
return objs, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncCallResults {
|
||||
|
||||
log.Debug().Msgf("LLM result: %s", llmresult)
|
||||
@ -157,9 +201,13 @@ func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncC
|
||||
}
|
||||
log.Debug().Msgf("LLM result(function cleanup): %s", llmresult)
|
||||
|
||||
functionNameKey := "function"
|
||||
if functionConfig.FunctionName {
|
||||
functionNameKey = "name"
|
||||
functionNameKey := defaultFunctionNameKey
|
||||
functionArgumentsKey := defaultFunctionArgumentsKey
|
||||
if functionConfig.FunctionNameKey != "" {
|
||||
functionNameKey = functionConfig.FunctionNameKey
|
||||
}
|
||||
if functionConfig.FunctionArgumentsKey != "" {
|
||||
functionArgumentsKey = functionConfig.FunctionArgumentsKey
|
||||
}
|
||||
|
||||
results := []FuncCallResults{}
|
||||
@ -170,19 +218,13 @@ func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncC
|
||||
result = make([]FuncCallResults, 0)
|
||||
|
||||
for _, s := range results {
|
||||
var ss []map[string]interface{}
|
||||
var ss []map[string]any
|
||||
|
||||
s = utils.EscapeNewLines(s)
|
||||
err := json.Unmarshal([]byte(s), &ss)
|
||||
ss, err := ParseJSON(s)
|
||||
//err := json.Unmarshal([]byte(s), &ss)
|
||||
if err != nil {
|
||||
// If the LLM result is a single object, try unmarshaling it into a single map
|
||||
var singleObj map[string]interface{}
|
||||
err = json.Unmarshal([]byte(s), &singleObj)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Str("escapedLLMResult", s).Msg("unable to unmarshal llm result in a single object or an array of JSON objects")
|
||||
} else {
|
||||
ss = []map[string]interface{}{singleObj}
|
||||
}
|
||||
log.Debug().Err(err).Str("escapedLLMResult", s).Msg("unable to unmarshal llm result in a single object or an array of JSON objects")
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Function return: %s %+v", s, ss)
|
||||
@ -195,7 +237,7 @@ func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncC
|
||||
//return result, fmt.Errorf("unable to find function name in result")
|
||||
}
|
||||
// Similarly, while here arguments is a map[string]interface{}, OpenAI actually want a stringified object
|
||||
args, ok := s["arguments"] // arguments needs to be a string, but we return an object from the grammar result (TODO: fix)
|
||||
args, ok := s[functionArgumentsKey] // arguments needs to be a string, but we return an object from the grammar result (TODO: fix)
|
||||
if !ok {
|
||||
continue
|
||||
//return result, fmt.Errorf("unable to find arguments in result")
|
||||
@ -253,7 +295,7 @@ func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncC
|
||||
if functionName == "" {
|
||||
return results
|
||||
}
|
||||
results = append(results, FuncCallResults{Name: result[functionNameKey], Arguments: result["arguments"]})
|
||||
results = append(results, FuncCallResults{Name: result[functionNameKey], Arguments: result[functionArgumentsKey]})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -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…
x
Reference in New Issue
Block a user