Tres defectos que solo se veian llamando al modelo de verdad.
1. min(2) en los factores obligaba a inventar relleno. Con una dimension sin
cifras, MiniMax produjo "herramienta_de_seguimiento_actual=0 x canal=1" solo
para satisfacer la restriccion. Ahora los dos factores se exigen unicamente
cuando hay montoAnualMXN.
2. El modelo OMITE montoAnualMXN en vez de mandar null explicito, que es lo
natural para un LLM. Exigirlo presente quemaba los tres intentos del paso.
Ahora es .default(null) y la omision se tolera.
3. Sin confianza por factor, el modelo la metia dentro del nombre
("tasa_conversion (por_validar)=0.1"). Ahora es un campo.
Y el hallazgo que mas importa, porque es comercial y no de formato: el modelo
puede OMITIR un factor y aun asi cuadrar la aritmetica. En una corrida calculo
40 mensajes x 0.5 sin contestar x 52 semanas x 3,000 de utilidad POR CLIENTE =
3,120,000, asumiendo que cada mensaje sin responder es un cliente perdido. R5 lo
acepto porque los factores si multiplican al monto: R5 no puede ver lo que falta.
El ratio habria dicho "subcotizado" con el denominador inflado diez veces. Como
no se puede impedir que el modelo produzca estimaciones plausibles y erroneas, lo
que se hace es que el asesor las cache de un vistazo:
- R5b avisa cuando una cifra no tiene ni un factor confirmado.
- El anexo interno desglosa cada factor con su confianza, y alerta en rojo si el
valor descansa entero en estimaciones.
Ademas, el presupuesto de reintentos sube (5 en extraccion, 4 en los otros dos):
MiniMax se equivoca de array de vez en cuando con schemas anidados, y la
extraccion es el paso fundacional. Una corrida real necesito los 5.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
123 lines
7.6 KiB
TypeScript
123 lines
7.6 KiB
TypeScript
import { validarPropuesta, normalizar, hayBloqueantes, type Aviso } from "@/lib/propuesta/validacion";
|
|
import type { PropuestaConsultiva } from "@/lib/propuesta/schemas";
|
|
import type { DatosEconomicos } from "@/lib/propuesta/economia";
|
|
|
|
let fallas = 0;
|
|
function check(n: string, ok: boolean, d = "") {
|
|
console.log(`${ok ? " OK " : " FALLA"} | ${n}${d ? " -> " + d : ""}`);
|
|
if (!ok) fallas++;
|
|
}
|
|
const tiene = (a: Aviso[], regla: string) => a.some((x) => x.regla === regla);
|
|
|
|
const CITA = "no llevamos control de quien contesta el whatsapp el fin de semana";
|
|
|
|
const econ: DatosEconomicos = {
|
|
partidas: [{ refPartida: "P01", servicioCotizadoId: "c1", nombre: "Sitio web", fase: 1, tipoPago: "unico", precio: 12000, tiempoEntrega: "2 semanas", modeloCobro: "fijo", horas: null, tarifaHora: null, entregables: [] }],
|
|
totales: { subtotalUnico: 12000, subtotalMensual: 0, ivaUnico: 1920, ivaMensual: 0, totalUnico: 13920, totalMensual: 0, totalPrimerAnio: 13920, incluyeIva: true },
|
|
moneda: "MXN",
|
|
};
|
|
|
|
const base = (): PropuestaConsultiva => ({
|
|
hechos: {
|
|
citas: [{ id: "C01", textoLiteral: CITA, quienLoDijo: "el socio" }],
|
|
hechos: [{ id: "H01", enunciado: "No hay control de la atencion", confianza: "confirmado", citas: ["C01"] }],
|
|
materialesPendientes: [{ texto: "Accesos al dominio", bloqueaEntrega: true }],
|
|
decisionesPendientes: [{ texto: "Definir responsable de atencion", quienDecide: "el socio" }],
|
|
mencionesFueraDeAlcance: [],
|
|
redFlags: [],
|
|
},
|
|
diagnostico: {
|
|
hallazgos: [{ id: "D01", urgencia: "rojo", titulo: "Prospectos sin seguimiento", descripcion: "Los mensajes del fin de semana se pierden.", confianza: "confirmado", citas: ["C01"], hechos: ["H01"] }],
|
|
valorProblema: {
|
|
dimensiones: [{ tipo: "tiempo", descripcion: "Horas perdidas de atencion", calculo: { factores: [{ nombre: "horas por semana", valor: 8, confianza: "confirmado" }, { nombre: "semanas", valor: 52, confianza: "confirmado" }, { nombre: "costo por hora", valor: 400, confianza: "estimado" }], montoAnualMXN: 166400 }, confianza: "estimado", hechos: ["H01"] }],
|
|
notaMetodologia: "Ocho horas por semana a costo cargado.",
|
|
},
|
|
resultados: [{ enunciado: "Ningun mensaje sin respuesta en mas de 24 horas", metrica: "tiempo de respuesta", lineaBase: null, periodoMedicion: "mensual" }],
|
|
},
|
|
redaccion: {
|
|
hero: { titulo: "Propuesta de digitalizacion", subtitulo: "Ordenar la atencion para dejar de perder prospectos." },
|
|
citaDestacadaId: "C01",
|
|
alcance: [{ refPartida: "P01", descripcionResultado: "Un lugar a donde mandar a los prospectos que llegan de los anuncios." }],
|
|
beneficios: [{ etiqueta: "Respuesta", texto: "Cada mensaje queda registrado y con responsable.", hallazgoId: "D01" }],
|
|
exclusiones: [{ texto: "No incluye migrar el historico de pedidos de los ultimos 4 anos.", razon: "Se evalua en fase 2." }],
|
|
backlogEvolucion: [],
|
|
notasInternas: ["El socio es quien decide."],
|
|
},
|
|
});
|
|
|
|
const fuentes = { textoCliente: `El socio dijo: ${CITA}. Nada mas.`, textoInterno: "" };
|
|
|
|
check("normalizar quita acentos y puntuacion", normalizar("¿Como estas, Juan?") === "como estas juan");
|
|
check("normalizar colapsa espacios", normalizar(" a b ") === "a b");
|
|
|
|
const limpia = validarPropuesta(base(), econ, fuentes);
|
|
check("una propuesta valida no produce bloqueantes", !hayBloqueantes(limpia), JSON.stringify(limpia.slice(0, 2)));
|
|
|
|
let p = base(); p.redaccion.alcance[0].refPartida = "P99";
|
|
check("R2 detecta una partida inventada", tiene(validarPropuesta(p, econ, fuentes), "R2"));
|
|
|
|
p = base(); p.redaccion.alcance = [];
|
|
check("R2b avisa de una partida cotizada y no descrita", tiene(validarPropuesta(p, econ, fuentes), "R2b"));
|
|
|
|
p = base(); p.redaccion.citaDestacadaId = "C09";
|
|
check("R3 detecta una cita inexistente", tiene(validarPropuesta(p, econ, fuentes), "R3"));
|
|
|
|
p = base(); p.hechos.citas[0].textoLiteral = "esto nunca lo dijo nadie en la reunion";
|
|
check("R3 detecta una cita no verificable", tiene(validarPropuesta(p, econ, fuentes), "R3"));
|
|
|
|
p = base(); p.hechos.citas[0].textoLiteral = "NO LLEVAMOS CONTROL, de quien contesta... el WhatsApp el fin de semana!";
|
|
check("R3 tolera acentos, mayusculas y puntuacion", !tiene(validarPropuesta(p, econ, fuentes), "R3"));
|
|
|
|
p = base(); p.diagnostico.hallazgos[0].citas = []; p.diagnostico.hallazgos[0].hechos = [];
|
|
check("R4 degrada un confirmado sin evidencia", tiene(validarPropuesta(p, econ, fuentes), "R4"));
|
|
|
|
p = base(); p.diagnostico.valorProblema.dimensiones[0].calculo.montoAnualMXN = 999999;
|
|
check("R5 detecta aritmetica que no cuadra", tiene(validarPropuesta(p, econ, fuentes), "R5"));
|
|
|
|
p = base(); p.diagnostico.valorProblema.dimensiones[0].calculo.montoAnualMXN = null; p.diagnostico.valorProblema.dimensiones[0].calculo.factores = [];
|
|
check("R5 no se queja si no hay cifra", !tiene(validarPropuesta(p, econ, fuentes), "R5"));
|
|
|
|
p = base();
|
|
p.diagnostico.valorProblema.dimensiones[0].calculo.factores = p.diagnostico.valorProblema.dimensiones[0].calculo.factores.map((f) => ({ ...f, confianza: "estimado" as const }));
|
|
check("R5b avisa si el valor anual no tiene ni un factor confirmado", tiene(validarPropuesta(p, econ, fuentes), "R5b"));
|
|
check("R5b no avisa si hay al menos un confirmado", !tiene(validarPropuesta(base(), econ, fuentes), "R5b"));
|
|
|
|
p = base(); p.redaccion.beneficios[0].hallazgoId = "D99";
|
|
check("R6 detecta un beneficio colgante", tiene(validarPropuesta(p, econ, fuentes), "R6"));
|
|
|
|
p = base(); p.redaccion.beneficios[0].texto = "Lo integramos con HubSpot sin problema alguno.";
|
|
check("MARCA detecta otro CRM", tiene(validarPropuesta(p, econ, fuentes), "MARCA"));
|
|
|
|
p = base(); p.redaccion.hero.subtitulo = "Una inversion de 1,800 USD al mes para tu negocio.";
|
|
check("MONEDA detecta USD", tiene(validarPropuesta(p, econ, fuentes), "MONEDA"));
|
|
|
|
p = base(); p.redaccion.beneficios[0].texto = "Te garantizamos la posicion numero uno en Google.";
|
|
check("GARANTIA detecta la promesa", tiene(validarPropuesta(p, econ, fuentes), "GARANTIA"));
|
|
|
|
p = base(); p.redaccion.hero.subtitulo = "El sueldo de quien atiende sale muy caro hoy en dia.";
|
|
check("PII advierte sobre tema salarial", tiene(validarPropuesta(p, econ, fuentes), "PII"));
|
|
check("PII es advertencia, no bloqueante",
|
|
validarPropuesta(p, econ, fuentes).filter((a) => a.regla === "PII").every((a) => a.severidad === "advertencia"));
|
|
|
|
// ── R0: la fuga de notas internas, que es la razon de que exista ──
|
|
const interno = "el socio pidio descuento antes de entender el alcance del proyecto completo";
|
|
p = base(); p.redaccion.hero.subtitulo = `Contexto del proyecto: ${interno}.`;
|
|
check("R0 detecta la fuga de notas internas", tiene(validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }), "R0"));
|
|
check("R0 es bloqueante",
|
|
validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }).filter((a) => a.regla === "R0").every((a) => a.severidad === "bloqueante"));
|
|
check("R0 no dispara si no hay fuga", !tiene(validarPropuesta(base(), econ, { ...fuentes, textoInterno: interno }), "R0"));
|
|
check("R0 sin notas internas no hace nada", !tiene(validarPropuesta(base(), econ, fuentes), "R0"));
|
|
|
|
// La fuga tambien se detecta en un hallazgo, no solo en el hero.
|
|
p = base(); p.diagnostico.hallazgos[0].descripcion = `Observado: ${interno}.`;
|
|
check("R0 mira todos los campos visibles, no solo el hero",
|
|
tiene(validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }), "R0"));
|
|
|
|
// Las notasInternas del propio objeto NO deben disparar R0: no se imprimen.
|
|
p = base(); p.redaccion.notasInternas = [interno];
|
|
check("R0 no se queja de notasInternas (no se imprimen)",
|
|
!tiene(validarPropuesta(p, econ, { ...fuentes, textoInterno: interno }), "R0"));
|
|
|
|
console.log(fallas === 0 ? "\ntodo paso\n" : `\n${fallas} fallas\n`);
|
|
process.exit(fallas === 0 ? 0 : 1);
|