From b3e5491e41811294de9d81649a96581af6522d08 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Mon, 22 Jul 2024 12:38:03 -0400 Subject: [PATCH] server: collect nested tool call objects when parsing (#5824) --- server/model.go | 43 +++++++++++++++++++++-------- server/model_test.go | 1 + server/routes.go | 4 +-- server/testdata/tools/xlam.gotmpl | 45 +++++++++++++++++++++++++++++++ server/testdata/tools/xlam.out | 40 +++++++++++++++++++++++++++ 5 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 server/testdata/tools/xlam.gotmpl create mode 100644 server/testdata/tools/xlam.out diff --git a/server/model.go b/server/model.go index a084dd8c..bf38c415 100644 --- a/server/model.go +++ b/server/model.go @@ -344,6 +344,10 @@ func (m *Model) parseToolCalls(s string) ([]api.ToolCall, bool) { } } + if name == "" || arguments == "" { + return nil, false + } + var objs []map[string]any for offset := 0; offset < len(s); { var obj map[string]any @@ -361,23 +365,40 @@ func (m *Model) parseToolCalls(s string) ([]api.ToolCall, bool) { return nil, false } else { offset += int(decoder.InputOffset()) - objs = append(objs, obj) + + // collect all nested objects + var collect func(any) []map[string]any + collect = func(obj any) (all []map[string]any) { + switch o := obj.(type) { + case map[string]any: + all = append(all, o) + for _, v := range o { + all = append(all, collect(v)...) + } + case []any: + for _, v := range o { + all = append(all, collect(v)...) + } + } + + return all + } + objs = append(objs, collect(obj)...) } } var toolCalls []api.ToolCall for _, kv := range objs { - var call api.ToolCall - for k, v := range kv { - switch k { - case name: - call.Function.Name = v.(string) - case arguments: - call.Function.Arguments = v.(map[string]any) - } + n, nok := kv[name].(string) + a, aok := kv[arguments].(map[string]any) + if nok && aok { + toolCalls = append(toolCalls, api.ToolCall{ + Function: api.ToolCallFunction{ + Name: n, + Arguments: a, + }, + }) } - - toolCalls = append(toolCalls, call) } return toolCalls, len(toolCalls) > 0 diff --git a/server/model_test.go b/server/model_test.go index 7c826b06..5829adfc 100644 --- a/server/model_test.go +++ b/server/model_test.go @@ -166,6 +166,7 @@ The temperature in San Francisco, CA is 70°F and in Toronto, Canada is 20°C.`, {"name": "get_current_weather", "arguments": {"format":"fahrenheit","location":"San Francisco, CA"}} {"name": "get_current_weather", "arguments": {"format":"celsius","location":"Toronto, Canada"}} `, true}, + {"xlam", `{"tool_calls": [{"name": "get_current_weather", "arguments": {"format":"fahrenheit","location":"San Francisco, CA"}},{"name": "get_current_weather", "arguments": {"format":"celsius","location":"Toronto, Canada"}}]}`, true}, } var tools []api.Tool diff --git a/server/routes.go b/server/routes.go index 85db7924..0d7ca003 100644 --- a/server/routes.go +++ b/server/routes.go @@ -611,10 +611,10 @@ func (s *Server) CreateModelHandler(c *gin.Context) { quantization := cmp.Or(r.Quantize, r.Quantization) if err := CreateModel(ctx, name, filepath.Dir(r.Path), strings.ToUpper(quantization), f, fn); err != nil { if errors.Is(err, errBadTemplate) { - ch <- gin.H{"error": err.Error(), "status": http.StatusBadRequest} + ch <- gin.H{"error": err.Error(), "status": http.StatusBadRequest} } ch <- gin.H{"error": err.Error()} - } + } }() if r.Stream != nil && !*r.Stream { diff --git a/server/testdata/tools/xlam.gotmpl b/server/testdata/tools/xlam.gotmpl new file mode 100644 index 00000000..51513d69 --- /dev/null +++ b/server/testdata/tools/xlam.gotmpl @@ -0,0 +1,45 @@ +{{- if .System }}{{ .System }} +{{ end }} +{{- range $i, $_ := .Messages }} +{{- if eq .Role "user" }}### Instruction: +{{- if and $.Tools (le (len (slice $.Messages $i)) 2) }} +[BEGIN OF TASK INSTRUCTION] +You are an expert in composing functions. You are given a question and a set of possible functions. +Based on the question, you will need to make one or more function/tool calls to achieve the purpose. +If none of the functions can be used, point it out and refuse to answer. +If the given question lacks the parameters required by the function, also point it out. +[END OF TASK INSTRUCTION] + +[BEGIN OF AVAILABLE TOOLS] +{{ $.Tools }} +[END OF AVAILABLE TOOLS] + +[BEGIN OF FORMAT INSTRUCTION] +The output MUST strictly adhere to the following JSON format, and NO other text MUST be included. +The example format is as follows. Please make sure the parameter type is correct. If no function call is needed, please make tool_calls an empty list '[]'. +``` +{ + "tool_calls": [ + {"name": "func_name1", "arguments": {"argument1": "value1", "argument2": "value2"}}, + ... (more tool calls as required) + ] +} +``` +[END OF FORMAT INSTRUCTION] + +[BEGIN OF QUERY] +{{ .Content }} +[END OF QUERY] + + +{{ else }} +{{ .Content }} +{{ end }} +{{- else if .ToolCalls }}### Response: +{"tool_calls": [{{ range .ToolCalls }}{"name": "{{ .Function.Name }}", "arguments": {{ .Function.Arguments }}}{{ end }}]} +<|EOT|> +{{ else if eq .Role "assistant" }}### Response: +{{ .Content }} +<|EOT|> +{{ end }} +{{- end }}### Response: \ No newline at end of file diff --git a/server/testdata/tools/xlam.out b/server/testdata/tools/xlam.out new file mode 100644 index 00000000..a4a9952f --- /dev/null +++ b/server/testdata/tools/xlam.out @@ -0,0 +1,40 @@ +You are a knowledgable assistant. You can answer questions and perform tasks. +### Instruction: +What's the weather like today in Paris? +### Response: +{"tool_calls": [{"name": "get_current_weather", "arguments": {"format":"celsius","location":"Paris, France"}}]} +<|EOT|> +### Response: +The current temperature in Paris, France is 22 degrees Celsius. +<|EOT|> +### Instruction: +[BEGIN OF TASK INSTRUCTION] +You are an expert in composing functions. You are given a question and a set of possible functions. +Based on the question, you will need to make one or more function/tool calls to achieve the purpose. +If none of the functions can be used, point it out and refuse to answer. +If the given question lacks the parameters required by the function, also point it out. +[END OF TASK INSTRUCTION] + +[BEGIN OF AVAILABLE TOOLS] +[{"type":"function","function":{"name":"get_current_weather","description":"Get the current weather","parameters":{"type":"object","required":["location","format"],"properties":{"format":{"type":"string","description":"The temperature unit to use. Infer this from the users location.","enum":["celsius","fahrenheit"]},"location":{"type":"string","description":"The city and state, e.g. San Francisco, CA"}}}}}] +[END OF AVAILABLE TOOLS] + +[BEGIN OF FORMAT INSTRUCTION] +The output MUST strictly adhere to the following JSON format, and NO other text MUST be included. +The example format is as follows. Please make sure the parameter type is correct. If no function call is needed, please make tool_calls an empty list '[]'. +``` +{ + "tool_calls": [ + {"name": "func_name1", "arguments": {"argument1": "value1", "argument2": "value2"}}, + ... (more tool calls as required) + ] +} +``` +[END OF FORMAT INSTRUCTION] + +[BEGIN OF QUERY] +What's the weather like today in San Francisco and Toronto? +[END OF QUERY] + + +### Response: \ No newline at end of file