diff --git a/pkg/models/soporte_webhook.go b/pkg/models/soporte_webhook.go index 6c960e5..67f8c8e 100644 --- a/pkg/models/soporte_webhook.go +++ b/pkg/models/soporte_webhook.go @@ -45,6 +45,9 @@ type SoporteWebhookConfig struct { // clasifica cada correo nuevo antes de abrir ticket. ClasificarConIA bool `json:"clasificar_con_ia" gorm:"column:clasificar_con_ia;default:false"` ContextoNegocio string `json:"contexto_negocio" gorm:"column:contexto_negocio;type:text"` + // Agente de uMind cuya base de conocimiento se usa para redactar borradores + // de respuesta. Sin agente el borrador se arma solo con la conversación. + AgenteBorradorID *uint `json:"agente_borrador_id" gorm:"column:agente_borrador_id;index"` // Solo para la vista: dice si ya hay contraseña guardada sin exponerla, para // que el formulario sepa que puede mandar el campo vacío sin borrarla. diff --git a/pkg/models/umind_agente.go b/pkg/models/umind_agente.go index ae64d50..6e1341c 100644 --- a/pkg/models/umind_agente.go +++ b/pkg/models/umind_agente.go @@ -47,6 +47,15 @@ func GetUmindAgentesByTenant(tenantID uint) ([]UmindAgente, error) { return items, err } +// GetTodosLosUmindAgentes lista todos los agentes de todos los tenants. Es solo +// para pantallas de staff que necesitan elegir uno (ej. de qué base de +// conocimiento salen los borradores de soporte). +func GetTodosLosUmindAgentes() ([]UmindAgente, error) { + var items []UmindAgente + err := app.Http.Database.DB.Order("nombre ASC").Find(&items).Error + return items, err +} + func GetUmindAgenteByID(id uint) (*UmindAgente, error) { var a UmindAgente if err := app.Http.Database.DB.First(&a, id).Error; err != nil { diff --git a/pkg/services/soporte_borrador_service.go b/pkg/services/soporte_borrador_service.go new file mode 100644 index 0000000..fe09d95 --- /dev/null +++ b/pkg/services/soporte_borrador_service.go @@ -0,0 +1,77 @@ +package services + +import ( + "fmt" + "strings" + + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" +) + +// RedactarBorradorTicket propone una respuesta para un ticket, apoyándose en la +// base de conocimiento del agente de uMind que se haya elegido en la +// configuración de soporte. +// +// Devuelve un borrador, no una respuesta: no se manda a nadie. Contestarle solo +// a un cliente con lo que dijo un modelo es la forma más rápida de perderlo; +// acá lo lee una persona, lo corrige y aprieta enviar. +func RedactarBorradorTicket(ticket *models.ProyectoTicket) (string, error) { + if ticket == nil { + return "", fmt.Errorf("ticket no encontrado") + } + cfg, _ := models.GetSoporteWebhookActivo() + + var conversacion strings.Builder + fmt.Fprintf(&conversacion, "Asunto: %s\n\n%s: %s\n", ticket.Titulo, ticket.AutorNombre, ticket.Descripcion) + for _, m := range ticket.Mensajes { + quien := ticket.AutorNombre + if m.EsAdmin { + quien = "Soporte" + } + fmt.Fprintf(&conversacion, "\n%s: %s\n", quien, m.Contenido) + } + + sistema := `Redactás borradores de respuesta para un equipo de soporte. Los lee una +persona antes de enviarlos. + +- Escribí en el mismo idioma que usó el cliente. +- Respondé lo que preguntó, directo, sin relleno ni frases de manual. +- Usá SOLO lo que esté en la documentación de referencia y en la conversación. + Si falta información para resolverlo, decilo y proponé qué preguntarle. +- Nunca inventes precios, plazos, funciones ni pasos que no estén escritos. +- Sin asunto, sin firma: solo el cuerpo del mensaje.` + + if cfg != nil && strings.TrimSpace(cfg.ContextoNegocio) != "" { + sistema += "\n\nContexto del negocio:\n" + strings.TrimSpace(cfg.ContextoNegocio) + } + + usuario := conversacion.String() + if doc := documentacionRelevante(cfg, ticket); doc != "" { + usuario = "Documentación de referencia:\n---\n" + doc + "\n---\n\nConversación:\n" + usuario + } + + borrador, err := CompletarTextoIA("ia", sistema, usuario) + if err != nil { + return "", err + } + return strings.TrimSpace(borrador), nil +} + +// documentacionRelevante busca en la base de conocimiento del agente elegido. +// Sin agente configurado devuelve vacío y el borrador se arma solo con la +// conversación — peor, pero mejor que un error. +func documentacionRelevante(cfg *models.SoporteWebhookConfig, ticket *models.ProyectoTicket) string { + if cfg == nil || cfg.AgenteBorradorID == nil || *cfg.AgenteBorradorID == 0 { + return "" + } + consulta := ticket.Titulo + "\n" + ticket.Descripcion + chunks, err := BuscarConocimiento(*cfg.AgenteBorradorID, consulta, 4) + if err != nil || len(chunks) == 0 { + return "" + } + var b strings.Builder + for _, ch := range chunks { + b.WriteString(strings.TrimSpace(ch.Contenido)) + b.WriteString("\n\n") + } + return strings.TrimSpace(b.String()) +} diff --git a/resources/views/soporte_webhook.html b/resources/views/soporte_webhook.html index 506d1db..893ca6a 100644 --- a/resources/views/soporte_webhook.html +++ b/resources/views/soporte_webhook.html @@ -217,6 +217,24 @@

Ayuda a la IA a distinguir tus casos raros. Los correos masivos se descartan por sus encabezados, sin gastar una llamada.

+ +
+

Borradores de respuesta

+

+ En cada ticket aparece un botón ✨ Borrador que propone una respuesta usando la base + de conocimiento del agente que elijas. Nunca la envía: cae en el cuadro de respuesta + para que la revises y la mandes vos. Sin agente elegido el borrador sale igual, pero solo con la + conversación del ticket. +

+ + +
+