|
|
package functions |
|
|
|
|
|
import ( |
|
|
"encoding/json" |
|
|
"errors" |
|
|
"io" |
|
|
"regexp" |
|
|
"slices" |
|
|
"strings" |
|
|
"unicode/utf8" |
|
|
|
|
|
"github.com/mudler/LocalAI/pkg/functions/grammars" |
|
|
"github.com/mudler/LocalAI/pkg/utils" |
|
|
"github.com/mudler/xlog" |
|
|
) |
|
|
|
|
|
|
|
|
type GrammarConfig struct { |
|
|
|
|
|
ParallelCalls bool `yaml:"parallel_calls,omitempty" json:"parallel_calls,omitempty"` |
|
|
|
|
|
DisableParallelNewLines bool `yaml:"disable_parallel_new_lines,omitempty" json:"disable_parallel_new_lines,omitempty"` |
|
|
|
|
|
|
|
|
|
|
|
MixedMode bool `yaml:"mixed_mode,omitempty" json:"mixed_mode,omitempty"` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NoMixedFreeString bool `yaml:"no_mixed_free_string,omitempty" json:"no_mixed_free_string,omitempty"` |
|
|
|
|
|
|
|
|
NoGrammar bool `yaml:"disable,omitempty" json:"disable,omitempty"` |
|
|
|
|
|
|
|
|
|
|
|
Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"` |
|
|
|
|
|
|
|
|
ExpectStringsAfterJSON bool `yaml:"expect_strings_after_json,omitempty" json:"expect_strings_after_json,omitempty"` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PropOrder string `yaml:"properties_order,omitempty" json:"properties_order,omitempty"` |
|
|
|
|
|
|
|
|
|
|
|
SchemaType string `yaml:"schema_type,omitempty" json:"schema_type,omitempty"` |
|
|
|
|
|
GrammarTriggers []GrammarTrigger `yaml:"triggers,omitempty" json:"triggers,omitempty"` |
|
|
} |
|
|
|
|
|
|
|
|
type GrammarTrigger struct { |
|
|
|
|
|
Word string `yaml:"word,omitempty" json:"word,omitempty"` |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type FunctionsConfig struct { |
|
|
|
|
|
|
|
|
DisableNoAction bool `yaml:"disable_no_action,omitempty" json:"disable_no_action,omitempty"` |
|
|
|
|
|
|
|
|
GrammarConfig GrammarConfig `yaml:"grammar,omitempty" json:"grammar,omitempty"` |
|
|
|
|
|
|
|
|
NoActionFunctionName string `yaml:"no_action_function_name,omitempty" json:"no_action_function_name,omitempty"` |
|
|
|
|
|
|
|
|
NoActionDescriptionName string `yaml:"no_action_description_name,omitempty" json:"no_action_description_name,omitempty"` |
|
|
|
|
|
|
|
|
ResponseRegex []string `yaml:"response_regex,omitempty" json:"response_regex,omitempty"` |
|
|
|
|
|
|
|
|
JSONRegexMatch []string `yaml:"json_regex_match,omitempty" json:"json_regex_match,omitempty"` |
|
|
|
|
|
|
|
|
ArgumentRegex []string `yaml:"argument_regex,omitempty" json:"argument_regex,omitempty"` |
|
|
|
|
|
ArgumentRegexKey string `yaml:"argument_regex_key_name,omitempty" json:"argument_regex_key_name,omitempty"` |
|
|
ArgumentRegexValue string `yaml:"argument_regex_value_name,omitempty" json:"argument_regex_value_name,omitempty"` |
|
|
|
|
|
|
|
|
ReplaceFunctionResults []ReplaceResult `yaml:"replace_function_results,omitempty" json:"replace_function_results,omitempty"` |
|
|
|
|
|
|
|
|
ReplaceLLMResult []ReplaceResult `yaml:"replace_llm_results,omitempty" json:"replace_llm_results,omitempty"` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CaptureLLMResult []string `yaml:"capture_llm_results,omitempty" json:"capture_llm_results,omitempty"` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
FunctionNameKey string `yaml:"function_name_key,omitempty" json:"function_name_key,omitempty"` |
|
|
FunctionArgumentsKey string `yaml:"function_arguments_key,omitempty" json:"function_arguments_key,omitempty"` |
|
|
|
|
|
|
|
|
|
|
|
XMLFormatPreset string `yaml:"xml_format_preset,omitempty" json:"xml_format_preset,omitempty"` |
|
|
|
|
|
|
|
|
XMLFormat *XMLToolCallFormat `yaml:"xml_format,omitempty" json:"xml_format,omitempty"` |
|
|
} |
|
|
|
|
|
|
|
|
type ReplaceResult struct { |
|
|
Key string `yaml:"key,omitempty" json:"key,omitempty"` |
|
|
Value string `yaml:"value,omitempty" json:"value,omitempty"` |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type XMLToolCallFormat struct { |
|
|
|
|
|
ScopeStart string `yaml:"scope_start,omitempty" json:"scope_start,omitempty"` |
|
|
|
|
|
ToolStart string `yaml:"tool_start,omitempty" json:"tool_start,omitempty"` |
|
|
|
|
|
ToolSep string `yaml:"tool_sep,omitempty" json:"tool_sep,omitempty"` |
|
|
|
|
|
KeyStart string `yaml:"key_start,omitempty" json:"key_start,omitempty"` |
|
|
|
|
|
KeyValSep string `yaml:"key_val_sep,omitempty" json:"key_val_sep,omitempty"` |
|
|
|
|
|
ValEnd string `yaml:"val_end,omitempty" json:"val_end,omitempty"` |
|
|
|
|
|
ToolEnd string `yaml:"tool_end,omitempty" json:"tool_end,omitempty"` |
|
|
|
|
|
ScopeEnd string `yaml:"scope_end,omitempty" json:"scope_end,omitempty"` |
|
|
|
|
|
KeyValSep2 *string `yaml:"key_val_sep2,omitempty" json:"key_val_sep2,omitempty"` |
|
|
|
|
|
RawArgVal *bool `yaml:"raw_argval,omitempty" json:"raw_argval,omitempty"` |
|
|
|
|
|
LastValEnd *string `yaml:"last_val_end,omitempty" json:"last_val_end,omitempty"` |
|
|
|
|
|
LastToolEnd *string `yaml:"last_tool_end,omitempty" json:"last_tool_end,omitempty"` |
|
|
|
|
|
TrimRawArgVal bool `yaml:"trim_raw_argval,omitempty" json:"trim_raw_argval,omitempty"` |
|
|
|
|
|
AllowToolcallInThink bool `yaml:"allow_toolcall_in_think,omitempty" json:"allow_toolcall_in_think,omitempty"` |
|
|
} |
|
|
|
|
|
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 { |
|
|
xlog.Debug("LLM result", "result", llmresult) |
|
|
|
|
|
for _, item := range functionConfig.ReplaceLLMResult { |
|
|
k, v := item.Key, item.Value |
|
|
xlog.Debug("Replacing", "key", k, "value", v) |
|
|
re := regexp.MustCompile(k) |
|
|
llmresult = re.ReplaceAllString(llmresult, v) |
|
|
} |
|
|
xlog.Debug("LLM result(processed)", "result", llmresult) |
|
|
|
|
|
return llmresult |
|
|
} |
|
|
|
|
|
func ParseTextContent(llmresult string, functionConfig FunctionsConfig) string { |
|
|
xlog.Debug("ParseTextContent", "result", llmresult) |
|
|
xlog.Debug("CaptureLLMResult", "config", 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) { |
|
|
|
|
|
results, err := ParseJSONIterative(s, false) |
|
|
if err == nil && len(results) > 0 { |
|
|
return results, nil |
|
|
} |
|
|
|
|
|
return parseJSONLegacy(s) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func ParseJSONIterative(s string, isPartial bool) ([]map[string]any, error) { |
|
|
parser := NewChatMsgParser(s, isPartial) |
|
|
var results []map[string]any |
|
|
|
|
|
|
|
|
for parser.Pos() < len(parser.Input()) { |
|
|
jsonValue, isPartialJSON, _, err := parser.TryConsumeJSON() |
|
|
if err != nil { |
|
|
|
|
|
if _, ok := err.(*ChatMsgPartialException); ok && isPartial { |
|
|
break |
|
|
} |
|
|
|
|
|
return parseJSONLegacy(s) |
|
|
} |
|
|
if jsonValue != nil { |
|
|
|
|
|
if obj, ok := jsonValue.(map[string]any); ok { |
|
|
results = append(results, obj) |
|
|
} else if arr, ok := jsonValue.([]any); ok { |
|
|
|
|
|
for _, item := range arr { |
|
|
if obj, ok := item.(map[string]any); ok { |
|
|
results = append(results, obj) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
if isPartialJSON { |
|
|
break |
|
|
} |
|
|
|
|
|
parser.ConsumeSpaces() |
|
|
} |
|
|
|
|
|
if len(results) > 0 { |
|
|
return results, nil |
|
|
} |
|
|
|
|
|
|
|
|
return parseJSONLegacy(s) |
|
|
} |
|
|
|
|
|
|
|
|
func parseJSONLegacy(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 GetXMLFormatPreset(name string) *XMLToolCallFormat { |
|
|
formats := getAllXMLFormats() |
|
|
for _, format := range formats { |
|
|
if format.name == name { |
|
|
return format.format |
|
|
} |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
type xmlFormatPreset struct { |
|
|
name string |
|
|
format *XMLToolCallFormat |
|
|
} |
|
|
|
|
|
|
|
|
func getAllXMLFormats() []xmlFormatPreset { |
|
|
falseVal := false |
|
|
commaSpace := ", " |
|
|
emptyValEnd := "" |
|
|
|
|
|
return []xmlFormatPreset{ |
|
|
{ |
|
|
name: "functionary", |
|
|
format: &XMLToolCallFormat{ |
|
|
ScopeStart: "", |
|
|
ToolStart: "<function=", |
|
|
ToolSep: ">", |
|
|
KeyStart: "", |
|
|
KeyValSep: "", |
|
|
ValEnd: "", |
|
|
ToolEnd: "</function>", |
|
|
ScopeEnd: "", |
|
|
RawArgVal: &falseVal, |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
name: "qwen3-coder", |
|
|
format: &XMLToolCallFormat{ |
|
|
ScopeStart: "<tool_call>", |
|
|
ToolStart: "<function=", |
|
|
ToolSep: ">", |
|
|
KeyStart: "<parameter=", |
|
|
KeyValSep: ">", |
|
|
ValEnd: "</parameter>", |
|
|
ToolEnd: "</function>", |
|
|
ScopeEnd: "</tool_call>", |
|
|
TrimRawArgVal: true, |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
name: "glm-4.5", |
|
|
format: &XMLToolCallFormat{ |
|
|
ScopeStart: "", |
|
|
ToolStart: "<tool_call>", |
|
|
ToolSep: "", |
|
|
KeyStart: "<arg_key>", |
|
|
KeyValSep: "</arg_key>", |
|
|
KeyValSep2: func() *string { s := "<arg_value>"; return &s }(), |
|
|
ValEnd: "</arg_value>", |
|
|
ToolEnd: "</tool_call>", |
|
|
ScopeEnd: "", |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
name: "minimax-m2", |
|
|
format: &XMLToolCallFormat{ |
|
|
ScopeStart: "<minimax:tool_call>", |
|
|
ToolStart: "<invoke name=\"", |
|
|
ToolSep: "\">", |
|
|
KeyStart: "<parameter name=\"", |
|
|
KeyValSep: "\">", |
|
|
ValEnd: "</parameter>", |
|
|
ToolEnd: "</invoke>", |
|
|
ScopeEnd: "</minimax:tool_call>", |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
name: "kimi-k2", |
|
|
format: &XMLToolCallFormat{ |
|
|
ScopeStart: "<|tool_calls_section_begin|>", |
|
|
ToolStart: "<|tool_call_begin|>", |
|
|
ToolSep: "<|tool_call_argument_begin|>{", |
|
|
KeyStart: "\"", |
|
|
KeyValSep: "\":", |
|
|
ValEnd: ",", |
|
|
ToolEnd: "}<|tool_call_end|>", |
|
|
ScopeEnd: "<|tool_calls_section_end|>", |
|
|
LastValEnd: &emptyValEnd, |
|
|
RawArgVal: &falseVal, |
|
|
AllowToolcallInThink: true, |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
name: "apriel-1.5", |
|
|
format: &XMLToolCallFormat{ |
|
|
ScopeStart: "<tool_calls>[", |
|
|
ToolStart: "{\"name\": \"", |
|
|
ToolSep: "\", \"arguments\": {", |
|
|
KeyStart: "\"", |
|
|
KeyValSep: "\": ", |
|
|
ValEnd: commaSpace, |
|
|
ToolEnd: "}, ", |
|
|
ScopeEnd: "]</tool_calls>", |
|
|
LastValEnd: &emptyValEnd, |
|
|
LastToolEnd: func() *string { s := "}"; return &s }(), |
|
|
RawArgVal: &falseVal, |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
name: "xiaomi-mimo", |
|
|
format: &XMLToolCallFormat{ |
|
|
ScopeStart: "", |
|
|
ToolStart: "<tool_call>\n{\"name\": \"", |
|
|
ToolSep: "\", \"arguments\": {", |
|
|
KeyStart: "\"", |
|
|
KeyValSep: "\": ", |
|
|
ValEnd: commaSpace, |
|
|
ToolEnd: "}\n</tool_call>", |
|
|
ScopeEnd: "", |
|
|
LastValEnd: &emptyValEnd, |
|
|
RawArgVal: &falseVal, |
|
|
}, |
|
|
}, |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func parseXMLAutoDetect(s string) ([]FuncCallResults, error) { |
|
|
formats := getAllXMLFormats() |
|
|
for _, preset := range formats { |
|
|
results, err := parseXMLWithFormat(s, preset.format) |
|
|
if err == nil && len(results) > 0 { |
|
|
xlog.Debug("XML auto-detection succeeded", "format", preset.name, "count", len(results)) |
|
|
return results, nil |
|
|
} |
|
|
} |
|
|
return nil, nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func ParseXML(s string, format *XMLToolCallFormat) ([]FuncCallResults, error) { |
|
|
|
|
|
results, err := ParseXMLIterative(s, format, false) |
|
|
if err == nil && len(results) > 0 { |
|
|
return results, nil |
|
|
} |
|
|
|
|
|
if format == nil { |
|
|
return parseXMLAutoDetect(s) |
|
|
} |
|
|
return parseXMLWithFormat(s, format) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func ParseXMLIterative(s string, format *XMLToolCallFormat, isPartial bool) ([]FuncCallResults, error) { |
|
|
parser := NewChatMsgParser(s, isPartial) |
|
|
|
|
|
|
|
|
if format == nil { |
|
|
formats := getAllXMLFormats() |
|
|
for _, fmtPreset := range formats { |
|
|
if fmtPreset.format != nil { |
|
|
|
|
|
parser.MoveTo(0) |
|
|
parser.ClearTools() |
|
|
success, err := parser.TryConsumeXMLToolCalls(fmtPreset.format) |
|
|
if err != nil { |
|
|
|
|
|
if _, ok := err.(*ChatMsgPartialException); ok { |
|
|
|
|
|
return parser.ToolCalls(), nil |
|
|
} |
|
|
|
|
|
continue |
|
|
} |
|
|
if success && len(parser.ToolCalls()) > 0 { |
|
|
return parser.ToolCalls(), nil |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return []FuncCallResults{}, nil |
|
|
} |
|
|
|
|
|
|
|
|
success, err := parser.TryConsumeXMLToolCalls(format) |
|
|
if err != nil { |
|
|
|
|
|
if _, ok := err.(*ChatMsgPartialException); ok { |
|
|
|
|
|
return parser.ToolCalls(), nil |
|
|
} |
|
|
return nil, err |
|
|
} |
|
|
|
|
|
if !success { |
|
|
return []FuncCallResults{}, nil |
|
|
} |
|
|
|
|
|
return parser.ToolCalls(), nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func ParseXMLPartial(s string, format *XMLToolCallFormat) (*PartialXMLResult, error) { |
|
|
|
|
|
results, err := ParseXMLIterative(s, format, true) |
|
|
if err != nil { |
|
|
return nil, err |
|
|
} |
|
|
|
|
|
|
|
|
isPartial := false |
|
|
trimmed := strings.TrimSpace(s) |
|
|
|
|
|
|
|
|
if format == nil { |
|
|
formats := getAllXMLFormats() |
|
|
for _, fmtPreset := range formats { |
|
|
if fmtPreset.format != nil { |
|
|
format = fmtPreset.format |
|
|
break |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if format != nil { |
|
|
|
|
|
|
|
|
if !strings.HasSuffix(trimmed, format.ToolEnd) { |
|
|
if format.LastToolEnd != nil && !strings.HasSuffix(trimmed, *format.LastToolEnd) { |
|
|
|
|
|
if len(trimmed) > 0 && len(format.ToolEnd) > 0 { |
|
|
suffix := trimmed[max(0, len(trimmed)-len(format.ToolEnd)):] |
|
|
if strings.HasPrefix(format.ToolEnd, suffix) && suffix != format.ToolEnd { |
|
|
isPartial = true |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if strings.HasSuffix(trimmed, "<") || strings.HasSuffix(trimmed, "</") { |
|
|
isPartial = true |
|
|
} |
|
|
} |
|
|
if !strings.HasSuffix(trimmed, format.ValEnd) { |
|
|
if format.LastValEnd != nil && !strings.HasSuffix(trimmed, *format.LastValEnd) { |
|
|
if len(trimmed) > 0 && len(format.ValEnd) > 0 { |
|
|
suffix := trimmed[max(0, len(trimmed)-len(format.ValEnd)):] |
|
|
if strings.HasPrefix(format.ValEnd, suffix) && suffix != format.ValEnd { |
|
|
isPartial = true |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if strings.HasSuffix(trimmed, "<") || strings.HasSuffix(trimmed, "</") { |
|
|
isPartial = true |
|
|
} |
|
|
} |
|
|
|
|
|
if format.KeyStart != "" && (strings.HasSuffix(trimmed, "<parameter") || strings.HasSuffix(trimmed, "<parameter=")) { |
|
|
isPartial = true |
|
|
} |
|
|
|
|
|
if strings.Contains(trimmed, format.ToolStart) && !strings.HasSuffix(trimmed, format.ToolEnd) { |
|
|
if format.LastToolEnd == nil || !strings.HasSuffix(trimmed, *format.LastToolEnd) { |
|
|
|
|
|
if !strings.Contains(trimmed, format.ToolEnd) { |
|
|
isPartial = true |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return &PartialXMLResult{ |
|
|
Results: results, |
|
|
IsPartial: isPartial, |
|
|
}, nil |
|
|
} |
|
|
|
|
|
func max(a, b int) int { |
|
|
if a > b { |
|
|
return a |
|
|
} |
|
|
return b |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func parseXMLWithFormat(s string, format *XMLToolCallFormat) ([]FuncCallResults, error) { |
|
|
var results []FuncCallResults |
|
|
|
|
|
|
|
|
if format.KeyStart == "" && format.ToolStart == "<function=" { |
|
|
return parseFunctionaryFormat(s, format) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if format.ToolStart != "" && strings.Contains(format.ToolStart, "{\"name\"") { |
|
|
return parseJSONLikeXMLFormat(s, format) |
|
|
} |
|
|
|
|
|
|
|
|
if format.ToolStart == "<tool_call>" && format.ToolSep == "" && format.KeyStart == "<arg_key>" { |
|
|
return parseGLM45Format(s, format) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
escapeRegex := func(str string) string { |
|
|
return regexp.QuoteMeta(str) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var scopePattern *regexp.Regexp |
|
|
if format.ScopeStart != "" { |
|
|
|
|
|
scopeRegex := `(?s)(\s*)` + escapeRegex(format.ScopeStart) + `\s*(.*?)\s*` + escapeRegex(format.ScopeEnd) |
|
|
scopePattern = regexp.MustCompile(scopeRegex) |
|
|
} |
|
|
|
|
|
|
|
|
var toolCallPatterns []*regexp.Regexp |
|
|
|
|
|
buildToolCallPattern := func(toolEnd string) string { |
|
|
toolCallRegex := `(?s)` + escapeRegex(format.ToolStart) |
|
|
if format.ToolSep != "" { |
|
|
|
|
|
|
|
|
|
|
|
toolCallRegex += `(.*?)` + escapeRegex(format.ToolSep) |
|
|
toolCallRegex += `(.*?)` + escapeRegex(toolEnd) |
|
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
if format.KeyStart != "" { |
|
|
|
|
|
|
|
|
toolCallRegex += `\s*([^\n` + escapeRegex(format.KeyStart) + `]+?)\s*` + escapeRegex(format.KeyStart) + `(.*?)` + escapeRegex(toolEnd) |
|
|
} else { |
|
|
|
|
|
toolCallRegex += `\s*([^\n]+)\s*(.*?)` + escapeRegex(toolEnd) |
|
|
} |
|
|
} |
|
|
return toolCallRegex |
|
|
} |
|
|
|
|
|
|
|
|
toolCallPatterns = append(toolCallPatterns, regexp.MustCompile(buildToolCallPattern(format.ToolEnd))) |
|
|
|
|
|
if format.LastToolEnd != nil && *format.LastToolEnd != "" { |
|
|
toolCallPatterns = append(toolCallPatterns, regexp.MustCompile(buildToolCallPattern(*format.LastToolEnd))) |
|
|
} |
|
|
|
|
|
|
|
|
searchContent := s |
|
|
if scopePattern != nil { |
|
|
scopeMatches := scopePattern.FindAllStringSubmatch(s, -1) |
|
|
if len(scopeMatches) == 0 { |
|
|
|
|
|
|
|
|
|
|
|
if strings.TrimSpace(format.ScopeEnd) != "" { |
|
|
|
|
|
|
|
|
xlog.Debug("scope_start not found but scope_end is non-empty", "scope_end", format.ScopeEnd) |
|
|
} |
|
|
searchContent = s |
|
|
} else { |
|
|
|
|
|
for _, scopeMatch := range scopeMatches { |
|
|
if len(scopeMatch) >= 3 { |
|
|
|
|
|
|
|
|
prelude := scopeMatch[1] |
|
|
|
|
|
allWhitespace := true |
|
|
for _, r := range prelude { |
|
|
if !strings.ContainsRune(" \t\n\r", r) { |
|
|
allWhitespace = false |
|
|
break |
|
|
} |
|
|
} |
|
|
if !allWhitespace { |
|
|
|
|
|
|
|
|
xlog.Debug("non-whitespace before scope_start, skipping match", "prelude", prelude) |
|
|
continue |
|
|
} |
|
|
scopeContent := scopeMatch[2] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var toolCallMatches [][]string |
|
|
for _, pattern := range toolCallPatterns { |
|
|
matches := pattern.FindAllStringSubmatch(scopeContent, -1) |
|
|
toolCallMatches = append(toolCallMatches, matches...) |
|
|
} |
|
|
for _, match := range toolCallMatches { |
|
|
if len(match) >= 3 { |
|
|
functionName := strings.TrimSpace(match[1]) |
|
|
|
|
|
|
|
|
if strings.HasPrefix(functionName, "functions.") { |
|
|
|
|
|
functionName = functionName[10:] |
|
|
|
|
|
if idx := strings.LastIndex(functionName, ":"); idx != -1 { |
|
|
|
|
|
suffix := functionName[idx+1:] |
|
|
if len(suffix) > 0 { |
|
|
allDigits := true |
|
|
for _, r := range suffix { |
|
|
if r < '0' || r > '9' { |
|
|
allDigits = false |
|
|
break |
|
|
} |
|
|
} |
|
|
if allDigits { |
|
|
functionName = functionName[:idx] |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
var functionContent string |
|
|
if format.ToolSep == "" && format.KeyStart != "" { |
|
|
|
|
|
functionContent = format.KeyStart + match[2] |
|
|
} else { |
|
|
functionContent = match[2] |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if strings.Contains(functionName, format.ToolEnd) || (format.LastToolEnd != nil && strings.Contains(functionName, *format.LastToolEnd)) { |
|
|
|
|
|
cleanName := strings.TrimSpace(functionName) |
|
|
if idx := strings.Index(cleanName, format.ToolEnd); idx != -1 { |
|
|
cleanName = strings.TrimSpace(cleanName[:idx]) |
|
|
} else if format.LastToolEnd != nil { |
|
|
if idx := strings.Index(cleanName, *format.LastToolEnd); idx != -1 { |
|
|
cleanName = strings.TrimSpace(cleanName[:idx]) |
|
|
} |
|
|
} |
|
|
results = append(results, FuncCallResults{ |
|
|
Name: cleanName, |
|
|
Arguments: "{}", |
|
|
}) |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
if strings.TrimSpace(functionContent) == "" { |
|
|
|
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: "{}", |
|
|
}) |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
args, err := parseXMLParametersWithFormat(functionContent, format) |
|
|
if err != nil { |
|
|
xlog.Debug("error parsing XML parameters", "error", err, "content", functionContent) |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
if len(args) == 0 && strings.TrimSpace(functionContent) != "" { |
|
|
|
|
|
if !strings.Contains(functionContent, format.KeyStart) { |
|
|
argsJSON, _ := json.Marshal(args) |
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: string(argsJSON), |
|
|
}) |
|
|
continue |
|
|
} |
|
|
} |
|
|
|
|
|
argsJSON, _ := json.Marshal(args) |
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: string(argsJSON), |
|
|
}) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
return results, nil |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
var toolCallMatches [][]string |
|
|
for _, pattern := range toolCallPatterns { |
|
|
matches := pattern.FindAllStringSubmatch(searchContent, -1) |
|
|
toolCallMatches = append(toolCallMatches, matches...) |
|
|
} |
|
|
if len(toolCallMatches) == 0 { |
|
|
return nil, nil |
|
|
} |
|
|
|
|
|
|
|
|
for _, match := range toolCallMatches { |
|
|
if len(match) < 3 { |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fullMatch := match[0] |
|
|
expectedToolEnd := format.ToolEnd |
|
|
if format.LastToolEnd != nil && strings.HasSuffix(fullMatch, *format.LastToolEnd) { |
|
|
expectedToolEnd = *format.LastToolEnd |
|
|
} |
|
|
if !strings.HasSuffix(fullMatch, expectedToolEnd) { |
|
|
|
|
|
xlog.Debug("tool_end validation failed", "expected", expectedToolEnd, "match", fullMatch) |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
if len(fullMatch) < len(expectedToolEnd) { |
|
|
|
|
|
continue |
|
|
} |
|
|
actualToolEnd := fullMatch[len(fullMatch)-len(expectedToolEnd):] |
|
|
if actualToolEnd != expectedToolEnd { |
|
|
|
|
|
xlog.Debug("tool_end size validation failed", "expected", expectedToolEnd, "actual", actualToolEnd) |
|
|
continue |
|
|
} |
|
|
|
|
|
functionName := strings.TrimSpace(match[1]) |
|
|
|
|
|
|
|
|
if strings.HasPrefix(functionName, "functions.") { |
|
|
|
|
|
functionName = functionName[10:] |
|
|
|
|
|
if idx := strings.LastIndex(functionName, ":"); idx != -1 { |
|
|
|
|
|
suffix := functionName[idx+1:] |
|
|
if len(suffix) > 0 { |
|
|
allDigits := true |
|
|
for _, r := range suffix { |
|
|
if r < '0' || r > '9' { |
|
|
allDigits = false |
|
|
break |
|
|
} |
|
|
} |
|
|
if allDigits { |
|
|
functionName = functionName[:idx] |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
var functionContent string |
|
|
if len(match) >= 3 { |
|
|
if format.ToolSep == "" && format.KeyStart != "" { |
|
|
|
|
|
functionContent = match[2] |
|
|
} else { |
|
|
functionContent = match[2] |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if strings.Contains(functionName, format.ToolEnd) || (format.LastToolEnd != nil && strings.Contains(functionName, *format.LastToolEnd)) { |
|
|
|
|
|
results = append(results, FuncCallResults{ |
|
|
Name: strings.TrimSpace(strings.Split(functionName, format.ToolEnd)[0]), |
|
|
Arguments: "{}", |
|
|
}) |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
if strings.TrimSpace(functionContent) == "" { |
|
|
|
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: "{}", |
|
|
}) |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
args, err := parseXMLParametersWithFormat(functionContent, format) |
|
|
if err != nil { |
|
|
xlog.Debug("error parsing XML parameters", "error", err, "content", functionContent) |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if len(args) == 0 && strings.TrimSpace(functionContent) != "" { |
|
|
|
|
|
|
|
|
if !strings.Contains(functionContent, format.KeyStart) { |
|
|
argsJSON, _ := json.Marshal(args) |
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: string(argsJSON), |
|
|
}) |
|
|
continue |
|
|
} |
|
|
} |
|
|
|
|
|
argsJSON, _ := json.Marshal(args) |
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: string(argsJSON), |
|
|
}) |
|
|
} |
|
|
|
|
|
return results, nil |
|
|
} |
|
|
|
|
|
|
|
|
func parseGLM45Format(s string, format *XMLToolCallFormat) ([]FuncCallResults, error) { |
|
|
var results []FuncCallResults |
|
|
|
|
|
|
|
|
pattern := regexp.MustCompile(`(?s)<tool_call>\s*([^\n<]+)\s*(.*?)\s*</tool_call>`) |
|
|
matches := pattern.FindAllStringSubmatch(s, -1) |
|
|
|
|
|
for _, match := range matches { |
|
|
if len(match) >= 3 { |
|
|
functionName := strings.TrimSpace(match[1]) |
|
|
|
|
|
|
|
|
if strings.HasPrefix(functionName, "functions.") { |
|
|
|
|
|
functionName = functionName[10:] |
|
|
|
|
|
if idx := strings.LastIndex(functionName, ":"); idx != -1 { |
|
|
|
|
|
suffix := functionName[idx+1:] |
|
|
if len(suffix) > 0 { |
|
|
allDigits := true |
|
|
for _, r := range suffix { |
|
|
if r < '0' || r > '9' { |
|
|
allDigits = false |
|
|
break |
|
|
} |
|
|
} |
|
|
if allDigits { |
|
|
functionName = functionName[:idx] |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
functionContent := match[2] |
|
|
|
|
|
|
|
|
if strings.TrimSpace(functionContent) == "" { |
|
|
|
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: "{}", |
|
|
}) |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
args, err := parseXMLParametersWithFormat(functionContent, format) |
|
|
if err != nil { |
|
|
xlog.Debug("error parsing GLM 4.5 parameters", "error", err, "content", functionContent) |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
if len(args) == 0 { |
|
|
argsJSON, _ := json.Marshal(args) |
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: string(argsJSON), |
|
|
}) |
|
|
continue |
|
|
} |
|
|
|
|
|
argsJSON, _ := json.Marshal(args) |
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: string(argsJSON), |
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
return results, nil |
|
|
} |
|
|
|
|
|
|
|
|
func parseFunctionaryFormat(s string, format *XMLToolCallFormat) ([]FuncCallResults, error) { |
|
|
var results []FuncCallResults |
|
|
|
|
|
|
|
|
pattern := regexp.MustCompile(`(?s)<function=([^>]+)>(.*?)</function>`) |
|
|
matches := pattern.FindAllStringSubmatch(s, -1) |
|
|
|
|
|
for _, match := range matches { |
|
|
if len(match) >= 3 { |
|
|
functionName := strings.TrimSpace(match[1]) |
|
|
jsonContent := strings.TrimSpace(match[2]) |
|
|
|
|
|
|
|
|
var args map[string]any |
|
|
if err := json.Unmarshal([]byte(jsonContent), &args); err != nil { |
|
|
xlog.Debug("error parsing Functionary JSON", "error", err, "content", jsonContent) |
|
|
continue |
|
|
} |
|
|
|
|
|
argsJSON, _ := json.Marshal(args) |
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: string(argsJSON), |
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
return results, nil |
|
|
} |
|
|
|
|
|
|
|
|
func parseJSONLikeXMLFormat(s string, format *XMLToolCallFormat) ([]FuncCallResults, error) { |
|
|
var results []FuncCallResults |
|
|
|
|
|
|
|
|
escapeRegex := func(str string) string { |
|
|
return regexp.QuoteMeta(str) |
|
|
} |
|
|
|
|
|
|
|
|
var pattern *regexp.Regexp |
|
|
if format.ScopeStart != "" { |
|
|
patternStr := `(?s)` + escapeRegex(format.ScopeStart) + `(.*?)` + escapeRegex(format.ScopeEnd) |
|
|
pattern = regexp.MustCompile(patternStr) |
|
|
} else { |
|
|
patternStr := `(?s)` + escapeRegex(format.ToolStart) + `([^"]+)"` + escapeRegex(format.ToolSep) + `(.*?)` + escapeRegex(format.ToolEnd) |
|
|
pattern = regexp.MustCompile(patternStr) |
|
|
} |
|
|
|
|
|
matches := pattern.FindAllStringSubmatch(s, -1) |
|
|
for _, match := range matches { |
|
|
if len(match) < 2 { |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
jsonContent := match[1] |
|
|
if format.ScopeStart != "" { |
|
|
|
|
|
|
|
|
toolPattern := regexp.MustCompile(`(?s)\{\s*"name"\s*:\s*"([^"]+)"\s*,\s*"arguments"\s*:\s*(\{.*?\})\s*\}`) |
|
|
toolMatches := toolPattern.FindAllStringSubmatch(jsonContent, -1) |
|
|
for _, toolMatch := range toolMatches { |
|
|
if len(toolMatch) >= 3 { |
|
|
functionName := strings.TrimSpace(toolMatch[1]) |
|
|
argsJSON := toolMatch[2] |
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: argsJSON, |
|
|
}) |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
namePattern := regexp.MustCompile(`"name"\s*:\s*"([^"]+)"`) |
|
|
nameMatch := namePattern.FindStringSubmatch(jsonContent) |
|
|
if len(nameMatch) >= 2 { |
|
|
functionName := strings.TrimSpace(nameMatch[1]) |
|
|
argsPattern := regexp.MustCompile(`"arguments"\s*:\s*(\{.*\})`) |
|
|
argsMatch := argsPattern.FindStringSubmatch(jsonContent) |
|
|
argsJSON := "{}" |
|
|
if len(argsMatch) >= 2 { |
|
|
argsJSON = argsMatch[1] |
|
|
} |
|
|
results = append(results, FuncCallResults{ |
|
|
Name: functionName, |
|
|
Arguments: argsJSON, |
|
|
}) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return results, nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func utf8TruncateSafe(s string) string { |
|
|
if len(s) == 0 { |
|
|
return s |
|
|
} |
|
|
|
|
|
|
|
|
for i := len(s); i > 0 && i > len(s)-4; i-- { |
|
|
if utf8.ValidString(s[:i]) { |
|
|
return s[:i] |
|
|
} |
|
|
} |
|
|
|
|
|
if len(s) > 3 { |
|
|
return s[:len(s)-3] |
|
|
} |
|
|
return "" |
|
|
} |
|
|
|
|
|
|
|
|
type PartialXMLResult struct { |
|
|
Results []FuncCallResults |
|
|
IsPartial bool |
|
|
PartialArg string |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const XML_TOOL_CALL_PARTIAL_FLAG = "XML_TOOL_CALL_PARTIAL_FLAG" |
|
|
|
|
|
|
|
|
|
|
|
func partialJSON(jsonStr string) (string, bool) { |
|
|
pos := strings.LastIndex(jsonStr, XML_TOOL_CALL_PARTIAL_FLAG) |
|
|
if pos == -1 { |
|
|
return jsonStr, false |
|
|
} |
|
|
|
|
|
for i := pos + len(XML_TOOL_CALL_PARTIAL_FLAG); i < len(jsonStr); i++ { |
|
|
ch := jsonStr[i] |
|
|
if ch != '\'' && ch != '"' && ch != '}' && ch != ':' && ch != ']' && !strings.ContainsRune(" \t\n\r", rune(ch)) { |
|
|
return jsonStr, false |
|
|
} |
|
|
} |
|
|
|
|
|
if pos > 0 && jsonStr[pos-1] == '"' { |
|
|
pos-- |
|
|
} |
|
|
return jsonStr[:pos], true |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func genPartialJSON(args map[string]any, functionName string, rest string, needle string) (string, bool) { |
|
|
|
|
|
args[rest+needle] = XML_TOOL_CALL_PARTIAL_FLAG |
|
|
jsonBytes, err := json.Marshal(args) |
|
|
if err != nil { |
|
|
return "", false |
|
|
} |
|
|
jsonStr := string(jsonBytes) |
|
|
|
|
|
if cleaned, isPartial := partialJSON(jsonStr); isPartial { |
|
|
return cleaned, true |
|
|
} |
|
|
return jsonStr, false |
|
|
} |
|
|
|
|
|
|
|
|
func parseXMLParametersWithFormat(content string, format *XMLToolCallFormat) (map[string]any, error) { |
|
|
args := make(map[string]any) |
|
|
|
|
|
|
|
|
if format.KeyValSep2 != nil && *format.KeyValSep2 == "<arg_value>" { |
|
|
return parseGLM45Parameters(content, format) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if format.KeyStart == "\"" && format.KeyValSep == "\":" && (format.RawArgVal == nil || !*format.RawArgVal) { |
|
|
|
|
|
content = strings.TrimSpace(content) |
|
|
if strings.HasPrefix(content, "{") && strings.HasSuffix(content, "}") { |
|
|
var jsonArgs map[string]any |
|
|
if err := json.Unmarshal([]byte(content), &jsonArgs); err == nil { |
|
|
|
|
|
return jsonArgs, nil |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if format.KeyStart != "" { |
|
|
return parseStandardParameters(content, format) |
|
|
} |
|
|
|
|
|
return args, nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func parseMsgWithXMLToolCalls(s string, format *XMLToolCallFormat, startThink string, endThink string) ([]FuncCallResults, string, error) { |
|
|
if startThink == "" { |
|
|
startThink = "<think>" |
|
|
} |
|
|
if endThink == "" { |
|
|
endThink = "</think>" |
|
|
} |
|
|
|
|
|
var results []FuncCallResults |
|
|
var reasoningContent strings.Builder |
|
|
var content strings.Builder |
|
|
|
|
|
|
|
|
|
|
|
thinkStartIdx := strings.Index(s, startThink) |
|
|
|
|
|
if thinkStartIdx == -1 { |
|
|
|
|
|
xmlResults, err := parseXMLWithFormat(s, format) |
|
|
return xmlResults, "", err |
|
|
} |
|
|
|
|
|
|
|
|
if thinkStartIdx > 0 { |
|
|
preContent := s[:thinkStartIdx] |
|
|
xmlResults, _ := parseXMLWithFormat(preContent, format) |
|
|
results = append(results, xmlResults...) |
|
|
content.WriteString(preContent) |
|
|
} |
|
|
|
|
|
|
|
|
pos := 0 |
|
|
for pos < len(s) { |
|
|
thinkStart := strings.Index(s[pos:], startThink) |
|
|
if thinkStart == -1 { |
|
|
|
|
|
remaining := s[pos:] |
|
|
xmlResults, _ := parseXMLWithFormat(remaining, format) |
|
|
results = append(results, xmlResults...) |
|
|
content.WriteString(remaining) |
|
|
break |
|
|
} |
|
|
thinkStart += pos |
|
|
|
|
|
thinkEnd := strings.Index(s[thinkStart+len(startThink):], endThink) |
|
|
if thinkEnd == -1 { |
|
|
|
|
|
if format.AllowToolcallInThink { |
|
|
|
|
|
thinkingContent := s[thinkStart+len(startThink):] |
|
|
reasoningContent.WriteString(thinkingContent) |
|
|
|
|
|
xmlResults, _ := parseXMLWithFormat(thinkingContent, format) |
|
|
results = append(results, xmlResults...) |
|
|
} else { |
|
|
|
|
|
content.WriteString(s[pos:thinkStart]) |
|
|
} |
|
|
break |
|
|
} |
|
|
thinkEnd += thinkStart + len(startThink) |
|
|
|
|
|
|
|
|
thinkingContent := s[thinkStart+len(startThink) : thinkEnd] |
|
|
reasoningContent.WriteString(thinkingContent) |
|
|
|
|
|
|
|
|
betweenContent := s[pos:thinkStart] |
|
|
if len(betweenContent) > 0 { |
|
|
xmlResults, _ := parseXMLWithFormat(betweenContent, format) |
|
|
results = append(results, xmlResults...) |
|
|
content.WriteString(betweenContent) |
|
|
} |
|
|
|
|
|
|
|
|
pos = thinkEnd + len(endThink) |
|
|
} |
|
|
|
|
|
return results, reasoningContent.String(), nil |
|
|
} |
|
|
|
|
|
|
|
|
func parseGLM45Parameters(content string, format *XMLToolCallFormat) (map[string]any, error) { |
|
|
args := make(map[string]any) |
|
|
|
|
|
|
|
|
pattern := regexp.MustCompile(`(?s)<arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value>`) |
|
|
matches := pattern.FindAllStringSubmatch(content, -1) |
|
|
|
|
|
for _, match := range matches { |
|
|
if len(match) >= 3 { |
|
|
paramName := strings.TrimSpace(match[1]) |
|
|
paramValue := strings.TrimSpace(match[2]) |
|
|
args[paramName] = parseParameterValue(paramValue, format) |
|
|
} |
|
|
} |
|
|
|
|
|
return args, nil |
|
|
} |
|
|
|
|
|
|
|
|
func parseStandardParameters(content string, format *XMLToolCallFormat) (map[string]any, error) { |
|
|
args := make(map[string]any) |
|
|
|
|
|
escapeRegex := func(str string) string { |
|
|
return regexp.QuoteMeta(str) |
|
|
} |
|
|
|
|
|
|
|
|
var parameterPatterns []*regexp.Regexp |
|
|
|
|
|
if strings.Contains(format.KeyStart, "=") { |
|
|
|
|
|
patternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^>]+)` + escapeRegex(format.KeyValSep) + `(.*?)` + escapeRegex(format.ValEnd) |
|
|
parameterPatterns = append(parameterPatterns, regexp.MustCompile(patternStr)) |
|
|
|
|
|
if format.LastValEnd != nil && *format.LastValEnd != "" { |
|
|
altPatternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^>]+)` + escapeRegex(format.KeyValSep) + `(.*?)` + escapeRegex(*format.LastValEnd) |
|
|
parameterPatterns = append(parameterPatterns, regexp.MustCompile(altPatternStr)) |
|
|
} |
|
|
} else if strings.Contains(format.KeyStart, "name=\"") { |
|
|
|
|
|
patternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^"]+)"` + escapeRegex(format.KeyValSep) + `(.*?)` + escapeRegex(format.ValEnd) |
|
|
parameterPatterns = append(parameterPatterns, regexp.MustCompile(patternStr)) |
|
|
|
|
|
if format.LastValEnd != nil && *format.LastValEnd != "" { |
|
|
altPatternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^"]+)"` + escapeRegex(format.KeyValSep) + `(.*?)` + escapeRegex(*format.LastValEnd) |
|
|
parameterPatterns = append(parameterPatterns, regexp.MustCompile(altPatternStr)) |
|
|
} |
|
|
} else { |
|
|
|
|
|
patternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^` + escapeRegex(format.KeyValSep) + `]+)` + escapeRegex(format.KeyValSep) |
|
|
if format.KeyValSep2 != nil { |
|
|
patternStr += escapeRegex(*format.KeyValSep2) |
|
|
} |
|
|
patternStr += `(.*?)` + escapeRegex(format.ValEnd) |
|
|
parameterPatterns = append(parameterPatterns, regexp.MustCompile(patternStr)) |
|
|
|
|
|
if format.LastValEnd != nil && *format.LastValEnd != "" { |
|
|
altPatternStr := `(?s)` + escapeRegex(format.KeyStart) + `([^` + escapeRegex(format.KeyValSep) + `]+)` + escapeRegex(format.KeyValSep) |
|
|
if format.KeyValSep2 != nil { |
|
|
altPatternStr += escapeRegex(*format.KeyValSep2) |
|
|
} |
|
|
altPatternStr += `(.*?)` + escapeRegex(*format.LastValEnd) |
|
|
parameterPatterns = append(parameterPatterns, regexp.MustCompile(altPatternStr)) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type paramMatch struct { |
|
|
name string |
|
|
value string |
|
|
position int |
|
|
} |
|
|
var allMatches []paramMatch |
|
|
|
|
|
|
|
|
for _, pattern := range parameterPatterns { |
|
|
matches := pattern.FindAllStringSubmatch(content, -1) |
|
|
for _, match := range matches { |
|
|
if len(match) >= 3 { |
|
|
paramName := strings.TrimSpace(match[1]) |
|
|
paramValue := strings.TrimSpace(match[2]) |
|
|
|
|
|
pos := strings.Index(content, match[0]) |
|
|
if pos != -1 { |
|
|
allMatches = append(allMatches, paramMatch{ |
|
|
name: paramName, |
|
|
value: paramValue, |
|
|
position: pos, |
|
|
}) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
seenParams := make(map[string]bool) |
|
|
for _, match := range allMatches { |
|
|
if !seenParams[match.name] { |
|
|
args[match.name] = parseParameterValue(match.value, format) |
|
|
seenParams[match.name] = true |
|
|
} |
|
|
} |
|
|
|
|
|
return args, nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func parseParameterValue(paramValue string, format *XMLToolCallFormat) any { |
|
|
|
|
|
if format.TrimRawArgVal { |
|
|
paramValue = strings.TrimSpace(paramValue) |
|
|
} |
|
|
|
|
|
|
|
|
if format.RawArgVal != nil { |
|
|
if *format.RawArgVal { |
|
|
|
|
|
return paramValue |
|
|
} |
|
|
|
|
|
var jsonValue any |
|
|
if err := json.Unmarshal([]byte(paramValue), &jsonValue); err == nil { |
|
|
|
|
|
return jsonValue |
|
|
} |
|
|
|
|
|
|
|
|
return paramValue |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var jsonValue any |
|
|
if err := json.Unmarshal([]byte(paramValue), &jsonValue); err != nil { |
|
|
|
|
|
return paramValue |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return jsonValue |
|
|
} |
|
|
|
|
|
func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncCallResults { |
|
|
|
|
|
xlog.Debug("LLM result", "result", llmresult) |
|
|
|
|
|
for _, item := range functionConfig.ReplaceFunctionResults { |
|
|
k, v := item.Key, item.Value |
|
|
xlog.Debug("Replacing", "key", k, "value", v) |
|
|
re := regexp.MustCompile(k) |
|
|
llmresult = re.ReplaceAllString(llmresult, v) |
|
|
} |
|
|
xlog.Debug("LLM result(function cleanup)", "result", llmresult) |
|
|
|
|
|
functionNameKey := defaultFunctionNameKey |
|
|
functionArgumentsKey := defaultFunctionArgumentsKey |
|
|
if functionConfig.FunctionNameKey != "" { |
|
|
functionNameKey = functionConfig.FunctionNameKey |
|
|
} |
|
|
if functionConfig.FunctionArgumentsKey != "" { |
|
|
functionArgumentsKey = functionConfig.FunctionArgumentsKey |
|
|
} |
|
|
|
|
|
results := []FuncCallResults{} |
|
|
llmResults := []string{} |
|
|
|
|
|
extractJSON := 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 { |
|
|
xlog.Debug("unable to unmarshal llm result in a single object or an array of JSON objects", "error", err, "escapedLLMResult", s) |
|
|
} |
|
|
|
|
|
xlog.Debug("Function return", "result", s, "parsed", ss) |
|
|
|
|
|
for _, s := range ss { |
|
|
|
|
|
func_name, ok := s[functionNameKey] |
|
|
if !ok { |
|
|
continue |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
args, ok := s[functionArgumentsKey] |
|
|
if !ok { |
|
|
continue |
|
|
|
|
|
} |
|
|
|
|
|
var d []byte |
|
|
if argsStr, ok := args.(string); ok { |
|
|
|
|
|
d = []byte(argsStr) |
|
|
} else { |
|
|
|
|
|
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 { |
|
|
|
|
|
|
|
|
|
|
|
compiledRegexes := make([]*regexp.Regexp, 0, len(functionConfig.ResponseRegex)) |
|
|
for _, r := range functionConfig.ResponseRegex { |
|
|
compiledRegexes = append(compiledRegexes, regexp.MustCompile(r)) |
|
|
} |
|
|
for _, respRegex := range compiledRegexes { |
|
|
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: ParseFunctionCallArgs(result[functionArgumentsKey], functionConfig)}) |
|
|
} |
|
|
} |
|
|
} else { |
|
|
if len(llmResults) == 0 { |
|
|
llmResults = append(llmResults, llmresult) |
|
|
} |
|
|
results, _ = extractJSON(llmResults) |
|
|
} |
|
|
|
|
|
|
|
|
var xmlFormat *XMLToolCallFormat |
|
|
if functionConfig.XMLFormat != nil { |
|
|
|
|
|
xmlFormat = functionConfig.XMLFormat |
|
|
xlog.Debug("Using custom XML format") |
|
|
} else if functionConfig.XMLFormatPreset != "" { |
|
|
|
|
|
xmlFormat = GetXMLFormatPreset(functionConfig.XMLFormatPreset) |
|
|
if xmlFormat == nil { |
|
|
xlog.Debug("Unknown XML format preset, falling back to auto-detection", "preset", functionConfig.XMLFormatPreset) |
|
|
} else { |
|
|
xlog.Debug("Using XML format preset", "preset", functionConfig.XMLFormatPreset) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
skipXMLParsing := (len(functionConfig.JSONRegexMatch) > 0 || len(functionConfig.ResponseRegex) > 0) && len(results) > 0 |
|
|
if len(results) == 0 && !skipXMLParsing { |
|
|
xmlResults, err := ParseXML(llmresult, xmlFormat) |
|
|
if err == nil && len(xmlResults) > 0 { |
|
|
xlog.Debug("Found XML tool calls", "count", len(xmlResults)) |
|
|
results = append(results, xmlResults...) |
|
|
} |
|
|
} else if len(results) > 0 && !skipXMLParsing { |
|
|
|
|
|
|
|
|
|
|
|
xmlResults, err := ParseXML(llmresult, xmlFormat) |
|
|
if err == nil && len(xmlResults) > 0 { |
|
|
|
|
|
for _, result := range xmlResults { |
|
|
jsonResults, _ := extractJSON([]string{result.Name}) |
|
|
if len(jsonResults) > 0 { |
|
|
xlog.Debug("Found valid JSON inside XML tags, skipping XML parsing", "json_count", len(jsonResults)) |
|
|
} else { |
|
|
xlog.Debug("Found additional XML tool calls alongside JSON", "xml_count", len(xmlResults)) |
|
|
results = append(results, xmlResults...) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return results |
|
|
} |
|
|
|
|
|
func ParseFunctionCallArgs(functionArguments string, functionConfig FunctionsConfig) string { |
|
|
|
|
|
|
|
|
|
|
|
cleaned := functionArguments |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if len(functionConfig.ArgumentRegex) == 0 { |
|
|
return cleaned |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
args := make(map[string]string) |
|
|
|
|
|
agrsRegexKeyName := "key" |
|
|
agrsRegexValueName := "value" |
|
|
|
|
|
if functionConfig.ArgumentRegexKey != "" { |
|
|
agrsRegexKeyName = functionConfig.ArgumentRegexKey |
|
|
} |
|
|
if functionConfig.ArgumentRegexValue != "" { |
|
|
agrsRegexValueName = functionConfig.ArgumentRegexValue |
|
|
} |
|
|
|
|
|
for _, r := range functionConfig.ArgumentRegex { |
|
|
var respRegex = regexp.MustCompile(r) |
|
|
var nameRange []string = respRegex.SubexpNames() |
|
|
var keyIndex = slices.Index(nameRange, agrsRegexKeyName) |
|
|
var valueIndex = slices.Index(nameRange, agrsRegexValueName) |
|
|
matches := respRegex.FindAllStringSubmatch(functionArguments, -1) |
|
|
for _, match := range matches { |
|
|
args[match[keyIndex]] = match[valueIndex] |
|
|
} |
|
|
} |
|
|
|
|
|
jsonBytes, _ := json.Marshal(args) |
|
|
|
|
|
return string(jsonBytes) |
|
|
} |
|
|
|