package claude import ( "encoding/json" "fmt" "strings" ) func normalizeClaudeMessages(messages []any) []any { out := make([]any, 0, len(messages)) for _, m := range messages { msg, ok := m.(map[string]any) if !ok { continue } role := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", msg["role"]))) switch content := msg["content"].(type) { case []any: textParts := make([]string, 0, len(content)) flushText := func() { if len(textParts) == 0 { return } out = append(out, map[string]any{ "role": role, "content": strings.Join(textParts, "\n"), }) textParts = textParts[:0] } for _, block := range content { b, ok := block.(map[string]any) if !ok { continue } typeStr := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", b["type"]))) switch typeStr { case "text": if t, ok := b["text"].(string); ok { textParts = append(textParts, t) } case "tool_use": flushText() if toolMsg := normalizeClaudeToolUseToAssistant(b); toolMsg != nil { out = append(out, toolMsg) } case "tool_result": flushText() if toolMsg := normalizeClaudeToolResultToToolMessage(b); toolMsg != nil { out = append(out, toolMsg) } default: if raw := strings.TrimSpace(formatClaudeBlockRaw(b)); raw != "" { textParts = append(textParts, raw) } } } flushText() default: copied := cloneMap(msg) out = append(out, copied) } } return out } func buildClaudeToolPrompt(tools []any) string { parts := []string{"You are Claude, a helpful AI assistant. You have access to these tools:"} for _, t := range tools { m, ok := t.(map[string]any) if !ok { continue } name, desc, schemaObj := extractClaudeToolMeta(m) schema, _ := json.Marshal(schemaObj) parts = append(parts, fmt.Sprintf("Tool: %s\nDescription: %s\nParameters: %s", name, desc, schema)) } parts = append(parts, "When you need a tool, respond with Claude-native tool use (tool_use) using the provided tool schema. Do not print tool-call JSON in text.", "Tool roundtrip context is included directly in the conversation messages (assistant tool_use/tool_calls and tool results).", "After receiving a valid tool result, continue with final answer instead of repeating the same call unless required fields are still missing.", ) return strings.Join(parts, "\n\n") } func formatClaudeToolResultForPrompt(block map[string]any) string { if block == nil { return "" } payload := map[string]any{ "type": "tool_result", "content": block["content"], } if toolCallID := strings.TrimSpace(fmt.Sprintf("%v", block["tool_use_id"])); toolCallID != "" { payload["tool_call_id"] = toolCallID } else if toolCallID := strings.TrimSpace(fmt.Sprintf("%v", block["tool_call_id"])); toolCallID != "" { payload["tool_call_id"] = toolCallID } if name := strings.TrimSpace(fmt.Sprintf("%v", block["name"])); name != "" { payload["name"] = name } b, err := json.Marshal(payload) if err != nil { return strings.TrimSpace(fmt.Sprintf("%v", payload)) } return string(b) } func normalizeClaudeToolUseToAssistant(block map[string]any) map[string]any { if block == nil { return nil } name := strings.TrimSpace(fmt.Sprintf("%v", block["name"])) if name == "" { return nil } callID := strings.TrimSpace(fmt.Sprintf("%v", block["id"])) if callID == "" { callID = strings.TrimSpace(fmt.Sprintf("%v", block["tool_use_id"])) } if callID == "" { callID = "call_claude" } arguments := block["input"] if arguments == nil { arguments = map[string]any{} } argsJSON, err := json.Marshal(arguments) if err != nil || len(argsJSON) == 0 { argsJSON = []byte("{}") } return map[string]any{ "role": "assistant", "tool_calls": []any{ map[string]any{ "id": callID, "type": "function", "function": map[string]any{ "name": name, "arguments": string(argsJSON), }, }, }, } } func normalizeClaudeToolResultToToolMessage(block map[string]any) map[string]any { if block == nil { return nil } toolCallID := strings.TrimSpace(fmt.Sprintf("%v", block["tool_use_id"])) if toolCallID == "" { toolCallID = strings.TrimSpace(fmt.Sprintf("%v", block["tool_call_id"])) } if toolCallID == "" { toolCallID = "call_claude" } out := map[string]any{ "role": "tool", "tool_call_id": toolCallID, "content": block["content"], } if name := strings.TrimSpace(fmt.Sprintf("%v", block["name"])); name != "" { out["name"] = name } return out } func formatClaudeBlockRaw(block map[string]any) string { if block == nil { return "" } b, err := json.Marshal(block) if err != nil { return strings.TrimSpace(fmt.Sprintf("%v", block)) } return string(b) } func hasSystemMessage(messages []any) bool { for _, m := range messages { msg, ok := m.(map[string]any) if ok && msg["role"] == "system" { return true } } return false } func extractClaudeToolNames(tools []any) []string { out := make([]string, 0, len(tools)) for _, t := range tools { m, ok := t.(map[string]any) if !ok { continue } name, _, _ := extractClaudeToolMeta(m) if name != "" { out = append(out, name) } } return out } func extractClaudeToolMeta(m map[string]any) (string, string, any) { name, _ := m["name"].(string) desc, _ := m["description"].(string) schemaObj := m["input_schema"] if schemaObj == nil { schemaObj = m["parameters"] } if fn, ok := m["function"].(map[string]any); ok { if strings.TrimSpace(name) == "" { name, _ = fn["name"].(string) } if strings.TrimSpace(desc) == "" { desc, _ = fn["description"].(string) } if schemaObj == nil { if v, ok := fn["input_schema"]; ok { schemaObj = v } } if schemaObj == nil { if v, ok := fn["parameters"]; ok { schemaObj = v } } } return strings.TrimSpace(name), strings.TrimSpace(desc), schemaObj } func toMessageMaps(v any) []map[string]any { arr, ok := v.([]any) if !ok { return nil } out := make([]map[string]any, 0, len(arr)) for _, item := range arr { if m, ok := item.(map[string]any); ok { out = append(out, m) } } return out } func extractMessageContent(v any) string { switch x := v.(type) { case string: return x case []any: parts := make([]string, 0, len(x)) for _, it := range x { parts = append(parts, fmt.Sprintf("%v", it)) } return strings.Join(parts, "\n") default: return fmt.Sprintf("%v", x) } } func cloneMap(in map[string]any) map[string]any { out := make(map[string]any, len(in)) for k, v := range in { out[k] = v } return out }