@quantyapp/quanty-mcp-server 1.0.11 → 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/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
- conteudo: QUANTY_MANUAL,
267
- _info: 'Este manual contém tudo sobre BDI, Composições e estrutura do Quanty.'
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
- const budgets = await budgetService.listBudgets(auth, args.limite || 10);
273
- return {
274
- _info: 'Use quanty_abrir_orcamento com o ID para ver detalhes completos',
275
- total: budgets.length,
276
- orcamentos: budgets.map(b => ({
277
- id: b.id,
278
- titulo: b.title,
279
- criado_em: b.createdAt,
280
- modificado_em: b.lastModified,
281
- criador: b.creator,
282
- compartilhado: b.shared,
283
- qtd_itens: b.itemCount,
284
- valor_total: `R$ ${b.totalValue.toFixed(2)}`
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
- const budget = await budgetService.getBudget(auth, args.id);
290
- if (!budget)
291
- return { erro: 'Orçamento não encontrado' };
292
- const totalValue = budget.rows.reduce((sum, r) => sum + (r.quantity * r.unitCost), 0);
293
- return {
294
- id: budget.id,
295
- titulo: budget.title,
296
- bdi_principal: `${budget.bdiPrincipal}%`,
297
- bdi_diferenciado: `${budget.bdiDiferenciado}%`,
298
- valor_total: `R$ ${totalValue.toFixed(2)}`,
299
- _info_itens: 'Itens com is_composicao=true são COMPOSIÇÕES que possuem insumos internos',
300
- itens: budget.rows.map(r => ({
301
- id: r.id,
302
- nivel: r.level,
303
- codigo: r.code,
304
- descricao: r.description,
305
- unidade: r.unit,
306
- quantidade: r.quantity,
307
- custo_unitario: r.unitCost,
308
- total: r.quantity * r.unitCost,
309
- is_composicao: r.isComposition || false,
310
- bdi_tipo: r.bdiType || 'Principal'
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
- const insumos = await budgetService.getCompositionDetails(auth, args.orcamento_id, args.item_id);
316
- if (!insumos || insumos.length === 0) {
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
- mensagem: 'Esta composição não possui insumos listados ou não é uma composição válida.'
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
- const banks = await bankService.listBanks(auth);
340
- return {
341
- _info: 'Use quanty_buscar_insumo com o ID do banco para buscar itens',
342
- total: banks.length,
343
- bancos: banks.map(b => ({
344
- id: b.id,
345
- titulo: b.title,
346
- descricao: b.description,
347
- autor: b.author,
348
- regiao: b.region,
349
- visibilidade: b.visibility,
350
- qtd_categorias: b.categoryCount
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
- const items = await bankService.searchBankItems(args.banco_id, args.busca);
356
- return {
357
- _info: 'Itens do tipo COMPOSIÇÃO possuem insumos internos que definem o custo',
358
- total: items.length,
359
- resultados: items.map(i => ({
360
- id: i.id,
361
- codigo: i.code,
362
- descricao: i.description,
363
- unidade: i.unit,
364
- custo_unitario: i.unitCost,
365
- tipo: i.itemType,
366
- is_composicao: i.isComposition,
367
- qtd_insumos: i.composition?.length || 0
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
- const item = await bankService.getBankItemById(args.banco_id, args.banco_item_id);
374
- if (!item)
375
- return { erro: 'Item não encontrado no banco de dados.' };
376
- // Passa a quantidade diretamente para o preparador
377
- const prepared = bankService.prepareImportItem(args.orcamento_id, item, args.quantidade);
378
- const pendingId = generatePendingId();
379
- pendingOperations.set(pendingId, {
380
- action: prepared.action,
381
- data: prepared.data,
382
- expiresAt: Date.now() + 5 * 60 * 1000
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
- const prepared = budgetService.prepareCreateBudget(auth, args.titulo);
394
- const pendingId = generatePendingId();
395
- pendingOperations.set(pendingId, {
396
- action: prepared.action,
397
- data: prepared.data,
398
- expiresAt: Date.now() + 5 * 60 * 1000
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
- const prepared = budgetService.prepareAddItem(args.orcamento_id, {
408
- description: args.descricao,
409
- unit: args.unidade,
410
- quantity: args.quantidade,
411
- unitCost: args.custo_unitario
412
- });
413
- const pendingId = generatePendingId();
414
- pendingOperations.set(pendingId, {
415
- action: prepared.action,
416
- data: prepared.data,
417
- expiresAt: Date.now() + 5 * 60 * 1000
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
- const pending = pendingOperations.get(args.pending_id);
427
- if (!pending) {
428
- return { erro: 'Operação pendente não encontrada ou expirada' };
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
- case 'add_item': {
441
- await budgetService.executeAddItem(auth, pending.data);
442
- return { sucesso: true, mensagem: 'Item adicionado com sucesso!' };
939
+ if (Date.now() > pending.expiresAt) {
940
+ pendingOperations.delete(args.pending_id);
941
+ return { erro: 'Operação expirada. Prepare novamente.' };
443
942
  }
444
- case 'import_item': {
445
- await budgetService.executeAddItem(auth, pending.data);
446
- return { sucesso: true, mensagem: 'Item importado com sucesso!' };
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
- default:
449
- return { erro: `Ação desconhecida: ${pending.action}` };
450
- }
1004
+ });
451
1005
  }
452
1006
  // === ANALYSIS ===
453
1007
  case 'quanty_orcamentos_similares': {
454
- const similar = await analysisService.findSimilarBudgets(auth, args.orcamento_id);
455
- return {
456
- total: similar.length,
457
- similares: similar.map(s => ({
458
- id: s.id,
459
- titulo: s.title,
460
- similaridade: `${s.similarity}%`,
461
- itens_em_comum: s.commonItems
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
- const suggestions = await analysisService.suggestMissingItems(auth, args.orcamento_id);
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
- total: suggestions.length,
469
- sugestoes: suggestions.map(s => ({
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 'quanty_custo_medio': {
479
- const metric = await analysisService.calculateCostPerUnit(auth, args.categoria || '', args.driver);
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
- driver: metric.driver,
482
- custo_medio: `R$ ${metric.avgCostPerUnit.toFixed(2)}`,
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: '1.0.0' }, { capabilities: { tools: {}, resources: {} } });
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://manual', mimeType: 'text/plain', text: QUANTY_MANUAL }]
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}`);