Files
soft_usite/pkg/services/soporte_borrador_service.go
T
Lizandro GuarnizoandClaude Opus 5 1b599606f7 feat(soporte): botón de borrador que redacta con la base de conocimiento
En cada ticket, " Borrador" propone una respuesta usando los fragmentos más
parecidos de la base de conocimiento del agente de uMind que se elija en la
configuración de soporte, más el hilo de la conversación.

El borrador cae en el cuadro de respuesta y no se manda: lo revisa una persona
y aprieta enviar. Contestarle a un cliente con lo que dijo un modelo, sin que
nadie lo lea, es la forma más rápida de perderlo.

Sin agente configurado el borrador se arma igual, solo con la conversación —
peor, pero mejor que un error. El prompt le prohíbe inventar precios, plazos o
pasos que no estén en la documentación, y le pide decir qué falta preguntar
cuando no alcanza para resolver.

El cuadro de respuesta pasa a textarea: un borrador de varios renglones no
entraba en un input de una línea.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 19:45:38 -05:00

78 lines
2.7 KiB
Go

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())
}