|
package functions |
|
|
|
import ( |
|
"encoding/json" |
|
"errors" |
|
"io" |
|
"regexp" |
|
"strings" |
|
|
|
"github.com/mudler/LocalAI/pkg/functions/grammars" |
|
"github.com/mudler/LocalAI/pkg/utils" |
|
"github.com/rs/zerolog/log" |
|
) |
|
|
|
type GrammarConfig struct { |
|
|
|
ParallelCalls bool `yaml:"parallel_calls"` |
|
|
|
DisableParallelNewLines bool `yaml:"disable_parallel_new_lines"` |
|
|
|
|
|
|
|
MixedMode bool `yaml:"mixed_mode"` |
|
|
|
|
|
|
|
|
|
|
|
NoMixedFreeString bool `yaml:"no_mixed_free_string"` |
|
|
|
|
|
NoGrammar bool `yaml:"disable"` |
|
|
|
|
|
|
|
Prefix string `yaml:"prefix"` |
|
|
|
|
|
ExpectStringsAfterJSON bool `yaml:"expect_strings_after_json"` |
|
|
|
|
|
|
|
|
|
PropOrder string `yaml:"properties_order"` |
|
|
|
|
|
|
|
SchemaType string `yaml:"schema_type"` |
|
} |
|
|
|
|
|
|
|
|
|
type FunctionsConfig struct { |
|
|
|
|
|
DisableNoAction bool `yaml:"disable_no_action"` |
|
|
|
|
|
GrammarConfig GrammarConfig `yaml:"grammar"` |
|
|
|
|
|
NoActionFunctionName string `yaml:"no_action_function_name"` |
|
|
|
|
|
NoActionDescriptionName string `yaml:"no_action_description_name"` |
|
|
|
|
|
ResponseRegex []string `yaml:"response_regex"` |
|
|
|
|
|
JSONRegexMatch []string `yaml:"json_regex_match"` |
|
|
|
|
|
ReplaceFunctionResults []ReplaceResult `yaml:"replace_function_results"` |
|
|
|
|
|
ReplaceLLMResult []ReplaceResult `yaml:"replace_llm_results"` |
|
|
|
|
|
|
|
|
|
CaptureLLMResult []string `yaml:"capture_llm_results"` |
|
|
|
|
|
|
|
|
|
FunctionNameKey string `yaml:"function_name_key"` |
|
FunctionArgumentsKey string `yaml:"function_arguments_key"` |
|
} |
|
|
|
type ReplaceResult struct { |
|
Key string `yaml:"key"` |
|
Value string `yaml:"value"` |
|
} |
|
|
|
type FuncCallResults struct { |
|
Name string |
|
Arguments string |
|
} |
|
|
|
func (g FunctionsConfig) GrammarOptions() []func(o *grammars.GrammarOption) { |
|
opts := []func(o *grammars.GrammarOption){} |
|
if g.GrammarConfig.MixedMode { |
|
opts = append(opts, grammars.EnableMaybeString) |
|
} |
|
if g.GrammarConfig.ParallelCalls { |
|
opts = append(opts, grammars.EnableMaybeArray) |
|
} |
|
if g.GrammarConfig.DisableParallelNewLines { |
|
opts = append(opts, grammars.DisableParallelNewLines) |
|
} |
|
if g.GrammarConfig.Prefix != "" { |
|
opts = append(opts, grammars.SetPrefix(g.GrammarConfig.Prefix)) |
|
} |
|
if g.GrammarConfig.NoMixedFreeString { |
|
opts = append(opts, grammars.NoMixedFreeString) |
|
} |
|
if g.GrammarConfig.ExpectStringsAfterJSON { |
|
opts = append(opts, grammars.ExpectStringsAfterJSON) |
|
} |
|
|
|
if g.GrammarConfig.SchemaType != "" { |
|
opts = append(opts, grammars.WithSchemaType(grammars.NewType(g.GrammarConfig.SchemaType))) |
|
} |
|
|
|
if g.FunctionNameKey != "" { |
|
opts = append(opts, grammars.WithFunctionName(g.FunctionNameKey)) |
|
} |
|
|
|
opts = append(opts, grammars.SetPropOrder(g.GrammarConfig.PropOrder)) |
|
return opts |
|
} |
|
|
|
func CleanupLLMResult(llmresult string, functionConfig FunctionsConfig) string { |
|
log.Debug().Msgf("LLM result: %s", llmresult) |
|
|
|
for _, item := range functionConfig.ReplaceLLMResult { |
|
k, v := item.Key, item.Value |
|
log.Debug().Msgf("Replacing %s with %s", k, v) |
|
re := regexp.MustCompile(k) |
|
llmresult = re.ReplaceAllString(llmresult, v) |
|
} |
|
log.Debug().Msgf("LLM result(processed): %s", llmresult) |
|
|
|
return llmresult |
|
} |
|
|
|
func ParseTextContent(llmresult string, functionConfig FunctionsConfig) string { |
|
log.Debug().Msgf("ParseTextContent: %s", llmresult) |
|
log.Debug().Msgf("CaptureLLMResult: %s", functionConfig.CaptureLLMResult) |
|
|
|
for _, r := range functionConfig.CaptureLLMResult { |
|
|
|
var respRegex = regexp.MustCompile(r) |
|
match := respRegex.FindStringSubmatch(llmresult) |
|
if len(match) >= 1 { |
|
m := strings.TrimSpace(match[1]) |
|
return m |
|
} |
|
} |
|
|
|
return "" |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: |
|
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) |
|
|
|
for _, item := range functionConfig.ReplaceFunctionResults { |
|
k, v := item.Key, item.Value |
|
log.Debug().Msgf("Replacing %s with %s", k, v) |
|
re := regexp.MustCompile(k) |
|
llmresult = re.ReplaceAllString(llmresult, v) |
|
} |
|
log.Debug().Msgf("LLM result(function cleanup): %s", llmresult) |
|
|
|
functionNameKey := defaultFunctionNameKey |
|
functionArgumentsKey := defaultFunctionArgumentsKey |
|
if functionConfig.FunctionNameKey != "" { |
|
functionNameKey = functionConfig.FunctionNameKey |
|
} |
|
if functionConfig.FunctionArgumentsKey != "" { |
|
functionArgumentsKey = functionConfig.FunctionArgumentsKey |
|
} |
|
|
|
results := []FuncCallResults{} |
|
llmResults := []string{} |
|
|
|
returnResult := func(results []string) (result []FuncCallResults, e error) { |
|
|
|
result = make([]FuncCallResults, 0) |
|
|
|
for _, s := range results { |
|
var ss []map[string]any |
|
|
|
s = utils.EscapeNewLines(s) |
|
ss, err := ParseJSON(s) |
|
|
|
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") |
|
} |
|
|
|
log.Debug().Msgf("Function return: %s %+v", s, ss) |
|
|
|
for _, s := range ss { |
|
|
|
func_name, ok := s[functionNameKey] |
|
if !ok { |
|
continue |
|
|
|
} |
|
|
|
args, ok := s[functionArgumentsKey] |
|
if !ok { |
|
continue |
|
|
|
} |
|
d, _ := json.Marshal(args) |
|
funcName, ok := func_name.(string) |
|
if !ok { |
|
continue |
|
|
|
} |
|
|
|
result = append(result, FuncCallResults{Name: funcName, Arguments: string(d)}) |
|
} |
|
} |
|
|
|
return result, nil |
|
} |
|
|
|
|
|
result := make(map[string]string) |
|
if len(functionConfig.JSONRegexMatch) != 0 { |
|
for _, r := range functionConfig.JSONRegexMatch { |
|
|
|
var respRegex = regexp.MustCompile(r) |
|
match := respRegex.FindAllStringSubmatch(llmresult, -1) |
|
var allMatches []string |
|
for _, m := range match { |
|
if len(m) > 1 { |
|
|
|
allMatches = append(allMatches, m[1]) |
|
} |
|
} |
|
if len(allMatches) > 0 { |
|
llmResults = append(llmResults, allMatches...) |
|
break |
|
} |
|
} |
|
} |
|
|
|
if len(functionConfig.ResponseRegex) > 0 { |
|
|
|
|
|
|
|
for _, r := range functionConfig.ResponseRegex { |
|
var respRegex = regexp.MustCompile(r) |
|
matches := respRegex.FindAllStringSubmatch(llmresult, -1) |
|
for _, match := range matches { |
|
for i, name := range respRegex.SubexpNames() { |
|
if i != 0 && name != "" && len(match) > i { |
|
result[name] = match[i] |
|
} |
|
} |
|
|
|
functionName := result[functionNameKey] |
|
if functionName == "" { |
|
return results |
|
} |
|
results = append(results, FuncCallResults{Name: result[functionNameKey], Arguments: result[functionArgumentsKey]}) |
|
} |
|
} |
|
} else { |
|
if len(llmResults) == 0 { |
|
llmResults = append(llmResults, llmresult) |
|
} |
|
results, _ = returnResult(llmResults) |
|
} |
|
|
|
return results |
|
} |
|
|