Propuesta IA: schemas Zod de los tres pasos, sin campos de dinero de E3

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
urieljareth
2026-07-28 18:12:54 -06:00
co-authored by Claude Opus 5
parent 4e103beb27
commit 5bad273977
2 changed files with 275 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
import { z } from "zod";
/**
* Contrato con el modelo. MiniMax no soporta structured outputs, asi que estos
* schemas viajan como `input_schema` de una herramienta y Zod valida la respuesta.
*
* REGLA ESTRUCTURAL: ningun schema de aqui contiene un campo de dinero DE E3.
* El modelo devuelve `refPartida` y prosa; los precios, totales e IVA los inyecta
* el codigo al renderizar. `montoAnualMXN` si existe, pero es lo que el problema
* le cuesta AL CLIENTE cada ano — no un precio nuestro.
*/
const REF_PARTIDA = /^P\d{2}$/;
const ID_CITA = /^C\d{2}$/;
const ID_HECHO = /^H\d{2}$/;
const ID_HALLAZGO = /^D\d{2}$/;
export const CONFIANZA = ["confirmado", "estimado", "por_validar"] as const;
export const URGENCIA = ["rojo", "ambar", "azul", "verde"] as const;
export const DIMENSION_COSTO = ["dinero", "tiempo", "oportunidad", "error"] as const;
// ───────────────────── Paso 1: extraccion de hechos ─────────────────────
export const hechosSchema = z
.object({
citas: z
.array(
z.object({
id: z.string().regex(ID_CITA, "El id de cita debe ser C01, C02..."),
textoLiteral: z
.string()
.min(10)
.describe("Cita TEXTUAL del cliente, palabra por palabra. Nunca parafraseada ni corregida."),
quienLoDijo: z
.string()
.describe("Rol del interlocutor comercial ('el socio', 'quien atiende el WhatsApp'). No nombres de empleados."),
})
)
.max(30),
hechos: z
.array(
z.object({
id: z.string().regex(ID_HECHO, "El id de hecho debe ser H01, H02..."),
enunciado: z.string().min(10),
confianza: z.enum(CONFIANZA),
citas: z.array(z.string().regex(ID_CITA)).describe("IDs de las citas que sostienen este hecho."),
})
)
.max(40),
materialesPendientes: z
.array(z.object({ texto: z.string().min(5), bloqueaEntrega: z.boolean() }))
.max(15),
decisionesPendientes: z
.array(z.object({ texto: z.string().min(5), quienDecide: z.string() }))
.max(15),
mencionesFueraDeAlcance: z.array(z.string().min(5)).max(15),
redFlags: z
.array(z.object({ senal: z.string().min(5), severidad: z.enum(["baja", "media", "alta"]) }))
.max(10),
})
.strict();
// ─────────────── Paso 2: diagnostico y valoracion del problema ───────────────
export const diagnosticoSchema = z
.object({
hallazgos: z
.array(
z.object({
id: z.string().regex(ID_HALLAZGO, "El id de hallazgo debe ser D01, D02..."),
urgencia: z
.enum(URGENCIA)
.describe("rojo=problema critico, ambar=area de mejora, azul=oportunidad, verde=ventaja que el cliente YA tiene"),
titulo: z.string().min(3).max(80),
descripcion: z.string().min(20),
confianza: z.enum(CONFIANZA),
citas: z.array(z.string().regex(ID_CITA)),
hechos: z.array(z.string().regex(ID_HECHO)),
})
)
.min(1)
.max(12),
valorProblema: z.object({
dimensiones: z
.array(
z.object({
tipo: z.enum(DIMENSION_COSTO),
descripcion: z.string().min(10),
calculo: z.object({
factores: z.array(z.object({ nombre: z.string(), valor: z.number() })).min(2),
montoAnualMXN: z
.number()
.nullable()
.describe("Costo ANUAL del problema DEL CLIENTE. No es un precio de E3. null si no hay cifras."),
}),
confianza: z.enum(CONFIANZA),
hechos: z.array(z.string().regex(ID_HECHO)),
})
)
.max(4),
notaMetodologia: z.string().describe("Como se llego a las cifras, en una o dos frases."),
}),
resultados: z
.array(
z.object({
enunciado: z.string().min(15),
metrica: z.string(),
lineaBase: z.string().nullable(),
periodoMedicion: z.string(),
})
)
.max(5),
})
.strict();
// ───────────────────────── Paso 3: redaccion final ─────────────────────────
export const redaccionSchema = z
.object({
hero: z.object({
titulo: z.string().min(5).max(90),
subtitulo: z.string().min(20).max(300),
}),
citaDestacadaId: z
.string()
.regex(ID_CITA)
.describe("ID de una cita YA extraida en el paso 1. Se imprime literal; no la reescribas."),
alcance: z
.array(
z.object({
refPartida: z.string().regex(REF_PARTIDA, "Debe ser P01, P02..."),
descripcionResultado: z
.string()
.min(20)
.describe("La partida traducida a resultado de negocio. Sin jerga tecnica y SIN mencionar importes."),
})
)
.max(40),
beneficios: z
.array(
z.object({
etiqueta: z.string().min(3).max(40),
texto: z.string().min(20),
hallazgoId: z
.string()
.regex(ID_HALLAZGO)
.describe("El hallazgo que este beneficio resuelve. Si no responde a ninguno, no lo incluyas."),
})
)
.max(8),
exclusiones: z
.array(
z.object({
texto: z.string().min(15).describe("Especifica de ESTE proyecto, nunca generica."),
razon: z.string().min(5),
})
)
.min(1)
.max(10),
backlogEvolucion: z
.array(z.object({ problema: z.string().min(10), momentoSugerido: z.string() }))
.max(8),
notasInternas: z
.array(z.string().min(5))
.max(10)
.describe("SOLO para el asesor. Nunca se imprime en el documento del cliente."),
})
.strict();
export type Hechos = z.infer<typeof hechosSchema>;
export type Diagnostico = z.infer<typeof diagnosticoSchema>;
export type Redaccion = z.infer<typeof redaccionSchema>;
/** El objeto completo que se persiste, se edita y se renderiza. */
export interface PropuestaConsultiva {
hechos: Hechos;
diagnostico: Diagnostico;
redaccion: Redaccion;
}
/** Schema del objeto completo, para validar lo que el asesor edita a mano. */
export const propuestaCompletaSchema = z.object({
hechos: hechosSchema,
diagnostico: diagnosticoSchema,
redaccion: redaccionSchema,
});
/**
* Zod -> JSON Schema para el `input_schema` de la herramienta.
* zod 4.3.6 trae `z.toJSONSchema` nativo: no hace falta `zod-to-json-schema`.
* Se borra `$schema` porque el endpoint no lo espera.
*/
export function aJsonSchema(schema: z.ZodType): Record<string, unknown> {
const js = z.toJSONSchema(schema, { io: "input" }) as Record<string, unknown>;
delete js.$schema;
return js;
}