feat: registrar proyectos con fases, adjuntar documentos y asignar tareas por Telegram
- crear_fase_proyecto: agrega fases/etapas a un proyecto (se puede llamar varias veces seguidas para cargar todas las fases de un proyecto nuevo). - adjuntar_documento_proyecto: guarda el archivo que el usuario acaba de enviar por Telegram como documento del proyecto (mismo mecanismo de adjuntos ya usado para facturas — se generalizó TelegramAttachment para servir a ambos casos). - listar_usuarios + asignado_id en crear_tarea + asignar_tarea: permite asignar tareas a alguien del equipo por Telegram, disparando la misma notificación (Telegram/email/sistema) que ya dispara el dashboard. - El prompt del sistema ahora indica cómo decidir entre adjuntar_factura y adjuntar_documento_proyecto según el contexto del archivo recibido. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cd0ca6d232
commit
a0b2e9ae0a
@@ -12,20 +12,21 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// FacturaAttachment es un archivo que un usuario envió por Telegram y que queda
|
||||
// "pendiente" hasta que el agente lo asocie a una factura (o expire).
|
||||
type FacturaAttachment struct {
|
||||
// TelegramAttachment es un archivo que un usuario envió por Telegram y que queda
|
||||
// "pendiente" hasta que el agente lo asocie a una factura o a un documento de
|
||||
// proyecto (o expire).
|
||||
type TelegramAttachment struct {
|
||||
Path string
|
||||
OriginalName string
|
||||
MimeType string
|
||||
StagedAt time.Time
|
||||
}
|
||||
|
||||
const facturaAttachmentTTL = 15 * time.Minute
|
||||
const telegramAttachmentTTL = 15 * time.Minute
|
||||
|
||||
var (
|
||||
facturaAttachmentsMu sync.Mutex
|
||||
facturaAttachments = map[int64]*FacturaAttachment{}
|
||||
telegramAttachmentsMu sync.Mutex
|
||||
telegramAttachments = map[int64]*TelegramAttachment{}
|
||||
)
|
||||
|
||||
// DescargarDocumentoTelegram descarga un archivo de Telegram (por file_id) usando
|
||||
@@ -75,41 +76,42 @@ func DescargarDocumentoTelegram(botToken, fileID, originalName, mimeType string)
|
||||
return localPath, nil
|
||||
}
|
||||
|
||||
// StageFacturaAttachment registra un archivo ya descargado como pendiente para el
|
||||
// chat, a la espera de que el agente lo asocie a una factura con la tool adjuntar_factura.
|
||||
func StageFacturaAttachment(chatID int64, path, originalName, mimeType string) {
|
||||
facturaAttachmentsMu.Lock()
|
||||
defer facturaAttachmentsMu.Unlock()
|
||||
facturaAttachments[chatID] = &FacturaAttachment{
|
||||
// StageTelegramAttachment registra un archivo ya descargado como pendiente para el
|
||||
// chat, a la espera de que el agente lo asocie a algo (adjuntar_factura o
|
||||
// adjuntar_documento_proyecto).
|
||||
func StageTelegramAttachment(chatID int64, path, originalName, mimeType string) {
|
||||
telegramAttachmentsMu.Lock()
|
||||
defer telegramAttachmentsMu.Unlock()
|
||||
telegramAttachments[chatID] = &TelegramAttachment{
|
||||
Path: path, OriginalName: originalName, MimeType: mimeType, StagedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// PopFacturaAttachment retorna y remueve el archivo pendiente de un chat, si
|
||||
// PopTelegramAttachment retorna y remueve el archivo pendiente de un chat, si
|
||||
// existe y no expiró (si expiró, borra el archivo temporal del disco).
|
||||
func PopFacturaAttachment(chatID int64) (*FacturaAttachment, bool) {
|
||||
facturaAttachmentsMu.Lock()
|
||||
defer facturaAttachmentsMu.Unlock()
|
||||
a, ok := facturaAttachments[chatID]
|
||||
func PopTelegramAttachment(chatID int64) (*TelegramAttachment, bool) {
|
||||
telegramAttachmentsMu.Lock()
|
||||
defer telegramAttachmentsMu.Unlock()
|
||||
a, ok := telegramAttachments[chatID]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
delete(facturaAttachments, chatID)
|
||||
if time.Since(a.StagedAt) > facturaAttachmentTTL {
|
||||
delete(telegramAttachments, chatID)
|
||||
if time.Since(a.StagedAt) > telegramAttachmentTTL {
|
||||
_ = os.Remove(a.Path)
|
||||
return nil, false
|
||||
}
|
||||
return a, true
|
||||
}
|
||||
|
||||
// PeekFacturaAttachment consulta el archivo pendiente de un chat sin removerlo
|
||||
// PeekTelegramAttachment 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 {
|
||||
func PeekTelegramAttachment(chatID int64) (*TelegramAttachment, bool) {
|
||||
telegramAttachmentsMu.Lock()
|
||||
defer telegramAttachmentsMu.Unlock()
|
||||
a, ok := telegramAttachments[chatID]
|
||||
if !ok || time.Since(a.StagedAt) > telegramAttachmentTTL {
|
||||
return nil, false
|
||||
}
|
||||
return a, true
|
||||
|
||||
@@ -2,7 +2,10 @@ package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
)
|
||||
@@ -50,3 +53,80 @@ func CrearProyectoSimple(clienteID uint, nombre, stack, descripcion string) (*mo
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// CrearFaseProyecto agrega una fase/etapa a un proyecto ya existente, para el
|
||||
// flujo de automatización con IA ("registra el proyecto y coloca las fases").
|
||||
func CrearFaseProyecto(proyectoID uint, nombre, descripcion, estado string, orden int, fechaEstimada *time.Time) (*models.ProyectoFase, error) {
|
||||
if proyectoID == 0 {
|
||||
return nil, fmt.Errorf("proyecto_id requerido")
|
||||
}
|
||||
if strings.TrimSpace(nombre) == "" {
|
||||
return nil, fmt.Errorf("nombre requerido")
|
||||
}
|
||||
if estado == "" {
|
||||
estado = "pendiente"
|
||||
}
|
||||
f := &models.ProyectoFase{
|
||||
ProyectoID: proyectoID,
|
||||
Nombre: nombre,
|
||||
Descripcion: descripcion,
|
||||
Estado: estado,
|
||||
Orden: orden,
|
||||
FechaEstimada: fechaEstimada,
|
||||
}
|
||||
if err := models.CreateProyectoFase(f); err != nil {
|
||||
return nil, fmt.Errorf("no se pudo crear la fase: %w", err)
|
||||
}
|
||||
_ = models.ActualizarProgresoProyecto(proyectoID)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// AdjuntarDocumentoProyecto toma el archivo pendiente de Telegram para este chat
|
||||
// (descargado por el webhook) y lo guarda como documento del proyecto, igual que
|
||||
// si se subiera desde el dashboard.
|
||||
func AdjuntarDocumentoProyecto(chatID int64, proyectoID uint, tipo, nombre, descripcion string) (*models.ProyectoDocumento, error) {
|
||||
if proyectoID == 0 {
|
||||
return nil, fmt.Errorf("proyecto_id requerido")
|
||||
}
|
||||
att, ok := PopTelegramAttachment(chatID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no hay ningún documento pendiente en este chat; pide al usuario que lo envíe de nuevo")
|
||||
}
|
||||
if tipo == "" {
|
||||
tipo = "otro"
|
||||
}
|
||||
if nombre == "" {
|
||||
nombre = att.OriginalName
|
||||
}
|
||||
|
||||
dir := fmt.Sprintf("uploads/proyectos/%d/docs", proyectoID)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
_ = os.Remove(att.Path)
|
||||
return nil, fmt.Errorf("no se pudo crear el directorio: %w", err)
|
||||
}
|
||||
ext := filepath.Ext(att.OriginalName)
|
||||
finalPath := filepath.Join(dir, fmt.Sprintf("telegram_%d%s", time.Now().UnixNano(), ext))
|
||||
if err := os.Rename(att.Path, finalPath); err != nil {
|
||||
return nil, fmt.Errorf("no se pudo guardar el archivo: %w", err)
|
||||
}
|
||||
|
||||
var tamanio int64
|
||||
if info, err := os.Stat(finalPath); err == nil {
|
||||
tamanio = info.Size()
|
||||
}
|
||||
|
||||
d := &models.ProyectoDocumento{
|
||||
ProyectoID: proyectoID,
|
||||
Tipo: tipo,
|
||||
Nombre: nombre,
|
||||
Descripcion: descripcion,
|
||||
Archivo: finalPath,
|
||||
OriginalName: att.OriginalName,
|
||||
TipoMime: att.MimeType,
|
||||
Tamanio: tamanio,
|
||||
}
|
||||
if err := models.CreateProyectoDocumento(d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
)
|
||||
|
||||
// CrearTareaAsignada crea una tarea, opcionalmente ya asignada a alguien, y
|
||||
// dispara la notificación correspondiente (Telegram/email/sistema, según la
|
||||
// config de notif_evento_configs) si quedó asignada a un usuario.
|
||||
func CrearTareaAsignada(titulo, descripcion, estado, prioridad string, asignadoID *uint, fechaLimite *time.Time) (*models.Tarea, error) {
|
||||
if titulo == "" {
|
||||
return nil, fmt.Errorf("titulo requerido")
|
||||
}
|
||||
if estado == "" {
|
||||
estado = "pendiente"
|
||||
}
|
||||
if prioridad == "" {
|
||||
prioridad = "media"
|
||||
}
|
||||
t := &models.Tarea{
|
||||
Titulo: titulo,
|
||||
Descripcion: descripcion,
|
||||
Estado: estado,
|
||||
Prioridad: prioridad,
|
||||
AsignadoID: asignadoID,
|
||||
FechaLimite: fechaLimite,
|
||||
}
|
||||
if err := models.CreateTarea(t); err != nil {
|
||||
return nil, fmt.Errorf("no se pudo crear la tarea: %w", err)
|
||||
}
|
||||
if asignadoID != nil {
|
||||
if full, err := models.GetTareaByID(t.ID); err == nil {
|
||||
go NotificarTareaAsignada(full)
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// AsignarTarea cambia el responsable de una tarea existente y notifica al nuevo
|
||||
// asignado si el responsable realmente cambió. asignadoID puede ser nil para
|
||||
// dejar la tarea sin asignar.
|
||||
func AsignarTarea(id uint, asignadoID *uint) (*models.Tarea, error) {
|
||||
if id == 0 {
|
||||
return nil, fmt.Errorf("id requerido")
|
||||
}
|
||||
tarea, err := models.GetTareaByID(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tarea no encontrada: %w", err)
|
||||
}
|
||||
prevAsignado := tarea.AsignadoID
|
||||
tarea.AsignadoID = asignadoID
|
||||
if err := models.SaveTarea(tarea); err != nil {
|
||||
return nil, fmt.Errorf("no se pudo asignar la tarea: %w", err)
|
||||
}
|
||||
full, _ := models.GetTareaByID(id)
|
||||
if cambioAsignado(prevAsignado, asignadoID) && full != nil {
|
||||
go NotificarTareaAsignada(full)
|
||||
}
|
||||
return full, nil
|
||||
}
|
||||
|
||||
func cambioAsignado(a, b *uint) bool {
|
||||
if a == nil && b == nil {
|
||||
return false
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return true
|
||||
}
|
||||
return *a != *b
|
||||
}
|
||||
@@ -282,6 +282,22 @@ func agentTools() []agentTool {
|
||||
"stack": str("Stack tecnológico, ej: Go + React + PostgreSQL"),
|
||||
"descripcion": str("Descripción adicional del proyecto"),
|
||||
}, []string{"cliente_id", "nombre"})),
|
||||
tool("crear_fase_proyecto", "Agrega una fase/etapa a un proyecto existente (ej: Diseño, Desarrollo, QA, Entrega). Se puede llamar varias veces seguidas para registrar todas las fases de un proyecto de una vez.",
|
||||
obj(map[string]agentToolParam{
|
||||
"proyecto_id": num("ID del proyecto (usa listar_proyectos si no lo sabes)"),
|
||||
"nombre": str("Nombre de la fase"),
|
||||
"descripcion": str("Descripción de la fase"),
|
||||
"estado": agentToolParam{Type: "string", Enum: []string{"pendiente", "en_progreso", "completado"}, Description: "Estado inicial (default pendiente)"},
|
||||
"orden": num("Orden de la fase dentro del proyecto (0, 1, 2...)"),
|
||||
"fecha_estimada": str("Fecha estimada de entrega, formato YYYY-MM-DD"),
|
||||
}, []string{"proyecto_id", "nombre"})),
|
||||
tool("adjuntar_documento_proyecto", "Guarda como documento del proyecto el archivo (PDF/foto) que el usuario acaba de enviar por Telegram en este chat. Solo funciona si hay un archivo adjunto pendiente.",
|
||||
obj(map[string]agentToolParam{
|
||||
"proyecto_id": num("ID del proyecto (usa listar_proyectos si no lo sabes)"),
|
||||
"tipo": agentToolParam{Type: "string", Enum: []string{"contrato", "orden_servicio", "otro"}, Description: "Tipo de documento (default otro)"},
|
||||
"nombre": str("Nombre del documento"),
|
||||
"descripcion": str("Descripción breve"),
|
||||
}, []string{"proyecto_id"})),
|
||||
|
||||
// ── Tickets ──────────────────────────────────────────────────────────
|
||||
tool("listar_tickets", "Lista tickets de soporte de todos los proyectos.",
|
||||
@@ -300,13 +316,22 @@ func agentTools() []agentTool {
|
||||
obj(map[string]agentToolParam{
|
||||
"estado": str("pendiente | en_progreso | completada | cancelada"),
|
||||
}, nil)),
|
||||
tool("crear_tarea", "Crea una nueva tarea.",
|
||||
tool("listar_usuarios", "Lista los usuarios internos del sistema (staff), para poder asignarles tareas.",
|
||||
obj(map[string]agentToolParam{"search": str("Búsqueda por nombre")}, nil)),
|
||||
tool("crear_tarea", "Crea una nueva tarea, opcionalmente asignada a alguien desde el inicio.",
|
||||
obj(map[string]agentToolParam{
|
||||
"titulo": str("Título de la tarea"),
|
||||
"descripcion": str("Descripción"),
|
||||
"estado": agentToolParam{Type: "string", Enum: []string{"pendiente", "en_progreso"}, Description: "Estado inicial"},
|
||||
"prioridad": agentToolParam{Type: "string", Enum: []string{"baja", "media", "alta"}, Description: "Prioridad"},
|
||||
"titulo": str("Título de la tarea"),
|
||||
"descripcion": str("Descripción"),
|
||||
"estado": agentToolParam{Type: "string", Enum: []string{"pendiente", "en_progreso"}, Description: "Estado inicial"},
|
||||
"prioridad": agentToolParam{Type: "string", Enum: []string{"baja", "media", "alta"}, Description: "Prioridad"},
|
||||
"asignado_id": num("ID del usuario responsable (usa listar_usuarios si no lo sabes)"),
|
||||
"fecha_limite": str("Fecha límite, formato YYYY-MM-DD"),
|
||||
}, []string{"titulo"})),
|
||||
tool("asignar_tarea", "Asigna (o reasigna) una tarea existente a un usuario. Notifica al usuario asignado.",
|
||||
obj(map[string]agentToolParam{
|
||||
"id": num("ID de la tarea"),
|
||||
"asignado_id": num("ID del usuario responsable (usa listar_usuarios si no lo sabes)"),
|
||||
}, []string{"id", "asignado_id"})),
|
||||
tool("actualizar_estado_tarea", "Cambia el estado de una tarea.",
|
||||
obj(map[string]agentToolParam{
|
||||
"id": num("ID de la tarea"),
|
||||
@@ -470,7 +495,7 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
||||
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
||||
|
||||
case "adjuntar_factura":
|
||||
att, ok := PopFacturaAttachment(chatID)
|
||||
att, ok := PopTelegramAttachment(chatID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no hay ningún documento pendiente en este chat; pide al usuario que lo envíe de nuevo")
|
||||
}
|
||||
@@ -805,6 +830,26 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
||||
}
|
||||
return map[string]interface{}{"ok": true, "id": p.ID, "slug": p.Slug}, nil
|
||||
|
||||
case "crear_fase_proyecto":
|
||||
var fecha *time.Time
|
||||
if s := getStr("fecha_estimada"); s != "" {
|
||||
if t, errParse := time.Parse("2006-01-02", s); errParse == nil {
|
||||
fecha = &t
|
||||
}
|
||||
}
|
||||
f, err := CrearFaseProyecto(uint(getInt("proyecto_id", 0)), getStr("nombre"), getStr("descripcion"), getStr("estado"), getInt("orden", 0), fecha)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"ok": true, "fase_id": f.ID}, nil
|
||||
|
||||
case "adjuntar_documento_proyecto":
|
||||
d, err := AdjuntarDocumentoProyecto(chatID, uint(getInt("proyecto_id", 0)), getStr("tipo"), getStr("nombre"), getStr("descripcion"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"ok": true, "documento_id": d.ID}, nil
|
||||
|
||||
// ── Tickets ────────────────────────────────────────────────────────────
|
||||
case "listar_tickets":
|
||||
estado := getStr("estado")
|
||||
@@ -849,24 +894,52 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
||||
}
|
||||
return filtered, nil
|
||||
|
||||
case "listar_usuarios":
|
||||
users, _, err := models.AllUsersSistema(50, 0, getStr("search"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
type usuarioItem struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
out := make([]usuarioItem, len(users))
|
||||
for i, u := range users {
|
||||
out[i] = usuarioItem{ID: u.ID, Name: u.Name}
|
||||
}
|
||||
return out, nil
|
||||
|
||||
case "crear_tarea":
|
||||
t := &models.Tarea{
|
||||
Titulo: getStr("titulo"),
|
||||
Descripcion: getStr("descripcion"),
|
||||
Estado: getStr("estado"),
|
||||
Prioridad: getStr("prioridad"),
|
||||
var asignadoID *uint
|
||||
if v := getInt("asignado_id", 0); v > 0 {
|
||||
u := uint(v)
|
||||
asignadoID = &u
|
||||
}
|
||||
if t.Estado == "" {
|
||||
t.Estado = "pendiente"
|
||||
var fechaLimite *time.Time
|
||||
if s := getStr("fecha_limite"); s != "" {
|
||||
if t, errParse := time.Parse("2006-01-02", s); errParse == nil {
|
||||
fechaLimite = &t
|
||||
}
|
||||
}
|
||||
if t.Prioridad == "" {
|
||||
t.Prioridad = "media"
|
||||
}
|
||||
if err := models.CreateTarea(t); err != nil {
|
||||
t, err := CrearTareaAsignada(getStr("titulo"), getStr("descripcion"), getStr("estado"), getStr("prioridad"), asignadoID, fechaLimite)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"ok": true, "id": t.ID, "titulo": t.Titulo}, nil
|
||||
|
||||
case "asignar_tarea":
|
||||
id := uint(getInt("id", 0))
|
||||
asignadoIDVal := getInt("asignado_id", 0)
|
||||
if id == 0 || asignadoIDVal == 0 {
|
||||
return nil, fmt.Errorf("id y asignado_id requeridos")
|
||||
}
|
||||
asignadoID := uint(asignadoIDVal)
|
||||
t, err := AsignarTarea(id, &asignadoID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"ok": true, "id": t.ID, "asignado_id": asignadoID}, nil
|
||||
|
||||
case "actualizar_estado_tarea":
|
||||
id := uint(getInt("id", 0))
|
||||
estado := getStr("estado")
|
||||
@@ -1269,7 +1342,12 @@ 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 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
|
||||
- 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). Decide primero de qué se trata el archivo antes de guardarlo:
|
||||
· Si es una factura/cuenta de cobro → identifica cliente, monto y número, resuelve el cliente_id con listar_clientes, y llama a adjuntar_factura
|
||||
· Si es un documento de un proyecto (contrato, orden de servicio, entregable, etc.) → identifica a qué proyecto corresponde (usa listar_proyectos si hace falta) y llama a adjuntar_documento_proyecto
|
||||
Si logras identificar los datos necesarios (del documento o de lo que escribió el usuario), guarda directamente sin pedir confirmación — solo pregunta si de verdad no hay forma de determinar el cliente/proyecto o falta un dato imprescindible. No uses estas tools si el mensaje no menciona ningún documento adjunto
|
||||
- Para registrar un proyecto nuevo con sus fases (el usuario puede describirlo en texto o mandarlo en un documento), primero usa crear_proyecto y luego llama a crear_fase_proyecto una vez por cada fase que corresponda — no hace falta preguntar confirmación entre cada fase
|
||||
- Para asignar una tarea a alguien, resuelve el nombre con listar_usuarios y usa asignado_id en crear_tarea (si es nueva) o asignar_tarea (si ya existe) — la persona asignada recibe una notificación
|
||||
|
||||
COMANDOS ESPECIALES (el usuario puede escribirlos):
|
||||
- /reset — olvidar el historial de esta conversación
|
||||
@@ -1342,7 +1420,7 @@ Comandos: /reset /instancias /ayuda`, nil
|
||||
// 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 att, ok := PeekTelegramAttachment(chatID); ok {
|
||||
if data, err := os.ReadFile(att.Path); err == nil {
|
||||
userMsg.AttachmentBase64 = base64.StdEncoding.EncodeToString(data)
|
||||
userMsg.AttachmentMime = ResolverMimeType(att.MimeType, att.OriginalName)
|
||||
|
||||
@@ -132,7 +132,7 @@ func TelegramAgentWebhook(c *fiber.Ctx) error {
|
||||
sendAgentReply(tgCfg.BotToken, chatID, "No pude descargar el archivo que enviaste, intenta de nuevo.")
|
||||
return c.SendStatus(200)
|
||||
}
|
||||
services.StageFacturaAttachment(chatID, path, fileName, mimeType)
|
||||
services.StageTelegramAttachment(chatID, path, fileName, mimeType)
|
||||
nota := fmt.Sprintf("[Documento adjunto recibido: %s]", fileName)
|
||||
switch {
|
||||
case caption != "":
|
||||
|
||||
Reference in New Issue
Block a user