"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>
121 lines
4.0 KiB
Go
121 lines
4.0 KiB
Go
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"}`
|
|
}
|