| package toolcall |
|
|
| import ( |
| "encoding/json" |
| "strings" |
| ) |
|
|
| func NormalizeParsedToolCallsForSchemas(calls []ParsedToolCall, toolsRaw any) []ParsedToolCall { |
| if len(calls) == 0 { |
| return calls |
| } |
| schemas := buildToolSchemaIndex(toolsRaw) |
| if len(schemas) == 0 { |
| return calls |
| } |
|
|
| var changedAny bool |
| out := make([]ParsedToolCall, len(calls)) |
| for i, call := range calls { |
| out[i] = call |
| schema, ok := schemas[strings.ToLower(strings.TrimSpace(call.Name))] |
| if !ok || call.Input == nil { |
| continue |
| } |
| normalized, changed := normalizeToolValueWithSchema(call.Input, schema) |
| if !changed { |
| continue |
| } |
| changedAny = true |
| if input, ok := normalized.(map[string]any); ok { |
| out[i].Input = input |
| } |
| } |
| if !changedAny { |
| return calls |
| } |
| return out |
| } |
|
|
| func buildToolSchemaIndex(toolsRaw any) map[string]any { |
| tools, ok := toolsRaw.([]any) |
| if !ok || len(tools) == 0 { |
| return nil |
| } |
| out := make(map[string]any, len(tools)) |
| for _, item := range tools { |
| tool, ok := item.(map[string]any) |
| if !ok { |
| continue |
| } |
| name, _, schema := ExtractToolMeta(tool) |
| if name == "" || schema == nil { |
| continue |
| } |
| out[strings.ToLower(name)] = schema |
| } |
| if len(out) == 0 { |
| return nil |
| } |
| return out |
| } |
|
|
| func ExtractToolMeta(tool map[string]any) (string, string, any) { |
| name := strings.TrimSpace(asStringValue(tool["name"])) |
| desc := strings.TrimSpace(asStringValue(tool["description"])) |
| schema := firstNonNil( |
| tool["parameters"], |
| tool["input_schema"], |
| tool["inputSchema"], |
| tool["schema"], |
| ) |
| if fn, ok := tool["function"].(map[string]any); ok { |
| if name == "" { |
| name = strings.TrimSpace(asStringValue(fn["name"])) |
| } |
| if desc == "" { |
| desc = strings.TrimSpace(asStringValue(fn["description"])) |
| } |
| schema = firstNonNil( |
| schema, |
| fn["parameters"], |
| fn["input_schema"], |
| fn["inputSchema"], |
| fn["schema"], |
| ) |
| } |
| return name, desc, schema |
| } |
|
|
| func normalizeToolValueWithSchema(value any, schema any) (any, bool) { |
| if value == nil || schema == nil { |
| return value, false |
| } |
| schemaMap, ok := schema.(map[string]any) |
| if !ok || len(schemaMap) == 0 { |
| return value, false |
| } |
| if shouldCoerceSchemaToString(schemaMap) { |
| return stringifySchemaValue(value) |
| } |
| if looksLikeObjectSchema(schemaMap) { |
| obj, ok := value.(map[string]any) |
| if !ok || len(obj) == 0 { |
| return value, false |
| } |
| properties, _ := schemaMap["properties"].(map[string]any) |
| additional := schemaMap["additionalProperties"] |
| changed := false |
| out := make(map[string]any, len(obj)) |
| for key, current := range obj { |
| next := current |
| var fieldChanged bool |
| if propSchema, ok := properties[key]; ok { |
| next, fieldChanged = normalizeToolValueWithSchema(current, propSchema) |
| } else if additional != nil { |
| next, fieldChanged = normalizeToolValueWithSchema(current, additional) |
| } |
| out[key] = next |
| changed = changed || fieldChanged |
| } |
| if !changed { |
| return value, false |
| } |
| return out, true |
| } |
| if looksLikeArraySchema(schemaMap) { |
| arr, ok := value.([]any) |
| if !ok || len(arr) == 0 { |
| return value, false |
| } |
| itemsSchema := schemaMap["items"] |
| if itemsSchema == nil { |
| return value, false |
| } |
| changed := false |
| out := make([]any, len(arr)) |
| switch itemSchemas := itemsSchema.(type) { |
| case []any: |
| for i, item := range arr { |
| if i >= len(itemSchemas) { |
| out[i] = item |
| continue |
| } |
| next, itemChanged := normalizeToolValueWithSchema(item, itemSchemas[i]) |
| out[i] = next |
| changed = changed || itemChanged |
| } |
| default: |
| for i, item := range arr { |
| next, itemChanged := normalizeToolValueWithSchema(item, itemsSchema) |
| out[i] = next |
| changed = changed || itemChanged |
| } |
| } |
| if !changed { |
| return value, false |
| } |
| return out, true |
| } |
| return value, false |
| } |
|
|
| func shouldCoerceSchemaToString(schema map[string]any) bool { |
| if schema == nil { |
| return false |
| } |
| if isStringConst(schema["const"]) { |
| return true |
| } |
| if isStringEnum(schema["enum"]) { |
| return true |
| } |
| switch v := schema["type"].(type) { |
| case string: |
| return strings.EqualFold(strings.TrimSpace(v), "string") |
| case []any: |
| return isOnlyStringLikeTypes(v) |
| case []string: |
| items := make([]any, 0, len(v)) |
| for _, item := range v { |
| items = append(items, item) |
| } |
| return isOnlyStringLikeTypes(items) |
| default: |
| return false |
| } |
| } |
|
|
| func looksLikeObjectSchema(schema map[string]any) bool { |
| if schema == nil { |
| return false |
| } |
| if typ, ok := schema["type"].(string); ok && strings.EqualFold(strings.TrimSpace(typ), "object") { |
| return true |
| } |
| if _, ok := schema["properties"].(map[string]any); ok { |
| return true |
| } |
| _, hasAdditional := schema["additionalProperties"] |
| return hasAdditional |
| } |
|
|
| func looksLikeArraySchema(schema map[string]any) bool { |
| if schema == nil { |
| return false |
| } |
| if typ, ok := schema["type"].(string); ok && strings.EqualFold(strings.TrimSpace(typ), "array") { |
| return true |
| } |
| _, hasItems := schema["items"] |
| return hasItems |
| } |
|
|
| func isOnlyStringLikeTypes(values []any) bool { |
| if len(values) == 0 { |
| return false |
| } |
| hasString := false |
| for _, item := range values { |
| typ, ok := item.(string) |
| if !ok { |
| return false |
| } |
| switch strings.ToLower(strings.TrimSpace(typ)) { |
| case "string": |
| hasString = true |
| case "null": |
| continue |
| default: |
| return false |
| } |
| } |
| return hasString |
| } |
|
|
| func isStringConst(v any) bool { |
| _, ok := v.(string) |
| return ok |
| } |
|
|
| func isStringEnum(v any) bool { |
| values, ok := v.([]any) |
| if !ok || len(values) == 0 { |
| return false |
| } |
| for _, item := range values { |
| if _, ok := item.(string); !ok { |
| return false |
| } |
| } |
| return true |
| } |
|
|
| func stringifySchemaValue(value any) (any, bool) { |
| if value == nil { |
| return value, false |
| } |
| if s, ok := value.(string); ok { |
| return s, false |
| } |
| b, err := json.Marshal(value) |
| if err != nil { |
| return value, false |
| } |
| return string(b), true |
| } |
|
|
| func asStringValue(v any) string { |
| if s, ok := v.(string); ok { |
| return s |
| } |
| return "" |
| } |
|
|
| func firstNonNil(values ...any) any { |
| for _, value := range values { |
| if value != nil { |
| return value |
| } |
| } |
| return nil |
| } |
|
|