package services import ( "crypto/sha256" "encoding/hex" "fmt" "log" "strings" "sync" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) // La vigilancia de APIs: "avisame cuando esta API devuelva stock en cero". var vigilanciasEnCurso sync.Mutex // vigilanciaFallosMax es cuántos errores seguidos aguanta antes de apagarse. // Una API que dejó de existir no puede consultarse para siempre. const vigilanciaFallosMax = 10 // RevisarVigilancias corre cada minuto y atiende solo las que ya les toca. func RevisarVigilancias() { if !vigilanciasEnCurso.TryLock() { return } defer vigilanciasEnCurso.Unlock() vigilancias, err := models.GetUmindVigilanciasPendientes() if err != nil { log.Printf("[UMIND_VIGILA] no se pudieron listar: %v", err) return } if len(vigilancias) == 0 { return } const trabajadores = 4 cola := make(chan models.UmindVigilancia) var wg sync.WaitGroup for w := 0; w < trabajadores; w++ { wg.Add(1) go func() { defer wg.Done() for v := range cola { revisarUnaVigilancia(v) } }() } for _, v := range vigilancias { cola <- v } close(cola) wg.Wait() } func revisarUnaVigilancia(v models.UmindVigilancia) { herramienta, err := models.GetUmindHerramientaByNombre(v.AgenteID, v.Herramienta) if err != nil { models.DesactivarVigilancia(v.ID, "la herramienta que vigilaba ya no existe") models.RegistrarEventoUmind(v.AgenteID, "warn", "vigilancia", "Se apagó la vigilancia "+v.Nombre+": la herramienta que usaba ya no existe", "") return } authValor := "" if herramienta.AuthHeaderValorEnc != "" { if authValor, err = DescifrarSecretoUmind(herramienta.AuthHeaderValorEnc); err != nil { models.ActualizarVigilancia(v.ID, map[string]interface{}{"ultimo_error": "credencial ilegible"}) return } } respuesta, err := LlamarHerramientaWebhook(herramienta.URL, herramienta.AuthHeaderNombre, authValor, map[string]interface{}{}) if err != nil { fallos := v.FallosNoOk + 1 if fallos >= vigilanciaFallosMax { models.DesactivarVigilancia(v.ID, err.Error()) models.RegistrarEventoUmind(v.AgenteID, "warn", "vigilancia", "Se apagó la vigilancia "+v.Nombre+" tras fallar muchas veces seguidas", err.Error()) return } models.ActualizarVigilancia(v.ID, map[string]interface{}{"ultimo_error": err.Error(), "fallos_no_ok": fallos}) return } // Acá está el ahorro: si la respuesta es idéntica a la anterior, no hay // nada que evaluar. Preguntarle a la IA en cada chequeo costaría una // fortuna por una respuesta que ya sabemos. suma := sha256.Sum256([]byte(respuesta)) hash := hex.EncodeToString(suma[:]) if hash == v.HashUltimo { models.ActualizarVigilancia(v.ID, map[string]interface{}{"ultimo_error": "", "fallos_no_ok": 0}) return } cumple, err := EvaluarCondicionVigilancia(v.Condicion, respuesta) if err != nil { models.ActualizarVigilancia(v.ID, map[string]interface{}{ "hash_ultimo": hash, "ultimo_error": "no se pudo evaluar la condición: " + err.Error(), }) return } models.RegistrarUsoUmind(v.AgenteID, models.UsoTipoIA, 400, "tokens") updates := map[string]interface{}{"hash_ultimo": hash, "ultimo_error": "", "fallos_no_ok": 0, "en_condicion": cumple} // Se avisa al ENTRAR en condición. Si ya estaba y sigue, no se repite: // una alerta que llega cada media hora deja de leerse a la segunda. if cumple && !v.EnCondicion { if err := avisarVigilancia(v, respuesta); err != nil { log.Printf("[UMIND_VIGILA] no se pudo avisar de %q: %v", v.Nombre, err) models.RegistrarEventoUmind(v.AgenteID, "error", "vigilancia", "No se pudo avisar de la vigilancia "+v.Nombre, err.Error()) // Sin marcar en_condicion: se reintenta el aviso al próximo ciclo. delete(updates, "en_condicion") } } models.ActualizarVigilancia(v.ID, updates) } // EvaluarCondicionVigilancia le pregunta a la IA si lo que devolvió la API // cumple la condición. Se le exige SI o NO para que la respuesta sea usable: // un modelo que explica su razonamiento acá no sirve de nada. func EvaluarCondicionVigilancia(condicion, respuesta string) (bool, error) { if len(respuesta) > 6000 { respuesta = respuesta[:6000] } sistema := "Sos un evaluador. Te dan la respuesta de una API y una condición. " + "Respondé EXACTAMENTE una palabra: SI si la condición se cumple, NO si no se cumple. " + "Ante la duda respondé NO." usuario := fmt.Sprintf("Condición: %s\n\nRespuesta de la API:\n%s", condicion, respuesta) out, err := CompletarTextoIA("ia", sistema, usuario) if err != nil { return false, err } return interpretarVeredicto(out), nil } // interpretarVeredicto acepta solo un SI explícito. Cualquier otra cosa —una // explicación, un error, un modelo confundido— se toma como que no: es // preferible un aviso que no llega a uno que llega sin motivo a las 3 AM. func interpretarVeredicto(out string) bool { limpio := strings.ToUpper(strings.TrimSpace(out)) limpio = strings.Trim(limpio, ".!\"' \n") return limpio == "SI" || limpio == "SÍ" || strings.HasPrefix(limpio, "SI ") || strings.HasPrefix(limpio, "SI,") || strings.HasPrefix(limpio, "SÍ ") || strings.HasPrefix(limpio, "SÍ,") } // avisarVigilancia reusa la entrega de los recordatorios: mismo destino, mismo // camino, una sola implementación que mantener. func avisarVigilancia(v models.UmindVigilancia, respuesta string) error { detalle := v.Condicion if len(respuesta) < 800 { detalle += "\n\nLo que devolvió la API:\n" + respuesta } return entregarAviso(models.UmindAviso{ AgenteID: v.AgenteID, Titulo: "Se cumplió lo que estabas vigilando: " + v.Nombre, Detalle: detalle, Destino: v.Destino, }) }