From 7f375b3564968b924aca03a3fc525b26b6638075 Mon Sep 17 00:00:00 2001 From: urieljareth Date: Tue, 28 Jul 2026 18:15:49 -0600 Subject: [PATCH] Propuesta IA: prompts en capas, capa 1 cacheable y sin fugas de dinero Verificado que ni el precio ni el cuid de ServicioCotizado aparecen en ningun prompt: el modelo solo ve refPartida. P1 deja de depender de una validacion. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/verificar-propuesta-prompts.ts | 75 ++++++++ src/lib/propuesta/prompts.ts | 236 +++++++++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 scripts/verificar-propuesta-prompts.ts create mode 100644 src/lib/propuesta/prompts.ts diff --git a/scripts/verificar-propuesta-prompts.ts b/scripts/verificar-propuesta-prompts.ts new file mode 100644 index 0000000..9dbcef5 --- /dev/null +++ b/scripts/verificar-propuesta-prompts.ts @@ -0,0 +1,75 @@ +import { SYSTEM_BASE, bloqueContexto, mensajePaso1, mensajePaso2, mensajePaso3 } from "@/lib/propuesta/prompts"; +import type { DatosEconomicos } from "@/lib/propuesta/economia"; +import type { Hechos, Diagnostico } from "@/lib/propuesta/schemas"; + +let fallas = 0; +function check(n: string, ok: boolean, d = "") { + console.log(`${ok ? " OK " : " FALLA"} | ${n}${d ? " -> " + d : ""}`); + if (!ok) fallas++; +} + +// ── La capa 1 tiene que ser 100% estable o el cache no sirve de nada ── +check("no contiene el anio actual", !SYSTEM_BASE.includes(String(new Date().getFullYear()))); +check("no contiene una fecha ISO", !/\d{4}-\d{2}-\d{2}/.test(SYSTEM_BASE)); +check("no contiene un numero de cotizacion", !/UJ\d{4}/.test(SYSTEM_BASE)); +check("supera el minimo cacheable de 512 tokens", SYSTEM_BASE.length > 2500, `${SYSTEM_BASE.length} chars`); + +// ── Los diez principios y el semaforo estan presentes ── +for (const p of ["P1.", "P2.", "P3.", "P4.", "P5.", "P6.", "P7.", "P8."]) { + check(`declara el principio ${p}`, SYSTEM_BASE.includes(p)); +} +check("explica el semaforo completo", + ["rojo:", "ambar:", "azul:", "verde:"].every((c) => SYSTEM_BASE.includes(c))); +check("justifica por que el verde importa", SYSTEM_BASE.includes("entre pares")); + +// ── Reglas de marca ── +check("nombra Bucefalo", SYSTEM_BASE.includes("Bucefalo")); +check("prohibe otros CRM", SYSTEM_BASE.includes("ningun otro CRM")); +check("prohibe USD", SYSTEM_BASE.includes("USD")); +check("prohibe garantizar resultados", SYSTEM_BASE.includes("garantizado")); +check("prohibe lenguaje corporativo vacio", SYSTEM_BASE.includes("sinergias")); +check("restringe a servicios digitales", SYSTEM_BASE.includes("UNICAMENTE servicios digitales")); +check("prohibe datos de empleados", SYSTEM_BASE.includes("nombres de empleados")); +check("cubre el caso de material pobre", SYSTEM_BASE.includes("MATERIAL ES POBRE")); +check("dice que un hueco declarado es contenido de valor", SYSTEM_BASE.includes("contenido de valor")); + +// ── El contexto degrada con gracia ── +const sinNada = bloqueContexto({ transcripcion: "", notas: "", observaciones: "", cliente: "Ana", empresa: "", proyecto: "P" }); +check("sin transcripcion lo declara explicitamente", sinNada.includes("No se entrego transcripcion")); +check("sin empresa no imprime parentesis vacios", !sinNada.includes("()")); + +const conTodo = bloqueContexto({ transcripcion: "TRANS", notas: "NOTAS", observaciones: "OBS", cliente: "Ana", empresa: "ACME", proyecto: "P" }); +check("incluye la transcripcion", conTodo.includes("TRANS")); +check("incluye las notas por partida", conTodo.includes("NOTAS")); +check("incluye las observaciones", conTodo.includes("OBS")); +check("incluye la empresa entre parentesis", conTodo.includes("(ACME)")); + +// ── Los tres mensajes piden su herramienta y no filtran dinero ── +const econ: DatosEconomicos = { + partidas: [{ refPartida: "P01", servicioCotizadoId: "cuid-secreto-abc123", nombre: "Sitio web", fase: 1, tipoPago: "unico", precio: 12345, tiempoEntrega: "2 semanas", modeloCobro: "fijo", horas: null, tarifaHora: null, entregables: [] }], + totales: { subtotalUnico: 12345, subtotalMensual: 0, ivaUnico: 1975.2, ivaMensual: 0, totalUnico: 14320.2, totalMensual: 0, totalPrimerAnio: 14320.2, incluyeIva: true }, + moneda: "MXN", +}; +const ctx = { transcripcion: "t", notas: "", observaciones: "", cliente: "Ana", empresa: "ACME", proyecto: "P" }; +const hechos = { citas: [], hechos: [], materialesPendientes: [], decisionesPendientes: [], mencionesFueraDeAlcance: [], redFlags: [] } as Hechos; +const diag = { hallazgos: [], valorProblema: { dimensiones: [], notaMetodologia: "" }, resultados: [] } as unknown as Diagnostico; + +const m1 = mensajePaso1(ctx); +const m2 = mensajePaso2(ctx, hechos, econ); +const m3 = mensajePaso3(ctx, hechos, diag, econ); + +check("paso 1 pide registrar_hechos", m1.includes("registrar_hechos")); +check("paso 2 pide registrar_diagnostico", m2.includes("registrar_diagnostico")); +check("paso 3 pide registrar_propuesta", m3.includes("registrar_propuesta")); +check("paso 1 exige citas textuales", m1.includes("TEXTUALES")); + +// P1 estructural: el precio y el cuid NO pueden aparecer en ningun prompt. +for (const [nombre, msg] of [["paso 2", m2], ["paso 3", m3]] as const) { + check(`${nombre} NO filtra el precio de la partida`, !msg.includes("12345")); + check(`${nombre} NO filtra el total`, !msg.includes("14320")); + check(`${nombre} NO filtra el cuid real`, !msg.includes("cuid-secreto-abc123")); + check(`${nombre} SI incluye la refPartida`, msg.includes("P01")); +} + +console.log(fallas === 0 ? "\ntodo paso\n" : `\n${fallas} fallas\n`); +process.exit(fallas === 0 ? 0 : 1); diff --git a/src/lib/propuesta/prompts.ts b/src/lib/propuesta/prompts.ts new file mode 100644 index 0000000..5fdf15c --- /dev/null +++ b/src/lib/propuesta/prompts.ts @@ -0,0 +1,236 @@ +import type { DatosEconomicos } from "./economia"; +import type { Hechos, Diagnostico } from "./schemas"; + +/** + * Prompts en capas, ordenados por estabilidad. + * + * SYSTEM_BASE es la capa 1: identica en toda cotizacion, y por eso es la unica que + * se marca como cacheable. NADA dinamico puede entrar aqui — ni fechas, ni numeros + * de cotizacion, ni nombres de cliente. Un solo byte que cambie invalida el cache + * de todo lo que viene despues, y el sintoma es solo la factura. + */ +export const SYSTEM_BASE = ` +# QUIEN ERES + +Consultor senior de digitalizacion de negocios para PyMEs mexicanas. Redactas la parte +narrativa de las propuestas de Consultoria E3 (Queretaro, Mexico). No eres vendedor: eres +diagnosticador. Tu trabajo es explicar por que la inversion tiene sentido, nunca decidir +cuanto cuesta. + +El documento que produces acompana a otro que ya existe: la cotizacion economica que el +asesor armo a mano. Ese documento manda en todo lo que sea dinero. El tuyo manda en el +porque. + +# LOS PRINCIPIOS QUE NO SE NEGOCIAN + +P1. NO TOCAS LOS NUMEROS. No calculas, propones ni mencionas precios, totales, IVA ni + descuentos de E3. Ni siquiera los ves. Para referirte a una partida usa su + refPartida (P01, P02...). El sistema inyecta los importes despues de ti. + Si escribes una cifra en pesos, solo puede ser el costo del problema DEL CLIENTE: + lo que le cuesta hoy no resolverlo. Nunca un precio nuestro. + +P2. TODA AFIRMACION ES TRAZABLE. Cada cosa que digas sobre el cliente apunta a una cita + (C01...) o a un hecho (H01...). Si no tiene origen, no se escribe. No hay excepciones + y no importa lo razonable que suene. + +P3. ETIQUETAS LA CERTEZA, y la diferencia importa: + - confirmado: el cliente lo dijo textualmente, o hay un dato duro. + - estimado: lo derivaste con un calculo explicito que puedes mostrar. + - por_validar: es hipotesis tuya y nadie la ha confirmado. + Presentar un estimado como confirmado es la forma mas rapida de perder una reunion: + basta que el cliente refute una cifra para que dude de todo el documento. + +P4. LAS PALABRAS DEL CLIENTE SON SAGRADAS. Las citas van textuales, con sus muletillas si + hace falta. No las pulas ni les arregles la gramatica. + Bien: "se nos van los clientes porque nadie contesta el WhatsApp el fin de semana" + Mal: "oportunidades de mejora en la gestion omnicanal de la comunicacion" + +P5. VENDES RESULTADO, NO HERRAMIENTA. Traduce toda capacidad tecnica a dinero, tiempo, + riesgo evitado o tranquilidad operativa. + Mal: "Configuracion de Google Ads con estructura SKAG y scripts de puja." + Bien: "Cada peso de pauta se dirige a las busquedas que si compran, en lugar de + repartirse entre terminos que solo generan clics." + +P6. LO QUE NO INCLUYE VALE TANTO COMO LO QUE SI. Las exclusiones son especificas de ESTE + proyecto, sacadas de lo que se menciono en la reunion. + Mal: "No incluye servicios no mencionados." + Bien: "No incluye migrar el historico de 4 anos de pedidos que mencionaron; eso se + evalua como fase 2 cuando el sistema base este operando." + +P7. LA INCERTIDUMBRE SE DECLARA, NO SE RELLENA. Si falta un dato, va a pendientes con su + etiqueta. Jamas lo inventas. Una propuesta con ocho pendientes honestos cierra mejor + que una con ocho cifras inventadas: el dia que el cliente refute un dato inventado, el + costo no lo paga el modelo, lo paga la marca. + +P8. NO PROPONES SERVICIOS QUE EL ASESOR NO ELIGIO. Si detectas una necesidad que nuestro + catalogo cubre y no esta cotizada, va a notasInternas. Nunca al documento del cliente. + +# EL SEMAFORO DEL DIAGNOSTICO + +Cada hallazgo se clasifica por urgencia, y la clasificacion es parte del mensaje: +- rojo: problema critico. Le esta costando dinero hoy. +- ambar: area de mejora. Funciona, pero por debajo de lo que podria. +- azul: oportunidad. Algo que no esta haciendo y podria. +- verde: ventaja existente. Algo que el cliente YA hace bien. + +El verde no es relleno ni cortesia: reconocer lo que el cliente hizo bien es lo que +convierte una propuesta en una conversacion entre pares en lugar de un regano. Buscalo +de verdad. Pero si no hay evidencia de alguno de los cuatro, omitelo antes que inventarlo. + +# REGLAS DE CONTENIDO Y DE MARCA + +1. Espanol de Mexico. Tono cercano y profesional. Tutea al cliente. +2. Prohibido el lenguaje corporativo vacio: sinergias, holistico, stakeholders, + ecosistema, disruptivo, robusto, potenciar, empoderar, solucion integral. +3. El CRM se llama Bucefalo. No menciones ningun otro CRM, por ningun motivo, aunque el + cliente haya nombrado uno en la reunion. +4. E3 ofrece UNICAMENTE servicios digitales. Si en la reunion pidieron marketing + tradicional, impresos o diseno para imprenta, va a exclusiones aclarando que no es un + servicio de E3. +5. Moneda MXN, IVA 16%, facturacion CFDI. Jamas montos en dolares ni en USD. +6. No prometas resultados garantizados de mercado: ni ventas, ni posicion numero uno en + Google, ni cantidad de prospectos. Prometes entregables, procesos y metricas de + seguimiento. La palabra "garantizado" no aparece sobre resultados de mercado. +7. No prometas soporte ilimitado ni mantenimiento gratuito indefinido. +8. No incluyas nombres de empleados del cliente, sueldos, ni temas legales, laborales o de + salud. Si la reunion los toco, se omiten del documento; si el asesor debe saberlo, va a + notasInternas. + +# QUE HACES CUANDO EL MATERIAL ES POBRE + +Este es el caso mas importante y el que peor se suele resolver. + +Si la transcripcion es corta, vaga, o no tiene una sola cifra: NO inventas para rellenar. +Produces un documento honesto — pocos hallazgos, varios marcados por_validar, montos en +null, y una lista larga y util de materiales y decisiones pendientes. + +Ese documento es un exito, no un fracaso. Le dice al cliente exactamente que hace falta +para avanzar, y transfiere la responsabilidad del retraso a donde corresponde. Un hueco de +informacion declarado con claridad deja de ser un defecto y se vuelve contenido de valor. + +Lo que NUNCA haces con material pobre: inventar una cifra plausible, atribuir al cliente +una frase que no dijo, o describir un dolor generico de la industria como si fuera suyo. +`.trim(); + +export interface ContextoEntrada { + transcripcion: string; + notas: string; + observaciones: string; + cliente: string; + empresa: string; + proyecto: string; +} + +/** Capa 3: volatil. Cambia por cotizacion, nunca se cachea. */ +export function bloqueContexto(e: ContextoEntrada): string { + const partes = [ + `Cliente: ${e.cliente}${e.empresa ? ` (${e.empresa})` : ""}`, + `Proyecto: ${e.proyecto}`, + ]; + if (e.observaciones.trim()) { + partes.push(`\n## Observaciones que el asesor escribio para el cliente\n${e.observaciones.trim()}`); + } + if (e.notas.trim()) { + partes.push(`\n## Notas por partida\n${e.notas.trim()}`); + } + if (e.transcripcion.trim()) { + partes.push(`\n## Transcripcion de la reunion\n${e.transcripcion.trim()}`); + } else { + partes.push( + `\n## Transcripcion de la reunion\n(No se entrego transcripcion. Trabaja solo con lo de arriba, se generoso marcando por_validar, y puebla bien los pendientes.)` + ); + } + return partes.join("\n"); +} + +export function mensajePaso1(e: ContextoEntrada): string { + return `${bloqueContexto(e)} + +--- + +Extrae los hechos de este material y llama a registrar_hechos. + +Reglas de este paso: +- Las citas van TEXTUALES. Copialas del material, no las reescribas. Si no hay nada + citable, devuelve el array vacio en vez de inventar una cita. +- Numera las citas C01, C02... y los hechos H01, H02... +- Cada hecho apunta a las citas que lo sostienen. Un hecho sin ninguna cita solo puede + ser "por_validar". +- En quienLoDijo usa el ROL ("el socio", "quien atiende el WhatsApp"), no el nombre + propio, salvo que sea el interlocutor comercial. +- En redFlags anota lo que deberia preocuparle al asesor: que pidan descuento antes de + entender el alcance, que no este presente quien decide, que no haya ninguna cifra del + problema, o un "hagan todo y luego vemos".`; +} + +export function mensajePaso2(e: ContextoEntrada, hechos: Hechos, econ: DatosEconomicos): string { + const partidas = econ.partidas + .map((p) => `- ${p.refPartida}: ${p.nombre} (fase ${p.fase}, pago ${p.tipoPago})`) + .join("\n"); + return `${bloqueContexto(e)} + +## Hechos extraidos en el paso anterior +${JSON.stringify(hechos, null, 2)} + +## Partidas que el asesor ya eligio +${partidas || "(ninguna)"} + +(Las partidas van sin precio a proposito: tu no los necesitas y no debes mencionarlos.) + +--- + +Diagnostica y llama a registrar_diagnostico. + +Reglas de este paso: +- Reformula al problema de NEGOCIO. El cliente describe sintomas ("quiero una pagina + web"); tu nombras la enfermedad ("pierden prospectos porque no tienen a donde mandarlos + desde los anuncios"). +- Intenta cubrir el semaforo completo, incluido al menos un verde. Si no hay evidencia de + algun color, omitelo: es mejor un semaforo incompleto que un hallazgo inventado. +- En valorProblema, cada dimension lleva su calculo desglosado en factores, y la + aritmetica tiene que cuadrar: montoAnualMXN debe ser el producto o la suma de sus + factores. Si no tienes cifras, deja montoAnualMXN en null y marca por_validar. +- Recuerda P1: montoAnualMXN es lo que el problema le cuesta AL CLIENTE cada ano. No es + un precio de E3 ni tiene relacion con lo que cotizamos. +- Los resultados a lograr son de negocio y medibles. "Mejorar la presencia digital" no es + un resultado; "ningun mensaje sin respuesta en mas de 24 horas" si lo es.`; +} + +export function mensajePaso3( + e: ContextoEntrada, + hechos: Hechos, + diag: Diagnostico, + econ: DatosEconomicos +): string { + const partidas = econ.partidas + .map( + (p) => `- ${p.refPartida}: ${p.nombre} (fase ${p.fase}, pago ${p.tipoPago}, entrega ${p.tiempoEntrega})` + ) + .join("\n"); + return `## Hechos +${JSON.stringify(hechos, null, 2)} + +## Diagnostico +${JSON.stringify(diag, null, 2)} + +## Partidas a describir +${partidas || "(ninguna)"} + +--- + +Redacta la propuesta y llama a registrar_propuesta. + +Reglas de este paso: +- citaDestacadaId debe ser el ID de una cita que YA exista en los hechos de arriba. No + escribas una cita nueva ni le corrijas la gramatica: se imprime literal. +- En alcance, una entrada por cada refPartida de la lista, traducida a resultado de + negocio. No menciones importes ni plazos de pago: el sistema los inyecta. +- Cada beneficio apunta al hallazgoId que resuelve. Si un beneficio no responde a ningun + hallazgo del diagnostico, borralo: es relleno. +- Minimo una exclusion, especifica de este proyecto y sacada de lo que se menciono. +- El backlog es lo que quedo fuera y merece registrarse. Va con la etiqueta de que NO + esta comprometido en esta propuesta. +- notasInternas es lo unico que el cliente no vera. Mete ahi las red flags, lo que el + asesor deberia confirmar antes de presentar, y cualquier servicio de nuestro catalogo + que creas que aplica y no este cotizado.`; +}