Files
soft_usite/pkg/services/umind_aviso_tools.go
T
Lizandro GuarnizoandClaude Opus 5 b2e2e83fe8 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>
2026-08-24 21:32:48 -05:00

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"}`
}