@ssolis-ti/mcp-compra-agil 1.0.2 → 2.1.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 +113 -25
- package/dist/api/compra-agil-client.d.ts +2 -1
- package/dist/api/compra-agil-client.d.ts.map +1 -1
- package/dist/api/compra-agil-client.js +12 -2
- package/dist/api/compra-agil-client.js.map +1 -1
- package/dist/index.js +31 -7
- 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 +37 -2
- 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 +64 -46
- 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.d.ts.map +1 -1
- package/dist/tools/detalle-oc.js +6 -5
- package/dist/tools/detalle-oc.js.map +1 -1
- package/dist/tools/documentos.d.ts.map +1 -1
- package/dist/tools/documentos.js +51 -50
- package/dist/tools/documentos.js.map +1 -1
- package/dist/tools/generar-borrador.d.ts.map +1 -1
- package/dist/tools/generar-borrador.js +37 -26
- 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 +54 -58
- 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 +9 -7
- 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/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 +7 -4
- 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 +8 -1
- package/dist/utils/rate-limiter.d.ts.map +1 -1
- package/dist/utils/rate-limiter.js +25 -1
- 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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: generar_informe
|
|
3
|
+
*
|
|
4
|
+
* Genera informes profesionales (HTML imprimible) a partir de los datos del MCP.
|
|
5
|
+
*
|
|
6
|
+
* DISEÑO: el HTML nunca se devuelve al LLM — se escribe a disco y la tool
|
|
7
|
+
* retorna solo la ruta y un resumen breve. Un informe pesa decenas de KB y
|
|
8
|
+
* retornarlo consumiría miles de tokens de contexto por llamada.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { CompraAgilApiError } from '../utils/error-handler.js';
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
import { recolectarDatosRadar } from './radar-oportunidades.js';
|
|
14
|
+
import { renderRadarInforme } from '../reports/templates/radar-oportunidades.js';
|
|
15
|
+
import { escribirInforme, slug, stamp, defaultOutputDir } from '../reports/export.js';
|
|
16
|
+
import { clp } from '../reports/format.js';
|
|
17
|
+
import { PAPEL, FORMATO_POR_DEFECTO } from '../reports/theme.js';
|
|
18
|
+
import { safeError } from '../utils/redact.js';
|
|
19
|
+
const TOOL_NAME = 'generar_informe';
|
|
20
|
+
const TOOL_DESCRIPTION = `Genera un informe profesional imprimible (HTML con diseño de impresión) a partir de los datos de Compra Ágil.
|
|
21
|
+
El archivo se guarda en disco y la herramienta devuelve la RUTA del archivo (no su contenido, para no saturar el contexto).
|
|
22
|
+
El usuario puede abrirlo en su navegador y exportarlo a PDF con Ctrl+P (el diseño está optimizado para impresión: saltos de página controlados y cabeceras de tabla repetidas).
|
|
23
|
+
Formatos de papel: "carta" (216×279mm, estándar de oficina en Chile, por defecto), "oficio" (216×330mm, folio chileno para documentos oficiales) y "a4" (210×297mm, estándar ISO).
|
|
24
|
+
Tipos disponibles:
|
|
25
|
+
- "radar": Radar de oportunidades activas priorizadas por facilidad de adjudicación (KPIs, gráfico de puntuación, fichas destacadas y listado completo).`;
|
|
26
|
+
const inputSchema = {
|
|
27
|
+
tipo: z.enum(['radar']).describe('Tipo de informe a generar. Actualmente disponible: "radar".'),
|
|
28
|
+
formato_papel: z.enum(['carta', 'oficio', 'a4']).default('carta').optional().describe('Tamaño de papel: "carta" (216×279mm, el más usado en oficinas chilenas, por defecto), "oficio" (216×330mm, folio chileno para documentos oficiales/legales) o "a4" (210×297mm, estándar ISO).'),
|
|
29
|
+
region: z.string().optional().describe('Código de región para acotar (1-16). Ej: "13" para Metropolitana.'),
|
|
30
|
+
q: z.string().optional().describe('Término de búsqueda para acotar a un rubro o producto (ej: "licencias").'),
|
|
31
|
+
presupuesto_minimo: z.number().optional().describe('Filtrar procesos con presupuesto disponible mayor o igual a este monto en CLP.'),
|
|
32
|
+
limite_resultados: z.number().min(1).max(50).default(20).optional().describe('Cantidad máxima de oportunidades a incluir en el informe (1-50, default 20).'),
|
|
33
|
+
max_paginas: z.number().min(1).max(10).default(3).optional().describe('Páginas de 50 resultados a escanear (1-10, default 3).'),
|
|
34
|
+
ruta_salida: z.string().optional().describe('Directorio donde guardar el informe. Si se omite, se usa la carpeta "informes/" del directorio de trabajo.'),
|
|
35
|
+
};
|
|
36
|
+
export function registerGenerarInforme(server, client) {
|
|
37
|
+
server.registerTool(TOOL_NAME, {
|
|
38
|
+
description: TOOL_DESCRIPTION,
|
|
39
|
+
inputSchema: inputSchema,
|
|
40
|
+
}, async (args) => {
|
|
41
|
+
try {
|
|
42
|
+
if (args.tipo !== 'radar') {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: `Tipo de informe no soportado: "${args.tipo}".` }],
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
logger.info(`generar_informe: construyendo informe "${args.tipo}"`);
|
|
49
|
+
const generadoEn = new Date();
|
|
50
|
+
// 1. Recolectar datos con la MISMA función que usa la tool JSON del radar
|
|
51
|
+
const datos = await recolectarDatosRadar(client, {
|
|
52
|
+
region: args.region,
|
|
53
|
+
q: args.q,
|
|
54
|
+
presupuesto_minimo: args.presupuesto_minimo,
|
|
55
|
+
limite_resultados: args.limite_resultados ?? 20,
|
|
56
|
+
max_paginas: args.max_paginas,
|
|
57
|
+
}, generadoEn.getTime());
|
|
58
|
+
// 2. Renderizar el HTML en el formato de papel solicitado
|
|
59
|
+
const formato = args.formato_papel ?? FORMATO_POR_DEFECTO;
|
|
60
|
+
const html = renderRadarInforme({
|
|
61
|
+
oportunidades: datos.oportunidades,
|
|
62
|
+
totalAnalizadas: datos.totalAnalizadas,
|
|
63
|
+
filtros: {
|
|
64
|
+
region: args.region,
|
|
65
|
+
q: args.q,
|
|
66
|
+
presupuestoMinimo: args.presupuesto_minimo,
|
|
67
|
+
paginasEscaneadas: datos.paginasEscaneadas,
|
|
68
|
+
},
|
|
69
|
+
generadoEn,
|
|
70
|
+
formato,
|
|
71
|
+
});
|
|
72
|
+
// 3. Escribir a disco y devolver SOLO la ruta + resumen
|
|
73
|
+
const ambito = args.region ? `region-${args.region}` : 'nacional';
|
|
74
|
+
const rubro = args.q ? `-${slug(args.q)}` : '';
|
|
75
|
+
const nombre = `radar-${ambito}${rubro}-${formato}-${stamp(generadoEn)}.html`;
|
|
76
|
+
const { ruta, bytes } = escribirInforme(html, nombre, args.ruta_salida);
|
|
77
|
+
const ops = datos.oportunidades;
|
|
78
|
+
const sinOferentes = ops.filter((o) => o.ofertas_recibidas === 0).length;
|
|
79
|
+
const montoTotal = ops.reduce((acc, o) => acc + (o.presupuesto_disponible || 0), 0);
|
|
80
|
+
const mejor = ops[0];
|
|
81
|
+
const resumen = [
|
|
82
|
+
`✅ Informe generado: ${ruta}`,
|
|
83
|
+
` (${(bytes / 1024).toFixed(1)} KB · formato ${PAPEL[formato].glosa})`,
|
|
84
|
+
` Ábrelo en el navegador y usa Ctrl+P para exportar a PDF.`,
|
|
85
|
+
` ⚠ En el diálogo de impresión selecciona el papel "${formato === 'carta' ? 'Carta / Letter' : formato === 'oficio' ? 'Oficio / Folio (216×330mm)' : 'A4'}" para que calce exactamente.`,
|
|
86
|
+
``,
|
|
87
|
+
`Resumen del contenido:`,
|
|
88
|
+
`• ${ops.length} oportunidades incluidas (de ${datos.totalAnalizadas} vigentes analizadas)`,
|
|
89
|
+
`• ${sinOferentes} sin oferentes (competencia cero)`,
|
|
90
|
+
`• ${ops.filter((o) => o.horas_restantes <= 24).length} cierran en menos de 24 horas`,
|
|
91
|
+
`• Monto total en juego: ${clp(montoTotal)}`,
|
|
92
|
+
mejor ? `• Mejor oportunidad: ${mejor.nombre} (${mejor.codigo}) — ${mejor.puntuacion_caliente} pts` : '',
|
|
93
|
+
``,
|
|
94
|
+
`Carpeta de informes: ${args.ruta_salida ? args.ruta_salida : defaultOutputDir()}`,
|
|
95
|
+
].filter(Boolean).join('\n');
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text', text: resumen }],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const message = error instanceof CompraAgilApiError
|
|
102
|
+
? error.actionableMessage
|
|
103
|
+
: `Error inesperado al generar el informe: ${safeError(error)}`;
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: 'text', text: message }],
|
|
106
|
+
isError: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=generar-informe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generar-informe.js","sourceRoot":"","sources":["../../src/tools/generar-informe.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool: monitorear_cambios_recientes
|
|
3
3
|
*
|
|
4
|
-
* Detecta procesos de Compra Ágil
|
|
5
|
-
*
|
|
4
|
+
* Detecta procesos de Compra Ágil creados o modificados dentro de una ventana
|
|
5
|
+
* de cambios: relativa (últimos N minutos) o absoluta (rango de fechas
|
|
6
|
+
* arbitrario). Ideal para alertas, monitoreo de oportunidades y sincronización
|
|
7
|
+
* incremental de un período pasado.
|
|
6
8
|
*/
|
|
7
9
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
10
|
import { CompraAgilClient } from '../api/compra-agil-client.js';
|
|
11
|
+
/** Ventana de cambios ya resuelta y lista para enviar a la API. */
|
|
12
|
+
export interface VentanaCambios {
|
|
13
|
+
/** Parámetros de la API que definen la ventana (uno u otro grupo, nunca ambos). */
|
|
14
|
+
params: {
|
|
15
|
+
ttl_cambio_ms?: number;
|
|
16
|
+
cambio_desde?: string;
|
|
17
|
+
cambio_hasta?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Glosa legible para el reporte de salida. */
|
|
20
|
+
descripcion: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resuelve la ventana de cambios a partir de los argumentos de la herramienta.
|
|
24
|
+
*
|
|
25
|
+
* La API expone dos formas mutuamente excluyentes de acotar los cambios
|
|
26
|
+
* (Guía API Compra Ágil v2 §5.1, Grupo 1): `ttl_cambio_ms` (opción A) o el par
|
|
27
|
+
* `cambio_desde`/`cambio_hasta` (opción B). Enviar ambas a la vez es un 400,
|
|
28
|
+
* así que se decide aquí y se falla localmente para no gastar cuota.
|
|
29
|
+
*
|
|
30
|
+
* Retorna un string con el error de validación, o la ventana resuelta.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolverVentanaCambios(args: {
|
|
33
|
+
minutos?: number;
|
|
34
|
+
cambio_desde?: string;
|
|
35
|
+
cambio_hasta?: string;
|
|
36
|
+
}): VentanaCambios | {
|
|
37
|
+
error: string;
|
|
38
|
+
};
|
|
9
39
|
export declare function registerMonitorearCambios(server: McpServer, client: CompraAgilClient): void;
|
|
10
40
|
//# sourceMappingURL=monitorear-cambios.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monitorear-cambios.d.ts","sourceRoot":"","sources":["../../src/tools/monitorear-cambios.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"monitorear-cambios.d.ts","sourceRoot":"","sources":["../../src/tools/monitorear-cambios.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAyChE,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B,mFAAmF;IACnF,MAAM,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjF,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,cAAc,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CA+CrC;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CA4D3F"}
|
|
@@ -1,31 +1,100 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool: monitorear_cambios_recientes
|
|
3
3
|
*
|
|
4
|
-
* Detecta procesos de Compra Ágil
|
|
5
|
-
*
|
|
4
|
+
* Detecta procesos de Compra Ágil creados o modificados dentro de una ventana
|
|
5
|
+
* de cambios: relativa (últimos N minutos) o absoluta (rango de fechas
|
|
6
|
+
* arbitrario). Ideal para alertas, monitoreo de oportunidades y sincronización
|
|
7
|
+
* incremental de un período pasado.
|
|
6
8
|
*/
|
|
7
9
|
import { z } from 'zod';
|
|
8
10
|
import { CompraAgilApiError } from '../utils/error-handler.js';
|
|
11
|
+
import { safeError } from '../utils/redact.js';
|
|
9
12
|
const TOOL_NAME = 'monitorear_cambios_recientes';
|
|
10
|
-
const TOOL_DESCRIPTION = `Busca Compras Ágiles que hayan sido creadas o modificadas
|
|
11
|
-
Ideal para monitoreo de nuevas oportunidades de negocio
|
|
12
|
-
|
|
13
|
+
const TOOL_DESCRIPTION = `Busca Compras Ágiles que hayan sido creadas o modificadas dentro de una ventana de cambios.
|
|
14
|
+
Ideal para monitoreo de nuevas oportunidades de negocio, alertas en tiempo real y sincronización incremental.
|
|
15
|
+
La ventana se define de UNA de dos formas, mutuamente excluyentes:
|
|
16
|
+
• Relativa: 'minutos' (últimos N minutos, máx 1440 = 24h). Es el modo por defecto (60 min).
|
|
17
|
+
• Absoluta: 'cambio_desde' + 'cambio_hasta' (rango ISO-8601 arbitrario), para resincronizar un período pasado
|
|
18
|
+
o retomar desde el último timestamp procesado, sin el techo de 24 horas.
|
|
13
19
|
Puede combinarse con filtros de estado y región para acotar los resultados.`;
|
|
20
|
+
/** Acepta ISO-8601 con 'Z' o con offset (±HH:MM). Los segundos son opcionales. */
|
|
21
|
+
const ISO_8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
14
22
|
const inputSchema = {
|
|
15
|
-
minutos: z.number().min(1).max(1440).
|
|
23
|
+
minutos: z.number().min(1).max(1440).optional().describe('Ventana RELATIVA: buscar cambios en los últimos N minutos. Ej: 60 = última hora, 1440 = últimas 24 horas. Máximo 1440 (24h). Se usa 60 por defecto si no se indica ni "minutos" ni un rango. No combinar con "cambio_desde"/"cambio_hasta".'),
|
|
24
|
+
cambio_desde: z.string().optional().describe('Ventana ABSOLUTA: fecha/hora ISO-8601 de inicio del rango de cambios. Ej: "2026-09-01T00:00:00Z". Úsala para sincronización incremental de un período arbitrario (sin el límite de 24h de "minutos"). No combinar con "minutos".'),
|
|
25
|
+
cambio_hasta: z.string().optional().describe('Ventana ABSOLUTA: fecha/hora ISO-8601 de fin del rango de cambios. Ej: "2026-09-02T00:00:00Z". Requiere "cambio_desde". No combinar con "minutos".'),
|
|
16
26
|
estado: z.string().optional().describe('Filtrar por estado(s), separados por coma. Ej: "publicada" para solo oportunidades abiertas.'),
|
|
17
27
|
region: z.string().optional().describe('Código(s) de región, separados por coma. Ej: "13" para Metropolitana.'),
|
|
18
28
|
tamano_pagina: z.number().min(10).max(50).default(50).describe('Resultados por página (10-50, default 50).'),
|
|
19
29
|
numero_pagina: z.number().min(1).optional().describe('Número de página a consultar (comienza en 1).'),
|
|
20
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Resuelve la ventana de cambios a partir de los argumentos de la herramienta.
|
|
33
|
+
*
|
|
34
|
+
* La API expone dos formas mutuamente excluyentes de acotar los cambios
|
|
35
|
+
* (Guía API Compra Ágil v2 §5.1, Grupo 1): `ttl_cambio_ms` (opción A) o el par
|
|
36
|
+
* `cambio_desde`/`cambio_hasta` (opción B). Enviar ambas a la vez es un 400,
|
|
37
|
+
* así que se decide aquí y se falla localmente para no gastar cuota.
|
|
38
|
+
*
|
|
39
|
+
* Retorna un string con el error de validación, o la ventana resuelta.
|
|
40
|
+
*/
|
|
41
|
+
export function resolverVentanaCambios(args) {
|
|
42
|
+
const usaRango = Boolean(args.cambio_desde || args.cambio_hasta);
|
|
43
|
+
if (usaRango && args.minutos !== undefined) {
|
|
44
|
+
return {
|
|
45
|
+
error: 'Error de validación: "minutos" (ventana relativa) y "cambio_desde"/"cambio_hasta" (ventana absoluta) son mutuamente excluyentes. Usa solo uno de los dos modos.',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (usaRango) {
|
|
49
|
+
if (!args.cambio_desde) {
|
|
50
|
+
return {
|
|
51
|
+
error: 'Error de validación: "cambio_hasta" requiere también "cambio_desde" para delimitar el rango.',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
for (const [nombre, valor] of [
|
|
55
|
+
['cambio_desde', args.cambio_desde],
|
|
56
|
+
['cambio_hasta', args.cambio_hasta],
|
|
57
|
+
]) {
|
|
58
|
+
if (valor && !ISO_8601.test(valor)) {
|
|
59
|
+
return {
|
|
60
|
+
error: `Error de validación: "${nombre}" debe estar en formato ISO-8601 con zona horaria. Ej: "2026-09-01T00:00:00Z". Recibido: "${valor}".`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (args.cambio_hasta && args.cambio_desde > args.cambio_hasta) {
|
|
65
|
+
return {
|
|
66
|
+
error: `Error de validación: "cambio_desde" (${args.cambio_desde}) es posterior a "cambio_hasta" (${args.cambio_hasta}). El rango quedaría vacío.`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
params: { cambio_desde: args.cambio_desde, cambio_hasta: args.cambio_hasta },
|
|
71
|
+
descripcion: args.cambio_hasta
|
|
72
|
+
? `Cambios entre ${args.cambio_desde} y ${args.cambio_hasta}`
|
|
73
|
+
: `Cambios desde ${args.cambio_desde}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Modo relativo (por defecto): 60 minutos si no se indicó nada.
|
|
77
|
+
const minutos = args.minutos ?? 60;
|
|
78
|
+
return {
|
|
79
|
+
params: { ttl_cambio_ms: minutos * 60 * 1000 },
|
|
80
|
+
descripcion: `Últimos ${minutos} minutos`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
21
83
|
export function registerMonitorearCambios(server, client) {
|
|
22
84
|
server.registerTool(TOOL_NAME, {
|
|
23
85
|
description: TOOL_DESCRIPTION,
|
|
24
86
|
inputSchema: inputSchema,
|
|
25
87
|
}, async (args) => {
|
|
26
88
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
89
|
+
const ventana = resolverVentanaCambios(args);
|
|
90
|
+
if ('error' in ventana) {
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text', text: ventana.error }],
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const response = await client.buscar({
|
|
97
|
+
...ventana.params,
|
|
29
98
|
estado: args.estado,
|
|
30
99
|
region: args.region,
|
|
31
100
|
tamano_pagina: args.tamano_pagina,
|
|
@@ -42,7 +111,7 @@ export function registerMonitorearCambios(server, client) {
|
|
|
42
111
|
ultimo_cambio: item.fechas.fecha_ultimo_cambio,
|
|
43
112
|
}));
|
|
44
113
|
const result = {
|
|
45
|
-
ventana_temporal:
|
|
114
|
+
ventana_temporal: ventana.descripcion,
|
|
46
115
|
total_resultados: response.paginacion.total_resultados,
|
|
47
116
|
pagina: `${response.paginacion.numero_pagina} de ${response.paginacion.total_paginas}`,
|
|
48
117
|
resultados: summary,
|
|
@@ -57,7 +126,7 @@ export function registerMonitorearCambios(server, client) {
|
|
|
57
126
|
catch (error) {
|
|
58
127
|
const message = error instanceof CompraAgilApiError
|
|
59
128
|
? error.actionableMessage
|
|
60
|
-
: `Error inesperado: ${
|
|
129
|
+
: `Error inesperado: ${safeError(error)}`;
|
|
61
130
|
return {
|
|
62
131
|
content: [{ type: 'text', text: message }],
|
|
63
132
|
isError: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monitorear-cambios.js","sourceRoot":"","sources":["../../src/tools/monitorear-cambios.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"monitorear-cambios.js","sourceRoot":"","sources":["../../src/tools/monitorear-cambios.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,45 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { CompraAgilClient } from '../api/compra-agil-client.js';
|
|
2
|
+
import { CompraAgilClient, CompraAgilItem } from '../api/compra-agil-client.js';
|
|
3
|
+
export interface OportunidadRadar {
|
|
4
|
+
codigo: string;
|
|
5
|
+
nombre: string;
|
|
6
|
+
organismo: string;
|
|
7
|
+
region: string;
|
|
8
|
+
presupuesto_disponible: number;
|
|
9
|
+
ofertas_recibidas: number;
|
|
10
|
+
horas_restantes: number;
|
|
11
|
+
fecha_cierre: string;
|
|
12
|
+
puntuacion_caliente: number;
|
|
13
|
+
factores_calificacion: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Calcula el "Hot Score" de una oportunidad de forma pura (sin efectos secundarios).
|
|
17
|
+
* Retorna null si el proceso debe omitirse (ya cerrado o bajo el presupuesto mínimo).
|
|
18
|
+
* Ponderación: baja competencia (máx 50) + urgencia (máx 30) + presupuesto (máx 20) + facilidad (máx 5).
|
|
19
|
+
*/
|
|
20
|
+
export declare function evaluarOportunidad(item: CompraAgilItem, now: number, minBudget?: number): OportunidadRadar | null;
|
|
21
|
+
export interface RadarParams {
|
|
22
|
+
region?: string;
|
|
23
|
+
q?: string;
|
|
24
|
+
presupuesto_minimo?: number;
|
|
25
|
+
limite_resultados?: number;
|
|
26
|
+
max_paginas?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface RadarDatos {
|
|
29
|
+
/** Oportunidades vigentes ya rankeadas y recortadas al límite pedido. */
|
|
30
|
+
oportunidades: OportunidadRadar[];
|
|
31
|
+
/** Total de oportunidades vigentes encontradas antes de aplicar el límite. */
|
|
32
|
+
totalAnalizadas: number;
|
|
33
|
+
limite: number;
|
|
34
|
+
paginasEscaneadas: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Recolecta y rankea las oportunidades del radar.
|
|
38
|
+
*
|
|
39
|
+
* Función pura de datos (sin formato de salida): la comparten la tool
|
|
40
|
+
* `radar_oportunidades_calientes` (que emite JSON para el LLM) y el informe
|
|
41
|
+
* HTML/PDF, garantizando que ambos muestren exactamente lo mismo.
|
|
42
|
+
*/
|
|
43
|
+
export declare function recolectarDatosRadar(client: CompraAgilClient, params: RadarParams, now?: number): Promise<RadarDatos>;
|
|
3
44
|
export declare function registerRadarOportunidades(server: McpServer, client: CompraAgilClient): void;
|
|
4
45
|
//# sourceMappingURL=radar-oportunidades.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radar-oportunidades.d.ts","sourceRoot":"","sources":["../../src/tools/radar-oportunidades.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"radar-oportunidades.d.ts","sourceRoot":"","sources":["../../src/tools/radar-oportunidades.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAkBhF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB,EAAE,MAAM,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,qBAAqB,EAAE,MAAM,EAAE,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,MAAM,EACX,SAAS,SAAI,GACZ,gBAAgB,GAAG,IAAI,CAyEzB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,yEAAyE;IACzE,aAAa,EAAE,gBAAgB,EAAE,CAAC;IAClC,8EAA8E;IAC9E,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,WAAW,EACnB,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,UAAU,CAAC,CA6BrB;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CA4C5F"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { CompraAgilApiError } from '../utils/error-handler.js';
|
|
3
3
|
import { logger } from '../utils/logger.js';
|
|
4
|
+
import { safeError } from '../utils/redact.js';
|
|
4
5
|
const TOOL_NAME = 'radar_oportunidades_calientes';
|
|
5
6
|
const TOOL_DESCRIPTION = `Escanea, califica y clasifica de forma priorizada los procesos de Compra Ágil activos (publicados).
|
|
6
7
|
Utiliza una fórmula ponderada (Hot Score) basada en la falta de oferentes, el presupuesto disponible y las horas restantes de cierre para destacar los llamados más convenientes y fáciles de ganar.`;
|
|
@@ -9,23 +10,131 @@ const inputSchema = {
|
|
|
9
10
|
q: z.string().optional().describe('Término de búsqueda opcional para acotar a un rubro o producto específico (ej: "licencias").'),
|
|
10
11
|
presupuesto_minimo: z.number().optional().describe('Filtrar solo procesos con presupuesto disponible mayor o igual a este monto en CLP.'),
|
|
11
12
|
limite_resultados: z.number().min(1).max(20).default(10).optional().describe('Cantidad máxima de oportunidades destacadas a retornar (1-20, default 10).'),
|
|
13
|
+
max_paginas: z.number().min(1).max(10).default(3).optional().describe('Cuántas páginas de 50 resultados escanear antes de rankear (1-10, default 3 = hasta 150 procesos). Más páginas = más cobertura pero más consumo de cuota.'),
|
|
12
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Calcula el "Hot Score" de una oportunidad de forma pura (sin efectos secundarios).
|
|
17
|
+
* Retorna null si el proceso debe omitirse (ya cerrado o bajo el presupuesto mínimo).
|
|
18
|
+
* Ponderación: baja competencia (máx 50) + urgencia (máx 30) + presupuesto (máx 20) + facilidad (máx 5).
|
|
19
|
+
*/
|
|
20
|
+
export function evaluarOportunidad(item, now, minBudget = 0) {
|
|
21
|
+
const budget = item.montos?.monto_disponible_clp || item.montos?.monto_disponible || 0;
|
|
22
|
+
if (budget < minBudget)
|
|
23
|
+
return null;
|
|
24
|
+
let hoursLeft = 0;
|
|
25
|
+
if (item.fechas?.fecha_cierre) {
|
|
26
|
+
const cierreTime = new Date(item.fechas.fecha_cierre).getTime();
|
|
27
|
+
hoursLeft = (cierreTime - now) / (1000 * 60 * 60);
|
|
28
|
+
}
|
|
29
|
+
if (hoursLeft <= 0)
|
|
30
|
+
return null;
|
|
31
|
+
let score = 0;
|
|
32
|
+
const factors = [];
|
|
33
|
+
// Factor 1: Baja Competencia — Máx 50 pts
|
|
34
|
+
const bids = item.resumen?.total_ofertas_recibidas ?? 0;
|
|
35
|
+
if (bids === 0) {
|
|
36
|
+
score += 50;
|
|
37
|
+
factors.push('Sin oferentes activos (+50 pts)');
|
|
38
|
+
}
|
|
39
|
+
else if (bids === 1) {
|
|
40
|
+
score += 30;
|
|
41
|
+
factors.push('Baja competencia: solo 1 oferente (+30 pts)');
|
|
42
|
+
}
|
|
43
|
+
else if (bids === 2) {
|
|
44
|
+
score += 15;
|
|
45
|
+
factors.push('Competencia moderada: 2 oferentes (+15 pts)');
|
|
46
|
+
}
|
|
47
|
+
// Factor 2: Urgencia del Cierre — Máx 30 pts
|
|
48
|
+
if (hoursLeft <= 4) {
|
|
49
|
+
score += 30;
|
|
50
|
+
factors.push('Urgencia crítica: cierra en menos de 4 horas (+30 pts)');
|
|
51
|
+
}
|
|
52
|
+
else if (hoursLeft <= 12) {
|
|
53
|
+
score += 20;
|
|
54
|
+
factors.push('Cierre inminente: cierra en menos de 12 horas (+20 pts)');
|
|
55
|
+
}
|
|
56
|
+
else if (hoursLeft <= 24) {
|
|
57
|
+
score += 10;
|
|
58
|
+
factors.push('Cierre cercano: cierra en menos de 24 horas (+10 pts)');
|
|
59
|
+
}
|
|
60
|
+
// Factor 3: Tamaño de Presupuesto — Máx 20 pts
|
|
61
|
+
if (budget >= 5000000) {
|
|
62
|
+
score += 20;
|
|
63
|
+
factors.push('Presupuesto de alto valor (>= $5.000.000 CLP) (+20 pts)');
|
|
64
|
+
}
|
|
65
|
+
else if (budget >= 2000000) {
|
|
66
|
+
score += 15;
|
|
67
|
+
factors.push('Presupuesto medio-alto (>= $2.000.000 CLP) (+15 pts)');
|
|
68
|
+
}
|
|
69
|
+
else if (budget >= 500000) {
|
|
70
|
+
score += 10;
|
|
71
|
+
factors.push('Presupuesto medio (>= $500.000 CLP) (+10 pts)');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
score += 5;
|
|
75
|
+
factors.push('Presupuesto bajo (< $500.000 CLP) (+5 pts)');
|
|
76
|
+
}
|
|
77
|
+
// Factor 4: Facilidad de Postulación — Máx 5 pts
|
|
78
|
+
const docsCount = item.documentos?.length || 0;
|
|
79
|
+
if (docsCount === 0) {
|
|
80
|
+
score += 5;
|
|
81
|
+
factors.push('Sin documentos adjuntos (postulación rápida sin leer bases) (+5 pts)');
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
codigo: item.codigo,
|
|
85
|
+
nombre: item.nombre,
|
|
86
|
+
organismo: item.institucion?.organismo_comprador || 'No especificado',
|
|
87
|
+
region: item.institucion?.nombre_region || 'No especificada',
|
|
88
|
+
presupuesto_disponible: budget,
|
|
89
|
+
ofertas_recibidas: bids,
|
|
90
|
+
horas_restantes: Math.round(hoursLeft * 10) / 10,
|
|
91
|
+
fecha_cierre: item.fechas?.fecha_cierre,
|
|
92
|
+
puntuacion_caliente: score,
|
|
93
|
+
factores_calificacion: factors,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Recolecta y rankea las oportunidades del radar.
|
|
98
|
+
*
|
|
99
|
+
* Función pura de datos (sin formato de salida): la comparten la tool
|
|
100
|
+
* `radar_oportunidades_calientes` (que emite JSON para el LLM) y el informe
|
|
101
|
+
* HTML/PDF, garantizando que ambos muestren exactamente lo mismo.
|
|
102
|
+
*/
|
|
103
|
+
export async function recolectarDatosRadar(client, params, now = Date.now()) {
|
|
104
|
+
const maxPaginas = params.max_paginas || 3;
|
|
105
|
+
logger.info(`radar: escaneando procesos activos (hasta ${maxPaginas} pág.) region="${params.region || 'Todas'}" q="${params.q || 'Todos'}"`);
|
|
106
|
+
// Auto-paginación: escanear varias páginas para no perder oportunidades en páginas 2+
|
|
107
|
+
const items = await client.buscarTodo({
|
|
108
|
+
estado: 'publicada',
|
|
109
|
+
region: params.region || undefined,
|
|
110
|
+
q: params.q || undefined,
|
|
111
|
+
tamano_pagina: 50,
|
|
112
|
+
}, maxPaginas);
|
|
113
|
+
const minBudget = params.presupuesto_minimo || 0;
|
|
114
|
+
const opportunities = [];
|
|
115
|
+
for (const item of items) {
|
|
116
|
+
const evaluada = evaluarOportunidad(item, now, minBudget);
|
|
117
|
+
if (evaluada)
|
|
118
|
+
opportunities.push(evaluada);
|
|
119
|
+
}
|
|
120
|
+
// Ordenar descendentemente por puntuación caliente
|
|
121
|
+
opportunities.sort((a, b) => b.puntuacion_caliente - a.puntuacion_caliente);
|
|
122
|
+
const limite = params.limite_resultados || 10;
|
|
123
|
+
return {
|
|
124
|
+
oportunidades: opportunities.slice(0, limite),
|
|
125
|
+
totalAnalizadas: opportunities.length,
|
|
126
|
+
limite,
|
|
127
|
+
paginasEscaneadas: maxPaginas,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
13
130
|
export function registerRadarOportunidades(server, client) {
|
|
14
131
|
server.registerTool(TOOL_NAME, {
|
|
15
132
|
description: TOOL_DESCRIPTION,
|
|
16
133
|
inputSchema: inputSchema,
|
|
17
134
|
}, async (args) => {
|
|
18
135
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
region: args.region || undefined,
|
|
22
|
-
q: args.q || undefined,
|
|
23
|
-
tamano_pagina: 50, // Obtener un lote amplio de 50 para poder rankear
|
|
24
|
-
numero_pagina: 1,
|
|
25
|
-
};
|
|
26
|
-
logger.info(`radar_oportunidades_calientes: Escaneando procesos activos para region="${args.region || 'Todas'}" q="${args.q || 'Todos'}"`);
|
|
27
|
-
const searchResponse = await client.buscar(queryParams);
|
|
28
|
-
if (!searchResponse.items || searchResponse.items.length === 0) {
|
|
136
|
+
const datos = await recolectarDatosRadar(client, args);
|
|
137
|
+
if (datos.totalAnalizadas === 0) {
|
|
29
138
|
return {
|
|
30
139
|
content: [{
|
|
31
140
|
type: 'text',
|
|
@@ -33,99 +142,10 @@ export function registerRadarOportunidades(server, client) {
|
|
|
33
142
|
}],
|
|
34
143
|
};
|
|
35
144
|
}
|
|
36
|
-
const now = Date.now();
|
|
37
|
-
const opportunities = [];
|
|
38
|
-
const minBudget = args.presupuesto_minimo || 0;
|
|
39
|
-
for (const item of searchResponse.items) {
|
|
40
|
-
const budget = item.montos?.monto_disponible_clp || item.montos?.monto_disponible || 0;
|
|
41
|
-
// Filtrar por presupuesto mínimo si corresponde
|
|
42
|
-
if (budget < minBudget) {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
// Calcular tiempo restante en horas
|
|
46
|
-
let hoursLeft = 0;
|
|
47
|
-
if (item.fechas?.fecha_cierre) {
|
|
48
|
-
const cierreTime = new Date(item.fechas.fecha_cierre).getTime();
|
|
49
|
-
hoursLeft = (cierreTime - now) / (1000 * 60 * 60);
|
|
50
|
-
}
|
|
51
|
-
// Omitir procesos que ya cerraron o están en negativo por zona horaria
|
|
52
|
-
if (hoursLeft <= 0) {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
let score = 0;
|
|
56
|
-
const factors = [];
|
|
57
|
-
// Factor 1: Baja Competencia (total_ofertas_recibidas) - Máx 50 pts
|
|
58
|
-
const bids = item.resumen?.total_ofertas_recibidas ?? 0;
|
|
59
|
-
if (bids === 0) {
|
|
60
|
-
score += 50;
|
|
61
|
-
factors.push('Sin oferentes activos (+50 pts)');
|
|
62
|
-
}
|
|
63
|
-
else if (bids === 1) {
|
|
64
|
-
score += 30;
|
|
65
|
-
factors.push('Baja competencia: solo 1 oferente (+30 pts)');
|
|
66
|
-
}
|
|
67
|
-
else if (bids === 2) {
|
|
68
|
-
score += 15;
|
|
69
|
-
factors.push('Competencia moderada: 2 oferentes (+15 pts)');
|
|
70
|
-
}
|
|
71
|
-
// Factor 2: Urgencia del Cierre (horas restantes) - Máx 30 pts
|
|
72
|
-
if (hoursLeft <= 4) {
|
|
73
|
-
score += 30;
|
|
74
|
-
factors.push('Urgencia crítica: cierra en menos de 4 horas (+30 pts)');
|
|
75
|
-
}
|
|
76
|
-
else if (hoursLeft <= 12) {
|
|
77
|
-
score += 20;
|
|
78
|
-
factors.push('Cierre inminente: cierra en menos de 12 horas (+20 pts)');
|
|
79
|
-
}
|
|
80
|
-
else if (hoursLeft <= 24) {
|
|
81
|
-
score += 10;
|
|
82
|
-
factors.push('Cierre cercano: cierra en menos de 24 horas (+10 pts)');
|
|
83
|
-
}
|
|
84
|
-
// Factor 3: Tamaño de Presupuesto - Máx 20 pts
|
|
85
|
-
if (budget >= 5000000) {
|
|
86
|
-
score += 20;
|
|
87
|
-
factors.push('Presupuesto de alto valor (>= $5.000.000 CLP) (+20 pts)');
|
|
88
|
-
}
|
|
89
|
-
else if (budget >= 2000000) {
|
|
90
|
-
score += 15;
|
|
91
|
-
factors.push('Presupuesto medio-alto (>= $2.000.000 CLP) (+15 pts)');
|
|
92
|
-
}
|
|
93
|
-
else if (budget >= 500000) {
|
|
94
|
-
score += 10;
|
|
95
|
-
factors.push('Presupuesto medio (>= $500.000 CLP) (+10 pts)');
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
score += 5;
|
|
99
|
-
factors.push('Presupuesto bajo (< $500.000 CLP) (+5 pts)');
|
|
100
|
-
}
|
|
101
|
-
// Factor 4: Facilidad de Postulación - Máx 5 pts
|
|
102
|
-
const docsCount = item.documentos?.length || 0;
|
|
103
|
-
if (docsCount === 0) {
|
|
104
|
-
score += 5;
|
|
105
|
-
factors.push('Sin documentos adjuntos (postulación rápida sin leer bases) (+5 pts)');
|
|
106
|
-
}
|
|
107
|
-
opportunities.push({
|
|
108
|
-
codigo: item.codigo,
|
|
109
|
-
nombre: item.nombre,
|
|
110
|
-
organismo: item.institucion?.organismo_comprador || 'No especificado',
|
|
111
|
-
region: item.institucion?.nombre_region || 'No especificada',
|
|
112
|
-
presupuesto_disponible: budget,
|
|
113
|
-
ofertas_recibidas: bids,
|
|
114
|
-
horas_restantes: Math.round(hoursLeft * 10) / 10,
|
|
115
|
-
fecha_cierre: item.fechas?.fecha_cierre,
|
|
116
|
-
puntuacion_caliente: score,
|
|
117
|
-
factores_calificacion: factors,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
// Ordenar descendentemente por puntuación caliente
|
|
121
|
-
opportunities.sort((a, b) => b.puntuacion_caliente - a.puntuacion_caliente);
|
|
122
|
-
// Aplicar límite
|
|
123
|
-
const limit = args.limite_resultados || 10;
|
|
124
|
-
const rankedOpportunities = opportunities.slice(0, limit);
|
|
125
145
|
const result = {
|
|
126
|
-
total_oportunidades_analizadas:
|
|
127
|
-
radar_limite_resultados:
|
|
128
|
-
oportunidades_calientes:
|
|
146
|
+
total_oportunidades_analizadas: datos.totalAnalizadas,
|
|
147
|
+
radar_limite_resultados: datos.limite,
|
|
148
|
+
oportunidades_calientes: datos.oportunidades,
|
|
129
149
|
};
|
|
130
150
|
return {
|
|
131
151
|
content: [{
|
|
@@ -137,7 +157,7 @@ export function registerRadarOportunidades(server, client) {
|
|
|
137
157
|
catch (error) {
|
|
138
158
|
const message = error instanceof CompraAgilApiError
|
|
139
159
|
? error.actionableMessage
|
|
140
|
-
: `Error inesperado en radar de oportunidades: ${
|
|
160
|
+
: `Error inesperado en radar de oportunidades: ${safeError(error)}`;
|
|
141
161
|
return {
|
|
142
162
|
content: [{ type: 'text', text: message }],
|
|
143
163
|
isError: true,
|