fix(openai): tool arguments를 schema 기준으로 정규화한다
This commit is contained in:
parent
1c03ee7c3c
commit
8a17e3a057
3 changed files with 377 additions and 39 deletions
|
|
@ -169,9 +169,11 @@ Workspace-bound route는 workspace가 없거나 상대 경로이면 OpenAI-compa
|
|||
단, backend가 assistant content에 Cline-style 텍스트 tool call 블록을 출력하고 그 function name이 요청의 `tools[].function.name`에 포함되어 있으면 Edge는 해당 블록을 OpenAI-compatible `message.tool_calls` 또는 stream `delta.tool_calls`로 변환하고 `finish_reason: "tool_calls"`를 반환한다.
|
||||
지원하는 텍스트 블록의 최소 형태는 `<tool_call><function=<name>><parameter=<key>>JSON-or-text</parameter></function></tool_call>` 또는 `{{function_name(key=Python/JSON-like-literal)}}`이다.
|
||||
템플릿 호출 형태의 인자는 single-quoted string, `True`/`False`/`None`, 배열, 객체를 JSON `arguments` 문자열로 정규화한다.
|
||||
`commands` 배열의 `{command: "git status", description, runInTerminal}`처럼 shell command 문자열을 command 객체에 담은 형태는 Cline의 direct-exec 경로로 오해되지 않도록 최신 Cline의 structured argv 형태인 `commands: [{"command":"git","args":["status"]}]`로 정규화한다.
|
||||
UI 설명용 `description`이나 실행 위치 힌트용 `runInTerminal`은 command 객체와 최상위 args 어디에도 출력하지 않는다.
|
||||
단, `cd`, `command -v`, `&&`, pipe, redirect, quote 등 shell 해석이 필요한 명령은 `commands: ["cd /work && git status"]` 같은 shell string으로 유지한다.
|
||||
텍스트 tool call을 구조화할 때 Edge는 요청의 `tools[].function.parameters` schema를 기준으로 arguments를 정규화한다.
|
||||
예를 들어 tool schema가 `commands: string[]`만 허용하면 command 객체 입력도 shell string 배열로 접고, `commands: {command,args}[]`를 허용하면 shell 문법이 없는 명령을 structured argv로 만든다.
|
||||
tool schema가 top-level `command`/`cmd` string만 허용하면 `commands` 배열의 첫 명령을 해당 field로 접는다.
|
||||
schema에 없는 UI 설명용 `description`이나 실행 위치 힌트용 `runInTerminal`은 command 객체와 최상위 args에서 제거한다.
|
||||
단, `cd`, `command -v`, `&&`, pipe, redirect, quote 등 shell 해석이 필요한 명령은 schema가 허용할 때 `commands: ["cd /work && git status"]` 같은 shell string으로 유지한다.
|
||||
`{command:"cd", args:["/work","&&","git","status"]}`처럼 shell builtin이나 shell operator가 structured argv에 섞인 형태도 direct-exec로 실행되지 않도록 shell string으로 정규화한다.
|
||||
호환성을 위해 `tool_choice`가 생략되었거나 `auto`/강제 tool 선택 형태로 들어오면 내부 실행 입력에서는 `tool_choice: "none"`으로 낮춰 백엔드의 auto tool-calling 요구 조건에 의해 요청 전체가 실패하지 않게 한다.
|
||||
`parallel_tool_calls`와 `stream_options`는 클라이언트 호환성을 위해 수신하지만 현재 Edge 실행 의미에는 반영하지 않는다.
|
||||
|
|
|
|||
|
|
@ -511,6 +511,81 @@ func TestSynthesizesTemplateToolCallConvertsStructuredShellOperatorsToString(t *
|
|||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesTemplateToolCallFollowsStringArrayCommandSchema(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'git status', 'description': 'status'}])}}"
|
||||
tools := []any{map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "run_commands",
|
||||
"parameters": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"commands": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
|
||||
|
||||
if len(toolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(toolCalls))
|
||||
}
|
||||
call := toolCalls[0].(map[string]any)
|
||||
fn := call["function"].(map[string]any)
|
||||
var args map[string][]string
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
if len(args["commands"]) != 1 || args["commands"][0] != "git status" {
|
||||
t.Fatalf("commands args: %+v", args["commands"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesTemplateToolCallFollowsSingleCommandSchema(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{bash(commands=[{'command': 'cd', 'args': ['/config/workspace/iop', '&&', 'git', 'status']}], runInTerminal=True)}}"
|
||||
tools := []any{map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "bash",
|
||||
"parameters": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"command": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
|
||||
|
||||
if len(toolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(toolCalls))
|
||||
}
|
||||
call := toolCalls[0].(map[string]any)
|
||||
fn := call["function"].(map[string]any)
|
||||
var args map[string]string
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
if args["command"] != "cd /config/workspace/iop && git status" {
|
||||
t.Fatalf("command arg: %+v", args)
|
||||
}
|
||||
if _, ok := args["commands"]; ok {
|
||||
t.Fatalf("commands should not be emitted: %+v", args)
|
||||
}
|
||||
if _, ok := args["runInTerminal"]; ok {
|
||||
t.Fatalf("runInTerminal should not be emitted: %+v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesShellCommandStringWithoutExtraExecutionHints(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{run_commands(commands=['command -v git && echo \"found\" || echo \"not found\"'])}}"
|
||||
tools := []any{map[string]any{
|
||||
|
|
|
|||
|
|
@ -196,12 +196,17 @@ type mustacheTextToolCallBlock struct {
|
|||
args string
|
||||
}
|
||||
|
||||
type textToolSpec struct {
|
||||
name string
|
||||
parameters map[string]any
|
||||
}
|
||||
|
||||
func synthesizeToolCallsFromText(content string, tools []any, runID string) (string, []any) {
|
||||
allowed := requestedToolNames(tools)
|
||||
if len(allowed) == 0 {
|
||||
specs := requestedToolSpecs(tools)
|
||||
if len(specs) == 0 {
|
||||
return content, nil
|
||||
}
|
||||
matches := collectTextToolCallMatches(content, allowed)
|
||||
matches := collectTextToolCallMatches(content, specs)
|
||||
if len(matches) == 0 {
|
||||
return content, nil
|
||||
}
|
||||
|
|
@ -231,10 +236,10 @@ func synthesizeToolCallsFromText(content string, tools []any, runID string) (str
|
|||
return strings.TrimSpace(cleaned.String()), toolCalls
|
||||
}
|
||||
|
||||
func collectTextToolCallMatches(content string, allowed map[string]struct{}) []textToolCallMatch {
|
||||
func collectTextToolCallMatches(content string, specs map[string]textToolSpec) []textToolCallMatch {
|
||||
var matches []textToolCallMatch
|
||||
matches = append(matches, collectXMLTextToolCallMatches(content, allowed)...)
|
||||
matches = append(matches, collectMustacheTextToolCallMatches(content, allowed)...)
|
||||
matches = append(matches, collectXMLTextToolCallMatches(content, specs)...)
|
||||
matches = append(matches, collectMustacheTextToolCallMatches(content, specs)...)
|
||||
sort.SliceStable(matches, func(i, j int) bool {
|
||||
if matches[i].start == matches[j].start {
|
||||
return matches[i].end > matches[j].end
|
||||
|
|
@ -244,7 +249,7 @@ func collectTextToolCallMatches(content string, allowed map[string]struct{}) []t
|
|||
return matches
|
||||
}
|
||||
|
||||
func collectXMLTextToolCallMatches(content string, allowed map[string]struct{}) []textToolCallMatch {
|
||||
func collectXMLTextToolCallMatches(content string, specs map[string]textToolSpec) []textToolCallMatch {
|
||||
if !strings.Contains(strings.ToLower(content), textToolCallOpeningHint) {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -255,7 +260,7 @@ func collectXMLTextToolCallMatches(content string, allowed map[string]struct{})
|
|||
out := make([]textToolCallMatch, 0, len(rawMatches))
|
||||
for _, rawMatch := range rawMatches {
|
||||
block := content[rawMatch[0]:rawMatch[1]]
|
||||
name, arguments, ok := parseTextToolCallBlock(block, allowed)
|
||||
name, arguments, ok := parseTextToolCallBlock(block, specs)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
|
@ -269,7 +274,7 @@ func collectXMLTextToolCallMatches(content string, allowed map[string]struct{})
|
|||
return out
|
||||
}
|
||||
|
||||
func collectMustacheTextToolCallMatches(content string, allowed map[string]struct{}) []textToolCallMatch {
|
||||
func collectMustacheTextToolCallMatches(content string, specs map[string]textToolSpec) []textToolCallMatch {
|
||||
if !strings.Contains(content, textMustacheToolCallOpenHint) {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -279,14 +284,15 @@ func collectMustacheTextToolCallMatches(content string, allowed map[string]struc
|
|||
}
|
||||
out := make([]textToolCallMatch, 0, len(blocks))
|
||||
for _, block := range blocks {
|
||||
if _, ok := allowed[block.name]; !ok {
|
||||
spec, ok := specs[block.name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
args, ok := parseMustacheToolCallArguments(block.args)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
args = normalizeTextToolCallArguments(block.name, args)
|
||||
args = normalizeTextToolCallArguments(block.name, args, spec)
|
||||
encoded, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
continue
|
||||
|
|
@ -450,13 +456,42 @@ func requestedToolNames(tools []any) map[string]struct{} {
|
|||
return names
|
||||
}
|
||||
|
||||
func parseTextToolCallBlock(block string, allowed map[string]struct{}) (string, string, bool) {
|
||||
func requestedToolSpecs(tools []any) map[string]textToolSpec {
|
||||
specs := make(map[string]textToolSpec, len(tools))
|
||||
for _, tool := range tools {
|
||||
m, ok := tool.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if name, ok := m["name"].(string); ok && strings.TrimSpace(name) != "" {
|
||||
name = strings.TrimSpace(name)
|
||||
specs[name] = textToolSpec{name: name, parameters: mapValue(m["parameters"])}
|
||||
}
|
||||
fn, ok := m["function"].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if name, ok := fn["name"].(string); ok && strings.TrimSpace(name) != "" {
|
||||
name = strings.TrimSpace(name)
|
||||
specs[name] = textToolSpec{name: name, parameters: mapValue(fn["parameters"])}
|
||||
}
|
||||
}
|
||||
return specs
|
||||
}
|
||||
|
||||
func mapValue(value any) map[string]any {
|
||||
m, _ := value.(map[string]any)
|
||||
return m
|
||||
}
|
||||
|
||||
func parseTextToolCallBlock(block string, specs map[string]textToolSpec) (string, string, bool) {
|
||||
match := textToolFunctionRE.FindStringSubmatchIndex(block)
|
||||
if match == nil {
|
||||
return "", "", false
|
||||
}
|
||||
name := strings.TrimSpace(block[match[2]:match[3]])
|
||||
if _, ok := allowed[name]; !ok {
|
||||
spec, ok := specs[name]
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
body := block[match[4]:match[5]]
|
||||
|
|
@ -468,7 +503,7 @@ func parseTextToolCallBlock(block string, allowed map[string]struct{}) (string,
|
|||
}
|
||||
args[key] = parseTextToolParameterValue(body[param[4]:param[5]])
|
||||
}
|
||||
args = normalizeTextToolCallArguments(name, args)
|
||||
args = normalizeTextToolCallArguments(name, args, spec)
|
||||
encoded, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
|
|
@ -488,11 +523,11 @@ func parseTextToolParameterValue(raw string) any {
|
|||
return trimmed
|
||||
}
|
||||
|
||||
func normalizeTextToolCallArguments(_ string, args map[string]any) map[string]any {
|
||||
func normalizeTextToolCallArguments(_ string, args map[string]any, spec textToolSpec) map[string]any {
|
||||
topProps := schemaObjectProperties(spec.parameters)
|
||||
commandsSchema := topProps["commands"]
|
||||
commandField := schemaSingleCommandField(topProps)
|
||||
commands, ok := args["commands"].([]any)
|
||||
if !ok {
|
||||
return args
|
||||
}
|
||||
var out map[string]any
|
||||
ensureOut := func() map[string]any {
|
||||
if out != nil {
|
||||
|
|
@ -500,24 +535,46 @@ func normalizeTextToolCallArguments(_ string, args map[string]any) map[string]an
|
|||
}
|
||||
out = make(map[string]any, len(args))
|
||||
for key, value := range args {
|
||||
switch key {
|
||||
case "description", "runInTerminal":
|
||||
if shouldStripToolArgumentKey(key, topProps) {
|
||||
continue
|
||||
default:
|
||||
out[key] = value
|
||||
}
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
if _, ok := args["description"]; ok {
|
||||
ensureOut()
|
||||
for key := range args {
|
||||
if shouldStripToolArgumentKey(key, topProps) {
|
||||
ensureOut()
|
||||
break
|
||||
}
|
||||
}
|
||||
if _, ok := args["runInTerminal"]; ok {
|
||||
ensureOut()
|
||||
if !ok {
|
||||
if out != nil {
|
||||
return out
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
if commandField != "" && !schemaHasProperty(topProps, "commands") {
|
||||
if rendered, ok := firstCommandAsShellString(commands); ok {
|
||||
collapsed := ensureOut()
|
||||
delete(collapsed, "commands")
|
||||
collapsed[commandField] = rendered
|
||||
return collapsed
|
||||
}
|
||||
}
|
||||
|
||||
itemSchema := schemaArrayItemSchema(commandsSchema)
|
||||
itemAllowsString := schemaAllowsType(itemSchema, "string")
|
||||
itemAllowsObject := schemaAllowsType(itemSchema, "object") || len(schemaObjectProperties(itemSchema)) > 0
|
||||
itemObjectProps := schemaObjectProperties(itemSchema)
|
||||
var normalized []any
|
||||
for i, command := range commands {
|
||||
cleaned, changed := normalizeCommandArgument(command)
|
||||
cleaned, changed := normalizeCommandArgument(command, commandArgSchema{
|
||||
allowsString: itemAllowsString,
|
||||
allowsObject: itemAllowsObject,
|
||||
objectProps: itemObjectProps,
|
||||
})
|
||||
if !changed {
|
||||
if normalized != nil {
|
||||
normalized[i] = command
|
||||
|
|
@ -540,7 +597,25 @@ func normalizeTextToolCallArguments(_ string, args map[string]any) map[string]an
|
|||
return out
|
||||
}
|
||||
|
||||
func normalizeCommandArgument(command any) (any, bool) {
|
||||
type commandArgSchema struct {
|
||||
allowsString bool
|
||||
allowsObject bool
|
||||
objectProps map[string]any
|
||||
}
|
||||
|
||||
func normalizeCommandArgument(command any, schema commandArgSchema) (any, bool) {
|
||||
if text, ok := command.(string); ok {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return command, false
|
||||
}
|
||||
if schema.allowsObject && !schema.allowsString && !commandStringNeedsShell(text) {
|
||||
if structured, ok := structuredCommandFromShellWords(text); ok {
|
||||
return structured, true
|
||||
}
|
||||
}
|
||||
return text, text != command
|
||||
}
|
||||
m, ok := command.(map[string]any)
|
||||
if !ok {
|
||||
return command, false
|
||||
|
|
@ -552,22 +627,32 @@ func normalizeCommandArgument(command any) (any, bool) {
|
|||
if args, ok := stringSliceArgument(m["args"]); ok {
|
||||
commandText = strings.TrimSpace(commandText)
|
||||
if commandExecutableNeedsShell(commandText) || commandArgsNeedShell(args) {
|
||||
return renderShellCommand(commandText, args), true
|
||||
if schema.allowsString || !schema.allowsObject {
|
||||
return renderShellCommand(commandText, args), true
|
||||
}
|
||||
return cleanedStructuredCommand(commandText, args, schema.objectProps), true
|
||||
}
|
||||
cleaned := map[string]any{
|
||||
"command": commandText,
|
||||
if schema.allowsObject || !schema.allowsString {
|
||||
cleaned := cleanedStructuredCommand(commandText, args, schema.objectProps)
|
||||
return cleaned, !commandStructuredArgsAlreadyClean(m, args) || commandObjectHasStrippedKeys(m, schema.objectProps)
|
||||
}
|
||||
if len(args) > 0 {
|
||||
cleaned["args"] = args
|
||||
}
|
||||
return cleaned, !commandStructuredArgsAlreadyClean(m, args)
|
||||
return renderShellCommand(commandText, args), true
|
||||
}
|
||||
commandText = strings.TrimSpace(commandText)
|
||||
if commandText != "" {
|
||||
if commandStringNeedsShell(commandText) {
|
||||
return commandText, true
|
||||
}
|
||||
if structured, ok := structuredCommandFromShellWords(commandText); ok {
|
||||
if (schema.allowsObject || !schema.allowsString) && !commandStringNeedsShell(commandText) {
|
||||
structured, ok := structuredCommandFromShellWords(commandText)
|
||||
if ok {
|
||||
if len(schema.objectProps) > 0 {
|
||||
structured = filterMapToSchemaProperties(structured, schema.objectProps)
|
||||
}
|
||||
return structured, true
|
||||
}
|
||||
}
|
||||
if structured, ok := structuredCommandFromShellWords(commandText); ok && schema.allowsObject && !schema.allowsString {
|
||||
return structured, true
|
||||
}
|
||||
return commandText, true
|
||||
|
|
@ -575,6 +660,89 @@ func normalizeCommandArgument(command any) (any, bool) {
|
|||
return command, false
|
||||
}
|
||||
|
||||
func shouldStripToolArgumentKey(key string, props map[string]any) bool {
|
||||
if len(props) > 0 {
|
||||
return !schemaHasProperty(props, key)
|
||||
}
|
||||
return key == "description" || key == "runInTerminal"
|
||||
}
|
||||
|
||||
func schemaHasProperty(props map[string]any, key string) bool {
|
||||
_, ok := props[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func schemaSingleCommandField(props map[string]any) string {
|
||||
for _, key := range []string{"command", "cmd"} {
|
||||
if schemaAllowsType(props[key], "string") {
|
||||
return key
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstCommandAsShellString(commands []any) (string, bool) {
|
||||
if len(commands) == 0 {
|
||||
return "", false
|
||||
}
|
||||
return commandAsShellString(commands[0])
|
||||
}
|
||||
|
||||
func commandAsShellString(command any) (string, bool) {
|
||||
switch v := command.(type) {
|
||||
case string:
|
||||
text := strings.TrimSpace(v)
|
||||
return text, text != ""
|
||||
case map[string]any:
|
||||
commandText, ok := v["command"].(string)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
args, _ := stringSliceArgument(v["args"])
|
||||
text := renderShellCommand(commandText, args)
|
||||
return text, strings.TrimSpace(text) != ""
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func cleanedStructuredCommand(commandText string, args []string, props map[string]any) map[string]any {
|
||||
cleaned := map[string]any{
|
||||
"command": commandText,
|
||||
}
|
||||
if len(args) > 0 {
|
||||
cleaned["args"] = args
|
||||
}
|
||||
if len(props) > 0 {
|
||||
return filterMapToSchemaProperties(cleaned, props)
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func filterMapToSchemaProperties(m map[string]any, props map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(m))
|
||||
for key, value := range m {
|
||||
if schemaHasProperty(props, key) {
|
||||
out[key] = value
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func commandObjectHasStrippedKeys(m map[string]any, props map[string]any) bool {
|
||||
for key := range m {
|
||||
switch key {
|
||||
case "command", "args":
|
||||
continue
|
||||
default:
|
||||
if len(props) == 0 || !schemaHasProperty(props, key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func stringSliceArgument(value any) ([]string, bool) {
|
||||
if value == nil {
|
||||
return nil, false
|
||||
|
|
@ -702,6 +870,99 @@ func firstShellWord(s string) string {
|
|||
return s
|
||||
}
|
||||
|
||||
func schemaObjectProperties(schema any) map[string]any {
|
||||
m, ok := schema.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
props, _ := m["properties"].(map[string]any)
|
||||
if len(props) > 0 {
|
||||
return props
|
||||
}
|
||||
for _, key := range []string{"oneOf", "anyOf", "allOf"} {
|
||||
for _, branch := range schemaBranches(m[key]) {
|
||||
if branchProps := schemaObjectProperties(branch); len(branchProps) > 0 {
|
||||
if props == nil {
|
||||
props = map[string]any{}
|
||||
}
|
||||
for propKey, propValue := range branchProps {
|
||||
props[propKey] = propValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
func schemaArrayItemSchema(schema any) any {
|
||||
m, ok := schema.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if items, ok := m["items"]; ok {
|
||||
return items
|
||||
}
|
||||
for _, key := range []string{"oneOf", "anyOf", "allOf"} {
|
||||
for _, branch := range schemaBranches(m[key]) {
|
||||
if item := schemaArrayItemSchema(branch); item != nil {
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func schemaAllowsType(schema any, want string) bool {
|
||||
if schema == nil {
|
||||
return false
|
||||
}
|
||||
m, ok := schema.(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
switch t := m["type"].(type) {
|
||||
case string:
|
||||
if t == want {
|
||||
return true
|
||||
}
|
||||
case []any:
|
||||
for _, item := range t {
|
||||
if item == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if want == "object" && len(schemaObjectProperties(schema)) > 0 {
|
||||
return true
|
||||
}
|
||||
if want == "array" && schemaArrayItemSchema(schema) != nil {
|
||||
return true
|
||||
}
|
||||
for _, key := range []string{"oneOf", "anyOf"} {
|
||||
for _, branch := range schemaBranches(m[key]) {
|
||||
if schemaAllowsType(branch, want) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if branches := schemaBranches(m["allOf"]); len(branches) > 0 {
|
||||
for _, branch := range branches {
|
||||
if schemaAllowsType(branch, want) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func schemaBranches(value any) []any {
|
||||
raw, ok := value.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func parseMustacheToolCallArguments(raw string) (map[string]any, bool) {
|
||||
if args, ok := parseMustacheToolCallArgumentsStrict(raw); ok {
|
||||
return args, true
|
||||
|
|
|
|||
Loading…
Reference in a new issue