/** * 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>, 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); });