Seis vueltas completas del pipeline contra los datos reales de UJ2606UR001
mostraron que la mayoria de los reintentos los provocaba el propio schema, no
el modelo:
- alcance.max(40) contra una cotizacion de 58 partidas garantizaba un rechazo de
Zod en CADA corrida. Sube a 120. El tope solo acota una respuesta desbocada;
la completitud del documento ya no depende de este array desde d20007d2.
- titulo.max(80) se quedaba corto y costo un reintento en 2 de 6 corridas.
Sube a 140.
Y dos rarezas del proveedor, ambas observadas contra la API real:
- MiniMax a veces envuelve los elementos de un array en {item: {...}}. Se
normaliza antes de validar, solo cuando "item" es la unica clave, para no
tocar un campo legitimo con ese nombre.
- A veces emite DOS bloques tool_use en una respuesta. Antes se tomaba el
primero a secas; ahora se prueban todos y gana el que valide.
Sobre el error de tipo de documento que se vio en produccion: NO se reprodujo en
seis corridas completas contra los mismos datos, y la generacion que lo siguio
completo sin problema (la fila quedo en la tabla). La evidencia apunta a un
fallo transitorio del proveedor, no a un defecto determinista nuestro. En vez de
inventar un arreglo para algo que no se puede reproducir, se reintentan los
fallos transitorios (5xx, 429, timeouts, red y los 400 con mensaje de parseo
interno) con espera creciente.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
114 lines
5.2 KiB
TypeScript
114 lines
5.2 KiB
TypeScript
/**
|
|
* Caza del error intermitente: corre el pipeline real contra UJ2606UR001 hasta que
|
|
* falle, e imprime TODO lo que sepamos de la peticion que lo provoco.
|
|
*/
|
|
import { cargarEconomia } from "@/lib/propuesta/economia";
|
|
import { crearCliente, modelo } from "@/lib/propuesta/cliente-ia";
|
|
import { hechosSchema, diagnosticoSchema, redaccionSchema, aJsonSchema, type Hechos, type Diagnostico } from "@/lib/propuesta/schemas";
|
|
import { SYSTEM_BASE, mensajePaso1, mensajePaso2, mensajePaso3, type ContextoEntrada } from "@/lib/propuesta/prompts";
|
|
import { prisma } from "@/lib/db";
|
|
import type Anthropic from "@anthropic-ai/sdk";
|
|
import type { z } from "zod";
|
|
import fs from "node:fs";
|
|
|
|
const LOG = process.argv[2];
|
|
// Escritura directa: el buffer de stdout se pierde si el proceso muere.
|
|
function log(...a: unknown[]) {
|
|
const linea = a.map((x) => (typeof x === "string" ? x : JSON.stringify(x))).join(" ");
|
|
fs.appendFileSync(LOG, linea + "\n");
|
|
}
|
|
const console = { log } as unknown as Console;
|
|
|
|
const NUMERO = "UJ2606UR001";
|
|
const TRANSCRIPCION = "Una propuesta de zero to hero para un negocio inicial";
|
|
|
|
/** Igual que llamarConHerramienta pero registra cada peticion y explota con detalle. */
|
|
async function correrPaso(nombre: string, msg: string, schema: z.ZodType, maxTokens: number, maxIntentos: number) {
|
|
const cliente = crearCliente();
|
|
const tools = [{ name: nombre, description: "Registra el resultado.", input_schema: aJsonSchema(schema) as never }];
|
|
const mensajes: Anthropic.MessageParam[] = [{ role: "user", content: msg }];
|
|
|
|
for (let intento = 1; intento <= maxIntentos; intento++) {
|
|
let res: Anthropic.Message;
|
|
try {
|
|
res = await cliente.messages.create({
|
|
model: modelo(), max_tokens: maxTokens,
|
|
system: [{ type: "text", text: SYSTEM_BASE, cache_control: { type: "ephemeral" } }],
|
|
tools, messages: mensajes,
|
|
});
|
|
} catch (e) {
|
|
const err = e as { status?: number; message?: string; error?: unknown };
|
|
console.log(`\n*** ERROR EN ${nombre} intento ${intento} ***`);
|
|
console.log(" status:", err.status);
|
|
console.log(" message:", String(err.message).slice(0, 600));
|
|
if (err.error) console.log(" error:", JSON.stringify(err.error).slice(0, 800));
|
|
console.log("\n --- FORMA DE LOS MENSAJES ENVIADOS ---");
|
|
mensajes.forEach((m, i) => {
|
|
const c = m.content;
|
|
const tipos = typeof c === "string" ? "string" : (c as { type: string }[]).map((b) => b.type).join(",");
|
|
console.log(` [${i}] role=${m.role} bloques=${tipos}`);
|
|
});
|
|
throw new Error("REPRODUCIDO");
|
|
}
|
|
|
|
const bloques = res.content.map((b) => b.type).join(",");
|
|
console.log(` ${nombre} intento ${intento}: stop=${res.stop_reason} bloques=[${bloques}] out=${res.usage.output_tokens}`);
|
|
|
|
const tus = res.content.filter((b): b is Anthropic.ToolUseBlock => b.type === "tool_use");
|
|
const ok = tus.find((b) => b.name === nombre);
|
|
if (ok) {
|
|
const parsed = schema.safeParse(ok.input);
|
|
if (parsed.success) return parsed.data;
|
|
const errTxt = (await import("zod")).z.prettifyError(parsed.error);
|
|
console.log(` zod rechazo: ${errTxt.split("\n")[0]}`);
|
|
mensajes.push(
|
|
{ role: "assistant", content: res.content },
|
|
{ role: "user", content: [
|
|
...tus.map((b) => ({ type: "tool_result" as const, tool_use_id: b.id, is_error: true, content: errTxt })),
|
|
{ type: "text" as const, text: "Corrige y reintenta." },
|
|
] },
|
|
);
|
|
continue;
|
|
}
|
|
mensajes.push(
|
|
{ role: "assistant", content: res.content },
|
|
{ role: "user", content: `Debes llamar a ${nombre}.` },
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function unaVuelta(entrada: ContextoEntrada, econ: Awaited<ReturnType<typeof cargarEconomia>>, n: number) {
|
|
console.log(`\n===== VUELTA ${n} =====`);
|
|
const h = (await correrPaso("registrar_hechos", mensajePaso1(entrada), hechosSchema, 8000, 5)) as Hechos | null;
|
|
if (!h) { console.log(" extraccion agoto intentos"); return; }
|
|
const d = (await correrPaso("registrar_diagnostico", mensajePaso2(entrada, h, econ), diagnosticoSchema, 8000, 4)) as Diagnostico | null;
|
|
if (!d) { console.log(" diagnostico agoto intentos"); return; }
|
|
const r = await correrPaso("registrar_propuesta", mensajePaso3(entrada, h, d, econ), redaccionSchema, 12000, 4);
|
|
console.log(r ? " vuelta completa OK" : " redaccion agoto intentos");
|
|
}
|
|
|
|
async function main() {
|
|
const cot = await prisma.cotizacion.findFirst({ where: { numero: NUMERO }, include: { cliente: true, servicios: true } });
|
|
if (!cot) throw new Error("no encontrada");
|
|
const econ = await cargarEconomia(cot.id);
|
|
const entrada: ContextoEntrada = {
|
|
transcripcion: TRANSCRIPCION,
|
|
notas: cot.servicios.map((s) => s.notas).filter(Boolean).join("\n"),
|
|
observaciones: cot.observaciones || "",
|
|
cliente: cot.cliente.nombre, empresa: cot.cliente.empresa || "", proyecto: cot.proyecto,
|
|
};
|
|
console.log(`${econ.partidas.length} partidas`);
|
|
|
|
for (let i = 1; i <= 3; i++) {
|
|
try { await unaVuelta(entrada, econ, i); }
|
|
catch (e) {
|
|
if (e instanceof Error && e.message === "REPRODUCIDO") { console.log("\n>>> error reproducido, deteniendo"); break; }
|
|
throw e;
|
|
}
|
|
}
|
|
await prisma.$disconnect();
|
|
}
|
|
|
|
main().catch((e) => { console.error("fallo:", e); process.exit(1); });
|