Logo. Hasta ahora no había: era el texto "uM" en un cuadrado, con tamaño y esquinas distintas en cada pantalla. Ahora hay un mark — la "u" del nombre con el punto de pulso integrado — probado a 16px, 20px y 32px sobre claro y oscuro, que es donde se cae la mayoría de los logos. Reemplaza el texto en el portal, el header, el sidebar del Studio y la promo, y va también de favicon. Plantillas por rubro. Un agente recién creado no sabía nada, y escribir el conocimiento desde cero frente a un campo en blanco es donde la mayoría abandona. Al crear uno se puede elegir un punto de partida —tienda, restaurante, consultorio, servicios, peluquería o la base genérica— y arranca con sus notas ya escritas, listas para editar. Los valores de ejemplo van entre corchetes a propósito, para que se vea que hay que reemplazarlos: una nota que parezca dato real termina en boca del asistente. Un test lo verifica nota por nota. Y la plantilla de salud abre con el límite — el asistente no da consejo médico ni interpreta síntomas — porque es el único rubro donde una respuesta inventada hace daño de verdad. El catálogo vive en código, no en base: es contenido editorial que mejoramos nosotros, no configuración que cada instalación toca por su lado. Las notas se cargan en segundo plano porque cada una necesita sus embeddings, y hacer esperar el alta sería castigar justo al que eligió plantilla. Con las opciones nuevas el formulario pasaba de largo la pantalla y el botón de guardar quedaba abajo del borde: el modal ahora tiene su propio scroll. Y la página promocional de uMind queda servida en /umind.html. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
301 lines
11 KiB
Go
301 lines
11 KiB
Go
package services
|
|
|
|
import (
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
)
|
|
|
|
// PlantillaRubro es un punto de partida para un agente nuevo: el conocimiento
|
|
// base que casi todos los negocios de ese rubro necesitan, ya escrito.
|
|
//
|
|
// No reemplaza la información real del cliente — la reemplaza el cliente
|
|
// editando estas notas. Existe porque un agente vacío no sirve para nada el
|
|
// primer día, y escribir desde cero frente a un campo en blanco es donde la
|
|
// mayoría abandona.
|
|
type PlantillaRubro struct {
|
|
Clave string `json:"clave"`
|
|
Nombre string `json:"nombre"`
|
|
Descripcion string `json:"descripcion"`
|
|
Tono string `json:"tono"`
|
|
Bienvenida string `json:"mensaje_bienvenida"`
|
|
Notas []NotaPlanura `json:"notas"`
|
|
}
|
|
|
|
// NotaPlanura es una nota de conocimiento con su texto de ejemplo. El texto
|
|
// está escrito para que se note que hay que cambiarlo: valores entre
|
|
// corchetes, nunca datos inventados que parezcan reales.
|
|
type NotaPlanura struct {
|
|
Titulo string `json:"titulo"`
|
|
Contenido string `json:"contenido"`
|
|
}
|
|
|
|
// PlantillasRubro es el catálogo. Vive en código y no en base de datos a
|
|
// propósito: son contenido editorial que cambia cuando lo mejoramos nosotros,
|
|
// no configuración que cada instalación toca por su lado.
|
|
var PlantillasRubro = []PlantillaRubro{
|
|
{
|
|
Clave: "generico",
|
|
Nombre: "Negocio general",
|
|
Descripcion: "La base que sirve para cualquier rubro: horarios, contacto, formas de pago.",
|
|
Tono: "amable y directo",
|
|
Bienvenida: "¡Hola! Contame en qué te puedo ayudar.",
|
|
Notas: []NotaPlanura{
|
|
{
|
|
Titulo: "Horarios y contacto",
|
|
Contenido: `Atendemos de [lunes a viernes] de [9:00] a [18:00], y los [sábados] de [9:00] a [13:00].
|
|
Los [domingos] y feriados no atendemos.
|
|
Nos pueden escribir por WhatsApp al [número] o al correo [correo].
|
|
Estamos en [dirección completa], [ciudad].`,
|
|
},
|
|
{
|
|
Titulo: "Formas de pago",
|
|
Contenido: `Aceptamos [efectivo, tarjeta débito y crédito, transferencia y Nequi].
|
|
[No] aceptamos [cheques].
|
|
Para transferencias, la cuenta es [banco, tipo de cuenta, número] a nombre de [titular].
|
|
El pago se confirma cuando [nos llega el comprobante / se acredita].`,
|
|
},
|
|
{
|
|
Titulo: "Qué NO hacemos",
|
|
Contenido: `Escribí acá lo que la gente pregunta seguido y la respuesta es que no.
|
|
Ejemplos: no hacemos [envíos fuera de la ciudad], no trabajamos con [cierta marca],
|
|
no atendemos [sin cita previa].
|
|
Esto evita que el asistente prometa cosas que después no podés cumplir.`,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Clave: "tienda",
|
|
Nombre: "Tienda / e-commerce",
|
|
Descripcion: "Envíos, cambios, garantía y estado de pedidos.",
|
|
Tono: "cercano y resolutivo",
|
|
Bienvenida: "¡Hola! ¿Buscás algo en particular o querés saber por un pedido?",
|
|
Notas: []NotaPlanura{
|
|
{
|
|
Titulo: "Envíos y zonas de cobertura",
|
|
Contenido: `Hacemos envíos a [ciudades o zonas].
|
|
El costo del envío es [$X] y demora [N] días hábiles.
|
|
Envío gratis por compras superiores a [$X].
|
|
A [zonas donde no llegan] no hacemos envíos.
|
|
El pedido se despacha [el mismo día si se hace antes de las X / al día siguiente].`,
|
|
},
|
|
{
|
|
Titulo: "Cambios y devoluciones",
|
|
Contenido: `Se pueden cambiar productos dentro de los [N] días desde la entrega.
|
|
El producto tiene que estar [sin uso, con etiqueta, en su empaque original].
|
|
[No] aceptamos cambios en [productos en promoción / ropa interior / etc.].
|
|
El costo del envío del cambio lo cubre [el cliente / nosotros].`,
|
|
},
|
|
{
|
|
Titulo: "Garantía",
|
|
Contenido: `Los productos tienen [N meses] de garantía por defectos de fábrica.
|
|
La garantía no cubre [mal uso, daño por golpes, desgaste normal].
|
|
Para hacerla efectiva hace falta [la factura / el número de pedido].`,
|
|
},
|
|
{
|
|
Titulo: "Estado de un pedido",
|
|
Contenido: `Si preguntan por un pedido, pediles el [número de pedido] y decíles que
|
|
alguien del equipo lo revisa y responde en [tiempo].
|
|
No inventes estados de pedido: el asistente no tiene acceso al sistema de envíos.`,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Clave: "restaurante",
|
|
Nombre: "Restaurante / comida",
|
|
Descripcion: "Menú, reservas, domicilios y restricciones alimentarias.",
|
|
Tono: "cálido y breve",
|
|
Bienvenida: "¡Hola! ¿Querés ver el menú, reservar o pedir a domicilio?",
|
|
Notas: []NotaPlanura{
|
|
{
|
|
Titulo: "Horarios de cocina y domicilios",
|
|
Contenido: `La cocina atiende de [12:00] a [15:00] y de [19:00] a [23:00].
|
|
Los domicilios salen hasta las [22:30].
|
|
[Día] cerramos.
|
|
El domicilio demora entre [N] y [M] minutos según la zona.`,
|
|
},
|
|
{
|
|
Titulo: "Zonas de domicilio y costo",
|
|
Contenido: `Llevamos a [barrios o zonas].
|
|
El costo del domicilio es [$X] o gratis por pedidos mayores a [$X].
|
|
A [zonas] no llegamos.
|
|
Pedido mínimo: [$X].`,
|
|
},
|
|
{
|
|
Titulo: "Reservas",
|
|
Contenido: `Tomamos reservas para grupos de [N] personas o más.
|
|
Se reserva con [N horas] de anticipación.
|
|
[Sí/No] cobramos seña.
|
|
Las mesas se guardan [N] minutos después de la hora reservada.`,
|
|
},
|
|
{
|
|
Titulo: "Restricciones y alergias",
|
|
Contenido: `Tenemos opciones [vegetarianas / veganas / sin gluten / sin lactosa]: [cuáles].
|
|
Nuestra cocina [sí/no] es libre de [maní, mariscos, gluten] — importante para alergias.
|
|
Ante una consulta por alergia grave, siempre derivar a una persona del equipo.`,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Clave: "salud",
|
|
Nombre: "Consultorio / clínica",
|
|
Descripcion: "Turnos, obras sociales y qué NO puede responder un asistente.",
|
|
Tono: "profesional y claro",
|
|
Bienvenida: "Hola, ¿querés sacar un turno o consultar por coberturas?",
|
|
Notas: []NotaPlanura{
|
|
{
|
|
Titulo: "Límite importante: no damos consejo médico",
|
|
Contenido: `Este asistente NO responde consultas médicas, no interpreta síntomas,
|
|
no sugiere tratamientos ni medicamentos, y no dice si algo es urgente.
|
|
Ante cualquier consulta de salud, la respuesta es derivar a un profesional
|
|
y ofrecer sacar un turno. Si la persona describe una urgencia, indicar que
|
|
llame al [número de emergencias] o vaya a la guardia más cercana.`,
|
|
},
|
|
{
|
|
Titulo: "Turnos",
|
|
Contenido: `Atendemos con turno previo de [lunes a viernes] de [horario].
|
|
Los turnos se sacan por [WhatsApp / teléfono / la web].
|
|
La consulta dura aproximadamente [N] minutos.
|
|
Si no podés venir, avisanos con [N horas] de anticipación.`,
|
|
},
|
|
{
|
|
Titulo: "Obras sociales y particulares",
|
|
Contenido: `Trabajamos con [lista de obras sociales / prepagas].
|
|
[No] atendemos por [obras sociales específicas].
|
|
La consulta particular cuesta [$X].
|
|
Traer [documento, credencial, orden] el día del turno.`,
|
|
},
|
|
{
|
|
Titulo: "Dirección y cómo llegar",
|
|
Contenido: `Estamos en [dirección], [ciudad].
|
|
[Referencias para llegar].
|
|
[Hay / no hay] estacionamiento.
|
|
El consultorio [es / no es] accesible para silla de ruedas.`,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Clave: "servicios",
|
|
Nombre: "Servicios profesionales",
|
|
Descripcion: "Para estudios, agencias, técnicos: qué hacen, cómo cotizan, plazos.",
|
|
Tono: "profesional y concreto",
|
|
Bienvenida: "Hola, contame qué necesitás y te oriento.",
|
|
Notas: []NotaPlanura{
|
|
{
|
|
Titulo: "Qué hacemos y qué no",
|
|
Contenido: `Nos dedicamos a [descripción del servicio].
|
|
Trabajamos con [tipo de clientes / rubros].
|
|
No hacemos [servicios fuera del alcance] — para eso conviene [alternativa].`,
|
|
},
|
|
{
|
|
Titulo: "Cómo cotizamos",
|
|
Contenido: `Cada trabajo se cotiza según [alcance, horas, complejidad].
|
|
Un [servicio típico] arranca en [$X].
|
|
La cotización es [gratuita] y demora [N] días.
|
|
Para cotizar necesitamos saber [qué datos].
|
|
Nunca des un precio cerrado si no está en esta lista: ofrecé armar una cotización.`,
|
|
},
|
|
{
|
|
Titulo: "Plazos y forma de trabajo",
|
|
Contenido: `Un proyecto [típico] lleva entre [N] y [M] [semanas].
|
|
Trabajamos con [N]% de anticipo y el resto [contra entrega / en cuotas].
|
|
Las entregas se coordinan por [canal].`,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Clave: "belleza",
|
|
Nombre: "Peluquería / estética",
|
|
Descripcion: "Servicios, precios, turnos y políticas de cancelación.",
|
|
Tono: "amable y cercano",
|
|
Bienvenida: "¡Hola! ¿Querés reservar un turno o consultar precios?",
|
|
Notas: []NotaPlanura{
|
|
{
|
|
Titulo: "Servicios y precios",
|
|
Contenido: `[Corte]: desde [$X] — dura [N] minutos.
|
|
[Color]: desde [$X] — dura [N] minutos.
|
|
[Otros servicios con precio y duración].
|
|
Los precios varían según [largo de pelo / cantidad de producto]; si preguntan
|
|
por un caso puntual, ofrecé una consulta previa sin cargo.`,
|
|
},
|
|
{
|
|
Titulo: "Turnos y cancelaciones",
|
|
Contenido: `Trabajamos con turno previo, de [lunes a sábado] de [horario].
|
|
Se reserva por [WhatsApp].
|
|
Si no podés venir, avisá con [N horas] de anticipación.
|
|
[Cobramos / no cobramos] seña para [servicios largos].
|
|
Llegar más de [N] minutos tarde puede significar reprogramar el turno.`,
|
|
},
|
|
{
|
|
Titulo: "Cuidados previos y posteriores",
|
|
Contenido: `Para [color / alisado], venir con el pelo [lavado / sin lavar].
|
|
Después de [servicio], no [lavarse el pelo / mojarse] por [N] horas.
|
|
Recomendamos [productos] para mantener el resultado.`,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// PlantillaRubroPorClave busca una plantilla del catálogo.
|
|
func PlantillaRubroPorClave(clave string) (*PlantillaRubro, bool) {
|
|
for i := range PlantillasRubro {
|
|
if PlantillasRubro[i].Clave == clave {
|
|
return &PlantillasRubro[i], true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// AplicarPlantillaRubro carga las notas de una plantilla como conocimiento del
|
|
// agente. Cada nota entra como una fuente de tipo "texto" editable: el dueño
|
|
// abre, reemplaza los corchetes por sus datos y guarda.
|
|
//
|
|
// Se ejecuta en segundo plano porque cada nota necesita sus embeddings, y el
|
|
// agente ya se creó — hacer esperar la respuesta del alta por esto sería
|
|
// castigar al que elige plantilla.
|
|
func AplicarPlantillaRubro(agenteID uint, clave string) {
|
|
plantilla, ok := PlantillaRubroPorClave(clave)
|
|
if !ok {
|
|
log.Printf("[UMIND] Plantilla de rubro %q no existe, el agente %d queda vacío", clave, agenteID)
|
|
return
|
|
}
|
|
|
|
for _, nota := range plantilla.Notas {
|
|
doc := &models.UmindDocumento{
|
|
AgenteID: agenteID,
|
|
Tipo: "texto",
|
|
Origen: nota.Titulo,
|
|
Contenido: nota.Contenido,
|
|
Estado: "procesando",
|
|
}
|
|
if err := models.CreateUmindDocumento(doc); err != nil {
|
|
log.Printf("[UMIND] No se pudo crear la nota %q del agente %d: %v", nota.Titulo, agenteID, err)
|
|
continue
|
|
}
|
|
IngestarTexto(agenteID, doc.ID, nota.Contenido)
|
|
}
|
|
log.Printf("[UMIND] Agente %d arrancó con la plantilla %q (%d notas)", agenteID, clave, len(plantilla.Notas))
|
|
}
|
|
|
|
// ResumenPlantillasRubro devuelve el catálogo sin el contenido completo de las
|
|
// notas — es lo que necesita la pantalla de elección para mostrar las opciones.
|
|
func ResumenPlantillasRubro() []map[string]interface{} {
|
|
out := make([]map[string]interface{}, 0, len(PlantillasRubro))
|
|
for _, p := range PlantillasRubro {
|
|
titulos := make([]string, 0, len(p.Notas))
|
|
for _, n := range p.Notas {
|
|
titulos = append(titulos, n.Titulo)
|
|
}
|
|
out = append(out, map[string]interface{}{
|
|
"clave": p.Clave,
|
|
"nombre": p.Nombre,
|
|
"descripcion": p.Descripcion,
|
|
"notas": len(p.Notas),
|
|
"titulos": titulos,
|
|
"resumen": strings.Join(titulos, " · "),
|
|
})
|
|
}
|
|
return out
|
|
}
|