mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-02 07:25:26 +08:00
283 lines
5.9 KiB
Go
283 lines
5.9 KiB
Go
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
|
|
}
|