feat: leer el contenido real del documento adjunto con Claude
Antes solo se avisaba en texto "llegó un documento" — la IA nunca veía el archivo, por eso siempre tenía que preguntar cliente y monto. Ahora, cuando el proveedor activo es Anthropic, se le adjunta la imagen/PDF real (base64) al mensaje para que Claude lo lea directamente y extraiga cliente, monto y número de factura, llamando a adjuntar_factura sin pedir confirmación salvo que de verdad no pueda determinar cliente o monto. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b292d98cf6
commit
cd0ca6d232
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -100,3 +101,38 @@ func PopFacturaAttachment(chatID int64) (*FacturaAttachment, bool) {
|
||||
}
|
||||
return a, true
|
||||
}
|
||||
|
||||
// PeekFacturaAttachment consulta el archivo pendiente de un chat sin removerlo
|
||||
// (se usa para leer su contenido y pasárselo al modelo antes de que la tool
|
||||
// adjuntar_factura lo consuma de verdad).
|
||||
func PeekFacturaAttachment(chatID int64) (*FacturaAttachment, bool) {
|
||||
facturaAttachmentsMu.Lock()
|
||||
defer facturaAttachmentsMu.Unlock()
|
||||
a, ok := facturaAttachments[chatID]
|
||||
if !ok || time.Since(a.StagedAt) > facturaAttachmentTTL {
|
||||
return nil, false
|
||||
}
|
||||
return a, true
|
||||
}
|
||||
|
||||
// ResolverMimeType usa el mime_type que mandó Telegram si vino, y si no,
|
||||
// adivina por la extensión del archivo (Claude solo acepta ciertos tipos).
|
||||
func ResolverMimeType(mimeType, filename string) string {
|
||||
if mimeType != "" {
|
||||
return mimeType
|
||||
}
|
||||
switch strings.ToLower(filepath.Ext(filename)) {
|
||||
case ".pdf":
|
||||
return "application/pdf"
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
case ".gif":
|
||||
return "image/gif"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -43,6 +44,9 @@ type agentMessage struct {
|
||||
ToolCalls []agentToolCall `json:"tool_calls,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
// Adjunto opcional para que el modelo lea el archivo real (solo Anthropic).
|
||||
AttachmentBase64 string `json:"-"`
|
||||
AttachmentMime string `json:"-"`
|
||||
}
|
||||
|
||||
type agentToolCall struct {
|
||||
@@ -951,13 +955,22 @@ type anthropicTool struct {
|
||||
}
|
||||
|
||||
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 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"`
|
||||
Source *anthropicBlockSource `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
// anthropicBlockSource es el contenido en base64 de una imagen o PDF adjunto,
|
||||
// para que Claude lea el archivo real en vez de solo un aviso en texto.
|
||||
type anthropicBlockSource struct {
|
||||
Type string `json:"type"` // "base64"
|
||||
MediaType string `json:"media_type"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type anthropicMsg struct {
|
||||
@@ -1077,6 +1090,25 @@ func callAnthropicAI(ai *models.AiConfig, messages []agentMessage, tools []agent
|
||||
continue
|
||||
}
|
||||
}
|
||||
if m.AttachmentBase64 != "" {
|
||||
blockType := "image"
|
||||
if m.AttachmentMime == "application/pdf" {
|
||||
blockType = "document"
|
||||
}
|
||||
blocks := []anthropicContentBlock{{
|
||||
Type: blockType,
|
||||
Source: &anthropicBlockSource{
|
||||
Type: "base64",
|
||||
MediaType: m.AttachmentMime,
|
||||
Data: m.AttachmentBase64,
|
||||
},
|
||||
}}
|
||||
if content != "" {
|
||||
blocks = append(blocks, anthropicContentBlock{Type: "text", Text: content})
|
||||
}
|
||||
anthropicMsgs = append(anthropicMsgs, anthropicMsg{Role: "user", Content: blocks})
|
||||
continue
|
||||
}
|
||||
anthropicMsgs = append(anthropicMsgs, anthropicMsg{Role: "user", Content: content})
|
||||
|
||||
case "assistant":
|
||||
@@ -1237,7 +1269,7 @@ COMPORTAMIENTO:
|
||||
- Los valores monetarios son en COP (pesos colombianos)
|
||||
- Si una herramienta falla, explica el error y sugiere alternativas
|
||||
- Para acciones en Coolify, primero usa coolify_instancias para saber qué IDs hay disponibles si el usuario no lo especifica
|
||||
- Cuando el mensaje empiece con "[Documento adjunto recibido: ...]", el usuario acaba de enviar un PDF o foto por Telegram. Identifica el cliente (usa listar_clientes si hace falta) y el monto de la factura a partir de lo que escribió; si falta alguno de los dos, pregúntalo antes de guardar. Cuando tengas cliente y monto, usa la tool adjuntar_factura para guardarlo — no la uses si el mensaje no menciona ningún documento adjunto
|
||||
- Cuando el mensaje empiece con "[Documento adjunto recibido: ...]", el usuario acaba de enviar un PDF o foto por Telegram y puedes leerlo directamente (está adjunto a este mismo mensaje, no es solo un nombre de archivo). Léelo para identificar cliente, monto y número de factura. Usa listar_clientes para encontrar el cliente_id que mejor coincida con el nombre/empresa que aparece en el documento o en el texto del usuario. Si logras identificar cliente y monto (del documento o de lo que escribió el usuario), llama a adjuntar_factura directamente sin pedir confirmación — solo pregunta si de verdad no hay forma de determinar el cliente o el monto. No uses esta tool si el mensaje no menciona ningún documento adjunto
|
||||
|
||||
COMANDOS ESPECIALES (el usuario puede escribirlos):
|
||||
- /reset — olvidar el historial de esta conversación
|
||||
@@ -1305,7 +1337,21 @@ Comandos: /reset /instancias /ayuda`, nil
|
||||
}
|
||||
messages = append(messages, agentMessage{Role: h.Role, Content: h.Content})
|
||||
}
|
||||
messages = append(messages, agentMessage{Role: "user", Content: userText})
|
||||
userMsg := agentMessage{Role: "user", Content: userText}
|
||||
// Si hay un documento/foto pendiente en este chat, se lo pasamos al modelo
|
||||
// como archivo real (solo Anthropic lo lee de verdad) en vez de que la IA
|
||||
// tenga que adivinar cliente/monto a partir del nombre del archivo.
|
||||
if strings.ToLower(ai.Provider) == "anthropic" {
|
||||
if att, ok := PeekFacturaAttachment(chatID); ok {
|
||||
if data, err := os.ReadFile(att.Path); err == nil {
|
||||
userMsg.AttachmentBase64 = base64.StdEncoding.EncodeToString(data)
|
||||
userMsg.AttachmentMime = ResolverMimeType(att.MimeType, att.OriginalName)
|
||||
} else {
|
||||
log.Printf("[AGENT] No se pudo leer el adjunto pendiente (%s): %v", att.Path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
messages = append(messages, userMsg)
|
||||
|
||||
tools := agentTools()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user