feat(umind): el agente programa recordatorios, y solo para su dueño
"Avisame el 15 de marzo que vence la póliza de Acme, y todos los años" queda programado desde la conversación, visible y cancelable en el panel. Tres tools: programar, listar, cancelar. El aviso vuelve por donde se pidió — Telegram a ese chat, correo a esa casilla — o al dueño del espacio por correo y campanita. Nunca a una dirección que dicte la conversación: eso sería un cañón de spam con destinatario libre. Y las tools de aviso solo se le OFRECEN al modelo cuando la sesión es interna: el chat de prueba del panel (que corre autenticado) o un canal marcado como línea privada del dueño. En el widget público escribe cualquiera, y cualquiera no puede programarle recordatorios ni gastarle el plan a otro. El gate está en dos capas: la tool no se declara, y si igual la pide, el ejecutor la rechaza. Tres detalles que se pagan una sola vez: - El cron corre en memoria del proceso, sin lock distribuido: con dos instancias cada aviso saldría dos veces. El reclamo es un UPDATE condicional — la base ya es el árbitro, no hace falta traer otro. - Si el servidor estuvo caído, una repetición diaria se saltea los ciclos perdidos en vez de disparar diez avisos viejos de golpe. - Un fallo de SMTP devuelve el aviso a pendiente: una caída de correo no puede perder un vencimiento de póliza. Una fecha sin hora se entrega a las 9, no a medianoche, que es cuando nadie mira el teléfono. De paso: los archivos de los espacios uMind nunca se sirven por el estático de /uploads. El guard genérico solo sabe si hay sesión de panel, no de quién es el archivo — salen por su endpoint, que sí valida propiedad. Cerrado por construcción y no por acordarse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9da2a057f2
commit
b2e2e83fe8
@@ -0,0 +1,120 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
)
|
||||
|
||||
// Las tools con las que el dueño programa recordatorios conversando.
|
||||
|
||||
func umindAvisoTools() []agentTool {
|
||||
return []agentTool{
|
||||
{Type: "function", Function: agentToolFunc{
|
||||
Name: "programar_aviso",
|
||||
Description: "Programa un recordatorio para el dueño del negocio (ej. vencimiento de una póliza, renovación de un contrato, llamar a alguien). " +
|
||||
"Calculá la fecha exacta a partir de lo que dijo el usuario; hoy es " + time.Now().Format("2006-01-02") + ".",
|
||||
Parameters: agentToolParam{
|
||||
Type: "object",
|
||||
Properties: map[string]agentToolParam{
|
||||
"titulo": {Type: "string", Description: "Qué hay que recordar, en una línea"},
|
||||
"cuando": {Type: "string", Description: "Fecha y hora en formato 2026-03-15 09:00"},
|
||||
"detalle": {Type: "string", Description: "Contexto adicional, opcional"},
|
||||
"repetir": {Type: "string", Description: "Vacío para una sola vez, o: diario, semanal, mensual, anual"},
|
||||
},
|
||||
Required: []string{"titulo", "cuando"},
|
||||
},
|
||||
}},
|
||||
{Type: "function", Function: agentToolFunc{
|
||||
Name: "listar_avisos",
|
||||
Description: "Lista los recordatorios programados que todavía no se enviaron.",
|
||||
Parameters: agentToolParam{Type: "object", Properties: map[string]agentToolParam{}, Required: []string{}},
|
||||
}},
|
||||
{Type: "function", Function: agentToolFunc{
|
||||
Name: "cancelar_aviso",
|
||||
Description: "Cancela un recordatorio programado. Usá listar_avisos primero para saber el id.",
|
||||
Parameters: agentToolParam{
|
||||
Type: "object",
|
||||
Properties: map[string]agentToolParam{"id": {Type: "number", Description: "El id del aviso a cancelar"}},
|
||||
Required: []string{"id"},
|
||||
},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func executeUmindAvisoTool(agenteID uint, sessionID, name string, args map[string]interface{}) string {
|
||||
switch name {
|
||||
case "programar_aviso":
|
||||
titulo, _ := args["titulo"].(string)
|
||||
cuando, _ := args["cuando"].(string)
|
||||
detalle, _ := args["detalle"].(string)
|
||||
repetir, _ := args["repetir"].(string)
|
||||
if strings.TrimSpace(titulo) == "" {
|
||||
return `{"error": "falta el título del recordatorio"}`
|
||||
}
|
||||
fecha, err := ParsearFechaAviso(cuando)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`{"error": %q}`, err.Error())
|
||||
}
|
||||
if fecha.Before(time.Now()) {
|
||||
return `{"error": "esa fecha ya pasó; confirmá con el usuario para cuándo lo quiere"}`
|
||||
}
|
||||
repetir = strings.ToLower(strings.TrimSpace(repetir))
|
||||
switch repetir {
|
||||
case "", "diario", "semanal", "mensual", "anual":
|
||||
default:
|
||||
repetir = ""
|
||||
}
|
||||
|
||||
agente, err := models.GetUmindAgenteByID(agenteID)
|
||||
if err != nil {
|
||||
return `{"error": "no se pudo programar el recordatorio"}`
|
||||
}
|
||||
aviso := &models.UmindAviso{
|
||||
AgenteID: agenteID, TenantID: agente.TenantID,
|
||||
Titulo: titulo, Detalle: detalle, ProximoAt: fecha,
|
||||
Repetir: repetir, Destino: sessionID, Estado: "pendiente",
|
||||
}
|
||||
if err := models.CreateUmindAviso(aviso); err != nil {
|
||||
return fmt.Sprintf(`{"error": %q}`, err.Error())
|
||||
}
|
||||
b, _ := json.Marshal(map[string]interface{}{
|
||||
"ok": true, "id": aviso.ID,
|
||||
"programado_para": fecha.Format("2006-01-02 15:04"),
|
||||
"repite": repetir,
|
||||
})
|
||||
return string(b)
|
||||
|
||||
case "listar_avisos":
|
||||
avisos, err := models.GetUmindAvisosByAgente(agenteID)
|
||||
if err != nil {
|
||||
return `{"error": "no se pudieron listar los recordatorios"}`
|
||||
}
|
||||
lista := []map[string]interface{}{}
|
||||
for _, a := range avisos {
|
||||
if a.Estado != "pendiente" {
|
||||
continue
|
||||
}
|
||||
lista = append(lista, map[string]interface{}{
|
||||
"id": a.ID, "titulo": a.Titulo,
|
||||
"cuando": a.ProximoAt.Format("2006-01-02 15:04"), "repite": a.Repetir,
|
||||
})
|
||||
}
|
||||
b, _ := json.Marshal(map[string]interface{}{"avisos": lista})
|
||||
return string(b)
|
||||
|
||||
case "cancelar_aviso":
|
||||
id, ok := args["id"].(float64)
|
||||
if !ok || id <= 0 {
|
||||
return `{"error": "falta el id del aviso"}`
|
||||
}
|
||||
if err := models.CancelarUmindAviso(uint(id), agenteID); err != nil {
|
||||
return `{"error": "no se pudo cancelar"}`
|
||||
}
|
||||
return `{"ok": true}`
|
||||
}
|
||||
return `{"error": "herramienta desconocida"}`
|
||||
}
|
||||
Reference in New Issue
Block a user