@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.
- package/.env +13 -0
- package/domain/contracts/IAgent.js +3 -0
- package/domain/contracts/IMemoryStore.js +3 -0
- package/domain/contracts/IModelProvider.js +3 -0
- package/domain/contracts/IToolset.js +3 -0
- package/domain/dtos/requests/RunAgentRequest.js +8 -0
- package/domain/dtos/responses/AgentResponse.js +8 -0
- package/domain/dtos/responses/ToolResult.js +8 -0
- package/domain/entities/AgentDefinition.js +11 -0
- package/domain/entities/Message.js +10 -0
- package/domain/entities/ModelInfo.js +11 -0
- package/index.js +66 -0
- package/infrastructure/agents/agent-factory.js +79 -0
- package/infrastructure/agents/agriculture_agent.js +13 -0
- package/infrastructure/agents/base_agent.js +19 -0
- package/infrastructure/agents/deforestacion_agent.js +13 -0
- package/infrastructure/agents/general_agent.js +13 -0
- package/infrastructure/agents/minning_agent.js +13 -0
- package/infrastructure/constants/agent-config.js +48 -0
- package/infrastructure/constants/agent-type.js +20 -0
- package/infrastructure/constants/index.js +8 -0
- package/infrastructure/constants/model-config.js +37 -0
- package/infrastructure/constants/system-prompts.js +88 -0
- package/infrastructure/constants/task-type.js +22 -0
- package/infrastructure/core/orchestrator.js +155 -0
- package/infrastructure/core/queue.js +53 -0
- package/infrastructure/core/workers.js +67 -0
- package/infrastructure/memory/store.js +115 -0
- package/infrastructure/providers/anthropic.js +59 -0
- package/infrastructure/providers/gateway.js +140 -0
- package/infrastructure/providers/gemini.js +50 -0
- package/infrastructure/providers/ollama.js +92 -0
- package/infrastructure/providers/openai.js +83 -0
- package/infrastructure/router/router.js +115 -0
- package/infrastructure/skills/gis.skill.js +105 -0
- package/infrastructure/tools/arcgis.js +187 -0
- package/infrastructure/tools/tool-formatters.js +110 -0
- package/package.json +32 -0
- 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
|
+
};
|