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:
103
api-gateway/scripts/actualizar-catalogos-ia.js
Normal file
103
api-gateway/scripts/actualizar-catalogos-ia.js
Normal 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)
|
||||
})
|
||||
Reference in New Issue
Block a user