feat: soporte nativo Anthropic en el agente (tool use format)
Agrega callAnthropicAI() que convierte mensajes y herramientas al formato nativo de Anthropic Messages API (tool_use / tool_result), con conversión de vuelta al formato interno OpenAI para mantener el historial unificado. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
5c81939bbf
commit
4f309ad9d5
@@ -675,9 +675,58 @@ func coolifyAppAction(configIDInt int, uuid, action string) (interface{}, error)
|
||||
return coolifyCall("GET", "/applications/"+uuid+"/"+action, nil, cfg)
|
||||
}
|
||||
|
||||
// ─── Tipos Anthropic ─────────────────────────────────────────────────────────
|
||||
|
||||
type anthropicTool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputSchema agentToolParam `json:"input_schema"`
|
||||
}
|
||||
|
||||
type anthropicContentBlock struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Input json.RawMessage `json:"input,omitempty"`
|
||||
ToolUseID string `json:"tool_use_id,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
type anthropicMsg struct {
|
||||
Role string `json:"role"`
|
||||
Content interface{} `json:"content"` // string o []anthropicContentBlock
|
||||
}
|
||||
|
||||
type anthropicReq struct {
|
||||
Model string `json:"model"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
System string `json:"system,omitempty"`
|
||||
Messages []anthropicMsg `json:"messages"`
|
||||
Tools []anthropicTool `json:"tools,omitempty"`
|
||||
}
|
||||
|
||||
type anthropicResp struct {
|
||||
Content []anthropicContentBlock `json:"content"`
|
||||
StopReason string `json:"stop_reason"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// ─── Llamada al AI con function calling ──────────────────────────────────────
|
||||
|
||||
// callAI despacha al provider correcto (Anthropic o OpenAI-compatible).
|
||||
func callAI(ai *models.AiConfig, messages []agentMessage, tools []agentTool) (*agentMessage, error) {
|
||||
if strings.ToLower(ai.Provider) == "anthropic" {
|
||||
return callAnthropicAI(ai, messages, tools)
|
||||
}
|
||||
return callOpenAICompatibleAI(ai, messages, tools)
|
||||
}
|
||||
|
||||
// callOpenAICompatibleAI usa el formato de OpenAI (también vale para qwen, groq, deepseek, etc.)
|
||||
func callOpenAICompatibleAI(ai *models.AiConfig, messages []agentMessage, tools []agentTool) (*agentMessage, error) {
|
||||
baseURL := ai.BaseURL
|
||||
if baseURL == "" {
|
||||
baseURL = providerDefaultURL(ai.Provider)
|
||||
@@ -722,13 +771,167 @@ func callAI(ai *models.AiConfig, messages []agentMessage, tools []agentTool) (*a
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// callAnthropicAI llama a la API nativa de Anthropic con tool use.
|
||||
func callAnthropicAI(ai *models.AiConfig, messages []agentMessage, tools []agentTool) (*agentMessage, error) {
|
||||
// Convertir herramientas al formato Anthropic
|
||||
anthropicTools := make([]anthropicTool, len(tools))
|
||||
for i, t := range tools {
|
||||
anthropicTools[i] = anthropicTool{
|
||||
Name: t.Function.Name,
|
||||
Description: t.Function.Description,
|
||||
InputSchema: t.Function.Parameters,
|
||||
}
|
||||
}
|
||||
|
||||
// Extraer system prompt y convertir mensajes
|
||||
var systemPrompt string
|
||||
var anthropicMsgs []anthropicMsg
|
||||
|
||||
for _, m := range messages {
|
||||
switch m.Role {
|
||||
case "system":
|
||||
if s, ok := m.Content.(string); ok {
|
||||
systemPrompt = s
|
||||
}
|
||||
|
||||
case "user":
|
||||
content := ""
|
||||
if s, ok := m.Content.(string); ok {
|
||||
content = s
|
||||
}
|
||||
// Si el último mensaje ya es user, agregar tool_result como bloque adicional
|
||||
if len(anthropicMsgs) > 0 && anthropicMsgs[len(anthropicMsgs)-1].Role == "user" {
|
||||
last := anthropicMsgs[len(anthropicMsgs)-1]
|
||||
if blocks, ok := last.Content.([]anthropicContentBlock); ok {
|
||||
anthropicMsgs[len(anthropicMsgs)-1].Content = append(blocks, anthropicContentBlock{
|
||||
Type: "text",
|
||||
Text: content,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
anthropicMsgs = append(anthropicMsgs, anthropicMsg{Role: "user", Content: content})
|
||||
|
||||
case "assistant":
|
||||
if len(m.ToolCalls) > 0 {
|
||||
// Convertir tool_calls a bloques tool_use
|
||||
var blocks []anthropicContentBlock
|
||||
if s, ok := m.Content.(string); ok && s != "" {
|
||||
blocks = append(blocks, anthropicContentBlock{Type: "text", Text: s})
|
||||
}
|
||||
for _, tc := range m.ToolCalls {
|
||||
blocks = append(blocks, anthropicContentBlock{
|
||||
Type: "tool_use",
|
||||
ID: tc.ID,
|
||||
Name: tc.Function.Name,
|
||||
Input: json.RawMessage(tc.Function.Arguments),
|
||||
})
|
||||
}
|
||||
anthropicMsgs = append(anthropicMsgs, anthropicMsg{Role: "assistant", Content: blocks})
|
||||
} else {
|
||||
content := ""
|
||||
if s, ok := m.Content.(string); ok {
|
||||
content = s
|
||||
}
|
||||
anthropicMsgs = append(anthropicMsgs, anthropicMsg{Role: "assistant", Content: content})
|
||||
}
|
||||
|
||||
case "tool":
|
||||
// Los resultados de tool deben ir en un mensaje de usuario con tipo tool_result
|
||||
block := anthropicContentBlock{
|
||||
Type: "tool_result",
|
||||
ToolUseID: m.ToolCallID,
|
||||
Content: fmt.Sprintf("%v", m.Content),
|
||||
}
|
||||
// Agrupar en el último mensaje user si existe, o crear uno nuevo
|
||||
if len(anthropicMsgs) > 0 && anthropicMsgs[len(anthropicMsgs)-1].Role == "user" {
|
||||
last := anthropicMsgs[len(anthropicMsgs)-1]
|
||||
switch c := last.Content.(type) {
|
||||
case []anthropicContentBlock:
|
||||
anthropicMsgs[len(anthropicMsgs)-1].Content = append(c, block)
|
||||
default:
|
||||
anthropicMsgs[len(anthropicMsgs)-1].Content = []anthropicContentBlock{block}
|
||||
}
|
||||
} else {
|
||||
anthropicMsgs = append(anthropicMsgs, anthropicMsg{
|
||||
Role: "user",
|
||||
Content: []anthropicContentBlock{block},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reqBody := anthropicReq{
|
||||
Model: ai.ModelName,
|
||||
MaxTokens: 4096,
|
||||
System: systemPrompt,
|
||||
Messages: anthropicMsgs,
|
||||
Tools: anthropicTools,
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(reqBody)
|
||||
req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("x-api-key", ai.ApiKey)
|
||||
req.Header.Set("anthropic-version", "2023-06-01")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 120 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
|
||||
|
||||
var anthropicRsp anthropicResp
|
||||
if err := json.Unmarshal(raw, &anthropicRsp); err != nil {
|
||||
return nil, fmt.Errorf("respuesta inesperada de Anthropic: %s", string(raw[:min(200, len(raw))]))
|
||||
}
|
||||
if anthropicRsp.Error != nil {
|
||||
return nil, fmt.Errorf("error de Anthropic: %s", anthropicRsp.Error.Message)
|
||||
}
|
||||
|
||||
// Convertir respuesta Anthropic → agentMessage (formato interno OpenAI)
|
||||
result := &agentMessage{Role: "assistant"}
|
||||
var textParts []string
|
||||
var toolCalls []agentToolCall
|
||||
|
||||
for _, block := range anthropicRsp.Content {
|
||||
switch block.Type {
|
||||
case "text":
|
||||
if block.Text != "" {
|
||||
textParts = append(textParts, block.Text)
|
||||
}
|
||||
case "tool_use":
|
||||
inputJSON := "{}"
|
||||
if block.Input != nil {
|
||||
inputJSON = string(block.Input)
|
||||
}
|
||||
toolCalls = append(toolCalls, agentToolCall{
|
||||
ID: block.ID,
|
||||
Type: "function",
|
||||
Function: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}{Name: block.Name, Arguments: inputJSON},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(textParts) > 0 {
|
||||
result.Content = strings.Join(textParts, "\n")
|
||||
}
|
||||
result.ToolCalls = toolCalls
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func providerDefaultURL(provider string) string {
|
||||
switch strings.ToLower(provider) {
|
||||
case "openai":
|
||||
return "https://api.openai.com/v1"
|
||||
case "anthropic":
|
||||
// Anthropic usa un formato diferente; para compatibilidad usar proxy OpenAI-compatible
|
||||
return "https://api.openai.com/v1"
|
||||
case "qwen", "dashscope":
|
||||
return "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
case "groq":
|
||||
|
||||
Reference in New Issue
Block a user