@gishubperu/ghp 0.0.1

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.
Files changed (39) hide show
  1. package/.env +13 -0
  2. package/domain/contracts/IAgent.js +3 -0
  3. package/domain/contracts/IMemoryStore.js +3 -0
  4. package/domain/contracts/IModelProvider.js +3 -0
  5. package/domain/contracts/IToolset.js +3 -0
  6. package/domain/dtos/requests/RunAgentRequest.js +8 -0
  7. package/domain/dtos/responses/AgentResponse.js +8 -0
  8. package/domain/dtos/responses/ToolResult.js +8 -0
  9. package/domain/entities/AgentDefinition.js +11 -0
  10. package/domain/entities/Message.js +10 -0
  11. package/domain/entities/ModelInfo.js +11 -0
  12. package/index.js +66 -0
  13. package/infrastructure/agents/agent-factory.js +79 -0
  14. package/infrastructure/agents/agriculture_agent.js +13 -0
  15. package/infrastructure/agents/base_agent.js +19 -0
  16. package/infrastructure/agents/deforestacion_agent.js +13 -0
  17. package/infrastructure/agents/general_agent.js +13 -0
  18. package/infrastructure/agents/minning_agent.js +13 -0
  19. package/infrastructure/constants/agent-config.js +48 -0
  20. package/infrastructure/constants/agent-type.js +20 -0
  21. package/infrastructure/constants/index.js +8 -0
  22. package/infrastructure/constants/model-config.js +37 -0
  23. package/infrastructure/constants/system-prompts.js +88 -0
  24. package/infrastructure/constants/task-type.js +22 -0
  25. package/infrastructure/core/orchestrator.js +155 -0
  26. package/infrastructure/core/queue.js +53 -0
  27. package/infrastructure/core/workers.js +67 -0
  28. package/infrastructure/memory/store.js +115 -0
  29. package/infrastructure/providers/anthropic.js +59 -0
  30. package/infrastructure/providers/gateway.js +140 -0
  31. package/infrastructure/providers/gemini.js +50 -0
  32. package/infrastructure/providers/ollama.js +92 -0
  33. package/infrastructure/providers/openai.js +83 -0
  34. package/infrastructure/router/router.js +115 -0
  35. package/infrastructure/skills/gis.skill.js +105 -0
  36. package/infrastructure/tools/arcgis.js +187 -0
  37. package/infrastructure/tools/tool-formatters.js +110 -0
  38. package/package.json +32 -0
  39. package/presentation/console/procedures/App.js +424 -0
@@ -0,0 +1,92 @@
1
+ // models/providers/ollama.js
2
+ // Ollama local provider implementation
3
+
4
+ const OLLAMA_BASE = process.env.OLLAMA_HOST ?? 'http://localhost:11434';
5
+
6
+ export class OllamaProvider {
7
+
8
+ #available = false;
9
+ #models = [];
10
+
11
+ async init() {
12
+ try {
13
+ const res = await fetch(`${OLLAMA_BASE}/api/tags`, { signal: AbortSignal.timeout(2000) });
14
+ if (!res.ok) return;
15
+ const data = await res.json();
16
+ this.#models = data.models?.map(m => m.name) ?? [];
17
+ this.#available = this.#models.length > 0;
18
+ } catch {
19
+ this.#available = false;
20
+ }
21
+ }
22
+
23
+ async isAvailable() { return this.#available; }
24
+
25
+ models() { return this.#models; }
26
+
27
+ async chat({ model = 'llama3.2', messages, tools = [], systemPrompt = '' }) {
28
+ const body = {
29
+ model,
30
+ stream: false,
31
+ messages: [
32
+ ...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
33
+ ...messages.map(m => this.#formatMessage(m)),
34
+ ],
35
+ ...(tools.length > 0 ? { tools: tools.map(t => this.#formatTool(t)) } : {}),
36
+ };
37
+
38
+ const res = await fetch(`${OLLAMA_BASE}/api/chat`, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify(body),
42
+ });
43
+
44
+ if (!res.ok) throw new Error(`Ollama error: ${res.status}`);
45
+
46
+ const data = await res.json();
47
+
48
+ const toolCalls = data.message?.tool_calls?.map(tc => ({
49
+ id: tc.id ?? `ollama-${Date.now()}`,
50
+ name: tc.function?.name ?? tc.name ?? '',
51
+ arguments: typeof tc.function?.arguments === 'string'
52
+ ? JSON.parse(tc.function.arguments)
53
+ : (tc.function?.arguments ?? tc.arguments ?? {}),
54
+ })) ?? [];
55
+
56
+ return {
57
+ text: data.message?.content ?? '',
58
+ rawContent: data.message?.content ?? '',
59
+ toolCalls,
60
+ model: `ollama/${model}`,
61
+ };
62
+ }
63
+
64
+ #formatMessage(m) {
65
+ if (m.role === 'tool') {
66
+ return { role: 'tool', content: m.content, tool_call_id: m.tool_call_id };
67
+ }
68
+ if (m.toolCalls) {
69
+ return {
70
+ role: 'assistant',
71
+ content: m.content ?? null,
72
+ tool_calls: m.toolCalls.map(tc => ({
73
+ id: tc.id,
74
+ type: 'function',
75
+ function: { name: tc.name, arguments: JSON.stringify(tc.arguments ?? {}) },
76
+ })),
77
+ };
78
+ }
79
+ return { role: m.role, content: m.content };
80
+ }
81
+
82
+ #formatTool(t) {
83
+ return {
84
+ type: 'function',
85
+ function: {
86
+ name: t.name,
87
+ description: t.description,
88
+ parameters: t.parameters ?? { type: 'object', properties: {} },
89
+ },
90
+ };
91
+ }
92
+ }
@@ -0,0 +1,83 @@
1
+ // models/providers/openai.js
2
+ // OpenAI provider implementation
3
+
4
+ import OpenAI from 'openai';
5
+
6
+ export class OpenAIProvider {
7
+
8
+ #client;
9
+ #available = false;
10
+
11
+ async init() {
12
+ if (!process.env.OPENAI_API_KEY) return;
13
+ this.#client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
14
+ this.#available = true;
15
+ }
16
+
17
+ async isAvailable() { return this.#available; }
18
+
19
+ models() { return ['gpt-4o', 'gpt-4o-mini']; }
20
+
21
+ async chat({ model = 'gpt-4o', messages, tools = [], systemPrompt = '' }) {
22
+ const allMessages = [
23
+ ...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
24
+ ...messages.map(m => this.#formatMessage(m)),
25
+ ];
26
+
27
+ const params = {
28
+ model,
29
+ messages: allMessages,
30
+ ...(tools.length ? { tools: tools.map(t => this.#formatTool(t)) } : {}),
31
+ tool_choice: tools.length > 0 ? 'auto' : undefined,
32
+ };
33
+
34
+ const response = await this.#client.chat.completions.create(params);
35
+ const message = response.choices[0]?.message;
36
+
37
+ if (!message) {
38
+ throw new Error('OpenAI no retornó contenido');
39
+ }
40
+
41
+ const toolCalls = message.tool_calls?.map(tc => ({
42
+ id: tc.id,
43
+ name: tc.function.name,
44
+ arguments: JSON.parse(tc.function.arguments || '{}'),
45
+ })) ?? [];
46
+
47
+ return {
48
+ text: message.content ?? '',
49
+ rawContent: message.content ?? '',
50
+ toolCalls,
51
+ model: `openai/${model}`,
52
+ };
53
+ }
54
+
55
+ #formatMessage(m) {
56
+ if (m.role === 'tool') {
57
+ return { role: 'tool', tool_call_id: m.tool_call_id, content: m.content };
58
+ }
59
+ if (m.toolCalls) {
60
+ return {
61
+ role: 'assistant',
62
+ content: m.content ?? null,
63
+ tool_calls: m.toolCalls.map(tc => ({
64
+ id: tc.id,
65
+ type: 'function',
66
+ function: { name: tc.name, arguments: JSON.stringify(tc.arguments ?? {}) },
67
+ })),
68
+ };
69
+ }
70
+ return { role: m.role, content: m.content };
71
+ }
72
+
73
+ #formatTool(t) {
74
+ return {
75
+ type: 'function',
76
+ function: {
77
+ name: t.name,
78
+ description: t.description,
79
+ parameters: t.parameters ?? { type: 'object', properties: {} },
80
+ },
81
+ };
82
+ }
83
+ }
@@ -0,0 +1,115 @@
1
+ // models/router.js
2
+
3
+ import { GeminiProvider } from '../providers/gemini.js';
4
+ import { AnthropicProvider } from '../providers/anthropic.js';
5
+ import { OpenAIProvider } from '../providers/openai.js';
6
+ import { OllamaProvider } from '../providers/ollama.js';
7
+ import { GatewayProvider } from '../providers/gateway.js';
8
+ import { TASK_MODEL_MAP, FALLBACK_CHAIN, TaskType } from '../constants/index.js';
9
+
10
+ export class ModelRouter {
11
+
12
+ #providers = {};
13
+ #active = null;
14
+ #recentUsed = [];
15
+
16
+ async init() {
17
+ const defs = [
18
+ { key: 'gateway', Cls: GatewayProvider },
19
+ { key: 'gemini', Cls: GeminiProvider },
20
+ { key: 'anthropic', Cls: AnthropicProvider },
21
+ { key: 'openai', Cls: OpenAIProvider },
22
+ { key: 'ollama', Cls: OllamaProvider },
23
+ ];
24
+ await Promise.allSettled(defs.map(async ({ key, Cls }) => {
25
+ try {
26
+ const p = new Cls();
27
+ await p.init();
28
+ if (await p.isAvailable()) this.#providers[key] = p;
29
+ } catch { }
30
+ }));
31
+ }
32
+
33
+ listAvailable() {
34
+ const list = [];
35
+ const seen = new Set();
36
+
37
+ const gw = this.#providers['gateway'];
38
+ if (gw) gw.getModelList().forEach(m => {
39
+ if (seen.has(m.id)) return;
40
+ seen.add(m.id);
41
+ list.push({ ...m, recent: this.#recentUsed.includes(m.id), gateway: true });
42
+ });
43
+
44
+ const a = this.#providers['anthropic'];
45
+ if (a) a.models().forEach(m => {
46
+ if (seen.has(m)) return;
47
+ seen.add(m);
48
+ list.push({ id: m, name: `Anthropic: ${m}`, provider: 'anthropic', free: false });
49
+ });
50
+
51
+ const g = this.#providers['gemini'];
52
+ if (g) g.models().forEach(m => {
53
+ if (seen.has(m)) return;
54
+ seen.add(m);
55
+ list.push({ id: m, name: `Gemini: ${m}`, provider: 'gemini', free: false });
56
+ });
57
+
58
+ return list;
59
+ }
60
+
61
+ setModel(provider, model) {
62
+ if (!this.#providers[provider]) throw new Error(`Provider '${provider}' no disponible`);
63
+ this.#active = { provider, model };
64
+ }
65
+
66
+ setModelFromMenu(item) {
67
+ const provider = item.gateway ? 'gateway'
68
+ : (item.provider?.toLowerCase() ?? 'gateway');
69
+ const resolved = this.#providers[provider] ? provider
70
+ : this.#providers['gateway'] ? 'gateway'
71
+ : null;
72
+ if (!resolved) throw new Error('No hay provider disponible para este modelo');
73
+ this.#active = { provider: resolved, model: item.id };
74
+ this.#recentUsed = [item.id, ...this.#recentUsed.filter(x => x !== item.id)].slice(0, 3);
75
+ }
76
+
77
+ setAuto() { this.#active = null; }
78
+ getActive() { return this.#active; }
79
+
80
+ routeFor(taskType = TaskType.FAST) {
81
+ if (this.#active) return this.#active;
82
+ const preferred = TASK_MODEL_MAP[taskType] ?? TASK_MODEL_MAP[TaskType.FAST];
83
+ if (this.#providers[preferred.provider]) return preferred;
84
+ return FALLBACK_CHAIN.find(f => this.#providers[f.provider]) ?? null;
85
+ }
86
+
87
+ async chat({ taskType = TaskType.FAST, messages, tools = [], systemPrompt = '', signal = null }) {
88
+ const base = this.#active ?? this.routeFor(taskType);
89
+ const chain = [base, ...FALLBACK_CHAIN].filter(Boolean);
90
+
91
+ const tried = new Set();
92
+ let lastErr;
93
+
94
+ for (const target of chain) {
95
+ const key = `${target.provider}:${target.model}`;
96
+ if (tried.has(key)) continue;
97
+ tried.add(key);
98
+
99
+ const provider = this.#providers[target.provider];
100
+ if (!provider) continue;
101
+
102
+ try {
103
+ const result = await provider.chat({ model: target.model, messages, tools, systemPrompt, signal });
104
+ if (this.#active)
105
+ this.#recentUsed = [target.model, ...this.#recentUsed.filter(x => x !== target.model)].slice(0, 3);
106
+ return result;
107
+ } catch (err) {
108
+ lastErr = err;
109
+ if (err.retryable === false) throw err;
110
+ }
111
+ }
112
+
113
+ throw lastErr ?? new Error('Todos los modelos fallaron.');
114
+ }
115
+ }
@@ -0,0 +1,105 @@
1
+ // infrastructure/skills/gis.skill.js
2
+ // SKILL: ArcGIS REST API completo para PPA
3
+
4
+ export const GIS_SKILL = `
5
+ # SKILL: Agente Agricultura — ArcGIS REST API + PPA
6
+
7
+ Eres el agente Agricultura del sistema GHP CLI. Tu especialidad es consultar el servicio ArcGIS REST del PPA y extraer inteligencia geoespacial real usando todas las capacidades avanzadas de la API.
8
+
9
+ ## SERVICIO
10
+ URL base: https://services5.arcgis.com/jQsv3VqjMgcZI7Fe/ArcGIS/rest/services/ALERTAS_RS_BK/FeatureServer/3
11
+ Endpoint de consulta: {URL_BASE}/query
12
+ Siempre usar: f=json
13
+
14
+ ---
15
+
16
+ ## FLUJO OBLIGATORIO
17
+ 1. **SIEMPRE** llama getServiceMetadata primero si no conoces los campos exactos o hay tantas consultas inválidas
18
+ 2. Construye la consulta con los nombres técnicos exactos (case-sensitive)
19
+ 3. Elige el modo de consulta correcto según lo que pide el usuario
20
+ 4. Presenta los resultados de forma clara, nunca como JSON crudo
21
+
22
+ ---
23
+
24
+ ## MODOS DE CONSULTA
25
+
26
+ ### 1. CONSULTA BÁSICA — getArcGISData
27
+ Para obtener registros individuales con filtros.
28
+
29
+ Parámetros clave:
30
+ - where: cláusula SQL (OBLIGATORIO, mínimo '1=1')
31
+ - outFields: campos separados por coma, o '*' para todos
32
+ - orderByFields: campo + ASC|DESC
33
+ - resultRecordCount: límite de registros
34
+ - resultOffset: para paginación
35
+ - returnGeometry: false para datos tabulares (más rápido)
36
+ - returnCountOnly: true para solo contar
37
+ - returnDistinctValues: true + outFields=CAMPO para valores únicos
38
+
39
+ Ejemplos WHERE:
40
+ '1=1' → todos los registros
41
+ "TXT_DEPARTAMENTO='PIURA'" → filtro exacto
42
+ "TXT_DEPARTAMENTO='PIURA' AND NUM_AREA>5" → múltiples condiciones
43
+ "TXT_CULTIVO LIKE '%MAIZ%'" → búsqueda parcial
44
+ "NUM_AREA BETWEEN 1 AND 10" → rango numérico
45
+ "TXT_ESTADO IS NOT NULL" → valores no nulos
46
+ "UPPER(TXT_DEPARTAMENTO)='PIURA'" → case-insensitive
47
+
48
+ ### 2. ESTADÍSTICAS — outStatistics
49
+ Para calcular agregaciones sin traer todos los registros.
50
+
51
+ Tipos: count, sum, min, max, avg, stddev, var
52
+
53
+ Formato:
54
+ [
55
+ {"statisticType":"count","onStatisticField":"OBJECTID","outStatisticFieldName":"TOTAL"},
56
+ {"statisticType":"sum","onStatisticField":"NUM_AREA","outStatisticFieldName":"AREA_TOTAL"},
57
+ {"statisticType":"avg","onStatisticField":"NUM_AREA","outStatisticFieldName":"AREA_PROMEDIO"}
58
+ ]
59
+
60
+ Combinar con groupByFieldsForStatistics: "TXT_DEPARTAMENTO"
61
+ Combinar con having: "SUM(NUM_AREA) > 100"
62
+
63
+ ### 3. VALORES ÚNICOS
64
+ outFields: "TXT_DEPARTAMENTO", returnDistinctValues: true
65
+
66
+ ### 4. SOLO CONTAR
67
+ returnCountOnly: true → responde {count: N}
68
+
69
+ ### 5. PAGINACIÓN
70
+ Primera página: resultOffset=0, resultRecordCount=1000
71
+ Si exceededTransferLimit=true → hay más datos, paginar
72
+
73
+ ---
74
+
75
+ ## REGLAS SQL
76
+
77
+ | Tipo | Correcto | Incorrecto |
78
+ |---------|---------------------------------|---------------------|
79
+ | String | TXT_CAMPO='VALOR' | TXT_CAMPO="VALOR" |
80
+ | Número | NUM_CAMPO > 5 | NUM_CAMPO > '5' |
81
+ | LIKE | TXT_CAMPO LIKE '%TEXTO%' | |
82
+ | NULL | TXT_CAMPO IS NULL | TXT_CAMPO = NULL |
83
+ | IN | TXT_CAMPO IN ('A','B','C') | |
84
+
85
+ ---
86
+
87
+ ## PRESENTACIÓN DE RESULTADOS
88
+
89
+ NUNCA mostrar JSON crudo. Siempre:
90
+ 1. Total de registros encontrados
91
+ 2. Estadísticas en tabla legible
92
+ 3. Top 3-5 valores cuando sea relevante
93
+ 4. Avisar si exceededTransferLimit=true
94
+
95
+ ---
96
+
97
+ ## ERRORES COMUNES
98
+
99
+ | Error | Causa | Solución |
100
+ |----------------------------|-----------------------------|---------------------------------|
101
+ | null masivo en campo | Nombre incorrecto | Verificar con getServiceMetadata|
102
+ | Error 400 | Sintaxis WHERE incorrecta | Revisar comillas y nombres |
103
+ | 0 resultados inesperado | String mal escrito | Probar UPPER() o LIKE |
104
+ | exceededTransferLimit | Demasiados registros | Reducir resultRecordCount |
105
+ `;
@@ -0,0 +1,187 @@
1
+ // tools/arcgis.js — ArcGIS REST API completo para PPA
2
+
3
+ import axios from 'axios';
4
+ import PDFDocument from 'pdfkit';
5
+ import fs from 'fs';
6
+
7
+ const SERVICE_URL = "https://services5.arcgis.com/jQsv3VqjMgcZI7Fe/ArcGIS/rest/services/ALERTAS_RS_BK/FeatureServer/3";
8
+
9
+ export const toolset = {
10
+ declarations: [
11
+ {
12
+ name: "getServiceMetadata",
13
+ description: "Consulta la definición completa del servicio ArcGIS. Úsala PRIMERO para conocer campos exactos (nombres, tipos), capacidades (supportsStatistics, supportsHavingClause) y maxRecordCount.",
14
+ },
15
+ {
16
+ name: "getArcGISData",
17
+ description: `Consulta el FeatureServer con todos los parámetros ArcGIS REST API.
18
+ Modos disponibles según parámetros:
19
+ - Registros normales: where + outFields + orderByFields + resultRecordCount + resultOffset
20
+ - Estadísticas: outStatistics + groupByFieldsForStatistics + having (requiere supportsStatistics=true)
21
+ - Solo contar: returnCountOnly=true
22
+ - Valores únicos: returnDistinctValues=true + outFields=CAMPO
23
+ - Paginación: resultOffset + resultRecordCount`,
24
+ parameters: {
25
+ type: "object",
26
+ properties: {
27
+ where: {
28
+ type: "string",
29
+ description: "Cláusula SQL. Strings con comillas simples: TXT_DEPARTAMENTO='PIURA'. Default: '1=1'"
30
+ },
31
+ outFields: {
32
+ type: "string",
33
+ description: "Campos separados por coma o '*'. Ej: 'TXT_DEPARTAMENTO,NUM_AREA,TXT_CULTIVO'"
34
+ },
35
+ orderByFields: {
36
+ type: "string",
37
+ description: "Ordenamiento. Ej: 'NUM_AREA DESC' o 'TXT_DEPARTAMENTO ASC'"
38
+ },
39
+ resultRecordCount: {
40
+ type: "number",
41
+ description: "Máximo registros a retornar. Respetar maxRecordCount del servicio."
42
+ },
43
+ resultOffset: {
44
+ type: "number",
45
+ description: "Para paginación: número de registros a saltar. Primera página=0."
46
+ },
47
+ returnCountOnly: {
48
+ type: "boolean",
49
+ description: "true = retorna solo el conteo {count: N}, sin features."
50
+ },
51
+ returnDistinctValues: {
52
+ type: "boolean",
53
+ description: "true = valores únicos del campo en outFields."
54
+ },
55
+ returnGeometry: {
56
+ type: "boolean",
57
+ description: "true = incluir geometría. false (default) = solo atributos, más rápido."
58
+ },
59
+ outStatistics: {
60
+ type: "string",
61
+ description: `JSON array de estadísticas. Tipos: count, sum, min, max, avg, stddev, var.
62
+ Formato: [{"statisticType":"sum","onStatisticField":"NUM_AREA","outStatisticFieldName":"AREA_TOTAL"},{"statisticType":"count","onStatisticField":"OBJECTID","outStatisticFieldName":"TOTAL"}]
63
+ Requiere supportsStatistics=true en el servicio.`
64
+ },
65
+ groupByFieldsForStatistics: {
66
+ type: "string",
67
+ description: "Campo(s) para agrupar estadísticas. Ej: 'TXT_DEPARTAMENTO' o 'TXT_DEPARTAMENTO,TXT_CULTIVO'"
68
+ },
69
+ having: {
70
+ type: "string",
71
+ description: "Filtro sobre grupos estadísticos. Ej: 'SUM(NUM_AREA) > 100'. Requiere supportsHavingClause=true."
72
+ }
73
+ }
74
+ }
75
+ },
76
+ {
77
+ name: "createPDFReport",
78
+ description: "Genera reporte PDF formal con datos PPA. Usar al final después de analizar los datos.",
79
+ parameters: {
80
+ type: "object",
81
+ properties: {
82
+ filename: { type: "string", description: "Nombre del archivo sin extensión" },
83
+ title: { type: "string", description: "Título del reporte" },
84
+ content: { type: "string", description: "Contenido completo del reporte con análisis" },
85
+ layout: { type: "string", enum: ["portrait", "landscape"] }
86
+ },
87
+ required: ["filename", "title", "content", "layout"]
88
+ }
89
+ }
90
+ ],
91
+
92
+ execute: async (name, args) => {
93
+ switch (name) {
94
+
95
+ case "getServiceMetadata": {
96
+ const res = await axios.get(`${SERVICE_URL}?f=json`, { timeout: 15000 });
97
+ const d = res.data;
98
+ return {
99
+ fields: d.fields?.map(f => ({
100
+ name: f.name,
101
+ type: f.type,
102
+ alias: f.alias,
103
+ length: f.length,
104
+ })) ?? [],
105
+ capabilities: d.capabilities,
106
+ maxRecordCount: d.maxRecordCount,
107
+ supportsStatistics: d.advancedQueryCapabilities?.supportsStatistics ?? false,
108
+ supportsHavingClause: d.advancedQueryCapabilities?.supportsHavingClause ?? false,
109
+ supportsDistinct: d.advancedQueryCapabilities?.supportsDistinct ?? false,
110
+ supportsPagination: d.advancedQueryCapabilities?.supportsPagination ?? false,
111
+ supportsOrderBy: d.advancedQueryCapabilities?.supportsOrderBy ?? false,
112
+ description: d.description,
113
+ geometryType: d.geometryType,
114
+ };
115
+ }
116
+
117
+ case "getArcGISData": {
118
+ const params = {
119
+ where: args.where || '1=1',
120
+ outFields: args.outFields || '*',
121
+ returnGeometry: args.returnGeometry ?? false,
122
+ returnCountOnly: args.returnCountOnly ?? false,
123
+ returnDistinctValues: args.returnDistinctValues ?? false,
124
+ f: 'json',
125
+ };
126
+
127
+ if (args.orderByFields) params.orderByFields = args.orderByFields;
128
+ if (args.resultRecordCount != null) params.resultRecordCount = args.resultRecordCount;
129
+ if (args.resultOffset != null) params.resultOffset = args.resultOffset;
130
+ if (args.outStatistics) params.outStatistics = args.outStatistics;
131
+ if (args.groupByFieldsForStatistics) params.groupByFieldsForStatistics = args.groupByFieldsForStatistics;
132
+ if (args.having) params.having = args.having;
133
+
134
+ const res = await axios.get(`${SERVICE_URL}/query`, {
135
+ params, timeout: 30000
136
+ });
137
+
138
+ const d = res.data;
139
+
140
+ // Error del servicio
141
+ if (d.error) return { error: d.error.message, code: d.error.code };
142
+
143
+ // Modo count
144
+ if (args.returnCountOnly) return { count: d.count };
145
+
146
+ // Modo normal o estadísticas
147
+ const features = d.features?.map(f => f.attributes) ?? [];
148
+ return {
149
+ features,
150
+ total: features.length,
151
+ exceededTransferLimit: d.exceededTransferLimit ?? false,
152
+ };
153
+ }
154
+
155
+ case "createPDFReport": {
156
+ const doc = new PDFDocument({ layout: args.layout || 'portrait', margin: 40 });
157
+ const path = `./${args.filename}.pdf`;
158
+ doc.pipe(fs.createWriteStream(path));
159
+
160
+ // Header azul institucional
161
+ doc.rect(0, 0, doc.page.width, 65).fill('#1a3a5c');
162
+ doc.fillColor('white').fontSize(16).font('Helvetica-Bold')
163
+ .text('GHP CLI | — ' + (args.title || 'Informe PPA'), 40, 22, { align: 'center' });
164
+ doc.fillColor('#a0c4ff').fontSize(9)
165
+ .text('Padrón de productores agrarios — PPA', 40, 44, { align: 'center' });
166
+
167
+ // Contenido
168
+ doc.fillColor('black').moveDown(3);
169
+ doc.fontSize(10).font('Helvetica').text(args.content, {
170
+ paragraphGap: 6, lineGap: 3, align: 'justify'
171
+ });
172
+
173
+ // Footer
174
+ doc.fontSize(8).fillColor('#666')
175
+ .text(
176
+ `GHP CLI Agent · Perú · Generado: ${new Date().toLocaleDateString('es-PE', { dateStyle: 'long' })}`,
177
+ 40, doc.page.height - 35, { align: 'center' }
178
+ );
179
+ doc.end();
180
+ return { success: true, path, message: `PDF generado: ${path}` };
181
+ }
182
+
183
+ default:
184
+ return { error: `Tool desconocida: ${name}` };
185
+ }
186
+ }
187
+ };