feat(ai): agente IA interno con RAG sobre NAN + app web en /ai

- API: /api/ai/chat, /conversations, /documents con embeddings qwen3-embedding,
  coseno en SQLite, dedup por hash, historial de conversaciones
- Base de conocimiento semilla (6 docs, 36 chunks): catálogo DIAGONAL,
  propuestas del cotizador, créditos, proceso de venta, FAQ, privacidad
- App agente-ia (Vite/React/Tailwind, StudioBlank) servida en /ai/ vía nginx
- 21 tests nuevos (91/91 verdes)
This commit is contained in:
2026-08-22 03:04:47 +00:00
parent 8407fc24c3
commit 05c44478ec
32 changed files with 5448 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
/**
* Seed de la base de conocimiento del agente IA.
* Lee knowledge/*.md, usa el primer encabezado H1 como título y los indexa
* vía indexarDocumento (idempotente: solo re-embebe si cambió el hash).
*
* Uso: npm run seed:ai (requiere NAN_API_KEY en el .env del ecosistema)
*/
import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import { indexarDocumento } from '../src/services/ai.service.js'
const KNOWLEDGE_DIR = path.resolve(process.cwd(), 'knowledge')
function tituloDe(contenido: string, archivo: string): string {
const h1 = contenido.split('\n').find((l) => l.startsWith('# '))
return h1 ? h1.replace(/^#\s+/, '').trim() : archivo.replace(/\.md$/, '')
}
async function main(): Promise<void> {
const archivos = (await readdir(KNOWLEDGE_DIR)).filter((f) => f.endsWith('.md')).sort()
if (archivos.length === 0) {
console.error(`No hay archivos .md en ${KNOWLEDGE_DIR}`)
process.exit(1)
}
console.log(`Indexando ${archivos.length} documentos desde ${KNOWLEDGE_DIR}…`)
let totalChunks = 0
for (const archivo of archivos) {
const contenido = await readFile(path.join(KNOWLEDGE_DIR, archivo), 'utf8')
const titulo = tituloDe(contenido, archivo)
const r = await indexarDocumento(titulo, contenido)
totalChunks += r.chunks
console.log(` ${r.estado.padEnd(12)} ${titulo} (${r.chunks} chunks)`)
}
console.log(`Listo: ${archivos.length} documentos procesados.`)
process.exit(0)
}
main().catch((err) => {
console.error('Falló el seed:', err instanceof Error ? err.message : err)
process.exit(1)
})