feat(chat-frasa): acceso total a datos (13 herramientas con escopo por rol), streaming SSE, caché de embeddings, markdown y mantenimiento en Configuraciones

This commit is contained in:
2026-09-17 08:41:21 +00:00
parent 278a79c984
commit 9643f866cc
17 changed files with 3133 additions and 177 deletions

View File

@@ -0,0 +1,103 @@
#!/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)
})