@ssolis-ti/mcp-compra-agil 1.0.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -23
- package/dist/api/compra-agil-client.d.ts +37 -2
- package/dist/api/compra-agil-client.d.ts.map +1 -1
- package/dist/api/compra-agil-client.js +76 -5
- package/dist/api/compra-agil-client.js.map +1 -1
- package/dist/index.js +32 -8
- package/dist/index.js.map +1 -1
- package/dist/reports/components.d.ts +59 -0
- package/dist/reports/components.d.ts.map +1 -0
- package/dist/reports/components.js +125 -0
- package/dist/reports/components.js.map +1 -0
- package/dist/reports/export.d.ts +27 -0
- package/dist/reports/export.d.ts.map +1 -0
- package/dist/reports/export.js +47 -0
- package/dist/reports/export.js.map +1 -0
- package/dist/reports/format.d.ts +24 -0
- package/dist/reports/format.d.ts.map +1 -0
- package/dist/reports/format.js +74 -0
- package/dist/reports/format.js.map +1 -0
- package/dist/reports/render.d.ts +18 -0
- package/dist/reports/render.d.ts.map +1 -0
- package/dist/reports/render.js +30 -0
- package/dist/reports/render.js.map +1 -0
- package/dist/reports/templates/radar-oportunidades.d.ts +41 -0
- package/dist/reports/templates/radar-oportunidades.d.ts.map +1 -0
- package/dist/reports/templates/radar-oportunidades.js +170 -0
- package/dist/reports/templates/radar-oportunidades.js.map +1 -0
- package/dist/reports/theme.d.ts +83 -0
- package/dist/reports/theme.d.ts.map +1 -0
- package/dist/reports/theme.js +284 -0
- package/dist/reports/theme.js.map +1 -0
- package/dist/resources/compras-template.d.ts.map +1 -1
- package/dist/resources/compras-template.js +2 -1
- package/dist/resources/compras-template.js.map +1 -1
- package/dist/resources/documentacion.d.ts.map +1 -1
- package/dist/resources/documentacion.js +5 -36
- package/dist/resources/documentacion.js.map +1 -1
- package/dist/resources/estados.d.ts.map +1 -1
- package/dist/resources/estados.js +24 -10
- package/dist/resources/estados.js.map +1 -1
- package/dist/services/monitor.js +38 -3
- package/dist/services/monitor.js.map +1 -1
- package/dist/tools/analizar-precios-mercado.d.ts +21 -0
- package/dist/tools/analizar-precios-mercado.d.ts.map +1 -0
- package/dist/tools/analizar-precios-mercado.js +217 -0
- package/dist/tools/analizar-precios-mercado.js.map +1 -0
- package/dist/tools/auditar-desiertas.d.ts.map +1 -1
- package/dist/tools/auditar-desiertas.js +66 -48
- package/dist/tools/auditar-desiertas.js.map +1 -1
- package/dist/tools/buscar-compras.d.ts.map +1 -1
- package/dist/tools/buscar-compras.js +2 -1
- package/dist/tools/buscar-compras.js.map +1 -1
- package/dist/tools/detalle-compra.d.ts.map +1 -1
- package/dist/tools/detalle-compra.js +6 -5
- package/dist/tools/detalle-compra.js.map +1 -1
- package/dist/tools/detalle-oc.js +6 -6
- package/dist/tools/detalle-oc.js.map +1 -1
- package/dist/tools/documentos.d.ts.map +1 -1
- package/dist/tools/documentos.js +75 -81
- package/dist/tools/documentos.js.map +1 -1
- package/dist/tools/estadisticas-uso.d.ts.map +1 -1
- package/dist/tools/estadisticas-uso.js +12 -2
- package/dist/tools/estadisticas-uso.js.map +1 -1
- package/dist/tools/generar-borrador.d.ts +25 -1
- package/dist/tools/generar-borrador.d.ts.map +1 -1
- package/dist/tools/generar-borrador.js +111 -82
- package/dist/tools/generar-borrador.js.map +1 -1
- package/dist/tools/generar-informe.d.ts +13 -0
- package/dist/tools/generar-informe.d.ts.map +1 -0
- package/dist/tools/generar-informe.js +111 -0
- package/dist/tools/generar-informe.js.map +1 -0
- package/dist/tools/monitorear-cambios.d.ts +32 -2
- package/dist/tools/monitorear-cambios.d.ts.map +1 -1
- package/dist/tools/monitorear-cambios.js +79 -10
- package/dist/tools/monitorear-cambios.js.map +1 -1
- package/dist/tools/radar-oportunidades.d.ts +42 -1
- package/dist/tools/radar-oportunidades.d.ts.map +1 -1
- package/dist/tools/radar-oportunidades.js +123 -103
- package/dist/tools/radar-oportunidades.js.map +1 -1
- package/dist/tools/recomendar-precio.d.ts +13 -0
- package/dist/tools/recomendar-precio.d.ts.map +1 -1
- package/dist/tools/recomendar-precio.js +53 -57
- package/dist/tools/recomendar-precio.js.map +1 -1
- package/dist/tools/verificar-oc.d.ts.map +1 -1
- package/dist/tools/verificar-oc.js +36 -8
- package/dist/tools/verificar-oc.js.map +1 -1
- package/dist/tools/verificar-ticket.d.ts +13 -0
- package/dist/tools/verificar-ticket.d.ts.map +1 -0
- package/dist/tools/verificar-ticket.js +79 -0
- package/dist/tools/verificar-ticket.js.map +1 -0
- package/dist/utils/cache.d.ts +53 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +131 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/doc-search.d.ts +52 -0
- package/dist/utils/doc-search.d.ts.map +1 -0
- package/dist/utils/doc-search.js +97 -0
- package/dist/utils/doc-search.js.map +1 -0
- package/dist/utils/docs-locator.d.ts +41 -0
- package/dist/utils/docs-locator.d.ts.map +1 -0
- package/dist/utils/docs-locator.js +82 -0
- package/dist/utils/docs-locator.js.map +1 -0
- package/dist/utils/error-handler.d.ts.map +1 -1
- package/dist/utils/error-handler.js +8 -7
- package/dist/utils/error-handler.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +9 -6
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/quotation.d.ts +71 -0
- package/dist/utils/quotation.d.ts.map +1 -0
- package/dist/utils/quotation.js +140 -0
- package/dist/utils/quotation.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +44 -2
- package/dist/utils/rate-limiter.d.ts.map +1 -1
- package/dist/utils/rate-limiter.js +125 -13
- package/dist/utils/rate-limiter.js.map +1 -1
- package/dist/utils/redact.d.ts +48 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/redact.js +100 -0
- package/dist/utils/redact.js.map +1 -0
- package/package.json +55 -53
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: verificar_ticket
|
|
3
|
+
*
|
|
4
|
+
* Comprueba que el ticket configurado funciona contra la API real,
|
|
5
|
+
* SIN revelar su valor. Solo muestra una pista (últimos 4 caracteres).
|
|
6
|
+
*
|
|
7
|
+
* Existe para que la primera prueba end-to-end no obligue a nadie a imprimir,
|
|
8
|
+
* pegar o compartir la credencial.
|
|
9
|
+
*/
|
|
10
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
import { CompraAgilClient } from '../api/compra-agil-client.js';
|
|
12
|
+
export declare function registerVerificarTicket(server: McpServer, client: CompraAgilClient): void;
|
|
13
|
+
//# sourceMappingURL=verificar-ticket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verificar-ticket.d.ts","sourceRoot":"","sources":["../../src/tools/verificar-ticket.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAUhE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAmEzF"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: verificar_ticket
|
|
3
|
+
*
|
|
4
|
+
* Comprueba que el ticket configurado funciona contra la API real,
|
|
5
|
+
* SIN revelar su valor. Solo muestra una pista (últimos 4 caracteres).
|
|
6
|
+
*
|
|
7
|
+
* Existe para que la primera prueba end-to-end no obligue a nadie a imprimir,
|
|
8
|
+
* pegar o compartir la credencial.
|
|
9
|
+
*/
|
|
10
|
+
import { CompraAgilApiError } from '../utils/error-handler.js';
|
|
11
|
+
import { safeError, pista } from '../utils/redact.js';
|
|
12
|
+
const TOOL_NAME = 'verificar_ticket';
|
|
13
|
+
const TOOL_DESCRIPTION = `Verifica que el ticket de acceso a la API de Mercado Público esté configurado y sea válido, realizando una consulta mínima de prueba.
|
|
14
|
+
NUNCA revela el valor del ticket: solo informa si funciona y muestra los últimos 4 caracteres como referencia.
|
|
15
|
+
Úsala como primer diagnóstico cuando otras herramientas fallen con errores de autenticación.`;
|
|
16
|
+
export function registerVerificarTicket(server, client) {
|
|
17
|
+
server.registerTool(TOOL_NAME, { description: TOOL_DESCRIPTION }, async () => {
|
|
18
|
+
const referencia = pista(process.env.COMPRA_AGIL_TICKET);
|
|
19
|
+
if (!process.env.COMPRA_AGIL_TICKET) {
|
|
20
|
+
return {
|
|
21
|
+
content: [{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: [
|
|
24
|
+
'❌ No hay ticket configurado.',
|
|
25
|
+
'',
|
|
26
|
+
'Configura la variable de entorno COMPRA_AGIL_TICKET (por ejemplo, en un archivo .env).',
|
|
27
|
+
'Obtén tu ticket en https://www.chilecompra.cl/api/ (requiere Clave Única).',
|
|
28
|
+
'',
|
|
29
|
+
'⚠ No pegues el ticket en el chat ni lo compartas: guárdalo solo en el .env,',
|
|
30
|
+
' que ya está protegido por .gitignore.',
|
|
31
|
+
].join('\n'),
|
|
32
|
+
}],
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
// Consulta de humo mínima: ventana de cambios de 1 hora.
|
|
38
|
+
//
|
|
39
|
+
// IMPORTANTE: la API NO acepta consultas sin filtros — devuelve HTTP 500
|
|
40
|
+
// (verificado contra el servicio real). Debe enviarse al menos un filtro.
|
|
41
|
+
// `ttl_cambio_ms` es el más liviano: responde en ~1s aunque no haya
|
|
42
|
+
// resultados, y un 200 basta para probar que el ticket es válido.
|
|
43
|
+
const resp = await client.buscar({ ttl_cambio_ms: 3_600_000, tamano_pagina: 10, numero_pagina: 1 });
|
|
44
|
+
return {
|
|
45
|
+
content: [{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: [
|
|
48
|
+
'✅ Ticket válido y operativo.',
|
|
49
|
+
'',
|
|
50
|
+
`Ticket configurado: ${referencia}`,
|
|
51
|
+
`Consulta de prueba: cambios en la última hora → ${resp.paginacion.total_resultados} resultado(s).`,
|
|
52
|
+
`Cuota consumida en esta sesión: ${client.getRateLimitStats().requestsToday} request(s).`,
|
|
53
|
+
'',
|
|
54
|
+
'Nota: un total de 0 es normal si no hubo movimientos en la última hora;',
|
|
55
|
+
'lo relevante es que la API respondió correctamente con este ticket.',
|
|
56
|
+
].join('\n'),
|
|
57
|
+
}],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const detalle = error instanceof CompraAgilApiError
|
|
62
|
+
? error.actionableMessage
|
|
63
|
+
: safeError(error);
|
|
64
|
+
return {
|
|
65
|
+
content: [{
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: [
|
|
68
|
+
'❌ El ticket está configurado pero la verificación falló.',
|
|
69
|
+
'',
|
|
70
|
+
`Ticket configurado: ${referencia}`,
|
|
71
|
+
`Motivo: ${detalle}`,
|
|
72
|
+
].join('\n'),
|
|
73
|
+
}],
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=verificar-ticket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verificar-ticket.js","sourceRoot":"","sources":["../../src/tools/verificar-ticket.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAEtD,MAAM,SAAS,GAAG,kBAAkB,CAAC;AAErC,MAAM,gBAAgB,GAAG;;6FAEoE,CAAC;AAE9F,MAAM,UAAU,uBAAuB,CAAC,MAAiB,EAAE,MAAwB;IACjF,MAAM,CAAC,YAAY,CACjB,SAAS,EACT,EAAE,WAAW,EAAE,gBAAgB,EAAE,EACjC,KAAK,IAAI,EAAE;QACT,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAEzD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC;YACpC,OAAO;gBACL,OAAO,EAAE,CAAC;wBACR,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE;4BACJ,8BAA8B;4BAC9B,EAAE;4BACF,wFAAwF;4BACxF,4EAA4E;4BAC5E,EAAE;4BACF,6EAA6E;4BAC7E,yCAAyC;yBAC1C,CAAC,IAAI,CAAC,IAAI,CAAC;qBACb,CAAC;gBACF,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,IAAI,CAAC;YACH,yDAAyD;YACzD,EAAE;YACF,yEAAyE;YACzE,0EAA0E;YAC1E,oEAAoE;YACpE,kEAAkE;YAClE,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC,CAAC;YACpG,OAAO;gBACL,OAAO,EAAE,CAAC;wBACR,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE;4BACJ,8BAA8B;4BAC9B,EAAE;4BACF,uBAAuB,UAAU,EAAE;4BACnC,mDAAmD,IAAI,CAAC,UAAU,CAAC,gBAAgB,gBAAgB;4BACnG,mCAAmC,MAAM,CAAC,iBAAiB,EAAE,CAAC,aAAa,cAAc;4BACzF,EAAE;4BACF,yEAAyE;4BACzE,qEAAqE;yBACtE,CAAC,IAAI,CAAC,IAAI,CAAC;qBACb,CAAC;aACH,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,kBAAkB;gBACjD,CAAC,CAAC,KAAK,CAAC,iBAAiB;gBACzB,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;YACrB,OAAO;gBACL,OAAO,EAAE,CAAC;wBACR,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE;4BACJ,0DAA0D;4BAC1D,EAAE;4BACF,uBAAuB,UAAU,EAAE;4BACnC,WAAW,OAAO,EAAE;yBACrB,CAAC,IAAI,CAAC,IAAI,CAAC;qBACb,CAAC;gBACF,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caché de respuestas de la API, con vencimiento y persistencia en disco.
|
|
3
|
+
*
|
|
4
|
+
* ⚠ POR QUÉ EXISTE: la cuota del ticket se agotaba en minutos. Medido en
|
|
5
|
+
* auditoría, evaluar UNA oportunidad con el flujo natural (radar → detalle →
|
|
6
|
+
* analizar_precios → generar_borrador → auditar_desiertas) costaba ~29
|
|
7
|
+
* consultas, porque las tres herramientas de análisis repiten exactamente la
|
|
8
|
+
* misma pareja de llamadas —`buscar({q, estado:'desierta'})` y luego
|
|
9
|
+
* `detalle()` sobre los primeros resultados— y no existía ninguna caché: el
|
|
10
|
+
* mismo histórico se descargaba hasta tres veces en cuestión de minutos.
|
|
11
|
+
*
|
|
12
|
+
* Un proceso ya declarado desierto es inmutable, así que reutilizarlo es
|
|
13
|
+
* gratis en exactitud y enorme en cuota.
|
|
14
|
+
*
|
|
15
|
+
* 🔒 El ticket NUNCA entra aquí: se excluye al construir la clave (el endpoint
|
|
16
|
+
* heredado de Órdenes de Compra lo lleva en el query string) y solo se
|
|
17
|
+
* guarda el cuerpo de la respuesta, que es información pública.
|
|
18
|
+
*/
|
|
19
|
+
export interface OpcionesCache {
|
|
20
|
+
/** Ruta del archivo de persistencia, o `null` para vivir solo en memoria. */
|
|
21
|
+
rutaEstado?: string | null;
|
|
22
|
+
/** Máximo de entradas retenidas; al excederlo se descartan las más antiguas. */
|
|
23
|
+
maxEntradas?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare class ResponseCache {
|
|
26
|
+
private entradas;
|
|
27
|
+
private readonly rutaEstado;
|
|
28
|
+
private readonly maxEntradas;
|
|
29
|
+
private aciertos;
|
|
30
|
+
private fallos;
|
|
31
|
+
constructor(opciones?: OpcionesCache);
|
|
32
|
+
/**
|
|
33
|
+
* Clave estable para una consulta: mismo endpoint y mismos parámetros
|
|
34
|
+
* producen la misma clave sin importar el orden en que se escribieron.
|
|
35
|
+
*/
|
|
36
|
+
static clave(path: string, params?: Record<string, unknown>): string;
|
|
37
|
+
/** Devuelve el valor vigente, o `undefined` si no está o ya venció. */
|
|
38
|
+
obtener<T>(clave: string): T | undefined;
|
|
39
|
+
guardar(clave: string, valor: unknown, ttlSegundos: number): void;
|
|
40
|
+
/** Elimina lo vencido y, si aún sobra, lo más antiguo. */
|
|
41
|
+
private purgar;
|
|
42
|
+
estadisticas(): {
|
|
43
|
+
entradas: number;
|
|
44
|
+
aciertos: number;
|
|
45
|
+
fallos: number;
|
|
46
|
+
ahorroPorcentaje: number;
|
|
47
|
+
};
|
|
48
|
+
/** Vacía la caché (memoria y disco). */
|
|
49
|
+
limpiar(): void;
|
|
50
|
+
private cargar;
|
|
51
|
+
private persistir;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/utils/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAcH,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,MAAM,CAAK;gBAEP,QAAQ,GAAE,aAAkB;IAMxC;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;IASpE,uEAAuE;IACvE,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAkBxC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAOjE,0DAA0D;IAC1D,OAAO,CAAC,MAAM;IAYd,YAAY,IAAI;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE;IAUhG,wCAAwC;IACxC,OAAO,IAAI,IAAI;IAKf,OAAO,CAAC,MAAM;IAmBd,OAAO,CAAC,SAAS;CAQlB"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caché de respuestas de la API, con vencimiento y persistencia en disco.
|
|
3
|
+
*
|
|
4
|
+
* ⚠ POR QUÉ EXISTE: la cuota del ticket se agotaba en minutos. Medido en
|
|
5
|
+
* auditoría, evaluar UNA oportunidad con el flujo natural (radar → detalle →
|
|
6
|
+
* analizar_precios → generar_borrador → auditar_desiertas) costaba ~29
|
|
7
|
+
* consultas, porque las tres herramientas de análisis repiten exactamente la
|
|
8
|
+
* misma pareja de llamadas —`buscar({q, estado:'desierta'})` y luego
|
|
9
|
+
* `detalle()` sobre los primeros resultados— y no existía ninguna caché: el
|
|
10
|
+
* mismo histórico se descargaba hasta tres veces en cuestión de minutos.
|
|
11
|
+
*
|
|
12
|
+
* Un proceso ya declarado desierto es inmutable, así que reutilizarlo es
|
|
13
|
+
* gratis en exactitud y enorme en cuota.
|
|
14
|
+
*
|
|
15
|
+
* 🔒 El ticket NUNCA entra aquí: se excluye al construir la clave (el endpoint
|
|
16
|
+
* heredado de Órdenes de Compra lo lleva en el query string) y solo se
|
|
17
|
+
* guarda el cuerpo de la respuesta, que es información pública.
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import { logger } from './logger.js';
|
|
21
|
+
/** Parámetros que jamás deben formar parte de la clave ni tocar el disco. */
|
|
22
|
+
const PARAMS_SECRETOS = new Set(['ticket']);
|
|
23
|
+
export class ResponseCache {
|
|
24
|
+
entradas = new Map();
|
|
25
|
+
rutaEstado;
|
|
26
|
+
maxEntradas;
|
|
27
|
+
aciertos = 0;
|
|
28
|
+
fallos = 0;
|
|
29
|
+
constructor(opciones = {}) {
|
|
30
|
+
this.rutaEstado = opciones.rutaEstado ?? null;
|
|
31
|
+
this.maxEntradas = opciones.maxEntradas ?? 500;
|
|
32
|
+
this.cargar();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Clave estable para una consulta: mismo endpoint y mismos parámetros
|
|
36
|
+
* producen la misma clave sin importar el orden en que se escribieron.
|
|
37
|
+
*/
|
|
38
|
+
static clave(path, params) {
|
|
39
|
+
const limpios = Object.entries(params ?? {})
|
|
40
|
+
.filter(([k, v]) => !PARAMS_SECRETOS.has(k.toLowerCase()) && v !== undefined && v !== null && v !== '')
|
|
41
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
42
|
+
.map(([k, v]) => `${k}=${String(v)}`)
|
|
43
|
+
.join('&');
|
|
44
|
+
return limpios ? `${path}?${limpios}` : path;
|
|
45
|
+
}
|
|
46
|
+
/** Devuelve el valor vigente, o `undefined` si no está o ya venció. */
|
|
47
|
+
obtener(clave) {
|
|
48
|
+
const e = this.entradas.get(clave);
|
|
49
|
+
if (!e) {
|
|
50
|
+
this.fallos++;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (Date.now() >= e.expira) {
|
|
54
|
+
this.entradas.delete(clave);
|
|
55
|
+
this.fallos++;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
// Renovar posición para que la purga descarte lo realmente frío.
|
|
59
|
+
this.entradas.delete(clave);
|
|
60
|
+
this.entradas.set(clave, e);
|
|
61
|
+
this.aciertos++;
|
|
62
|
+
return e.valor;
|
|
63
|
+
}
|
|
64
|
+
guardar(clave, valor, ttlSegundos) {
|
|
65
|
+
if (ttlSegundos <= 0)
|
|
66
|
+
return;
|
|
67
|
+
this.entradas.set(clave, { expira: Date.now() + ttlSegundos * 1000, valor });
|
|
68
|
+
this.purgar();
|
|
69
|
+
this.persistir();
|
|
70
|
+
}
|
|
71
|
+
/** Elimina lo vencido y, si aún sobra, lo más antiguo. */
|
|
72
|
+
purgar() {
|
|
73
|
+
const ahora = Date.now();
|
|
74
|
+
for (const [k, e] of this.entradas) {
|
|
75
|
+
if (ahora >= e.expira)
|
|
76
|
+
this.entradas.delete(k);
|
|
77
|
+
}
|
|
78
|
+
while (this.entradas.size > this.maxEntradas) {
|
|
79
|
+
const primera = this.entradas.keys().next();
|
|
80
|
+
if (primera.done)
|
|
81
|
+
break;
|
|
82
|
+
this.entradas.delete(primera.value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
estadisticas() {
|
|
86
|
+
const total = this.aciertos + this.fallos;
|
|
87
|
+
return {
|
|
88
|
+
entradas: this.entradas.size,
|
|
89
|
+
aciertos: this.aciertos,
|
|
90
|
+
fallos: this.fallos,
|
|
91
|
+
ahorroPorcentaje: total === 0 ? 0 : Math.round((this.aciertos / total) * 100),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Vacía la caché (memoria y disco). */
|
|
95
|
+
limpiar() {
|
|
96
|
+
this.entradas.clear();
|
|
97
|
+
this.persistir();
|
|
98
|
+
}
|
|
99
|
+
cargar() {
|
|
100
|
+
if (!this.rutaEstado)
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
if (!fs.existsSync(this.rutaEstado))
|
|
104
|
+
return;
|
|
105
|
+
const crudo = JSON.parse(fs.readFileSync(this.rutaEstado, 'utf8'));
|
|
106
|
+
const ahora = Date.now();
|
|
107
|
+
for (const [k, e] of Object.entries(crudo)) {
|
|
108
|
+
if (e && typeof e.expira === 'number' && ahora < e.expira) {
|
|
109
|
+
this.entradas.set(k, e);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (this.entradas.size > 0) {
|
|
113
|
+
logger.debug(`Caché: ${this.entradas.size} respuesta(s) vigente(s) recuperada(s) del disco.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Una caché ilegible se descarta: es una optimización, no un requisito.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
persistir() {
|
|
121
|
+
if (!this.rutaEstado)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
fs.writeFileSync(this.rutaEstado, JSON.stringify(Object.fromEntries(this.entradas)), 'utf8');
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Si el disco no deja escribir, se sigue con la caché en memoria.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/utils/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,6EAA6E;AAC7E,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;AAe5C,MAAM,OAAO,aAAa;IAChB,QAAQ,GAAG,IAAI,GAAG,EAAmB,CAAC;IAC7B,UAAU,CAAgB;IAC1B,WAAW,CAAS;IAC7B,QAAQ,GAAG,CAAC,CAAC;IACb,MAAM,GAAG,CAAC,CAAC;IAEnB,YAAY,WAA0B,EAAE;QACtC,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC,UAAU,IAAI,IAAI,CAAC;QAC9C,IAAI,CAAC,WAAW,GAAG,QAAQ,CAAC,WAAW,IAAI,GAAG,CAAC;QAC/C,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,IAAY,EAAE,MAAgC;QACzD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;aACzC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;aACtG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;aACtC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;aACpC,IAAI,CAAC,GAAG,CAAC,CAAC;QACb,OAAO,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC;IAED,uEAAuE;IACvE,OAAO,CAAI,KAAa;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC5B,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,iEAAiE;QACjE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAC5B,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,OAAO,CAAC,CAAC,KAAU,CAAC;IACtB,CAAC;IAED,OAAO,CAAC,KAAa,EAAE,KAAc,EAAE,WAAmB;QACxD,IAAI,WAAW,IAAI,CAAC;YAAE,OAAO;QAC7B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,WAAW,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7E,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;IAED,0DAA0D;IAClD,MAAM;QACZ,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,IAAI,KAAK,IAAI,CAAC,CAAC,MAAM;gBAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,OAAO,CAAC,IAAI;gBAAE,MAAM;YACxB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAED,YAAY;QACV,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1C,OAAO;YACL,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;YAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,gBAAgB,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC;SAC9E,CAAC;IACJ,CAAC;IAED,wCAAwC;IACxC,OAAO;QACL,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO;QAC7B,IAAI,CAAC;YACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC;gBAAE,OAAO;YAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAA4B,CAAC;YAC9F,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACzB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC3C,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;oBAC1D,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC;YACD,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBAC3B,MAAM,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,QAAQ,CAAC,IAAI,mDAAmD,CAAC,CAAC;YAChG,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,wEAAwE;QAC1E,CAAC;IACH,CAAC;IAEO,SAAS;QACf,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO;QAC7B,IAAI,CAAC;YACH,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QAC/F,CAAC;QAAC,MAAM,CAAC;YACP,kEAAkE;QACpE,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Búsqueda de texto dentro de los documentos locales (manuales, guías, normativa).
|
|
3
|
+
*
|
|
4
|
+
* ⚠ POR QUÉ EXISTE ESTE MÓDULO: la primera implementación buscaba la consulta
|
|
5
|
+
* completa como subcadena literal (`texto.includes(query)`). Con una palabra
|
|
6
|
+
* suelta funcionaba, pero cualquier pregunta en lenguaje natural —que es
|
|
7
|
+
* justo lo que envía un LLM— no coincidía nunca. Peor aún, el fallo era
|
|
8
|
+
* silencioso y seguro de sí mismo: "no se encontraron coincidencias en
|
|
9
|
+
* ninguno de los 10 documentos" ante la pregunta "¿qué multas me pueden
|
|
10
|
+
* aplicar?", existiendo un PDF entero dedicado a multas y sanciones. El
|
|
11
|
+
* modelo repetía ese "no hay información" al usuario como si fuera cierto.
|
|
12
|
+
*
|
|
13
|
+
* Ahora la consulta se descompone en términos, se ignoran las palabras
|
|
14
|
+
* vacías y se puntúa cada línea por cuántos términos distintos contiene.
|
|
15
|
+
* Además se normalizan los acentos, porque en documentos en español
|
|
16
|
+
* "sanción" y "sancion" deben encontrarse mutuamente.
|
|
17
|
+
*/
|
|
18
|
+
/** Minúsculas y sin acentos, para que "sanción" y "sancion" se encuentren. */
|
|
19
|
+
export declare function normalizar(texto: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Descompone la consulta en términos buscables: normalizados, sin palabras
|
|
22
|
+
* vacías y de al menos 3 caracteres.
|
|
23
|
+
*
|
|
24
|
+
* Si al filtrar no queda ningún término (consulta hecha solo de palabras
|
|
25
|
+
* vacías, ej. "qué es esto"), se devuelven todos los de 3+ caracteres antes
|
|
26
|
+
* que dejar al usuario sin búsqueda.
|
|
27
|
+
*/
|
|
28
|
+
export declare function tokenizar(query: string): string[];
|
|
29
|
+
export interface Fragmento {
|
|
30
|
+
/** Línea que coincidió, con su contexto inmediato. */
|
|
31
|
+
texto: string;
|
|
32
|
+
/** Términos de la consulta presentes en la línea. */
|
|
33
|
+
terminos: string[];
|
|
34
|
+
puntaje: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ResultadoBusqueda {
|
|
37
|
+
fragmentos: Fragmento[];
|
|
38
|
+
/** Términos en que se descompuso la consulta. */
|
|
39
|
+
terminos: string[];
|
|
40
|
+
/** Términos que no aparecen en ninguna parte del texto. */
|
|
41
|
+
ausentes: string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Busca los términos de `query` en `texto` y devuelve los fragmentos más
|
|
45
|
+
* relevantes, ordenados por cuántos términos distintos concentra cada uno.
|
|
46
|
+
*
|
|
47
|
+
* Una línea que contiene la consulta completa e íntegra se prioriza sobre
|
|
48
|
+
* cualquier coincidencia parcial: si el usuario acertó la frase exacta, esa
|
|
49
|
+
* es la mejor respuesta posible.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buscarEnTexto(texto: string, query: string, maxFragmentos?: number): ResultadoBusqueda;
|
|
52
|
+
//# sourceMappingURL=doc-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doc-search.d.ts","sourceRoot":"","sources":["../../src/utils/doc-search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAcH,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAQjD;AAED,MAAM,WAAW,SAAS;IACxB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,aAAa,SAAK,GACjB,iBAAiB,CAwCnB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Búsqueda de texto dentro de los documentos locales (manuales, guías, normativa).
|
|
3
|
+
*
|
|
4
|
+
* ⚠ POR QUÉ EXISTE ESTE MÓDULO: la primera implementación buscaba la consulta
|
|
5
|
+
* completa como subcadena literal (`texto.includes(query)`). Con una palabra
|
|
6
|
+
* suelta funcionaba, pero cualquier pregunta en lenguaje natural —que es
|
|
7
|
+
* justo lo que envía un LLM— no coincidía nunca. Peor aún, el fallo era
|
|
8
|
+
* silencioso y seguro de sí mismo: "no se encontraron coincidencias en
|
|
9
|
+
* ninguno de los 10 documentos" ante la pregunta "¿qué multas me pueden
|
|
10
|
+
* aplicar?", existiendo un PDF entero dedicado a multas y sanciones. El
|
|
11
|
+
* modelo repetía ese "no hay información" al usuario como si fuera cierto.
|
|
12
|
+
*
|
|
13
|
+
* Ahora la consulta se descompone en términos, se ignoran las palabras
|
|
14
|
+
* vacías y se puntúa cada línea por cuántos términos distintos contiene.
|
|
15
|
+
* Además se normalizan los acentos, porque en documentos en español
|
|
16
|
+
* "sanción" y "sancion" deben encontrarse mutuamente.
|
|
17
|
+
*/
|
|
18
|
+
/** Palabras sin valor discriminante en una consulta en español. */
|
|
19
|
+
const VACIAS = new Set([
|
|
20
|
+
'que', 'qué', 'cual', 'cuál', 'cuales', 'cuáles', 'como', 'cómo', 'cuando',
|
|
21
|
+
'cuándo', 'donde', 'dónde', 'quien', 'quién', 'por', 'para', 'con', 'sin',
|
|
22
|
+
'los', 'las', 'del', 'unos', 'unas', 'una', 'uno', 'sus', 'sobre', 'entre',
|
|
23
|
+
'este', 'esta', 'esto', 'estos', 'estas', 'ese', 'esa', 'eso', 'aquel',
|
|
24
|
+
'son', 'ser', 'está', 'esta', 'estan', 'están', 'hay', 'han', 'hace',
|
|
25
|
+
'puede', 'pueden', 'debe', 'deben', 'tiene', 'tienen', 'mas', 'más',
|
|
26
|
+
'pero', 'sus', 'les', 'nos', 'mis', 'tus', 'muy', 'ya', 'les', 'algun',
|
|
27
|
+
'algún', 'alguna', 'todo', 'toda', 'todos', 'todas', 'otro', 'otra',
|
|
28
|
+
]);
|
|
29
|
+
/** Minúsculas y sin acentos, para que "sanción" y "sancion" se encuentren. */
|
|
30
|
+
export function normalizar(texto) {
|
|
31
|
+
return texto
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.normalize('NFD')
|
|
34
|
+
.replace(/[̀-ͯ]/g, '');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Descompone la consulta en términos buscables: normalizados, sin palabras
|
|
38
|
+
* vacías y de al menos 3 caracteres.
|
|
39
|
+
*
|
|
40
|
+
* Si al filtrar no queda ningún término (consulta hecha solo de palabras
|
|
41
|
+
* vacías, ej. "qué es esto"), se devuelven todos los de 3+ caracteres antes
|
|
42
|
+
* que dejar al usuario sin búsqueda.
|
|
43
|
+
*/
|
|
44
|
+
export function tokenizar(query) {
|
|
45
|
+
const crudos = normalizar(query)
|
|
46
|
+
.split(/[^a-z0-9ñ]+/)
|
|
47
|
+
.filter((t) => t.length >= 3);
|
|
48
|
+
const utiles = crudos.filter((t) => !VACIAS.has(t));
|
|
49
|
+
const elegidos = utiles.length > 0 ? utiles : crudos;
|
|
50
|
+
return [...new Set(elegidos)];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Busca los términos de `query` en `texto` y devuelve los fragmentos más
|
|
54
|
+
* relevantes, ordenados por cuántos términos distintos concentra cada uno.
|
|
55
|
+
*
|
|
56
|
+
* Una línea que contiene la consulta completa e íntegra se prioriza sobre
|
|
57
|
+
* cualquier coincidencia parcial: si el usuario acertó la frase exacta, esa
|
|
58
|
+
* es la mejor respuesta posible.
|
|
59
|
+
*/
|
|
60
|
+
export function buscarEnTexto(texto, query, maxFragmentos = 12) {
|
|
61
|
+
const terminos = tokenizar(query);
|
|
62
|
+
if (terminos.length === 0) {
|
|
63
|
+
return { fragmentos: [], terminos: [], ausentes: [] };
|
|
64
|
+
}
|
|
65
|
+
const frase = normalizar(query).trim();
|
|
66
|
+
const lineas = texto.split('\n');
|
|
67
|
+
const normalizadas = lineas.map(normalizar);
|
|
68
|
+
const ausentes = terminos.filter((t) => !normalizadas.some((l) => l.includes(t)));
|
|
69
|
+
const candidatos = [];
|
|
70
|
+
for (let i = 0; i < lineas.length; i++) {
|
|
71
|
+
const linea = lineas[i]?.trim();
|
|
72
|
+
if (!linea)
|
|
73
|
+
continue;
|
|
74
|
+
const norm = normalizadas[i];
|
|
75
|
+
const presentes = terminos.filter((t) => norm.includes(t));
|
|
76
|
+
if (presentes.length === 0)
|
|
77
|
+
continue;
|
|
78
|
+
// La frase completa vale más que la suma de sus partes.
|
|
79
|
+
const bonus = frase.length > 0 && norm.includes(frase) ? terminos.length * 10 : 0;
|
|
80
|
+
const contexto = [];
|
|
81
|
+
const anterior = lineas[i - 1]?.trim();
|
|
82
|
+
const siguiente = lineas[i + 1]?.trim();
|
|
83
|
+
if (anterior)
|
|
84
|
+
contexto.push(`[Anterior] ${anterior}`);
|
|
85
|
+
contexto.push(`[COINCIDENCIA] ${linea}`);
|
|
86
|
+
if (siguiente)
|
|
87
|
+
contexto.push(`[Siguiente] ${siguiente}`);
|
|
88
|
+
candidatos.push({
|
|
89
|
+
texto: contexto.join('\n'),
|
|
90
|
+
terminos: presentes,
|
|
91
|
+
puntaje: presentes.length + bonus,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
candidatos.sort((a, b) => b.puntaje - a.puntaje);
|
|
95
|
+
return { fragmentos: candidatos.slice(0, maxFragmentos), terminos, ausentes };
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=doc-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doc-search.js","sourceRoot":"","sources":["../../src/utils/doc-search.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Localizador compartido de documentos locales (manuales/guías de Compra Ágil).
|
|
3
|
+
*
|
|
4
|
+
* Centraliza la lógica antes duplicada e inconsistente entre
|
|
5
|
+
* `tools/documentos.ts` y `resources/documentacion.ts`:
|
|
6
|
+
* - Detección recursiva de la carpeta docs/ (empaquetada vs. cwd).
|
|
7
|
+
* - Filtrado por extensión soportada (.pdf, .txt, .md), excluyendo readme.md.
|
|
8
|
+
* - Exclusión de subcarpetas internas del proyecto (ver EXCLUDED_DIRS).
|
|
9
|
+
*/
|
|
10
|
+
export declare const SUPPORTED_DOC_EXTENSIONS: readonly [".pdf", ".txt", ".md"];
|
|
11
|
+
/**
|
|
12
|
+
* Subcarpetas de docs/ que NO son documentación de Compra Ágil y por tanto no
|
|
13
|
+
* deben ofrecerse al LLM como tal.
|
|
14
|
+
*
|
|
15
|
+
* `internals/` contiene documentación de ingeniería del propio servidor
|
|
16
|
+
* (pendientes, decisiones de arquitectura, hallazgos de la API). Sin esta
|
|
17
|
+
* exclusión aparecería en `consultar_documentos_locales` y en el recurso
|
|
18
|
+
* `compra-agil://documentacion/{filename}`, que existen para consultar
|
|
19
|
+
* normativa y guías del mecanismo — no las notas internas del proyecto.
|
|
20
|
+
*/
|
|
21
|
+
export declare const EXCLUDED_DIRS: readonly ["internals"];
|
|
22
|
+
/**
|
|
23
|
+
* Obtiene de forma recursiva todas las rutas relativas de archivos dentro de un directorio,
|
|
24
|
+
* normalizadas con barras diagonales (/).
|
|
25
|
+
*/
|
|
26
|
+
export declare function getRelativeFilesRecursively(dir: string, baseDir?: string): string[];
|
|
27
|
+
/**
|
|
28
|
+
* True si el archivo es documentación consultable de Compra Ágil.
|
|
29
|
+
* Excluye READMEs y todo lo que viva en una carpeta interna del proyecto.
|
|
30
|
+
*
|
|
31
|
+
* @param file Ruta RELATIVA al directorio docs/, con separadores "/".
|
|
32
|
+
*/
|
|
33
|
+
export declare function isSupportedDoc(file: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Resuelve el directorio de documentación efectivo: prefiere la carpeta empaquetada
|
|
36
|
+
* (docs/ junto al build) si contiene documentos soportados; si no, usa docs/ del cwd.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveDocsDir(): string;
|
|
39
|
+
/** Lista las rutas relativas de documentos consultables dentro del docs dir resuelto. */
|
|
40
|
+
export declare function listSupportedDocs(docsDir: string): string[];
|
|
41
|
+
//# sourceMappingURL=docs-locator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs-locator.d.ts","sourceRoot":"","sources":["../../src/utils/docs-locator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH,eAAO,MAAM,wBAAwB,kCAAmC,CAAC;AAEzE;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,wBAAyB,CAAC;AAEpD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,MAAY,GAAG,MAAM,EAAE,CAexF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUpD;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAMvC;AAED,yFAAyF;AACzF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAE3D"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Localizador compartido de documentos locales (manuales/guías de Compra Ágil).
|
|
3
|
+
*
|
|
4
|
+
* Centraliza la lógica antes duplicada e inconsistente entre
|
|
5
|
+
* `tools/documentos.ts` y `resources/documentacion.ts`:
|
|
6
|
+
* - Detección recursiva de la carpeta docs/ (empaquetada vs. cwd).
|
|
7
|
+
* - Filtrado por extensión soportada (.pdf, .txt, .md), excluyendo readme.md.
|
|
8
|
+
* - Exclusión de subcarpetas internas del proyecto (ver EXCLUDED_DIRS).
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const packageDocsDir = path.resolve(__dirname, '../../docs');
|
|
15
|
+
const cwdDocsDir = path.resolve(process.cwd(), 'docs');
|
|
16
|
+
export const SUPPORTED_DOC_EXTENSIONS = ['.pdf', '.txt', '.md'];
|
|
17
|
+
/**
|
|
18
|
+
* Subcarpetas de docs/ que NO son documentación de Compra Ágil y por tanto no
|
|
19
|
+
* deben ofrecerse al LLM como tal.
|
|
20
|
+
*
|
|
21
|
+
* `internals/` contiene documentación de ingeniería del propio servidor
|
|
22
|
+
* (pendientes, decisiones de arquitectura, hallazgos de la API). Sin esta
|
|
23
|
+
* exclusión aparecería en `consultar_documentos_locales` y en el recurso
|
|
24
|
+
* `compra-agil://documentacion/{filename}`, que existen para consultar
|
|
25
|
+
* normativa y guías del mecanismo — no las notas internas del proyecto.
|
|
26
|
+
*/
|
|
27
|
+
export const EXCLUDED_DIRS = ['internals'];
|
|
28
|
+
/**
|
|
29
|
+
* Obtiene de forma recursiva todas las rutas relativas de archivos dentro de un directorio,
|
|
30
|
+
* normalizadas con barras diagonales (/).
|
|
31
|
+
*/
|
|
32
|
+
export function getRelativeFilesRecursively(dir, baseDir = dir) {
|
|
33
|
+
let results = [];
|
|
34
|
+
if (!fs.existsSync(dir))
|
|
35
|
+
return [];
|
|
36
|
+
const list = fs.readdirSync(dir);
|
|
37
|
+
for (const file of list) {
|
|
38
|
+
const filePath = path.join(dir, file);
|
|
39
|
+
const stat = fs.statSync(filePath);
|
|
40
|
+
if (stat && stat.isDirectory()) {
|
|
41
|
+
results = results.concat(getRelativeFilesRecursively(filePath, baseDir));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const relativePath = path.relative(baseDir, filePath).replace(/\\/g, '/');
|
|
45
|
+
results.push(relativePath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* True si el archivo es documentación consultable de Compra Ágil.
|
|
52
|
+
* Excluye READMEs y todo lo que viva en una carpeta interna del proyecto.
|
|
53
|
+
*
|
|
54
|
+
* @param file Ruta RELATIVA al directorio docs/, con separadores "/".
|
|
55
|
+
*/
|
|
56
|
+
export function isSupportedDoc(file) {
|
|
57
|
+
const normalizado = file.replace(/\\/g, '/');
|
|
58
|
+
// Cualquier archivo bajo una carpeta interna queda fuera, a cualquier profundidad.
|
|
59
|
+
const primerSegmento = normalizado.split('/')[0].toLowerCase();
|
|
60
|
+
if (EXCLUDED_DIRS.includes(primerSegmento))
|
|
61
|
+
return false;
|
|
62
|
+
const ext = path.extname(normalizado).toLowerCase();
|
|
63
|
+
const name = path.basename(normalizado).toLowerCase();
|
|
64
|
+
return SUPPORTED_DOC_EXTENSIONS.includes(ext) && name !== 'readme.md';
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resuelve el directorio de documentación efectivo: prefiere la carpeta empaquetada
|
|
68
|
+
* (docs/ junto al build) si contiene documentos soportados; si no, usa docs/ del cwd.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveDocsDir() {
|
|
71
|
+
if (fs.existsSync(packageDocsDir)) {
|
|
72
|
+
const hasLocalDocs = getRelativeFilesRecursively(packageDocsDir).some(isSupportedDoc);
|
|
73
|
+
if (hasLocalDocs)
|
|
74
|
+
return packageDocsDir;
|
|
75
|
+
}
|
|
76
|
+
return cwdDocsDir;
|
|
77
|
+
}
|
|
78
|
+
/** Lista las rutas relativas de documentos consultables dentro del docs dir resuelto. */
|
|
79
|
+
export function listSupportedDocs(docsDir) {
|
|
80
|
+
return getRelativeFilesRecursively(docsDir).filter(isSupportedDoc);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=docs-locator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs-locator.js","sourceRoot":"","sources":["../../src/utils/docs-locator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AAC7D,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;AAEvD,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAU,CAAC;AAEzE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,WAAW,CAAU,CAAC;AAEpD;;;GAGG;AACH,MAAM,UAAU,2BAA2B,CAAC,GAAW,EAAE,UAAkB,GAAG;IAC5E,IAAI,OAAO,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC,MAAM,IAAI,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACtC,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,IAAI,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YAC/B,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,2BAA2B,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;QAC3E,CAAC;aAAM,CAAC;YACN,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAC1E,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAE7C,mFAAmF;IACnF,MAAM,cAAc,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAC/D,IAAK,aAAmC,CAAC,QAAQ,CAAC,cAAc,CAAC;QAAE,OAAO,KAAK,CAAC;IAEhF,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;IACpD,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;IACtD,OAAQ,wBAA8C,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,KAAK,WAAW,CAAC;AAC/F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc;IAC5B,IAAI,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QAClC,MAAM,YAAY,GAAG,2BAA2B,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACtF,IAAI,YAAY;YAAE,OAAO,cAAc,CAAC;IAC1C,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,OAAO,2BAA2B,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;AACrE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../../src/utils/error-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../../src/utils/error-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,QAAQ,EAAE,CAAC;CACpB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtC,SAAgB,iBAAiB,EAAE,MAAM,CAAC;gBAE9B,UAAU,EAAE,MAAM,EAAE,SAAS,GAAE,QAAQ,EAAO;CAQ3D;AAoCD;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAoB5E"}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Cada mensaje está redactado para que un agente de IA entienda
|
|
5
5
|
* exactamente qué salió mal y qué acción tomar.
|
|
6
6
|
*/
|
|
7
|
+
import { redact } from './redact.js';
|
|
7
8
|
export class CompraAgilApiError extends Error {
|
|
8
9
|
httpStatus;
|
|
9
10
|
apiErrors;
|
|
@@ -18,8 +19,10 @@ export class CompraAgilApiError extends Error {
|
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
function getActionableMessage(httpStatus, apiErrors) {
|
|
22
|
+
// El mensaje viene de la API: no se controla su contenido y podría hacer eco
|
|
23
|
+
// de la URL solicitada (que en el endpoint legado lleva el ticket en la query).
|
|
21
24
|
const detail = apiErrors.length > 0
|
|
22
|
-
? ` Detalle de la API: "${apiErrors[0].mensaje}"`
|
|
25
|
+
? ` Detalle de la API: "${redact(apiErrors[0].mensaje ?? '')}"`
|
|
23
26
|
: '';
|
|
24
27
|
switch (httpStatus) {
|
|
25
28
|
case 400:
|
|
@@ -41,12 +44,10 @@ function getActionableMessage(httpStatus, apiErrors) {
|
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
function formatRateLimitMessage(detail) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const waitHours = Math.ceil((tomorrow.getTime() - now.getTime()) / (1000 * 60 * 60));
|
|
49
|
-
return `Cuota diaria de la API agotada. El límite se restablece al inicio del próximo día calendario UTC (en aproximadamente ${waitHours} horas). Para reducir el consumo: usa filtros más específicos (estado, región, fechas), evita consultas masivas, y usa 'ttl_cambio_ms' para sincronización incremental en vez de descargas completas. Si necesitas más cuota, contacta a ChileCompra.${detail}`;
|
|
47
|
+
// La cuota se comporta como un token bucket que se recarga solo (glosario de
|
|
48
|
+
// la guía oficial): medido en producción, la API volvió a responder ~13 min
|
|
49
|
+
// después de un 429. Por eso no se aconseja esperar al día siguiente.
|
|
50
|
+
return `Se agotaron temporalmente los tokens de cuota de la API. No es necesario esperar al día siguiente: la cuota se recarga sola, así que reintenta en unos minutos. Para gastar menos: usa filtros más específicos (estado, región, fechas), baja 'limite_analisis'/'max_paginas' en las herramientas de análisis, y prefiere 'ttl_cambio_ms' o un rango 'cambio_desde'/'cambio_hasta' para sincronización incremental en vez de descargas completas. Si el 429 persiste durante horas, la cuota diaria del ticket sí puede estar agotada: contacta a ChileCompra para uno de mayor límite.${detail}`;
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
52
53
|
* Parsea la respuesta de error de la API y lanza una excepción tipada.
|