@quantyapp/quanty-mcp-server 1.0.10 → 1.2.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/dist/bin/quanty-mcp-server.exe +0 -0
- package/dist/core/analysisService.d.ts +27 -0
- package/dist/core/analysisService.d.ts.map +1 -1
- package/dist/core/analysisService.js +84 -0
- package/dist/core/bankService.d.ts +26 -1
- package/dist/core/bankService.d.ts.map +1 -1
- package/dist/core/bankService.js +114 -5
- package/dist/core/budgetService.d.ts +190 -0
- package/dist/core/budgetService.d.ts.map +1 -1
- package/dist/core/budgetService.js +628 -4
- package/dist/core/learningService.d.ts +94 -0
- package/dist/core/learningService.d.ts.map +1 -0
- package/dist/core/learningService.js +462 -0
- package/dist/core/matchingService.d.ts +76 -0
- package/dist/core/matchingService.d.ts.map +1 -0
- package/dist/core/matchingService.js +437 -0
- package/dist/mcp/index.js +1180 -189
- package/package.json +10 -1
package/dist/mcp/index.js
CHANGED
|
@@ -7,11 +7,66 @@ import { initAuth } from '../core/auth.js';
|
|
|
7
7
|
import * as budgetService from '../core/budgetService.js';
|
|
8
8
|
import * as bankService from '../core/bankService.js';
|
|
9
9
|
import * as analysisService from '../core/analysisService.js';
|
|
10
|
+
import * as matchingService from '../core/matchingService.js';
|
|
11
|
+
import { initLearningSystem, getLearningSystem, getContextBuilder } from '../core/learningService.js';
|
|
12
|
+
// ================ GLOBAL STATE ================
|
|
13
|
+
let learningSystem = null;
|
|
14
|
+
let contextBuilder = null;
|
|
10
15
|
// Pending operations for confirmation workflow
|
|
11
16
|
const pendingOperations = new Map();
|
|
12
17
|
function generatePendingId() {
|
|
13
18
|
return `pending_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
14
19
|
}
|
|
20
|
+
// ================ LEARNING WRAPPER ================
|
|
21
|
+
/**
|
|
22
|
+
* Wraps tool execution with learning capabilities
|
|
23
|
+
*/
|
|
24
|
+
async function executeWithLearning(taskDescription, executionFn, extractSteps) {
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
let result;
|
|
27
|
+
let success = false;
|
|
28
|
+
let steps = [];
|
|
29
|
+
try {
|
|
30
|
+
// Get relevant context from learning system
|
|
31
|
+
if (contextBuilder) {
|
|
32
|
+
try {
|
|
33
|
+
const context = await contextBuilder.buildContext(taskDescription);
|
|
34
|
+
if (context) {
|
|
35
|
+
console.error(`[Learning] Applied context for: ${taskDescription}`);
|
|
36
|
+
// Optionally inject context into global scope or similar if possible
|
|
37
|
+
// For now, we mainly use it for system prompts or logging suggestions
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error(`[Learning] Failed to build context:`, err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Execute the actual function
|
|
45
|
+
result = await executionFn();
|
|
46
|
+
success = true;
|
|
47
|
+
// Extract steps if provided
|
|
48
|
+
if (extractSteps) {
|
|
49
|
+
try {
|
|
50
|
+
steps = extractSteps(result);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
console.error(`[Learning] Failed to extract steps:`, err);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Learn from successful execution
|
|
57
|
+
if (learningSystem && success) {
|
|
58
|
+
const executionTime = Date.now() - startTime;
|
|
59
|
+
// Fire and forget learning to not block response
|
|
60
|
+
learningSystem.learnFromExecution(taskDescription, steps, result, executionTime).catch(err => console.error(`[Learning] Failed to store execution:`, err));
|
|
61
|
+
console.error(`[Learning] Stored execution: ${taskDescription} (${executionTime}ms)`);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`[Learning] Execution failed: ${error}`);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
15
70
|
// ============================================================
|
|
16
71
|
// MANUAL DO QUANTY - Este conteúdo é lido automaticamente pela IA
|
|
17
72
|
// ============================================================
|
|
@@ -167,7 +222,8 @@ A importação traz automaticamente a unidade e custo do banco.`,
|
|
|
167
222
|
orcamento_id: { type: 'string', description: 'ID do orçamento destino' },
|
|
168
223
|
banco_item_id: { type: 'string', description: 'ID do item no banco (retornado por quanty_buscar_insumo)' },
|
|
169
224
|
banco_id: { type: 'string', description: 'ID do banco de origem' },
|
|
170
|
-
quantidade: { type: 'number', description: 'Quantidade a ser orçada' }
|
|
225
|
+
quantidade: { type: 'number', description: 'Quantidade a ser orçada' },
|
|
226
|
+
nivel: { type: 'number', description: 'Nível hierárquico/identação do item (0=raiz, 1=filho, 2=neto, etc). Padrão: 0' }
|
|
171
227
|
},
|
|
172
228
|
required: ['orcamento_id', 'banco_item_id', 'banco_id', 'quantidade']
|
|
173
229
|
}
|
|
@@ -198,11 +254,61 @@ Retorna preview e pending_id. Use quanty_executar para confirmar.`,
|
|
|
198
254
|
descricao: { type: 'string', description: 'Descrição do item (ex: "Demolição manual de alvenaria")' },
|
|
199
255
|
unidade: { type: 'string', description: 'Unidade de medida (m², m³, m, kg, un, h, vb)' },
|
|
200
256
|
quantidade: { type: 'number', description: 'Quantidade do serviço' },
|
|
201
|
-
custo_unitario: { type: 'number', description: 'Custo unitário em R$ (sem BDI)' }
|
|
257
|
+
custo_unitario: { type: 'number', description: 'Custo unitário em R$ (sem BDI)' },
|
|
258
|
+
nivel: { type: 'number', description: 'Nível hierárquico/identação do item (0=raiz, 1=filho, 2=neto, etc). Padrão: 0' }
|
|
202
259
|
},
|
|
203
260
|
required: ['orcamento_id', 'descricao', 'unidade', 'quantidade', 'custo_unitario']
|
|
204
261
|
}
|
|
205
262
|
},
|
|
263
|
+
{
|
|
264
|
+
name: 'quanty_criar_composicao',
|
|
265
|
+
description: `Cria uma COMPOSIÇÃO (serviço composto) com seus INSUMOS do zero.
|
|
266
|
+
Diferente de importar do banco, aqui você define todos os insumos manualmente.
|
|
267
|
+
O custo unitário da composição é calculado automaticamente pela soma dos custos dos insumos.
|
|
268
|
+
Retorna preview e pending_id. Use quanty_executar para confirmar.
|
|
269
|
+
|
|
270
|
+
EXEMPLO de uso:
|
|
271
|
+
{
|
|
272
|
+
"orcamento_id": "abc123",
|
|
273
|
+
"descricao": "Concretagem de pilar 20x40",
|
|
274
|
+
"unidade": "m³",
|
|
275
|
+
"quantidade": 15,
|
|
276
|
+
"insumos": [
|
|
277
|
+
{"descricao": "Concreto fck 30 MPa", "unidade": "m³", "quantidade": 1.05, "custo_unitario": 450.00},
|
|
278
|
+
{"descricao": "Forma de madeira", "unidade": "m²", "quantidade": 2.4, "custo_unitario": 85.00},
|
|
279
|
+
{"descricao": "Aço CA-50", "unidade": "kg", "quantidade": 120, "custo_unitario": 8.50},
|
|
280
|
+
{"descricao": "Pedreiro", "unidade": "h", "quantidade": 4, "custo_unitario": 25.00, "tipo": "MÃO DE OBRA"}
|
|
281
|
+
]
|
|
282
|
+
}`,
|
|
283
|
+
inputSchema: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {
|
|
286
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento destino' },
|
|
287
|
+
codigo: { type: 'string', description: 'Código opcional da composição (ex: "COMP-001")' },
|
|
288
|
+
descricao: { type: 'string', description: 'Descrição da composição (ex: "Concretagem de pilar 20x40")' },
|
|
289
|
+
unidade: { type: 'string', description: 'Unidade de medida da composição (m², m³, m, un, vb)' },
|
|
290
|
+
quantidade: { type: 'number', description: 'Quantidade da composição no orçamento' },
|
|
291
|
+
nivel: { type: 'number', description: 'Nível hierárquico/identação (0=raiz, 1=filho, etc). Padrão: 0' },
|
|
292
|
+
insumos: {
|
|
293
|
+
type: 'array',
|
|
294
|
+
description: 'Lista de insumos que compõem o serviço',
|
|
295
|
+
items: {
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: {
|
|
298
|
+
codigo: { type: 'string', description: 'Código opcional do insumo' },
|
|
299
|
+
descricao: { type: 'string', description: 'Descrição do insumo' },
|
|
300
|
+
unidade: { type: 'string', description: 'Unidade do insumo' },
|
|
301
|
+
quantidade: { type: 'number', description: 'Coeficiente/quantidade do insumo por unidade da composição' },
|
|
302
|
+
custo_unitario: { type: 'number', description: 'Custo unitário do insumo em R$' },
|
|
303
|
+
tipo: { type: 'string', description: 'Tipo: INSUMO, MÃO DE OBRA, EQUIPAMENTO, etc. Padrão: INSUMO' }
|
|
304
|
+
},
|
|
305
|
+
required: ['descricao', 'unidade', 'quantidade', 'custo_unitario']
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
required: ['orcamento_id', 'descricao', 'unidade', 'quantidade', 'insumos']
|
|
310
|
+
}
|
|
311
|
+
},
|
|
206
312
|
{
|
|
207
313
|
name: 'quanty_executar',
|
|
208
314
|
description: `Confirma e executa uma operação pendente (criar orçamento, adicionar item, etc).
|
|
@@ -256,236 +362,1095 @@ Retorna média, mínimo, máximo e percentil 90 (para detecção de outliers).`,
|
|
|
256
362
|
},
|
|
257
363
|
required: ['driver']
|
|
258
364
|
}
|
|
365
|
+
},
|
|
366
|
+
// === MANAGEMENT TOOLS ===
|
|
367
|
+
{
|
|
368
|
+
name: 'quanty_ajustar_bdi',
|
|
369
|
+
description: `Ajusta o BDI (Bonificações e Despesas Indiretas) do orçamento.
|
|
370
|
+
O BDI é aplicado sobre o custo para calcular o preço de venda.
|
|
371
|
+
Valor típico: 20% a 30%.
|
|
372
|
+
Retorna preview e pending_id. Use quanty_executar para confirmar.`,
|
|
373
|
+
inputSchema: {
|
|
374
|
+
type: 'object',
|
|
375
|
+
properties: {
|
|
376
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
377
|
+
bdi_principal: { type: 'number', description: 'BDI Principal em % (ex: 25 para 25%)' },
|
|
378
|
+
bdi_diferenciado: { type: 'number', description: 'BDI Diferenciado em % (para equipamentos/materiais especiais)' }
|
|
379
|
+
},
|
|
380
|
+
required: ['orcamento_id']
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: 'quanty_editar_item',
|
|
385
|
+
description: `Edita um item EXISTENTE no orçamento.
|
|
386
|
+
Permite alterar descrição, unidade, quantidade, custo unitário, nível e código.
|
|
387
|
+
Use quanty_abrir_orcamento para obter os IDs dos itens.
|
|
388
|
+
Retorna preview e pending_id. Use quanty_executar para confirmar.`,
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: 'object',
|
|
391
|
+
properties: {
|
|
392
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
393
|
+
item_id: { type: 'string', description: 'ID do item a editar' },
|
|
394
|
+
descricao: { type: 'string', description: 'Nova descrição (opcional)' },
|
|
395
|
+
unidade: { type: 'string', description: 'Nova unidade (opcional)' },
|
|
396
|
+
quantidade: { type: 'number', description: 'Nova quantidade (opcional)' },
|
|
397
|
+
custo_unitario: { type: 'number', description: 'Novo custo unitário (opcional)' },
|
|
398
|
+
nivel: { type: 'number', description: 'Novo nível hierárquico (opcional)' },
|
|
399
|
+
codigo: { type: 'string', description: 'Novo código (opcional)' }
|
|
400
|
+
},
|
|
401
|
+
required: ['orcamento_id', 'item_id']
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: 'quanty_deletar_item',
|
|
406
|
+
description: `Deleta um item do orçamento.
|
|
407
|
+
CUIDADO: Esta ação remove o item permanentemente.
|
|
408
|
+
Use quanty_abrir_orcamento para obter os IDs e descrições dos itens.
|
|
409
|
+
Retorna preview e pending_id. Use quanty_executar para confirmar.`,
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: 'object',
|
|
412
|
+
properties: {
|
|
413
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
414
|
+
item_id: { type: 'string', description: 'ID do item a deletar' },
|
|
415
|
+
descricao_item: { type: 'string', description: 'Descrição do item (para confirmação visual)' }
|
|
416
|
+
},
|
|
417
|
+
required: ['orcamento_id', 'item_id', 'descricao_item']
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: 'quanty_duplicar_orcamento',
|
|
422
|
+
description: `Cria uma CÓPIA de um orçamento existente.
|
|
423
|
+
Útil para criar variações ou versões de um orçamento.
|
|
424
|
+
A cópia inclui todos os itens, BDI e configurações.
|
|
425
|
+
Retorna preview e pending_id. Use quanty_executar para confirmar.`,
|
|
426
|
+
inputSchema: {
|
|
427
|
+
type: 'object',
|
|
428
|
+
properties: {
|
|
429
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento a duplicar' },
|
|
430
|
+
novo_titulo: { type: 'string', description: 'Título da cópia (ex: "Orçamento X - Versão 2")' }
|
|
431
|
+
},
|
|
432
|
+
required: ['orcamento_id', 'novo_titulo']
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: 'quanty_gerar_curva_abc',
|
|
437
|
+
description: `Gera análise CURVA ABC do orçamento.
|
|
438
|
+
Classifica itens por impacto no custo total:
|
|
439
|
+
- Classe A: ~20% dos itens que representam ~80% do custo (CRÍTICOS)
|
|
440
|
+
- Classe B: ~30% dos itens que representam ~15% do custo (IMPORTANTES)
|
|
441
|
+
- Classe C: ~50% dos itens que representam ~5% do custo (BAIXO IMPACTO)
|
|
442
|
+
|
|
443
|
+
Útil para priorizar negociações e controle de custos.`,
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: 'object',
|
|
446
|
+
properties: {
|
|
447
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento a analisar' },
|
|
448
|
+
limite_classe_a: { type: 'number', description: 'Percentual acumulado para Classe A (padrão: 80)' },
|
|
449
|
+
limite_classe_b: { type: 'number', description: 'Percentual acumulado para Classe B (padrão: 95)' }
|
|
450
|
+
},
|
|
451
|
+
required: ['orcamento_id']
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: 'quanty_mover_item',
|
|
456
|
+
description: `Altera o NÍVEL hierárquico de um item (identação).
|
|
457
|
+
Níveis: 0 = raiz, 1 = filho, 2 = neto, etc.
|
|
458
|
+
Use para organizar a estrutura do orçamento (EAP).
|
|
459
|
+
Retorna preview e pending_id. Use quanty_executar para confirmar.`,
|
|
460
|
+
inputSchema: {
|
|
461
|
+
type: 'object',
|
|
462
|
+
properties: {
|
|
463
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
464
|
+
item_id: { type: 'string', description: 'ID do item a mover' },
|
|
465
|
+
novo_nivel: { type: 'number', description: 'Novo nível hierárquico (0, 1, 2, ...)' },
|
|
466
|
+
descricao_item: { type: 'string', description: 'Descrição do item (para confirmação visual)' }
|
|
467
|
+
},
|
|
468
|
+
required: ['orcamento_id', 'item_id', 'novo_nivel', 'descricao_item']
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
// === LEARNING TOOLS ===
|
|
472
|
+
{
|
|
473
|
+
name: 'quanty_aprender',
|
|
474
|
+
description: 'Ensina explicitamente uma nova capacidade ou procedimento ao sistema.\nO sistema armazenará esse conhecimento para uso futuro.',
|
|
475
|
+
inputSchema: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
properties: {
|
|
478
|
+
capacidade: { type: 'string', description: 'Nome da capacidade' },
|
|
479
|
+
instrucoes: { type: 'string', description: 'Instruções detalhadas' }
|
|
480
|
+
},
|
|
481
|
+
required: ['capacidade', 'instrucoes']
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: 'quanty_feedback',
|
|
486
|
+
description: 'Fornece feedback sobre a última operação executada.\nAjuda o sistema a melhorar continuamente.',
|
|
487
|
+
inputSchema: {
|
|
488
|
+
type: 'object',
|
|
489
|
+
properties: {
|
|
490
|
+
tipo: {
|
|
491
|
+
type: 'string',
|
|
492
|
+
description: 'Tipo de feedback',
|
|
493
|
+
enum: ['success', 'failure', 'improvement']
|
|
494
|
+
},
|
|
495
|
+
detalhes: { type: 'string', description: 'Detalhes opcionais do feedback' }
|
|
496
|
+
},
|
|
497
|
+
required: ['tipo']
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
// === NEW TOOLS - EDIT BUDGET ===
|
|
501
|
+
{
|
|
502
|
+
name: 'quanty_editar_orcamento',
|
|
503
|
+
description: 'Edita metadados do orçamento: título, Data Base, BDI Principal e/ou BDI Diferenciado.\nPrepara a operação para confirmação.',
|
|
504
|
+
inputSchema: {
|
|
505
|
+
type: 'object',
|
|
506
|
+
properties: {
|
|
507
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
508
|
+
titulo: { type: 'string', description: 'Novo título do orçamento' },
|
|
509
|
+
data_base: { type: 'string', description: 'Data Base de preços (formato: MM/AAAA)' },
|
|
510
|
+
bdi_principal: { type: 'number', description: 'Novo BDI Principal (%)' },
|
|
511
|
+
bdi_diferenciado: { type: 'number', description: 'Novo BDI Diferenciado (%)' }
|
|
512
|
+
},
|
|
513
|
+
required: ['orcamento_id']
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: 'quanty_definir_bdi_item',
|
|
518
|
+
description: 'Define qual BDI um item usa: Principal ou Diferenciado.\nPrepara a operação para confirmação.',
|
|
519
|
+
inputSchema: {
|
|
520
|
+
type: 'object',
|
|
521
|
+
properties: {
|
|
522
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
523
|
+
item_id: { type: 'string', description: 'ID do item' },
|
|
524
|
+
tipo_bdi: { type: 'string', enum: ['Principal', 'Diferenciado'], description: 'Tipo de BDI' },
|
|
525
|
+
descricao_item: { type: 'string', description: 'Descrição do item para confirmação' }
|
|
526
|
+
},
|
|
527
|
+
required: ['orcamento_id', 'item_id', 'tipo_bdi']
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: 'quanty_classificar_abc',
|
|
532
|
+
description: 'Define o tipo de recurso para classificação na Curva ABC.\nTipos: Custo, Material, Mão de Obra, Equipamento.',
|
|
533
|
+
inputSchema: {
|
|
534
|
+
type: 'object',
|
|
535
|
+
properties: {
|
|
536
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
537
|
+
item_id: { type: 'string', description: 'ID do item' },
|
|
538
|
+
classificacao: { type: 'string', enum: ['Custo', 'Material', 'Mão de Obra', 'Equipamento'], description: 'Tipo de classificação ABC' },
|
|
539
|
+
descricao_item: { type: 'string', description: 'Descrição do item para confirmação' }
|
|
540
|
+
},
|
|
541
|
+
required: ['orcamento_id', 'item_id', 'classificacao']
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
// === NEW TOOLS - COMPOSITION EDITING ===
|
|
545
|
+
{
|
|
546
|
+
name: 'quanty_editar_insumo_composicao',
|
|
547
|
+
description: 'Edita um insumo dentro de uma composição existente.\nPode alterar código, descrição, unidade, quantidade ou custo unitário.',
|
|
548
|
+
inputSchema: {
|
|
549
|
+
type: 'object',
|
|
550
|
+
properties: {
|
|
551
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
552
|
+
composicao_id: { type: 'string', description: 'ID da composição' },
|
|
553
|
+
insumo_id: { type: 'string', description: 'ID do insumo a editar' },
|
|
554
|
+
codigo: { type: 'string', description: 'Novo código' },
|
|
555
|
+
descricao: { type: 'string', description: 'Nova descrição' },
|
|
556
|
+
unidade: { type: 'string', description: 'Nova unidade' },
|
|
557
|
+
quantidade: { type: 'number', description: 'Nova quantidade/coeficiente' },
|
|
558
|
+
custo_unitario: { type: 'number', description: 'Novo custo unitário' },
|
|
559
|
+
tipo_item: { type: 'string', description: 'Tipo do item (INSUMO, MATERIAL, MO, etc)' },
|
|
560
|
+
descricao_insumo: { type: 'string', description: 'Descrição do insumo para confirmação' }
|
|
561
|
+
},
|
|
562
|
+
required: ['orcamento_id', 'composicao_id', 'insumo_id']
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: 'quanty_adicionar_insumo_composicao',
|
|
567
|
+
description: 'Adiciona um novo insumo a uma composição existente.',
|
|
568
|
+
inputSchema: {
|
|
569
|
+
type: 'object',
|
|
570
|
+
properties: {
|
|
571
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
572
|
+
composicao_id: { type: 'string', description: 'ID da composição' },
|
|
573
|
+
codigo: { type: 'string', description: 'Código do insumo' },
|
|
574
|
+
descricao: { type: 'string', description: 'Descrição do insumo' },
|
|
575
|
+
unidade: { type: 'string', description: 'Unidade' },
|
|
576
|
+
quantidade: { type: 'number', description: 'Quantidade/coeficiente' },
|
|
577
|
+
custo_unitario: { type: 'number', description: 'Custo unitário' },
|
|
578
|
+
tipo_item: { type: 'string', description: 'Tipo do item (INSUMO, MATERIAL, MO, etc)' }
|
|
579
|
+
},
|
|
580
|
+
required: ['orcamento_id', 'composicao_id', 'descricao', 'unidade', 'quantidade', 'custo_unitario']
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: 'quanty_remover_insumo_composicao',
|
|
585
|
+
description: 'Remove um insumo de uma composição.',
|
|
586
|
+
inputSchema: {
|
|
587
|
+
type: 'object',
|
|
588
|
+
properties: {
|
|
589
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
590
|
+
composicao_id: { type: 'string', description: 'ID da composição' },
|
|
591
|
+
insumo_id: { type: 'string', description: 'ID do insumo a remover' },
|
|
592
|
+
descricao_insumo: { type: 'string', description: 'Descrição do insumo para confirmação' }
|
|
593
|
+
},
|
|
594
|
+
required: ['orcamento_id', 'composicao_id', 'insumo_id']
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: 'quanty_converter_para_composicao',
|
|
599
|
+
description: 'Converte um item de custo simples em composição.\nPermite depois adicionar insumos a ele.',
|
|
600
|
+
inputSchema: {
|
|
601
|
+
type: 'object',
|
|
602
|
+
properties: {
|
|
603
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
604
|
+
item_id: { type: 'string', description: 'ID do item a converter' },
|
|
605
|
+
descricao_item: { type: 'string', description: 'Descrição do item para confirmação' }
|
|
606
|
+
},
|
|
607
|
+
required: ['orcamento_id', 'item_id']
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
name: 'quanty_converter_para_custo',
|
|
612
|
+
description: 'Converte uma composição de volta para custo simples.\nREMOVE todos os insumos da composição.',
|
|
613
|
+
inputSchema: {
|
|
614
|
+
type: 'object',
|
|
615
|
+
properties: {
|
|
616
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento' },
|
|
617
|
+
item_id: { type: 'string', description: 'ID do item a converter' },
|
|
618
|
+
descricao_item: { type: 'string', description: 'Descrição do item para confirmação' }
|
|
619
|
+
},
|
|
620
|
+
required: ['orcamento_id', 'item_id']
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
// === NEW TOOLS - BANK MANAGEMENT ===
|
|
624
|
+
{
|
|
625
|
+
name: 'quanty_criar_banco',
|
|
626
|
+
description: 'Cria um banco de composições privado.',
|
|
627
|
+
inputSchema: {
|
|
628
|
+
type: 'object',
|
|
629
|
+
properties: {
|
|
630
|
+
nome: { type: 'string', description: 'Nome do banco' },
|
|
631
|
+
descricao: { type: 'string', description: 'Descrição do banco' }
|
|
632
|
+
},
|
|
633
|
+
required: ['nome']
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: 'quanty_editar_banco',
|
|
638
|
+
description: 'Edita metadados de um banco: título, descrição, imagem de capa, data da versão.',
|
|
639
|
+
inputSchema: {
|
|
640
|
+
type: 'object',
|
|
641
|
+
properties: {
|
|
642
|
+
banco_id: { type: 'string', description: 'ID do banco' },
|
|
643
|
+
titulo: { type: 'string', description: 'Novo título' },
|
|
644
|
+
descricao: { type: 'string', description: 'Nova descrição' },
|
|
645
|
+
imagem_capa: { type: 'string', description: 'URL da nova imagem de capa' },
|
|
646
|
+
data_versao: { type: 'string', description: 'Data da versão (formato: MM/AAAA)' }
|
|
647
|
+
},
|
|
648
|
+
required: ['banco_id']
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: 'quanty_adicionar_ao_banco',
|
|
653
|
+
description: 'Adiciona um item ou composição a um banco privado.',
|
|
654
|
+
inputSchema: {
|
|
655
|
+
type: 'object',
|
|
656
|
+
properties: {
|
|
657
|
+
banco_id: { type: 'string', description: 'ID do banco' },
|
|
658
|
+
categoria_id: { type: 'string', description: 'ID da categoria (ou novo ID para criar)' },
|
|
659
|
+
codigo: { type: 'string', description: 'Código do item' },
|
|
660
|
+
descricao: { type: 'string', description: 'Descrição do item' },
|
|
661
|
+
unidade: { type: 'string', description: 'Unidade' },
|
|
662
|
+
custo_unitario: { type: 'number', description: 'Custo unitário' },
|
|
663
|
+
tipo_item: { type: 'string', description: 'Tipo do item' },
|
|
664
|
+
eh_composicao: { type: 'boolean', description: 'Se é uma composição' },
|
|
665
|
+
insumos: { type: 'array', items: { type: 'object' }, description: 'Lista de insumos se for composição' }
|
|
666
|
+
},
|
|
667
|
+
required: ['banco_id', 'categoria_id', 'descricao', 'unidade', 'custo_unitario']
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
// === MATCHING TOOLS ===
|
|
671
|
+
{
|
|
672
|
+
name: 'quanty_buscar_similares',
|
|
673
|
+
description: 'Busca itens similares em outros orçamentos e bancos de composições.\nUsa normalização textual, sinônimos e múltiplas métricas de similaridade.\nIdeal para encontrar correspondências entre bases diferentes.',
|
|
674
|
+
inputSchema: {
|
|
675
|
+
type: 'object',
|
|
676
|
+
properties: {
|
|
677
|
+
descricao: { type: 'string', description: 'Descrição do item a buscar' },
|
|
678
|
+
buscar_orcamentos: { type: 'boolean', description: 'Buscar em outros orçamentos (default: true)' },
|
|
679
|
+
buscar_bancos: { type: 'boolean', description: 'Buscar em bancos de composições (default: true)' },
|
|
680
|
+
excluir_orcamento_id: { type: 'string', description: 'ID do orçamento a excluir da busca' },
|
|
681
|
+
banco_ids: { type: 'array', items: { type: 'string' }, description: 'IDs de bancos específicos para buscar' },
|
|
682
|
+
limite: { type: 'number', description: 'Máximo de resultados (default: 10)' },
|
|
683
|
+
similaridade_minima: { type: 'number', description: 'Similaridade mínima % (default: 50)' }
|
|
684
|
+
},
|
|
685
|
+
required: ['descricao']
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
name: 'quanty_comparar_precos',
|
|
690
|
+
description: 'Compara o preço de um item com preços de itens similares encontrados em outros orçamentos e bancos.\nRetorna estatísticas (min, max, média, mediana) e recomendação.',
|
|
691
|
+
inputSchema: {
|
|
692
|
+
type: 'object',
|
|
693
|
+
properties: {
|
|
694
|
+
descricao: { type: 'string', description: 'Descrição do item' },
|
|
695
|
+
preco_atual: { type: 'number', description: 'Preço unitário atual do item' },
|
|
696
|
+
excluir_orcamento_id: { type: 'string', description: 'ID do orçamento a excluir da comparação' },
|
|
697
|
+
limite: { type: 'number', description: 'Máximo de itens para comparar (default: 20)' },
|
|
698
|
+
similaridade_minima: { type: 'number', description: 'Similaridade mínima % (default: 60)' }
|
|
699
|
+
},
|
|
700
|
+
required: ['descricao', 'preco_atual']
|
|
701
|
+
}
|
|
259
702
|
}
|
|
260
703
|
];
|
|
261
704
|
async function handleToolCall(auth, name, args) {
|
|
262
705
|
switch (name) {
|
|
263
706
|
// === DOCUMENTATION ===
|
|
264
707
|
case 'quanty_ler_manual': {
|
|
265
|
-
return {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
708
|
+
return executeWithLearning('Ler manual do Quanty', async () => {
|
|
709
|
+
let content = QUANTY_MANUAL;
|
|
710
|
+
// Enrich with learning context
|
|
711
|
+
if (contextBuilder) {
|
|
712
|
+
try {
|
|
713
|
+
content = await contextBuilder.generateSystemPrompt(content);
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
console.error('Failed to generate system prompt:', err);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
conteudo: content,
|
|
721
|
+
_info: 'Este manual contém tudo sobre BDI, Composições e estrutura do Quanty.'
|
|
722
|
+
};
|
|
723
|
+
});
|
|
269
724
|
}
|
|
270
725
|
// === READ ===
|
|
271
726
|
case 'quanty_listar_orcamentos': {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
727
|
+
return executeWithLearning(`Listar ${args.limite || 10} orçamentos recentes`, async () => {
|
|
728
|
+
const budgets = await budgetService.listBudgets(auth, args.limite || 10);
|
|
729
|
+
return {
|
|
730
|
+
_info: 'Use quanty_abrir_orcamento com o ID para ver detalhes completos',
|
|
731
|
+
total: budgets.length,
|
|
732
|
+
orcamentos: budgets.map(b => ({
|
|
733
|
+
id: b.id,
|
|
734
|
+
titulo: b.title,
|
|
735
|
+
criado_em: b.createdAt,
|
|
736
|
+
modificado_em: b.lastModified,
|
|
737
|
+
criador: b.creator,
|
|
738
|
+
compartilhado: b.shared,
|
|
739
|
+
qtd_itens: b.itemCount,
|
|
740
|
+
valor_total: `R$ ${b.totalValue.toFixed(2)}`
|
|
741
|
+
}))
|
|
742
|
+
};
|
|
743
|
+
});
|
|
287
744
|
}
|
|
288
745
|
case 'quanty_abrir_orcamento': {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
746
|
+
return executeWithLearning(`Abrir orçamento ${args.id}`, async () => {
|
|
747
|
+
const budget = await budgetService.getBudget(auth, args.id);
|
|
748
|
+
if (!budget)
|
|
749
|
+
return { erro: 'Orçamento não encontrado' };
|
|
750
|
+
const totalValue = budget.rows.reduce((sum, r) => sum + (r.quantity * r.unitCost), 0);
|
|
751
|
+
return {
|
|
752
|
+
id: budget.id,
|
|
753
|
+
titulo: budget.title,
|
|
754
|
+
bdi_principal: `${budget.bdiPrincipal}%`,
|
|
755
|
+
bdi_diferenciado: `${budget.bdiDiferenciado}%`,
|
|
756
|
+
valor_total: `R$ ${totalValue.toFixed(2)}`,
|
|
757
|
+
_info_itens: 'Itens com is_composicao=true são COMPOSIÇÕES que possuem insumos internos',
|
|
758
|
+
itens: budget.rows.map(r => ({
|
|
759
|
+
id: r.id,
|
|
760
|
+
nivel: r.level,
|
|
761
|
+
codigo: r.code,
|
|
762
|
+
descricao: r.description,
|
|
763
|
+
unidade: r.unit,
|
|
764
|
+
quantidade: r.quantity,
|
|
765
|
+
custo_unitario: r.unitCost,
|
|
766
|
+
total: r.quantity * r.unitCost,
|
|
767
|
+
is_composicao: r.isComposition || false,
|
|
768
|
+
bdi_tipo: r.bdiType || 'Principal'
|
|
769
|
+
}))
|
|
770
|
+
};
|
|
771
|
+
});
|
|
313
772
|
}
|
|
314
773
|
case 'quanty_ver_composicao': {
|
|
315
|
-
|
|
316
|
-
|
|
774
|
+
return executeWithLearning(`Ver composição ${args.item_id}`, async () => {
|
|
775
|
+
const insumos = await budgetService.getCompositionDetails(auth, args.orcamento_id, args.item_id);
|
|
776
|
+
if (!insumos || insumos.length === 0) {
|
|
777
|
+
return {
|
|
778
|
+
mensagem: 'Esta composição não possui insumos listados ou não é uma composição válida.'
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
const totalComposicao = insumos.reduce((sum, i) => sum + (i.total || 0), 0);
|
|
317
782
|
return {
|
|
318
|
-
|
|
783
|
+
_info: 'Lista de insumos/filhos desta composição',
|
|
784
|
+
qtd_insumos: insumos.length,
|
|
785
|
+
custo_total_calculado: `R$ ${totalComposicao.toFixed(2)}`,
|
|
786
|
+
insumos: insumos.map(i => ({
|
|
787
|
+
id: i.id, // ID necessário para recursão
|
|
788
|
+
descricao: i.description,
|
|
789
|
+
unidade: i.unit,
|
|
790
|
+
coeficiente: i.quantity,
|
|
791
|
+
custo_unitario_insumo: i.unitCost,
|
|
792
|
+
custo_total_insumo: i.total,
|
|
793
|
+
is_composicao: i.is_composicao, // Indica se pode descer mais um nível
|
|
794
|
+
tipo: i.type
|
|
795
|
+
}))
|
|
319
796
|
};
|
|
320
|
-
}
|
|
321
|
-
const totalComposicao = insumos.reduce((sum, i) => sum + i.total, 0);
|
|
322
|
-
return {
|
|
323
|
-
_info: 'Lista de insumos/filhos desta composição',
|
|
324
|
-
qtd_insumos: insumos.length,
|
|
325
|
-
custo_total_calculado: `R$ ${totalComposicao.toFixed(2)}`,
|
|
326
|
-
insumos: insumos.map(i => ({
|
|
327
|
-
id: i.id, // ID necessário para recursão
|
|
328
|
-
descricao: i.description,
|
|
329
|
-
unidade: i.unit,
|
|
330
|
-
coeficiente: i.quantity,
|
|
331
|
-
custo_unitario_insumo: i.unitCost,
|
|
332
|
-
custo_total_insumo: i.total,
|
|
333
|
-
is_composicao: i.is_composicao, // Indica se pode descer mais um nível
|
|
334
|
-
tipo: i.type
|
|
335
|
-
}))
|
|
336
|
-
};
|
|
797
|
+
});
|
|
337
798
|
}
|
|
338
799
|
case 'quanty_listar_bancos': {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
800
|
+
return executeWithLearning('Listar bancos', async () => {
|
|
801
|
+
const banks = await bankService.listBanks(auth);
|
|
802
|
+
return {
|
|
803
|
+
_info: 'Use quanty_buscar_insumo com o ID do banco para buscar itens',
|
|
804
|
+
total: banks.length,
|
|
805
|
+
bancos: banks.map(b => ({
|
|
806
|
+
id: b.id,
|
|
807
|
+
titulo: b.title,
|
|
808
|
+
descricao: b.description,
|
|
809
|
+
autor: b.author,
|
|
810
|
+
regiao: b.region,
|
|
811
|
+
visibilidade: b.visibility,
|
|
812
|
+
qtd_categorias: b.categoryCount
|
|
813
|
+
}))
|
|
814
|
+
};
|
|
815
|
+
});
|
|
353
816
|
}
|
|
354
817
|
case 'quanty_buscar_insumo': {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
818
|
+
return executeWithLearning(`Buscar "${args.busca}" no banco`, async () => {
|
|
819
|
+
const items = await bankService.searchBankItems(args.banco_id, args.busca);
|
|
820
|
+
return {
|
|
821
|
+
_info: 'Itens do tipo COMPOSIÇÃO possuem insumos internos que definem o custo',
|
|
822
|
+
total: items.length,
|
|
823
|
+
resultados: items.map(i => ({
|
|
824
|
+
id: i.id,
|
|
825
|
+
codigo: i.code,
|
|
826
|
+
descricao: i.description,
|
|
827
|
+
unidade: i.unit,
|
|
828
|
+
custo_unitario: i.unitCost,
|
|
829
|
+
tipo: i.itemType,
|
|
830
|
+
is_composicao: i.isComposition,
|
|
831
|
+
qtd_insumos: i.composition?.length || 0
|
|
832
|
+
}))
|
|
833
|
+
};
|
|
834
|
+
});
|
|
370
835
|
}
|
|
371
836
|
// === WRITE (with confirmation) ===
|
|
372
837
|
case 'quanty_importar_item': {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
838
|
+
return executeWithLearning(`Importar item ${args.banco_item_id}`, async () => {
|
|
839
|
+
const item = await bankService.getBankItemById(args.banco_id, args.banco_item_id);
|
|
840
|
+
if (!item)
|
|
841
|
+
return { erro: 'Item não encontrado no banco de dados.' };
|
|
842
|
+
// Passa a quantidade e nivel diretamente para o preparador
|
|
843
|
+
const prepared = bankService.prepareImportItem(args.orcamento_id, item, args.quantidade, args.nivel);
|
|
844
|
+
const pendingId = generatePendingId();
|
|
845
|
+
pendingOperations.set(pendingId, {
|
|
846
|
+
action: prepared.action,
|
|
847
|
+
data: prepared.data,
|
|
848
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
849
|
+
});
|
|
850
|
+
return {
|
|
851
|
+
pending_id: pendingId,
|
|
852
|
+
preview: prepared.preview,
|
|
853
|
+
is_composicao: item.isComposition,
|
|
854
|
+
qtd_insumos: item.composition?.length || 0,
|
|
855
|
+
mensagem: 'Use quanty_executar para confirmar.'
|
|
856
|
+
};
|
|
383
857
|
});
|
|
384
|
-
return {
|
|
385
|
-
pending_id: pendingId,
|
|
386
|
-
preview: prepared.preview,
|
|
387
|
-
is_composicao: item.isComposition,
|
|
388
|
-
qtd_insumos: item.composition?.length || 0,
|
|
389
|
-
mensagem: 'Use quanty_executar para confirmar.'
|
|
390
|
-
};
|
|
391
858
|
}
|
|
392
859
|
case 'quanty_preparar_orcamento': {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
860
|
+
return executeWithLearning(`Preparar orçamento "${args.titulo}"`, async () => {
|
|
861
|
+
const prepared = budgetService.prepareCreateBudget(auth, args.titulo);
|
|
862
|
+
const pendingId = generatePendingId();
|
|
863
|
+
pendingOperations.set(pendingId, {
|
|
864
|
+
action: prepared.action,
|
|
865
|
+
data: prepared.data,
|
|
866
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
867
|
+
});
|
|
868
|
+
return {
|
|
869
|
+
pending_id: pendingId,
|
|
870
|
+
preview: prepared.preview,
|
|
871
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar a criação.'
|
|
872
|
+
};
|
|
399
873
|
});
|
|
400
|
-
return {
|
|
401
|
-
pending_id: pendingId,
|
|
402
|
-
preview: prepared.preview,
|
|
403
|
-
mensagem: 'Use quanty_executar com este pending_id para confirmar a criação.'
|
|
404
|
-
};
|
|
405
874
|
}
|
|
406
875
|
case 'quanty_preparar_item': {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
876
|
+
return executeWithLearning(`Preparar item "${args.descricao}"`, async () => {
|
|
877
|
+
const prepared = budgetService.prepareAddItem(args.orcamento_id, {
|
|
878
|
+
description: args.descricao,
|
|
879
|
+
unit: args.unidade,
|
|
880
|
+
quantity: args.quantidade,
|
|
881
|
+
unitCost: args.custo_unitario,
|
|
882
|
+
level: args.nivel
|
|
883
|
+
});
|
|
884
|
+
const pendingId = generatePendingId();
|
|
885
|
+
pendingOperations.set(pendingId, {
|
|
886
|
+
action: prepared.action,
|
|
887
|
+
data: prepared.data,
|
|
888
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
889
|
+
});
|
|
890
|
+
return {
|
|
891
|
+
pending_id: pendingId,
|
|
892
|
+
preview: prepared.preview,
|
|
893
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar a adição.'
|
|
894
|
+
};
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
case 'quanty_criar_composicao': {
|
|
898
|
+
return executeWithLearning(`Criar composição "${args.descricao}"`, async () => {
|
|
899
|
+
// Transforma os insumos do formato MCP para o formato interno
|
|
900
|
+
const insumos = (args.insumos || []).map((i) => ({
|
|
901
|
+
code: i.codigo,
|
|
902
|
+
description: i.descricao,
|
|
903
|
+
unit: i.unidade,
|
|
904
|
+
quantity: i.quantidade,
|
|
905
|
+
unitCost: i.custo_unitario,
|
|
906
|
+
itemType: i.tipo || 'INSUMO'
|
|
907
|
+
}));
|
|
908
|
+
const prepared = budgetService.prepareAddComposition(args.orcamento_id, {
|
|
909
|
+
code: args.codigo,
|
|
910
|
+
description: args.descricao,
|
|
911
|
+
unit: args.unidade,
|
|
912
|
+
quantity: args.quantidade,
|
|
913
|
+
level: args.nivel,
|
|
914
|
+
insumos
|
|
915
|
+
});
|
|
916
|
+
const pendingId = generatePendingId();
|
|
917
|
+
pendingOperations.set(pendingId, {
|
|
918
|
+
action: prepared.action,
|
|
919
|
+
data: prepared.data,
|
|
920
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
921
|
+
});
|
|
922
|
+
// Calcula o custo unitário para mostrar no preview
|
|
923
|
+
const custoUnitario = insumos.reduce((sum, i) => sum + (i.quantity * i.unitCost), 0);
|
|
924
|
+
return {
|
|
925
|
+
pending_id: pendingId,
|
|
926
|
+
preview: prepared.preview,
|
|
927
|
+
custo_unitario_calculado: `R$ ${custoUnitario.toFixed(2)}`,
|
|
928
|
+
qtd_insumos: insumos.length,
|
|
929
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar a criação da composição.'
|
|
930
|
+
};
|
|
418
931
|
});
|
|
419
|
-
return {
|
|
420
|
-
pending_id: pendingId,
|
|
421
|
-
preview: prepared.preview,
|
|
422
|
-
mensagem: 'Use quanty_executar com este pending_id para confirmar a adição.'
|
|
423
|
-
};
|
|
424
932
|
}
|
|
425
933
|
case 'quanty_executar': {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (Date.now() > pending.expiresAt) {
|
|
431
|
-
pendingOperations.delete(args.pending_id);
|
|
432
|
-
return { erro: 'Operação expirada. Prepare novamente.' };
|
|
433
|
-
}
|
|
434
|
-
pendingOperations.delete(args.pending_id);
|
|
435
|
-
switch (pending.action) {
|
|
436
|
-
case 'create_budget': {
|
|
437
|
-
const budgetId = await budgetService.executeCreateBudget(auth, pending.data);
|
|
438
|
-
return { sucesso: true, orcamento_id: budgetId, mensagem: 'Orçamento criado com sucesso!' };
|
|
934
|
+
return executeWithLearning(`Executar operação ${args.pending_id}`, async () => {
|
|
935
|
+
const pending = pendingOperations.get(args.pending_id);
|
|
936
|
+
if (!pending) {
|
|
937
|
+
return { erro: 'Operação pendente não encontrada ou expirada' };
|
|
439
938
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return {
|
|
939
|
+
if (Date.now() > pending.expiresAt) {
|
|
940
|
+
pendingOperations.delete(args.pending_id);
|
|
941
|
+
return { erro: 'Operação expirada. Prepare novamente.' };
|
|
443
942
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
943
|
+
pendingOperations.delete(args.pending_id);
|
|
944
|
+
switch (pending.action) {
|
|
945
|
+
case 'create_budget': {
|
|
946
|
+
const budgetId = await budgetService.executeCreateBudget(auth, pending.data);
|
|
947
|
+
return { sucesso: true, orcamento_id: budgetId, mensagem: 'Orçamento criado com sucesso!' };
|
|
948
|
+
}
|
|
949
|
+
case 'add_item': {
|
|
950
|
+
await budgetService.executeAddItem(auth, pending.data);
|
|
951
|
+
return { sucesso: true, mensagem: 'Item adicionado com sucesso!' };
|
|
952
|
+
}
|
|
953
|
+
case 'import_item': {
|
|
954
|
+
await budgetService.executeAddItem(auth, pending.data);
|
|
955
|
+
return { sucesso: true, mensagem: 'Item importado com sucesso!' };
|
|
956
|
+
}
|
|
957
|
+
case 'update_bdi': {
|
|
958
|
+
await budgetService.executeUpdateBdi(auth, pending.data);
|
|
959
|
+
return { sucesso: true, mensagem: 'BDI atualizado com sucesso!' };
|
|
960
|
+
}
|
|
961
|
+
case 'edit_item': {
|
|
962
|
+
await budgetService.executeEditItem(auth, pending.data);
|
|
963
|
+
return { sucesso: true, mensagem: 'Item editado com sucesso!' };
|
|
964
|
+
}
|
|
965
|
+
case 'delete_item': {
|
|
966
|
+
await budgetService.executeDeleteItem(auth, pending.data);
|
|
967
|
+
return { sucesso: true, mensagem: 'Item deletado com sucesso!' };
|
|
968
|
+
}
|
|
969
|
+
case 'duplicate_budget': {
|
|
970
|
+
const newBudgetId = await budgetService.executeDuplicateBudget(auth, pending.data);
|
|
971
|
+
return { sucesso: true, orcamento_id: newBudgetId, mensagem: 'Orçamento duplicado com sucesso!' };
|
|
972
|
+
}
|
|
973
|
+
case 'edit_budget': {
|
|
974
|
+
await budgetService.executeEditBudget(auth, pending.data);
|
|
975
|
+
return { sucesso: true, mensagem: 'Orçamento editado com sucesso!' };
|
|
976
|
+
}
|
|
977
|
+
case 'set_abc_classification': {
|
|
978
|
+
await budgetService.executeSetAbcClassification(auth, pending.data);
|
|
979
|
+
return { sucesso: true, mensagem: 'Classificação ABC atualizada com sucesso!' };
|
|
980
|
+
}
|
|
981
|
+
case 'edit_composition_input': {
|
|
982
|
+
await budgetService.executeEditCompositionInput(auth, pending.data);
|
|
983
|
+
return { sucesso: true, mensagem: 'Insumo da composição editado com sucesso!' };
|
|
984
|
+
}
|
|
985
|
+
case 'add_composition_input': {
|
|
986
|
+
await budgetService.executeAddCompositionInput(auth, pending.data);
|
|
987
|
+
return { sucesso: true, mensagem: 'Insumo adicionado à composição com sucesso!' };
|
|
988
|
+
}
|
|
989
|
+
case 'remove_composition_input': {
|
|
990
|
+
await budgetService.executeRemoveCompositionInput(auth, pending.data);
|
|
991
|
+
return { sucesso: true, mensagem: 'Insumo removido da composição com sucesso!' };
|
|
992
|
+
}
|
|
993
|
+
case 'convert_to_composition': {
|
|
994
|
+
await budgetService.executeConvertToComposition(auth, pending.data);
|
|
995
|
+
return { sucesso: true, mensagem: 'Item convertido para composição com sucesso! Agora você pode adicionar insumos.' };
|
|
996
|
+
}
|
|
997
|
+
case 'convert_to_cost': {
|
|
998
|
+
await budgetService.executeConvertToCost(auth, pending.data);
|
|
999
|
+
return { sucesso: true, mensagem: 'Composição convertida para custo simples com sucesso!' };
|
|
1000
|
+
}
|
|
1001
|
+
default:
|
|
1002
|
+
return { erro: `Ação desconhecida: ${pending.action}` };
|
|
447
1003
|
}
|
|
448
|
-
|
|
449
|
-
return { erro: `Ação desconhecida: ${pending.action}` };
|
|
450
|
-
}
|
|
1004
|
+
});
|
|
451
1005
|
}
|
|
452
1006
|
// === ANALYSIS ===
|
|
453
1007
|
case 'quanty_orcamentos_similares': {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1008
|
+
return executeWithLearning(`Buscar orçamentos similares`, async () => {
|
|
1009
|
+
const similar = await analysisService.findSimilarBudgets(auth, args.orcamento_id);
|
|
1010
|
+
return {
|
|
1011
|
+
total: similar.length,
|
|
1012
|
+
similares: similar.map(s => ({
|
|
1013
|
+
id: s.id,
|
|
1014
|
+
titulo: s.title,
|
|
1015
|
+
similaridade: `${s.similarity}%`,
|
|
1016
|
+
itens_em_comum: s.commonItems
|
|
1017
|
+
}))
|
|
1018
|
+
};
|
|
1019
|
+
});
|
|
464
1020
|
}
|
|
465
1021
|
case 'quanty_sugerir_itens': {
|
|
466
|
-
|
|
1022
|
+
return executeWithLearning(`Sugerir itens`, async () => {
|
|
1023
|
+
const suggestions = await analysisService.suggestMissingItems(auth, args.orcamento_id);
|
|
1024
|
+
return {
|
|
1025
|
+
total: suggestions.length,
|
|
1026
|
+
sugestoes: suggestions.map(s => ({
|
|
1027
|
+
descricao: s.description,
|
|
1028
|
+
unidade: s.unit,
|
|
1029
|
+
custo_medio: `R$ ${s.avgUnitCost.toFixed(2)}`,
|
|
1030
|
+
frequencia: s.frequency,
|
|
1031
|
+
razao: s.reason
|
|
1032
|
+
}))
|
|
1033
|
+
};
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
case 'quanty_custo_medio': {
|
|
1037
|
+
return executeWithLearning(`Calcular custo médio de ${args.driver}`, async () => {
|
|
1038
|
+
const metric = await analysisService.calculateCostPerUnit(auth, args.categoria || '', args.driver);
|
|
1039
|
+
return {
|
|
1040
|
+
driver: metric.driver,
|
|
1041
|
+
custo_medio: `R$ ${metric.avgCostPerUnit.toFixed(2)}`,
|
|
1042
|
+
custo_minimo: `R$ ${metric.minCost.toFixed(2)}`,
|
|
1043
|
+
custo_maximo: `R$ ${metric.maxCost.toFixed(2)}`,
|
|
1044
|
+
custo_p90: `R$ ${metric.p90Cost.toFixed(2)}`,
|
|
1045
|
+
amostra: `${metric.sampleSize} orçamentos analisados`
|
|
1046
|
+
};
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
// === MANAGEMENT HANDLERS ===
|
|
1050
|
+
case 'quanty_ajustar_bdi': {
|
|
1051
|
+
return executeWithLearning(`Ajustar BDI`, async () => {
|
|
1052
|
+
const prepared = budgetService.prepareUpdateBdi(args.orcamento_id, args.bdi_principal, args.bdi_diferenciado);
|
|
1053
|
+
const pendingId = generatePendingId();
|
|
1054
|
+
pendingOperations.set(pendingId, {
|
|
1055
|
+
action: prepared.action,
|
|
1056
|
+
data: prepared.data,
|
|
1057
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1058
|
+
});
|
|
1059
|
+
return {
|
|
1060
|
+
pending_id: pendingId,
|
|
1061
|
+
preview: prepared.preview,
|
|
1062
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar.'
|
|
1063
|
+
};
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
case 'quanty_editar_item': {
|
|
1067
|
+
return executeWithLearning(`Editar item`, async () => {
|
|
1068
|
+
const updates = {};
|
|
1069
|
+
if (args.descricao !== undefined)
|
|
1070
|
+
updates.description = args.descricao;
|
|
1071
|
+
if (args.unidade !== undefined)
|
|
1072
|
+
updates.unit = args.unidade;
|
|
1073
|
+
if (args.quantidade !== undefined)
|
|
1074
|
+
updates.quantity = args.quantidade;
|
|
1075
|
+
if (args.custo_unitario !== undefined)
|
|
1076
|
+
updates.unitCost = args.custo_unitario;
|
|
1077
|
+
if (args.nivel !== undefined)
|
|
1078
|
+
updates.level = args.nivel;
|
|
1079
|
+
if (args.codigo !== undefined)
|
|
1080
|
+
updates.code = args.codigo;
|
|
1081
|
+
const prepared = budgetService.prepareEditItem(args.orcamento_id, args.item_id, updates);
|
|
1082
|
+
const pendingId = generatePendingId();
|
|
1083
|
+
pendingOperations.set(pendingId, {
|
|
1084
|
+
action: prepared.action,
|
|
1085
|
+
data: prepared.data,
|
|
1086
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1087
|
+
});
|
|
1088
|
+
return {
|
|
1089
|
+
pending_id: pendingId,
|
|
1090
|
+
preview: prepared.preview,
|
|
1091
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar.'
|
|
1092
|
+
};
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
case 'quanty_deletar_item': {
|
|
1096
|
+
return executeWithLearning(`Deletar item`, async () => {
|
|
1097
|
+
const prepared = budgetService.prepareDeleteItem(args.orcamento_id, args.item_id, args.descricao_item);
|
|
1098
|
+
const pendingId = generatePendingId();
|
|
1099
|
+
pendingOperations.set(pendingId, {
|
|
1100
|
+
action: prepared.action,
|
|
1101
|
+
data: prepared.data,
|
|
1102
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1103
|
+
});
|
|
1104
|
+
return {
|
|
1105
|
+
pending_id: pendingId,
|
|
1106
|
+
preview: prepared.preview,
|
|
1107
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar a exclusão.'
|
|
1108
|
+
};
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
case 'quanty_duplicar_orcamento': {
|
|
1112
|
+
return executeWithLearning(`Duplicar orçamento`, async () => {
|
|
1113
|
+
const prepared = budgetService.prepareDuplicateBudget(auth, args.orcamento_id, args.novo_titulo);
|
|
1114
|
+
const pendingId = generatePendingId();
|
|
1115
|
+
pendingOperations.set(pendingId, {
|
|
1116
|
+
action: prepared.action,
|
|
1117
|
+
data: prepared.data,
|
|
1118
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1119
|
+
});
|
|
1120
|
+
return {
|
|
1121
|
+
pending_id: pendingId,
|
|
1122
|
+
preview: prepared.preview,
|
|
1123
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar a duplicação.'
|
|
1124
|
+
};
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
case 'quanty_gerar_curva_abc': {
|
|
1128
|
+
return executeWithLearning(`Gerar Curva ABC`, async () => {
|
|
1129
|
+
const result = await analysisService.generateAbcCurve(auth, args.orcamento_id, args.limite_classe_a || 80, args.limite_classe_b || 95);
|
|
1130
|
+
return {
|
|
1131
|
+
valor_total: `R$ ${result.totalValue.toFixed(2)}`,
|
|
1132
|
+
resumo: {
|
|
1133
|
+
classe_a: `${result.classACount} itens (${result.classAThreshold}% do valor)`,
|
|
1134
|
+
classe_b: `${result.classBCount} itens (${result.classBThreshold - result.classAThreshold}% do valor)`,
|
|
1135
|
+
classe_c: `${result.classCCount} itens (${100 - result.classBThreshold}% do valor)`
|
|
1136
|
+
},
|
|
1137
|
+
itens: result.items.map(item => ({
|
|
1138
|
+
id: item.id,
|
|
1139
|
+
descricao: item.description,
|
|
1140
|
+
total: `R$ ${item.total.toFixed(2)}`,
|
|
1141
|
+
percentual: `${item.percentage}%`,
|
|
1142
|
+
acumulado: `${item.accumulated}%`,
|
|
1143
|
+
classe: item.class
|
|
1144
|
+
}))
|
|
1145
|
+
};
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
case 'quanty_mover_item': {
|
|
1149
|
+
return executeWithLearning(`Mover item de nível`, async () => {
|
|
1150
|
+
const prepared = budgetService.prepareMoveItem(args.orcamento_id, args.item_id, args.novo_nivel, args.descricao_item);
|
|
1151
|
+
const pendingId = generatePendingId();
|
|
1152
|
+
pendingOperations.set(pendingId, {
|
|
1153
|
+
action: prepared.action,
|
|
1154
|
+
data: prepared.data,
|
|
1155
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1156
|
+
});
|
|
1157
|
+
return {
|
|
1158
|
+
pending_id: pendingId,
|
|
1159
|
+
preview: prepared.preview,
|
|
1160
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar.'
|
|
1161
|
+
};
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
// === LEARNING ===
|
|
1165
|
+
case 'quanty_aprender': {
|
|
1166
|
+
if (!learningSystem)
|
|
1167
|
+
throw new Error('Sistema de aprendizado não inicializado');
|
|
1168
|
+
await learningSystem.learnFromExecution(args.capacidade, [{ type: 'explicit_learning', instructions: args.instrucoes }], { success: true }, 0);
|
|
467
1169
|
return {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
descricao: s.description,
|
|
471
|
-
unidade: s.unit,
|
|
472
|
-
custo_medio: `R$ ${s.avgUnitCost.toFixed(2)}`,
|
|
473
|
-
frequencia: s.frequency,
|
|
474
|
-
razao: s.reason
|
|
475
|
-
}))
|
|
1170
|
+
status: 'success',
|
|
1171
|
+
message: `Aprendi sobre: ${args.capacidade}. Este conhecimento será aplicado em futuras operações.`
|
|
476
1172
|
};
|
|
477
1173
|
}
|
|
478
|
-
case '
|
|
479
|
-
|
|
1174
|
+
case 'quanty_feedback': {
|
|
1175
|
+
if (!learningSystem)
|
|
1176
|
+
throw new Error('Sistema de aprendizado não inicializado');
|
|
1177
|
+
await learningSystem.processFeedback('last_operation', // Simplified for now
|
|
1178
|
+
args.tipo, args.detalhes);
|
|
1179
|
+
console.error(`[Feedback] Type: ${args.tipo}, Details: ${args.detalhes || 'N/A'}`);
|
|
480
1180
|
return {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
custo_minimo: `R$ ${metric.minCost.toFixed(2)}`,
|
|
484
|
-
custo_maximo: `R$ ${metric.maxCost.toFixed(2)}`,
|
|
485
|
-
custo_p90: `R$ ${metric.p90Cost.toFixed(2)}`,
|
|
486
|
-
amostra: `${metric.sampleSize} orçamentos analisados`
|
|
1181
|
+
status: 'success',
|
|
1182
|
+
message: 'Feedback registrado. Obrigado por ajudar a melhorar o sistema!'
|
|
487
1183
|
};
|
|
488
1184
|
}
|
|
1185
|
+
// === NEW HANDLERS - EDIT BUDGET ===
|
|
1186
|
+
case 'quanty_editar_orcamento': {
|
|
1187
|
+
return executeWithLearning(`Editar orçamento`, async () => {
|
|
1188
|
+
const updates = {};
|
|
1189
|
+
if (args.titulo !== undefined)
|
|
1190
|
+
updates.title = args.titulo;
|
|
1191
|
+
if (args.data_base !== undefined)
|
|
1192
|
+
updates.baseDate = args.data_base;
|
|
1193
|
+
if (args.bdi_principal !== undefined)
|
|
1194
|
+
updates.bdiPrincipal = args.bdi_principal;
|
|
1195
|
+
if (args.bdi_diferenciado !== undefined)
|
|
1196
|
+
updates.bdiDiferenciado = args.bdi_diferenciado;
|
|
1197
|
+
const prepared = budgetService.prepareEditBudget(args.orcamento_id, updates);
|
|
1198
|
+
const pendingId = generatePendingId();
|
|
1199
|
+
pendingOperations.set(pendingId, {
|
|
1200
|
+
action: prepared.action,
|
|
1201
|
+
data: prepared.data,
|
|
1202
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1203
|
+
});
|
|
1204
|
+
return {
|
|
1205
|
+
pending_id: pendingId,
|
|
1206
|
+
preview: prepared.preview,
|
|
1207
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar.'
|
|
1208
|
+
};
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
case 'quanty_definir_bdi_item': {
|
|
1212
|
+
return executeWithLearning(`Definir BDI de item`, async () => {
|
|
1213
|
+
const prepared = budgetService.prepareSetBdiType(args.orcamento_id, args.item_id, args.tipo_bdi, args.descricao_item || 'Item');
|
|
1214
|
+
const pendingId = generatePendingId();
|
|
1215
|
+
pendingOperations.set(pendingId, {
|
|
1216
|
+
action: prepared.action,
|
|
1217
|
+
data: prepared.data,
|
|
1218
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1219
|
+
});
|
|
1220
|
+
return {
|
|
1221
|
+
pending_id: pendingId,
|
|
1222
|
+
preview: prepared.preview,
|
|
1223
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar.'
|
|
1224
|
+
};
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
case 'quanty_classificar_abc': {
|
|
1228
|
+
return executeWithLearning(`Classificar ABC`, async () => {
|
|
1229
|
+
const prepared = budgetService.prepareSetAbcClassification(args.orcamento_id, args.item_id, args.classificacao, args.descricao_item || 'Item');
|
|
1230
|
+
const pendingId = generatePendingId();
|
|
1231
|
+
pendingOperations.set(pendingId, {
|
|
1232
|
+
action: prepared.action,
|
|
1233
|
+
data: prepared.data,
|
|
1234
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1235
|
+
});
|
|
1236
|
+
return {
|
|
1237
|
+
pending_id: pendingId,
|
|
1238
|
+
preview: prepared.preview,
|
|
1239
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar.'
|
|
1240
|
+
};
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
// === NEW HANDLERS - COMPOSITION EDITING ===
|
|
1244
|
+
case 'quanty_editar_insumo_composicao': {
|
|
1245
|
+
return executeWithLearning(`Editar insumo de composição`, async () => {
|
|
1246
|
+
const updates = {};
|
|
1247
|
+
if (args.codigo !== undefined)
|
|
1248
|
+
updates.code = args.codigo;
|
|
1249
|
+
if (args.descricao !== undefined)
|
|
1250
|
+
updates.description = args.descricao;
|
|
1251
|
+
if (args.unidade !== undefined)
|
|
1252
|
+
updates.unit = args.unidade;
|
|
1253
|
+
if (args.quantidade !== undefined)
|
|
1254
|
+
updates.quantity = args.quantidade;
|
|
1255
|
+
if (args.custo_unitario !== undefined)
|
|
1256
|
+
updates.unitCost = args.custo_unitario;
|
|
1257
|
+
if (args.tipo_item !== undefined)
|
|
1258
|
+
updates.itemType = args.tipo_item;
|
|
1259
|
+
const prepared = budgetService.prepareEditCompositionInput(args.orcamento_id, args.composicao_id, args.insumo_id, updates, args.descricao_insumo || 'Insumo');
|
|
1260
|
+
const pendingId = generatePendingId();
|
|
1261
|
+
pendingOperations.set(pendingId, {
|
|
1262
|
+
action: prepared.action,
|
|
1263
|
+
data: prepared.data,
|
|
1264
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1265
|
+
});
|
|
1266
|
+
return {
|
|
1267
|
+
pending_id: pendingId,
|
|
1268
|
+
preview: prepared.preview,
|
|
1269
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar.'
|
|
1270
|
+
};
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
case 'quanty_adicionar_insumo_composicao': {
|
|
1274
|
+
return executeWithLearning(`Adicionar insumo a composição`, async () => {
|
|
1275
|
+
const input = {
|
|
1276
|
+
code: args.codigo || '',
|
|
1277
|
+
description: args.descricao,
|
|
1278
|
+
unit: args.unidade,
|
|
1279
|
+
quantity: args.quantidade,
|
|
1280
|
+
unitCost: args.custo_unitario,
|
|
1281
|
+
itemType: args.tipo_item || 'INSUMO'
|
|
1282
|
+
};
|
|
1283
|
+
const prepared = budgetService.prepareAddCompositionInput(args.orcamento_id, args.composicao_id, input);
|
|
1284
|
+
const pendingId = generatePendingId();
|
|
1285
|
+
pendingOperations.set(pendingId, {
|
|
1286
|
+
action: prepared.action,
|
|
1287
|
+
data: prepared.data,
|
|
1288
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1289
|
+
});
|
|
1290
|
+
return {
|
|
1291
|
+
pending_id: pendingId,
|
|
1292
|
+
preview: prepared.preview,
|
|
1293
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar.'
|
|
1294
|
+
};
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
case 'quanty_remover_insumo_composicao': {
|
|
1298
|
+
return executeWithLearning(`Remover insumo de composição`, async () => {
|
|
1299
|
+
const prepared = budgetService.prepareRemoveCompositionInput(args.orcamento_id, args.composicao_id, args.insumo_id, args.descricao_insumo || 'Insumo');
|
|
1300
|
+
const pendingId = generatePendingId();
|
|
1301
|
+
pendingOperations.set(pendingId, {
|
|
1302
|
+
action: prepared.action,
|
|
1303
|
+
data: prepared.data,
|
|
1304
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1305
|
+
});
|
|
1306
|
+
return {
|
|
1307
|
+
pending_id: pendingId,
|
|
1308
|
+
preview: prepared.preview,
|
|
1309
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar.'
|
|
1310
|
+
};
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
case 'quanty_converter_para_composicao': {
|
|
1314
|
+
return executeWithLearning(`Converter para composição`, async () => {
|
|
1315
|
+
const prepared = budgetService.prepareConvertToComposition(args.orcamento_id, args.item_id, args.descricao_item || 'Item');
|
|
1316
|
+
const pendingId = generatePendingId();
|
|
1317
|
+
pendingOperations.set(pendingId, {
|
|
1318
|
+
action: prepared.action,
|
|
1319
|
+
data: prepared.data,
|
|
1320
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1321
|
+
});
|
|
1322
|
+
return {
|
|
1323
|
+
pending_id: pendingId,
|
|
1324
|
+
preview: prepared.preview,
|
|
1325
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar.'
|
|
1326
|
+
};
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
case 'quanty_converter_para_custo': {
|
|
1330
|
+
return executeWithLearning(`Converter para custo`, async () => {
|
|
1331
|
+
const prepared = budgetService.prepareConvertToCost(args.orcamento_id, args.item_id, args.descricao_item || 'Item');
|
|
1332
|
+
const pendingId = generatePendingId();
|
|
1333
|
+
pendingOperations.set(pendingId, {
|
|
1334
|
+
action: prepared.action,
|
|
1335
|
+
data: prepared.data,
|
|
1336
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
1337
|
+
});
|
|
1338
|
+
return {
|
|
1339
|
+
pending_id: pendingId,
|
|
1340
|
+
preview: prepared.preview,
|
|
1341
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar.'
|
|
1342
|
+
};
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
// === NEW HANDLERS - BANK MANAGEMENT ===
|
|
1346
|
+
case 'quanty_criar_banco': {
|
|
1347
|
+
return executeWithLearning(`Criar banco privado`, async () => {
|
|
1348
|
+
const bankId = await bankService.createPrivateBank(auth, args.nome, args.descricao);
|
|
1349
|
+
return {
|
|
1350
|
+
sucesso: true,
|
|
1351
|
+
banco_id: bankId,
|
|
1352
|
+
mensagem: `Banco "${args.nome}" criado com sucesso!`
|
|
1353
|
+
};
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
case 'quanty_editar_banco': {
|
|
1357
|
+
return executeWithLearning(`Editar banco`, async () => {
|
|
1358
|
+
const updates = {};
|
|
1359
|
+
if (args.titulo !== undefined)
|
|
1360
|
+
updates.title = args.titulo;
|
|
1361
|
+
if (args.descricao !== undefined)
|
|
1362
|
+
updates.description = args.descricao;
|
|
1363
|
+
if (args.imagem_capa !== undefined)
|
|
1364
|
+
updates.background_image = args.imagem_capa;
|
|
1365
|
+
if (args.data_versao !== undefined)
|
|
1366
|
+
updates.versionDate = args.data_versao;
|
|
1367
|
+
await bankService.editBank(auth, args.banco_id, updates);
|
|
1368
|
+
return {
|
|
1369
|
+
sucesso: true,
|
|
1370
|
+
mensagem: 'Banco atualizado com sucesso!'
|
|
1371
|
+
};
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
case 'quanty_adicionar_ao_banco': {
|
|
1375
|
+
return executeWithLearning(`Adicionar item ao banco`, async () => {
|
|
1376
|
+
const item = {
|
|
1377
|
+
code: args.codigo || '',
|
|
1378
|
+
description: args.descricao,
|
|
1379
|
+
unit: args.unidade,
|
|
1380
|
+
unitCost: args.custo_unitario,
|
|
1381
|
+
itemType: args.tipo_item || 'Insumo',
|
|
1382
|
+
isComposition: args.eh_composicao || false,
|
|
1383
|
+
composition: args.insumos || []
|
|
1384
|
+
};
|
|
1385
|
+
const result = await bankService.addItemToBank(auth, args.banco_id, args.categoria_id, item);
|
|
1386
|
+
return {
|
|
1387
|
+
sucesso: true,
|
|
1388
|
+
mensagem: result
|
|
1389
|
+
};
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
// === MATCHING HANDLERS ===
|
|
1393
|
+
case 'quanty_buscar_similares': {
|
|
1394
|
+
return executeWithLearning(`Buscar itens similares: ${args.descricao.substring(0, 50)}...`, async () => {
|
|
1395
|
+
const results = await matchingService.findSimilarItems(auth, args.descricao, {
|
|
1396
|
+
searchBudgets: args.buscar_orcamentos !== false,
|
|
1397
|
+
searchBanks: args.buscar_bancos !== false,
|
|
1398
|
+
excludeBudgetId: args.excluir_orcamento_id,
|
|
1399
|
+
bankIds: args.banco_ids,
|
|
1400
|
+
limit: args.limite || 10,
|
|
1401
|
+
minSimilarity: args.similaridade_minima || 50
|
|
1402
|
+
});
|
|
1403
|
+
return {
|
|
1404
|
+
descricao_buscada: args.descricao,
|
|
1405
|
+
total_orcamentos: results.budgetResults.length,
|
|
1406
|
+
total_bancos: results.bankResults.length,
|
|
1407
|
+
melhores_correspondencias: results.topMatches.map(m => ({
|
|
1408
|
+
codigo: m.code,
|
|
1409
|
+
descricao: m.description,
|
|
1410
|
+
unidade: m.unit,
|
|
1411
|
+
custo_unitario: `R$ ${m.unitCost.toFixed(2)}`,
|
|
1412
|
+
fonte: m.source === 'budget' ? 'Orçamento' : 'Banco',
|
|
1413
|
+
fonte_nome: m.sourceName,
|
|
1414
|
+
similaridade: `${m.similarity}%`,
|
|
1415
|
+
detalhes: {
|
|
1416
|
+
fuzzy: `${m.similarityDetails.fuzzy}%`,
|
|
1417
|
+
palavras_chave: `${m.similarityDetails.keyword}%`,
|
|
1418
|
+
tfidf: `${m.similarityDetails.tfidf}%`
|
|
1419
|
+
}
|
|
1420
|
+
})),
|
|
1421
|
+
_info: 'Similaridade combina análise fuzzy, palavras-chave e TF-IDF. Itens com >70% são boas correspondências.'
|
|
1422
|
+
};
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
case 'quanty_comparar_precos': {
|
|
1426
|
+
return executeWithLearning(`Comparar preços: ${args.descricao.substring(0, 50)}...`, async () => {
|
|
1427
|
+
const comparison = await matchingService.comparePrices(auth, args.descricao, args.preco_atual, {
|
|
1428
|
+
excludeBudgetId: args.excluir_orcamento_id,
|
|
1429
|
+
limit: args.limite || 20,
|
|
1430
|
+
minSimilarity: args.similaridade_minima || 60
|
|
1431
|
+
});
|
|
1432
|
+
return {
|
|
1433
|
+
descricao: args.descricao,
|
|
1434
|
+
preco_atual: `R$ ${comparison.currentPrice.toFixed(2)}`,
|
|
1435
|
+
itens_comparados: comparison.matches.length,
|
|
1436
|
+
estatisticas: {
|
|
1437
|
+
preco_minimo: `R$ ${comparison.statistics.minPrice.toFixed(2)}`,
|
|
1438
|
+
preco_maximo: `R$ ${comparison.statistics.maxPrice.toFixed(2)}`,
|
|
1439
|
+
preco_medio: `R$ ${comparison.statistics.avgPrice.toFixed(2)}`,
|
|
1440
|
+
preco_mediano: `R$ ${comparison.statistics.medianPrice.toFixed(2)}`,
|
|
1441
|
+
recomendacao: comparison.statistics.recommendation
|
|
1442
|
+
},
|
|
1443
|
+
comparacoes: comparison.matches.slice(0, 10).map(m => ({
|
|
1444
|
+
descricao: m.description,
|
|
1445
|
+
unidade: m.unit,
|
|
1446
|
+
preco: `R$ ${m.unitCost.toFixed(2)}`,
|
|
1447
|
+
diferenca: `${m.priceDiffPercent >= 0 ? '+' : ''}${m.priceDiffPercent}%`,
|
|
1448
|
+
fonte: m.sourceName,
|
|
1449
|
+
similaridade: `${m.similarity}%`
|
|
1450
|
+
}))
|
|
1451
|
+
};
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
489
1454
|
default:
|
|
490
1455
|
return { erro: `Tool não reconhecida: ${name}` };
|
|
491
1456
|
}
|
|
@@ -497,13 +1462,18 @@ async function main() {
|
|
|
497
1462
|
try {
|
|
498
1463
|
auth = await initAuth();
|
|
499
1464
|
console.error(`[Quanty MCP] Authenticated as ${auth.userName} (${auth.userEmail})`);
|
|
1465
|
+
// Initialize Learning System
|
|
1466
|
+
initLearningSystem(auth);
|
|
1467
|
+
learningSystem = getLearningSystem();
|
|
1468
|
+
contextBuilder = getContextBuilder();
|
|
1469
|
+
console.error('[Quanty MCP] Learning System initialized');
|
|
500
1470
|
}
|
|
501
1471
|
catch (error) {
|
|
502
1472
|
console.error('[Quanty MCP] Auth failed:', error);
|
|
503
1473
|
process.exit(1);
|
|
504
1474
|
}
|
|
505
1475
|
// Create MCP server with resources capability
|
|
506
|
-
const server = new Server({ name: 'quanty-mcp-server', version: '
|
|
1476
|
+
const server = new Server({ name: 'quanty-mcp-server', version: '2.0.0' }, { capabilities: { tools: {}, resources: {} } });
|
|
507
1477
|
// List resources handler - Manual do Quanty
|
|
508
1478
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
509
1479
|
resources: [
|
|
@@ -512,14 +1482,35 @@ async function main() {
|
|
|
512
1482
|
name: 'Manual do Quanty',
|
|
513
1483
|
description: 'Documentação completa sobre orçamentação de engenharia, conceitos, terminologia e dicas de uso.',
|
|
514
1484
|
mimeType: 'text/plain'
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
uri: 'quanty://learning-stats',
|
|
1488
|
+
name: 'Estatísticas de Aprendizado',
|
|
1489
|
+
description: 'Métricas sobre o aprendizado do sistema, padrões identificados e eficiência.',
|
|
1490
|
+
mimeType: 'application/json'
|
|
515
1491
|
}
|
|
516
1492
|
]
|
|
517
1493
|
}));
|
|
518
1494
|
// Read resource handler
|
|
519
1495
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
520
1496
|
if (request.params.uri === 'quanty://manual') {
|
|
1497
|
+
let content = QUANTY_MANUAL;
|
|
1498
|
+
if (contextBuilder) {
|
|
1499
|
+
try {
|
|
1500
|
+
content = await contextBuilder.generateSystemPrompt(content);
|
|
1501
|
+
}
|
|
1502
|
+
catch (err) {
|
|
1503
|
+
console.error('Failed to enhance manual:', err);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return {
|
|
1507
|
+
contents: [{ uri: 'quanty://manual', mimeType: 'text/plain', text: content }]
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
if (request.params.uri === 'quanty://learning-stats') {
|
|
1511
|
+
const stats = learningSystem ? await learningSystem.getStatistics() : {};
|
|
521
1512
|
return {
|
|
522
|
-
contents: [{ uri: 'quanty://
|
|
1513
|
+
contents: [{ uri: 'quanty://learning-stats', mimeType: 'application/json', text: JSON.stringify(stats, null, 2) }]
|
|
523
1514
|
};
|
|
524
1515
|
}
|
|
525
1516
|
throw new Error(`Resource not found: ${request.params.uri}`);
|