#!/usr/bin/env node /** * Regenera los catálogos de unidades de la base de conocimiento del asistente * a partir del INVENTARIO VIVO (tabla Unidad), para que el chat nunca conteste * con un catálogo viejo. * * Uso: node scripts/actualizar-catalogos-ia.js * Requiere NAN_API_KEY (embeddings) en el .env del ecosistema. * * Idempotente: indexarDocumento re-embebe solo si el contenido cambió. */ import { PrismaClient } from '@prisma/client' import { readFileSync } from 'fs' import { indexarDocumento } from '../dist/services/ai.service.js' const prisma = new PrismaClient() // Carga el .env del ecosistema (NAN_API_KEY / DATABASE_URL) si no vienen del entorno for (const ruta of ['../.env', '../api-gateway/.env']) { try { const env = readFileSync(new URL(ruta, import.meta.url), 'utf8') for (const linea of env.split('\n')) { const m = linea.match(/^([A-Z_]+)=(.*)$/) if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim().replace(/^["']|["']$/g, '') } } catch { /* ignorar */ } } const fmtMoneda = (n) => `$${Math.round(n).toLocaleString('es-MX')}` /** Markdown del catálogo de un desarrollo: modelos, disponibilidad y precios */ function catalogoDe(desarrollo, unidades) { const porModelo = new Map() for (const u of unidades) { const modelo = u.modelo || '(sin modelo)' const lista = porModelo.get(modelo) ?? [] lista.push(u) porModelo.set(modelo, lista) } const conteoEstatus = {} for (const u of unidades) conteoEstatus[u.status] = (conteoEstatus[u.status] ?? 0) + 1 const lineas = [ `# Catálogo de unidades — Desarrollo ${desarrollo}`, '', `Inventario actualizado desde el sistema (${unidades.length} unidades).`, '', '## Disponibilidad por estatus', ...Object.entries(conteoEstatus).map(([estatus, n]) => `- ${estatus}: ${n} unidades`), '', '## Modelos y precios', ] for (const [modelo, lista] of [...porModelo.entries()].sort()) { const disponibles = lista.filter((u) => u.status === 'DISPONIBLE') const precios = lista.map((u) => u.precioLista).filter((p) => p > 0) const min = precios.length > 0 ? Math.min(...precios) : 0 const max = precios.length > 0 ? Math.max(...precios) : 0 lineas.push( `- **${modelo}**: ${lista.length} unidades (${disponibles.length} disponibles). ` + (precios.length > 0 ? `Precio de lista ${fmtMoneda(min)} a ${fmtMoneda(max)}.` : 'Sin precio capturado.'), ) if (disponibles.length > 0) { const codigos = disponibles.slice(0, 12).map((u) => u.codigo).join(', ') lineas.push(` - Disponibles: ${codigos}${disponibles.length > 12 ? ` (y ${disponibles.length - 12} más)` : ''}`) } } lineas.push( '', '## Notas para el asistente', '- Los precios son de lista, sin acabados (cocina y closet se cotizan aparte).', '- Para los números de una unidad concreta (enganche, mensualidades, pago final) usa la herramienta cotizar_unidad.', '- Para disponibilidad exacta en este momento usa buscar_unidad o contar_unidades: este documento es un resumen.', ) return lineas.join('\n') } async function main() { const unidades = await prisma.unidad.findMany({ orderBy: [{ desarrollo: 'asc' }, { modelo: 'asc' }, { codigo: 'asc' }] }) if (unidades.length === 0) { console.error('No hay unidades en el inventario local; nada que regenerar.') process.exit(1) } const porDesarrollo = new Map() for (const u of unidades) { const lista = porDesarrollo.get(u.desarrollo) ?? [] lista.push(u) porDesarrollo.set(u.desarrollo, lista) } console.log(`Regenerando catálogos de ${porDesarrollo.size} desarrollo(s)…`) for (const [desarrollo, lista] of [...porDesarrollo.entries()].sort()) { const r = await indexarDocumento(`Catálogo de unidades — Desarrollo ${desarrollo}`, catalogoDe(desarrollo, lista)) console.log(` ${r.estado.padEnd(12)} ${r.titulo} (${r.chunks} chunks)`) } console.log('Listo.') await prisma.$disconnect() } main().catch((err) => { console.error('Falló:', err instanceof Error ? err.message : err) process.exit(1) })