@quantyapp/quanty-mcp-server 1.0.11 → 1.2.6

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
  }
@@ -190,19 +246,79 @@ O orçamento será criado com BDI zerado (pode ser ajustado depois).`,
190
246
  name: 'quanty_preparar_item',
191
247
  description: `Prepara a adição de um item SIMPLES ao orçamento.
192
248
  Para adicionar COMPOSIÇÕES (serviços com insumos), use quanty_buscar_insumo + quanty_importar_composicao.
193
- Retorna preview e pending_id. Use quanty_executar para confirmar.`,
249
+ Retorna preview e pending_id. Use quanty_executar para confirmar.
250
+
251
+ ⚠️ ATENÇÃO CÓDIGOS: Os códigos são usados para autopreenchimento no sistema.
252
+ Antes de atribuir um código a um item novo, verifique os códigos existentes no orçamento para EVITAR DUPLICAÇÃO.
253
+ Use a aba "Curva ABC" para ver os códigos existentes, ou consulte os itens do orçamento primeiro.`,
194
254
  inputSchema: {
195
255
  type: 'object',
196
256
  properties: {
197
257
  orcamento_id: { type: 'string', description: 'ID do orçamento destino' },
258
+ codigo: { type: 'string', description: 'Código ÚNICO do item. VERIFIQUE se não existe no orçamento antes de usar!' },
198
259
  descricao: { type: 'string', description: 'Descrição do item (ex: "Demolição manual de alvenaria")' },
199
260
  unidade: { type: 'string', description: 'Unidade de medida (m², m³, m, kg, un, h, vb)' },
200
261
  quantidade: { type: 'number', description: 'Quantidade do serviço' },
201
- custo_unitario: { type: 'number', description: 'Custo unitário em R$ (sem BDI)' }
262
+ custo_unitario: { type: 'number', description: 'Custo unitário em R$ (sem BDI)' },
263
+ nivel: { type: 'number', description: 'Nível hierárquico/identação do item (0=raiz, 1=filho, 2=neto, etc). Padrão: 0' }
202
264
  },
203
265
  required: ['orcamento_id', 'descricao', 'unidade', 'quantidade', 'custo_unitario']
204
266
  }
205
267
  },
268
+ {
269
+ name: 'quanty_criar_composicao',
270
+ description: `Cria uma COMPOSIÇÃO (serviço composto) com seus INSUMOS do zero.
271
+ Diferente de importar do banco, aqui você define todos os insumos manualmente.
272
+ O custo unitário da composição é calculado automaticamente pela soma dos custos dos insumos.
273
+ Retorna preview e pending_id. Use quanty_executar para confirmar.
274
+
275
+ ⚠️ ATENÇÃO CÓDIGOS: Os códigos são usados para autopreenchimento no sistema.
276
+ Antes de atribuir códigos, verifique os existentes no orçamento via "Curva ABC" para EVITAR DUPLICAÇÃO.
277
+ Use códigos únicos tanto para a composição quanto para os insumos.
278
+
279
+ EXEMPLO de uso:
280
+ {
281
+ "orcamento_id": "abc123",
282
+ "codigo": "COMP-MeuCodigo",
283
+ "descricao": "Concretagem de pilar 20x40",
284
+ "unidade": "m³",
285
+ "quantidade": 15,
286
+ "insumos": [
287
+ {"codigo": "INS-001", "descricao": "Concreto fck 30 MPa", "unidade": "m³", "quantidade": 1.05, "custo_unitario": 450.00},
288
+ {"codigo": "INS-002", "descricao": "Forma de madeira", "unidade": "m²", "quantidade": 2.4, "custo_unitario": 85.00},
289
+ {"codigo": "INS-003", "descricao": "Aço CA-50", "unidade": "kg", "quantidade": 120, "custo_unitario": 8.50},
290
+ {"codigo": "MO-001", "descricao": "Pedreiro", "unidade": "h", "quantidade": 4, "custo_unitario": 25.00, "tipo": "MÃO DE OBRA"}
291
+ ]
292
+ }`,
293
+ inputSchema: {
294
+ type: 'object',
295
+ properties: {
296
+ orcamento_id: { type: 'string', description: 'ID do orçamento destino' },
297
+ codigo: { type: 'string', description: 'Código ÚNICO da composição. VERIFIQUE se não existe no orçamento!' },
298
+ descricao: { type: 'string', description: 'Descrição da composição (ex: "Concretagem de pilar 20x40")' },
299
+ unidade: { type: 'string', description: 'Unidade de medida da composição (m², m³, m, un, vb)' },
300
+ quantidade: { type: 'number', description: 'Quantidade da composição no orçamento' },
301
+ nivel: { type: 'number', description: 'Nível hierárquico/identação (0=raiz, 1=filho, etc). Padrão: 0' },
302
+ insumos: {
303
+ type: 'array',
304
+ description: 'Lista de insumos que compõem o serviço',
305
+ items: {
306
+ type: 'object',
307
+ properties: {
308
+ codigo: { type: 'string', description: 'Código ÚNICO do insumo. Verifique existentes para evitar duplicação!' },
309
+ descricao: { type: 'string', description: 'Descrição do insumo' },
310
+ unidade: { type: 'string', description: 'Unidade do insumo' },
311
+ quantidade: { type: 'number', description: 'Coeficiente/quantidade do insumo por unidade da composição' },
312
+ custo_unitario: { type: 'number', description: 'Custo unitário do insumo em R$' },
313
+ tipo: { type: 'string', description: 'Tipo: INSUMO, MÃO DE OBRA, EQUIPAMENTO, etc. Padrão: INSUMO' }
314
+ },
315
+ required: ['descricao', 'unidade', 'quantidade', 'custo_unitario']
316
+ }
317
+ }
318
+ },
319
+ required: ['orcamento_id', 'descricao', 'unidade', 'quantidade', 'insumos']
320
+ }
321
+ },
206
322
  {
207
323
  name: 'quanty_executar',
208
324
  description: `Confirma e executa uma operação pendente (criar orçamento, adicionar item, etc).
@@ -256,236 +372,1512 @@ Retorna média, mínimo, máximo e percentil 90 (para detecção de outliers).`,
256
372
  },
257
373
  required: ['driver']
258
374
  }
375
+ },
376
+ // === MANAGEMENT TOOLS ===
377
+ {
378
+ name: 'quanty_ajustar_bdi',
379
+ description: `Ajusta o BDI (Bonificações e Despesas Indiretas) do orçamento.
380
+ O BDI é aplicado sobre o custo para calcular o preço de venda.
381
+ Valor típico: 20% a 30%.
382
+ Retorna preview e pending_id. Use quanty_executar para confirmar.`,
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {
386
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
387
+ bdi_principal: { type: 'number', description: 'BDI Principal em % (ex: 25 para 25%)' },
388
+ bdi_diferenciado: { type: 'number', description: 'BDI Diferenciado em % (para equipamentos/materiais especiais)' }
389
+ },
390
+ required: ['orcamento_id']
391
+ }
392
+ },
393
+ {
394
+ name: 'quanty_adicionar_item_bdi',
395
+ description: `Adiciona um item detalhado à composição do BDI (Principal ou Diferenciado).
396
+ Use esta função para especificar os componentes do BDI, como:
397
+ - Administração Central
398
+ - Lucro
399
+ - Impostos (ISS, PIS, COFINS, etc)
400
+ - Seguros e Garantias
401
+ - Risco
402
+ - Despesas Financeiras
403
+
404
+ O percentual total do BDI será a soma de todos os itens adicionados.`,
405
+ inputSchema: {
406
+ type: 'object',
407
+ properties: {
408
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
409
+ tipo_bdi: {
410
+ type: 'string',
411
+ enum: ['Principal', 'Diferenciado'],
412
+ description: 'Tipo de BDI: Principal ou Diferenciado'
413
+ },
414
+ descricao: { type: 'string', description: 'Descrição do item (ex: "Administração Central", "Lucro", "ISS")' },
415
+ percentual: { type: 'number', description: 'Percentual do item (ex: 5 para 5%)' },
416
+ incidencia: {
417
+ type: 'string',
418
+ description: 'Sobre o que incide: "Custo Direto", "Preço de Venda", etc',
419
+ default: 'Custo Direto'
420
+ }
421
+ },
422
+ required: ['orcamento_id', 'tipo_bdi', 'descricao', 'percentual']
423
+ }
424
+ },
425
+ {
426
+ name: 'quanty_editar_item_bdi',
427
+ description: `Edita um item existente da composição do BDI.
428
+ Permite alterar descrição, percentual ou incidência de um item específico.`,
429
+ inputSchema: {
430
+ type: 'object',
431
+ properties: {
432
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
433
+ tipo_bdi: {
434
+ type: 'string',
435
+ enum: ['Principal', 'Diferenciado'],
436
+ description: 'Tipo de BDI'
437
+ },
438
+ item_id: { type: 'string', description: 'ID do item a editar' },
439
+ descricao: { type: 'string', description: 'Nova descrição (opcional)' },
440
+ percentual: { type: 'number', description: 'Novo percentual (opcional)' },
441
+ incidencia: { type: 'string', description: 'Nova incidência (opcional)' }
442
+ },
443
+ required: ['orcamento_id', 'tipo_bdi', 'item_id']
444
+ }
445
+ },
446
+ {
447
+ name: 'quanty_remover_item_bdi',
448
+ description: `Remove um item da composição do BDI.
449
+ Use quanty_abrir_orcamento para obter os IDs dos itens do BDI.`,
450
+ inputSchema: {
451
+ type: 'object',
452
+ properties: {
453
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
454
+ tipo_bdi: {
455
+ type: 'string',
456
+ enum: ['Principal', 'Diferenciado'],
457
+ description: 'Tipo de BDI'
458
+ },
459
+ item_id: { type: 'string', description: 'ID do item a remover' },
460
+ descricao_item: { type: 'string', description: 'Descrição do item (para confirmação)' }
461
+ },
462
+ required: ['orcamento_id', 'tipo_bdi', 'item_id', 'descricao_item']
463
+ }
464
+ },
465
+ {
466
+ name: 'quanty_editar_item',
467
+ description: `Edita um item EXISTENTE no orçamento.
468
+ Permite alterar descrição, unidade, quantidade, custo unitário, nível e código.
469
+ Use quanty_abrir_orcamento para obter os IDs dos itens.
470
+ Retorna preview e pending_id. Use quanty_executar para confirmar.`,
471
+ inputSchema: {
472
+ type: 'object',
473
+ properties: {
474
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
475
+ item_id: { type: 'string', description: 'ID do item a editar' },
476
+ descricao: { type: 'string', description: 'Nova descrição (opcional)' },
477
+ unidade: { type: 'string', description: 'Nova unidade (opcional)' },
478
+ quantidade: { type: 'number', description: 'Nova quantidade (opcional)' },
479
+ custo_unitario: { type: 'number', description: 'Novo custo unitário (opcional)' },
480
+ nivel: { type: 'number', description: 'Novo nível hierárquico (opcional)' },
481
+ codigo: { type: 'string', description: 'Novo código (opcional)' }
482
+ },
483
+ required: ['orcamento_id', 'item_id']
484
+ }
485
+ },
486
+ {
487
+ name: 'quanty_deletar_item',
488
+ description: `Deleta um item do orçamento.
489
+ CUIDADO: Esta ação remove o item permanentemente.
490
+ Use quanty_abrir_orcamento para obter os IDs e descrições dos itens.
491
+ Retorna preview e pending_id. Use quanty_executar para confirmar.`,
492
+ inputSchema: {
493
+ type: 'object',
494
+ properties: {
495
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
496
+ item_id: { type: 'string', description: 'ID do item a deletar' },
497
+ descricao_item: { type: 'string', description: 'Descrição do item (para confirmação visual)' }
498
+ },
499
+ required: ['orcamento_id', 'item_id', 'descricao_item']
500
+ }
501
+ },
502
+ {
503
+ name: 'quanty_duplicar_orcamento',
504
+ description: `Cria uma CÓPIA de um orçamento existente.
505
+ Útil para criar variações ou versões de um orçamento.
506
+ A cópia inclui todos os itens, BDI e configurações.
507
+ Retorna preview e pending_id. Use quanty_executar para confirmar.`,
508
+ inputSchema: {
509
+ type: 'object',
510
+ properties: {
511
+ orcamento_id: { type: 'string', description: 'ID do orçamento a duplicar' },
512
+ novo_titulo: { type: 'string', description: 'Título da cópia (ex: "Orçamento X - Versão 2")' }
513
+ },
514
+ required: ['orcamento_id', 'novo_titulo']
515
+ }
516
+ },
517
+ {
518
+ name: 'quanty_gerar_curva_abc',
519
+ description: `Gera análise CURVA ABC do orçamento.
520
+ Classifica itens por impacto no custo total:
521
+ - Classe A: ~20% dos itens que representam ~80% do custo (CRÍTICOS)
522
+ - Classe B: ~30% dos itens que representam ~15% do custo (IMPORTANTES)
523
+ - Classe C: ~50% dos itens que representam ~5% do custo (BAIXO IMPACTO)
524
+
525
+ Útil para priorizar negociações e controle de custos.`,
526
+ inputSchema: {
527
+ type: 'object',
528
+ properties: {
529
+ orcamento_id: { type: 'string', description: 'ID do orçamento a analisar' },
530
+ limite_classe_a: { type: 'number', description: 'Percentual acumulado para Classe A (padrão: 80)' },
531
+ limite_classe_b: { type: 'number', description: 'Percentual acumulado para Classe B (padrão: 95)' }
532
+ },
533
+ required: ['orcamento_id']
534
+ }
535
+ },
536
+ {
537
+ name: 'quanty_mover_item',
538
+ description: `Altera o NÍVEL hierárquico de um item (identação).
539
+ Níveis: 0 = raiz, 1 = filho, 2 = neto, etc.
540
+ Use para organizar a estrutura do orçamento (EAP).
541
+ Retorna preview e pending_id. Use quanty_executar para confirmar.`,
542
+ inputSchema: {
543
+ type: 'object',
544
+ properties: {
545
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
546
+ item_id: { type: 'string', description: 'ID do item a mover' },
547
+ novo_nivel: { type: 'number', description: 'Novo nível hierárquico (0, 1, 2, ...)' },
548
+ descricao_item: { type: 'string', description: 'Descrição do item (para confirmação visual)' }
549
+ },
550
+ required: ['orcamento_id', 'item_id', 'novo_nivel', 'descricao_item']
551
+ }
552
+ },
553
+ // === LEARNING TOOLS ===
554
+ {
555
+ name: 'quanty_aprender',
556
+ description: 'Ensina explicitamente uma nova capacidade ou procedimento ao sistema.\nO sistema armazenará esse conhecimento para uso futuro.',
557
+ inputSchema: {
558
+ type: 'object',
559
+ properties: {
560
+ capacidade: { type: 'string', description: 'Nome da capacidade' },
561
+ instrucoes: { type: 'string', description: 'Instruções detalhadas' }
562
+ },
563
+ required: ['capacidade', 'instrucoes']
564
+ }
565
+ },
566
+ {
567
+ name: 'quanty_feedback',
568
+ description: 'Fornece feedback sobre a última operação executada.\nAjuda o sistema a melhorar continuamente.',
569
+ inputSchema: {
570
+ type: 'object',
571
+ properties: {
572
+ tipo: {
573
+ type: 'string',
574
+ description: 'Tipo de feedback',
575
+ enum: ['success', 'failure', 'improvement']
576
+ },
577
+ detalhes: { type: 'string', description: 'Detalhes opcionais do feedback' }
578
+ },
579
+ required: ['tipo']
580
+ }
581
+ },
582
+ // === NEW TOOLS - EDIT BUDGET ===
583
+ {
584
+ name: 'quanty_editar_orcamento',
585
+ description: 'Edita metadados do orçamento: título, Data Base, BDI Principal e/ou BDI Diferenciado.\nPrepara a operação para confirmação.',
586
+ inputSchema: {
587
+ type: 'object',
588
+ properties: {
589
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
590
+ titulo: { type: 'string', description: 'Novo título do orçamento' },
591
+ data_base: { type: 'string', description: 'Data Base de preços (formato: MM/AAAA)' },
592
+ bdi_principal: { type: 'number', description: 'Novo BDI Principal (%)' },
593
+ bdi_diferenciado: { type: 'number', description: 'Novo BDI Diferenciado (%)' }
594
+ },
595
+ required: ['orcamento_id']
596
+ }
597
+ },
598
+ {
599
+ name: 'quanty_definir_bdi_item',
600
+ description: 'Define qual BDI um item usa: Principal ou Diferenciado.\nPrepara a operação para confirmação.',
601
+ inputSchema: {
602
+ type: 'object',
603
+ properties: {
604
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
605
+ item_id: { type: 'string', description: 'ID do item' },
606
+ tipo_bdi: { type: 'string', enum: ['Principal', 'Diferenciado'], description: 'Tipo de BDI' },
607
+ descricao_item: { type: 'string', description: 'Descrição do item para confirmação' }
608
+ },
609
+ required: ['orcamento_id', 'item_id', 'tipo_bdi']
610
+ }
611
+ },
612
+ {
613
+ name: 'quanty_classificar_abc',
614
+ description: 'Define o tipo de recurso para classificação na Curva ABC.\nTipos: Custo, Material, Mão de Obra, Equipamento.',
615
+ inputSchema: {
616
+ type: 'object',
617
+ properties: {
618
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
619
+ chave_item: { type: 'string', description: 'CÓDIGO do item da Curva ABC. Se não houver código, usar a DESCRIÇÃO exata.' },
620
+ classificacao: { type: 'string', enum: ['Custo', 'Material', 'Mão de Obra', 'Equipamento'], description: 'Tipo de classificação ABC' },
621
+ descricao_item: { type: 'string', description: 'Descrição do item para confirmação' }
622
+ },
623
+ required: ['orcamento_id', 'chave_item', 'classificacao']
624
+ }
625
+ },
626
+ // === NEW TOOLS - COMPOSITION EDITING ===
627
+ {
628
+ name: 'quanty_editar_insumo_composicao',
629
+ description: 'Edita um insumo dentro de uma composição existente.\nPode alterar código, descrição, unidade, quantidade ou custo unitário.',
630
+ inputSchema: {
631
+ type: 'object',
632
+ properties: {
633
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
634
+ composicao_id: { type: 'string', description: 'ID da composição' },
635
+ insumo_id: { type: 'string', description: 'ID do insumo a editar' },
636
+ codigo: { type: 'string', description: 'Novo código' },
637
+ descricao: { type: 'string', description: 'Nova descrição' },
638
+ unidade: { type: 'string', description: 'Nova unidade' },
639
+ quantidade: { type: 'number', description: 'Nova quantidade/coeficiente' },
640
+ custo_unitario: { type: 'number', description: 'Novo custo unitário' },
641
+ tipo_item: { type: 'string', description: 'Tipo do item (INSUMO, MATERIAL, MO, etc)' },
642
+ descricao_insumo: { type: 'string', description: 'Descrição do insumo para confirmação' }
643
+ },
644
+ required: ['orcamento_id', 'composicao_id', 'insumo_id']
645
+ }
646
+ },
647
+ {
648
+ name: 'quanty_adicionar_insumo_composicao',
649
+ description: 'Adiciona um novo insumo a uma composição existente.',
650
+ inputSchema: {
651
+ type: 'object',
652
+ properties: {
653
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
654
+ composicao_id: { type: 'string', description: 'ID da composição' },
655
+ codigo: { type: 'string', description: 'Código do insumo' },
656
+ descricao: { type: 'string', description: 'Descrição do insumo' },
657
+ unidade: { type: 'string', description: 'Unidade' },
658
+ quantidade: { type: 'number', description: 'Quantidade/coeficiente' },
659
+ custo_unitario: { type: 'number', description: 'Custo unitário' },
660
+ tipo_item: { type: 'string', description: 'Tipo do item (INSUMO, MATERIAL, MO, etc)' }
661
+ },
662
+ required: ['orcamento_id', 'composicao_id', 'descricao', 'unidade', 'quantidade', 'custo_unitario']
663
+ }
664
+ },
665
+ {
666
+ name: 'quanty_remover_insumo_composicao',
667
+ description: 'Remove um insumo de uma composição.',
668
+ inputSchema: {
669
+ type: 'object',
670
+ properties: {
671
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
672
+ composicao_id: { type: 'string', description: 'ID da composição' },
673
+ insumo_id: { type: 'string', description: 'ID do insumo a remover' },
674
+ descricao_insumo: { type: 'string', description: 'Descrição do insumo para confirmação' }
675
+ },
676
+ required: ['orcamento_id', 'composicao_id', 'insumo_id']
677
+ }
678
+ },
679
+ {
680
+ name: 'quanty_converter_para_composicao',
681
+ description: 'Converte um item de custo simples em composição.\nPermite depois adicionar insumos a ele.',
682
+ inputSchema: {
683
+ type: 'object',
684
+ properties: {
685
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
686
+ item_id: { type: 'string', description: 'ID do item a converter' },
687
+ descricao_item: { type: 'string', description: 'Descrição do item para confirmação' }
688
+ },
689
+ required: ['orcamento_id', 'item_id']
690
+ }
691
+ },
692
+ {
693
+ name: 'quanty_converter_para_custo',
694
+ description: 'Converte uma composição de volta para custo simples.\nREMOVE todos os insumos da composição.',
695
+ inputSchema: {
696
+ type: 'object',
697
+ properties: {
698
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
699
+ item_id: { type: 'string', description: 'ID do item a converter' },
700
+ descricao_item: { type: 'string', description: 'Descrição do item para confirmação' }
701
+ },
702
+ required: ['orcamento_id', 'item_id']
703
+ }
704
+ },
705
+ // === NEW TOOLS - BANK MANAGEMENT ===
706
+ {
707
+ name: 'quanty_criar_banco',
708
+ description: 'Cria um banco de composições privado.',
709
+ inputSchema: {
710
+ type: 'object',
711
+ properties: {
712
+ nome: { type: 'string', description: 'Nome do banco' },
713
+ descricao: { type: 'string', description: 'Descrição do banco' }
714
+ },
715
+ required: ['nome']
716
+ }
717
+ },
718
+ {
719
+ name: 'quanty_editar_banco',
720
+ description: 'Edita metadados de um banco: título, descrição, imagem de capa, data da versão.',
721
+ inputSchema: {
722
+ type: 'object',
723
+ properties: {
724
+ banco_id: { type: 'string', description: 'ID do banco' },
725
+ titulo: { type: 'string', description: 'Novo título' },
726
+ descricao: { type: 'string', description: 'Nova descrição' },
727
+ imagem_capa: { type: 'string', description: 'URL da nova imagem de capa' },
728
+ data_versao: { type: 'string', description: 'Data da versão (formato: MM/AAAA)' }
729
+ },
730
+ required: ['banco_id']
731
+ }
732
+ },
733
+ {
734
+ name: 'quanty_adicionar_ao_banco',
735
+ description: 'Adiciona um item ou composição a um banco privado.',
736
+ inputSchema: {
737
+ type: 'object',
738
+ properties: {
739
+ banco_id: { type: 'string', description: 'ID do banco' },
740
+ categoria_id: { type: 'string', description: 'ID da categoria (ou novo ID para criar)' },
741
+ codigo: { type: 'string', description: 'Código do item' },
742
+ descricao: { type: 'string', description: 'Descrição do item' },
743
+ unidade: { type: 'string', description: 'Unidade' },
744
+ custo_unitario: { type: 'number', description: 'Custo unitário' },
745
+ tipo_item: { type: 'string', description: 'Tipo do item' },
746
+ eh_composicao: { type: 'boolean', description: 'Se é uma composição' },
747
+ insumos: { type: 'array', items: { type: 'object' }, description: 'Lista de insumos se for composição' }
748
+ },
749
+ required: ['banco_id', 'categoria_id', 'descricao', 'unidade', 'custo_unitario']
750
+ }
751
+ },
752
+ // === MATCHING TOOLS ===
753
+ {
754
+ name: 'quanty_buscar_similares',
755
+ 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.',
756
+ inputSchema: {
757
+ type: 'object',
758
+ properties: {
759
+ descricao: { type: 'string', description: 'Descrição do item a buscar' },
760
+ buscar_orcamentos: { type: 'boolean', description: 'Buscar em outros orçamentos (default: true)' },
761
+ buscar_bancos: { type: 'boolean', description: 'Buscar em bancos de composições (default: true)' },
762
+ excluir_orcamento_id: { type: 'string', description: 'ID do orçamento a excluir da busca' },
763
+ banco_ids: { type: 'array', items: { type: 'string' }, description: 'IDs de bancos específicos para buscar' },
764
+ limite: { type: 'number', description: 'Máximo de resultados (default: 10)' },
765
+ similaridade_minima: { type: 'number', description: 'Similaridade mínima % (default: 50)' }
766
+ },
767
+ required: ['descricao']
768
+ }
769
+ },
770
+ {
771
+ name: 'quanty_comparar_precos',
772
+ 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.',
773
+ inputSchema: {
774
+ type: 'object',
775
+ properties: {
776
+ descricao: { type: 'string', description: 'Descrição do item' },
777
+ preco_atual: { type: 'number', description: 'Preço unitário atual do item' },
778
+ excluir_orcamento_id: { type: 'string', description: 'ID do orçamento a excluir da comparação' },
779
+ limite: { type: 'number', description: 'Máximo de itens para comparar (default: 20)' },
780
+ similaridade_minima: { type: 'number', description: 'Similaridade mínima % (default: 60)' }
781
+ },
782
+ required: ['descricao', 'preco_atual']
783
+ }
784
+ },
785
+ // === TAGS (RÓTULOS) TOOLS ===
786
+ {
787
+ name: 'quanty_adicionar_tag',
788
+ description: 'Adiciona um rótulo (tag) a um orçamento para organização.',
789
+ inputSchema: {
790
+ type: 'object',
791
+ properties: {
792
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
793
+ nome_tag: { type: 'string', description: 'Nome do rótulo (ex: "Urgente", "Em análise")' }
794
+ },
795
+ required: ['orcamento_id', 'nome_tag']
796
+ }
797
+ },
798
+ {
799
+ name: 'quanty_editar_tag',
800
+ description: 'Edita o nome de um rótulo existente.',
801
+ inputSchema: {
802
+ type: 'object',
803
+ properties: {
804
+ tag_id: { type: 'string', description: 'ID do rótulo' },
805
+ novo_nome: { type: 'string', description: 'Novo nome do rótulo' },
806
+ nome_atual: { type: 'string', description: 'Nome atual para confirmação' }
807
+ },
808
+ required: ['tag_id', 'novo_nome']
809
+ }
810
+ },
811
+ {
812
+ name: 'quanty_remover_tag',
813
+ description: 'Remove um rótulo de um orçamento.',
814
+ inputSchema: {
815
+ type: 'object',
816
+ properties: {
817
+ tag_id: { type: 'string', description: 'ID do rótulo a remover' },
818
+ nome_tag: { type: 'string', description: 'Nome do rótulo para confirmação' }
819
+ },
820
+ required: ['tag_id']
821
+ }
822
+ },
823
+ // === PROJETOS TOOLS ===
824
+ {
825
+ name: 'quanty_listar_projetos',
826
+ description: 'Lista todos os projetos disponíveis para organizar orçamentos.',
827
+ inputSchema: {
828
+ type: 'object',
829
+ properties: {}
830
+ }
831
+ },
832
+ {
833
+ name: 'quanty_criar_projeto',
834
+ description: 'Cria um novo projeto para organizar orçamentos.',
835
+ inputSchema: {
836
+ type: 'object',
837
+ properties: {
838
+ nome: { type: 'string', description: 'Nome do projeto' },
839
+ descricao: { type: 'string', description: 'Descrição do projeto (opcional)' },
840
+ codigo: { type: 'string', description: 'Código curto do projeto, máx 15 caracteres (opcional)' }
841
+ },
842
+ required: ['nome']
843
+ }
844
+ },
845
+ {
846
+ name: 'quanty_editar_projeto',
847
+ description: 'Edita um projeto existente.',
848
+ inputSchema: {
849
+ type: 'object',
850
+ properties: {
851
+ projeto_id: { type: 'string', description: 'ID do projeto' },
852
+ nome: { type: 'string', description: 'Novo nome (opcional)' },
853
+ descricao: { type: 'string', description: 'Nova descrição (opcional)' },
854
+ codigo: { type: 'string', description: 'Novo código (opcional)' },
855
+ nome_atual: { type: 'string', description: 'Nome atual para confirmação' }
856
+ },
857
+ required: ['projeto_id']
858
+ }
859
+ },
860
+ {
861
+ name: 'quanty_excluir_projeto',
862
+ description: 'Exclui um projeto. Orçamentos vinculados serão desvinculados.',
863
+ inputSchema: {
864
+ type: 'object',
865
+ properties: {
866
+ projeto_id: { type: 'string', description: 'ID do projeto a excluir' },
867
+ nome_projeto: { type: 'string', description: 'Nome do projeto para confirmação' }
868
+ },
869
+ required: ['projeto_id']
870
+ }
871
+ },
872
+ {
873
+ name: 'quanty_vincular_orcamento_projeto',
874
+ description: 'Vincula ou desvincula um orçamento de um projeto.',
875
+ inputSchema: {
876
+ type: 'object',
877
+ properties: {
878
+ orcamento_id: { type: 'string', description: 'ID do orçamento' },
879
+ projeto_id: { type: 'string', description: 'ID do projeto (ou null para desvincular)' },
880
+ titulo_orcamento: { type: 'string', description: 'Título do orçamento para confirmação' },
881
+ nome_projeto: { type: 'string', description: 'Nome do projeto para confirmação' }
882
+ },
883
+ required: ['orcamento_id']
884
+ }
259
885
  }
260
886
  ];
261
887
  async function handleToolCall(auth, name, args) {
262
888
  switch (name) {
263
889
  // === DOCUMENTATION ===
264
890
  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
- };
891
+ return executeWithLearning('Ler manual do Quanty', async () => {
892
+ let content = QUANTY_MANUAL;
893
+ // Enrich with learning context
894
+ if (contextBuilder) {
895
+ try {
896
+ content = await contextBuilder.generateSystemPrompt(content);
897
+ }
898
+ catch (err) {
899
+ console.error('Failed to generate system prompt:', err);
900
+ }
901
+ }
902
+ return {
903
+ conteudo: content,
904
+ _info: 'Este manual contém tudo sobre BDI, Composições e estrutura do Quanty.'
905
+ };
906
+ });
269
907
  }
270
908
  // === READ ===
271
909
  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
- };
910
+ return executeWithLearning(`Listar ${args.limite || 10} orçamentos recentes`, async () => {
911
+ const budgets = await budgetService.listBudgets(auth, args.limite || 10);
912
+ return {
913
+ _info: 'Use quanty_abrir_orcamento com o ID para ver detalhes completos',
914
+ total: budgets.length,
915
+ orcamentos: budgets.map(b => ({
916
+ id: b.id,
917
+ titulo: b.title,
918
+ criado_em: b.createdAt,
919
+ modificado_em: b.lastModified,
920
+ criador: b.creator,
921
+ compartilhado: b.shared,
922
+ qtd_itens: b.itemCount,
923
+ valor_total: `R$ ${b.totalValue.toFixed(2)}`
924
+ }))
925
+ };
926
+ });
287
927
  }
288
928
  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
- };
929
+ return executeWithLearning(`Abrir orçamento ${args.id}`, async () => {
930
+ const budget = await budgetService.getBudget(auth, args.id);
931
+ if (!budget)
932
+ return { erro: 'Orçamento não encontrado' };
933
+ const totalValue = budget.rows.reduce((sum, r) => sum + (r.quantity * r.unitCost), 0);
934
+ return {
935
+ id: budget.id,
936
+ titulo: budget.title,
937
+ data_base: budget.baseDate || '',
938
+ bdi_principal: `${budget.bdiPrincipal}%`,
939
+ bdi_diferenciado: `${budget.bdiDiferenciado}%`,
940
+ itens_bdi_principal: budget.bdiItemsPrincipal,
941
+ itens_bdi_diferenciado: budget.bdiItemsDiferenciado,
942
+ valor_total: `R$ ${totalValue.toFixed(2)}`,
943
+ _info_itens: 'Itens com is_composicao=true são COMPOSIÇÕES que possuem insumos internos',
944
+ itens: budget.rows.map(r => ({
945
+ id: r.id,
946
+ nivel: r.level,
947
+ codigo: r.code,
948
+ descricao: r.description,
949
+ unidade: r.unit,
950
+ quantidade: r.quantity,
951
+ custo_unitario: r.unitCost,
952
+ total: r.quantity * r.unitCost,
953
+ is_composicao: r.isComposition || false,
954
+ bdi_tipo: r.bdiType || 'Principal'
955
+ }))
956
+ };
957
+ });
313
958
  }
314
959
  case 'quanty_ver_composicao': {
315
- const insumos = await budgetService.getCompositionDetails(auth, args.orcamento_id, args.item_id);
316
- if (!insumos || insumos.length === 0) {
960
+ return executeWithLearning(`Ver composição ${args.item_id}`, async () => {
961
+ const insumos = await budgetService.getCompositionDetails(auth, args.orcamento_id, args.item_id);
962
+ if (!insumos || insumos.length === 0) {
963
+ return {
964
+ mensagem: 'Esta composição não possui insumos listados ou não é uma composição válida.'
965
+ };
966
+ }
967
+ const totalComposicao = insumos.reduce((sum, i) => sum + (i.total || 0), 0);
317
968
  return {
318
- mensagem: 'Esta composição não possui insumos listados ou não é uma composição válida.'
969
+ _info: 'Lista de insumos/filhos desta composição',
970
+ qtd_insumos: insumos.length,
971
+ custo_total_calculado: `R$ ${totalComposicao.toFixed(2)}`,
972
+ insumos: insumos.map(i => ({
973
+ id: i.id, // ID necessário para recursão
974
+ descricao: i.description,
975
+ unidade: i.unit,
976
+ coeficiente: i.quantity,
977
+ custo_unitario_insumo: i.unitCost,
978
+ custo_total_insumo: i.total,
979
+ is_composicao: i.is_composicao, // Indica se pode descer mais um nível
980
+ tipo: i.type
981
+ }))
319
982
  };
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
- };
983
+ });
337
984
  }
338
985
  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
- };
986
+ return executeWithLearning('Listar bancos', async () => {
987
+ const banks = await bankService.listBanks(auth);
988
+ return {
989
+ _info: 'Use quanty_buscar_insumo com o ID do banco para buscar itens',
990
+ total: banks.length,
991
+ bancos: banks.map(b => ({
992
+ id: b.id,
993
+ titulo: b.title,
994
+ descricao: b.description,
995
+ autor: b.author,
996
+ regiao: b.region,
997
+ visibilidade: b.visibility,
998
+ qtd_categorias: b.categoryCount
999
+ }))
1000
+ };
1001
+ });
353
1002
  }
354
1003
  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
- };
1004
+ return executeWithLearning(`Buscar "${args.busca}" no banco`, async () => {
1005
+ const items = await bankService.searchBankItems(args.banco_id, args.busca);
1006
+ return {
1007
+ _info: 'Itens do tipo COMPOSIÇÃO possuem insumos internos que definem o custo',
1008
+ total: items.length,
1009
+ resultados: items.map(i => ({
1010
+ id: i.id,
1011
+ codigo: i.code,
1012
+ descricao: i.description,
1013
+ unidade: i.unit,
1014
+ custo_unitario: i.unitCost,
1015
+ tipo: i.itemType,
1016
+ is_composicao: i.isComposition,
1017
+ qtd_insumos: i.composition?.length || 0
1018
+ }))
1019
+ };
1020
+ });
370
1021
  }
371
1022
  // === WRITE (with confirmation) ===
372
1023
  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
1024
+ return executeWithLearning(`Importar item ${args.banco_item_id}`, async () => {
1025
+ const item = await bankService.getBankItemById(args.banco_id, args.banco_item_id);
1026
+ if (!item)
1027
+ return { erro: 'Item não encontrado no banco de dados.' };
1028
+ // Passa a quantidade e nivel diretamente para o preparador
1029
+ const prepared = bankService.prepareImportItem(args.orcamento_id, item, args.quantidade, args.nivel);
1030
+ const pendingId = generatePendingId();
1031
+ pendingOperations.set(pendingId, {
1032
+ action: prepared.action,
1033
+ data: prepared.data,
1034
+ expiresAt: Date.now() + 5 * 60 * 1000
1035
+ });
1036
+ return {
1037
+ pending_id: pendingId,
1038
+ preview: prepared.preview,
1039
+ is_composicao: item.isComposition,
1040
+ qtd_insumos: item.composition?.length || 0,
1041
+ mensagem: 'Use quanty_executar para confirmar.'
1042
+ };
383
1043
  });
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
1044
  }
392
1045
  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
1046
+ return executeWithLearning(`Preparar orçamento "${args.titulo}"`, async () => {
1047
+ const prepared = budgetService.prepareCreateBudget(auth, args.titulo);
1048
+ const pendingId = generatePendingId();
1049
+ pendingOperations.set(pendingId, {
1050
+ action: prepared.action,
1051
+ data: prepared.data,
1052
+ expiresAt: Date.now() + 5 * 60 * 1000
1053
+ });
1054
+ return {
1055
+ pending_id: pendingId,
1056
+ preview: prepared.preview,
1057
+ mensagem: 'Use quanty_executar com este pending_id para confirmar a criação.'
1058
+ };
399
1059
  });
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
1060
  }
406
1061
  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
1062
+ return executeWithLearning(`Preparar item "${args.descricao}"`, async () => {
1063
+ const prepared = budgetService.prepareAddItem(args.orcamento_id, {
1064
+ description: args.descricao,
1065
+ unit: args.unidade,
1066
+ quantity: args.quantidade,
1067
+ unitCost: args.custo_unitario,
1068
+ level: args.nivel
1069
+ });
1070
+ const pendingId = generatePendingId();
1071
+ pendingOperations.set(pendingId, {
1072
+ action: prepared.action,
1073
+ data: prepared.data,
1074
+ expiresAt: Date.now() + 5 * 60 * 1000
1075
+ });
1076
+ return {
1077
+ pending_id: pendingId,
1078
+ preview: prepared.preview,
1079
+ mensagem: 'Use quanty_executar com este pending_id para confirmar a adição.'
1080
+ };
1081
+ });
1082
+ }
1083
+ case 'quanty_criar_composicao': {
1084
+ return executeWithLearning(`Criar composição "${args.descricao}"`, async () => {
1085
+ // Transforma os insumos do formato MCP para o formato interno
1086
+ const insumos = (args.insumos || []).map((i) => ({
1087
+ code: i.codigo,
1088
+ description: i.descricao,
1089
+ unit: i.unidade,
1090
+ quantity: i.quantidade,
1091
+ unitCost: i.custo_unitario,
1092
+ itemType: i.tipo || 'INSUMO'
1093
+ }));
1094
+ const prepared = budgetService.prepareAddComposition(args.orcamento_id, {
1095
+ code: args.codigo,
1096
+ description: args.descricao,
1097
+ unit: args.unidade,
1098
+ quantity: args.quantidade,
1099
+ level: args.nivel,
1100
+ insumos
1101
+ });
1102
+ const pendingId = generatePendingId();
1103
+ pendingOperations.set(pendingId, {
1104
+ action: prepared.action,
1105
+ data: prepared.data,
1106
+ expiresAt: Date.now() + 5 * 60 * 1000
1107
+ });
1108
+ // Calcula o custo unitário para mostrar no preview
1109
+ const custoUnitario = insumos.reduce((sum, i) => sum + (i.quantity * i.unitCost), 0);
1110
+ return {
1111
+ pending_id: pendingId,
1112
+ preview: prepared.preview,
1113
+ custo_unitario_calculado: `R$ ${custoUnitario.toFixed(2)}`,
1114
+ qtd_insumos: insumos.length,
1115
+ mensagem: 'Use quanty_executar com este pending_id para confirmar a criação da composição.'
1116
+ };
418
1117
  });
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
1118
  }
425
1119
  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!' };
1120
+ return executeWithLearning(`Executar operação ${args.pending_id}`, async () => {
1121
+ const pending = pendingOperations.get(args.pending_id);
1122
+ if (!pending) {
1123
+ return { erro: 'Operação pendente não encontrada ou expirada' };
439
1124
  }
440
- case 'add_item': {
441
- await budgetService.executeAddItem(auth, pending.data);
442
- return { sucesso: true, mensagem: 'Item adicionado com sucesso!' };
1125
+ if (Date.now() > pending.expiresAt) {
1126
+ pendingOperations.delete(args.pending_id);
1127
+ return { erro: 'Operação expirada. Prepare novamente.' };
443
1128
  }
444
- case 'import_item': {
445
- await budgetService.executeAddItem(auth, pending.data);
446
- return { sucesso: true, mensagem: 'Item importado com sucesso!' };
1129
+ pendingOperations.delete(args.pending_id);
1130
+ switch (pending.action) {
1131
+ case 'create_budget': {
1132
+ const budgetId = await budgetService.executeCreateBudget(auth, pending.data);
1133
+ return { sucesso: true, orcamento_id: budgetId, mensagem: 'Orçamento criado com sucesso!' };
1134
+ }
1135
+ case 'add_item': {
1136
+ await budgetService.executeAddItem(auth, pending.data);
1137
+ return { sucesso: true, mensagem: 'Item adicionado com sucesso!' };
1138
+ }
1139
+ case 'import_item': {
1140
+ await budgetService.executeAddItem(auth, pending.data);
1141
+ return { sucesso: true, mensagem: 'Item importado com sucesso!' };
1142
+ }
1143
+ case 'update_bdi': {
1144
+ await budgetService.executeUpdateBdi(auth, pending.data);
1145
+ return { sucesso: true, mensagem: 'BDI atualizado com sucesso!' };
1146
+ }
1147
+ case 'add_bdi_item': {
1148
+ await budgetService.executeAddBdiItem(auth, pending.data);
1149
+ return { sucesso: true, mensagem: 'Item adicionado à composição do BDI com sucesso!' };
1150
+ }
1151
+ case 'edit_bdi_item': {
1152
+ await budgetService.executeEditBdiItem(auth, pending.data);
1153
+ return { sucesso: true, mensagem: 'Item do BDI editado com sucesso!' };
1154
+ }
1155
+ case 'remove_bdi_item': {
1156
+ await budgetService.executeRemoveBdiItem(auth, pending.data);
1157
+ return { sucesso: true, mensagem: 'Item removido da composição do BDI com sucesso!' };
1158
+ }
1159
+ case 'edit_item': {
1160
+ await budgetService.executeEditItem(auth, pending.data);
1161
+ return { sucesso: true, mensagem: 'Item editado com sucesso!' };
1162
+ }
1163
+ case 'delete_item': {
1164
+ await budgetService.executeDeleteItem(auth, pending.data);
1165
+ return { sucesso: true, mensagem: 'Item deletado com sucesso!' };
1166
+ }
1167
+ case 'duplicate_budget': {
1168
+ const newBudgetId = await budgetService.executeDuplicateBudget(auth, pending.data);
1169
+ return { sucesso: true, orcamento_id: newBudgetId, mensagem: 'Orçamento duplicado com sucesso!' };
1170
+ }
1171
+ case 'edit_budget': {
1172
+ await budgetService.executeEditBudget(auth, pending.data);
1173
+ return { sucesso: true, mensagem: 'Orçamento editado com sucesso!' };
1174
+ }
1175
+ case 'set_abc_classification': {
1176
+ await budgetService.executeSetAbcClassification(auth, pending.data);
1177
+ return { sucesso: true, mensagem: 'Classificação ABC atualizada com sucesso!' };
1178
+ }
1179
+ case 'edit_composition_input': {
1180
+ await budgetService.executeEditCompositionInput(auth, pending.data);
1181
+ return { sucesso: true, mensagem: 'Insumo da composição editado com sucesso!' };
1182
+ }
1183
+ case 'add_composition_input': {
1184
+ await budgetService.executeAddCompositionInput(auth, pending.data);
1185
+ return { sucesso: true, mensagem: 'Insumo adicionado à composição com sucesso!' };
1186
+ }
1187
+ case 'remove_composition_input': {
1188
+ await budgetService.executeRemoveCompositionInput(auth, pending.data);
1189
+ return { sucesso: true, mensagem: 'Insumo removido da composição com sucesso!' };
1190
+ }
1191
+ case 'convert_to_composition': {
1192
+ await budgetService.executeConvertToComposition(auth, pending.data);
1193
+ return { sucesso: true, mensagem: 'Item convertido para composição com sucesso! Agora você pode adicionar insumos.' };
1194
+ }
1195
+ case 'convert_to_cost': {
1196
+ await budgetService.executeConvertToCost(auth, pending.data);
1197
+ return { sucesso: true, mensagem: 'Composição convertida para custo simples com sucesso!' };
1198
+ }
1199
+ // === TAGS ===
1200
+ case 'add_tag': {
1201
+ const tag = await budgetService.executeAddTag(auth, pending.data);
1202
+ return { sucesso: true, tag_id: tag.id, mensagem: 'Rótulo adicionado com sucesso!' };
1203
+ }
1204
+ case 'edit_tag': {
1205
+ await budgetService.executeEditTag(auth, pending.data);
1206
+ return { sucesso: true, mensagem: 'Rótulo editado com sucesso!' };
1207
+ }
1208
+ case 'remove_tag': {
1209
+ await budgetService.executeRemoveTag(auth, pending.data);
1210
+ return { sucesso: true, mensagem: 'Rótulo removido com sucesso!' };
1211
+ }
1212
+ // === PROJETOS ===
1213
+ case 'create_project': {
1214
+ const project = await budgetService.executeCreateProject(auth, pending.data);
1215
+ return { sucesso: true, projeto_id: project.id, mensagem: 'Projeto criado com sucesso!' };
1216
+ }
1217
+ case 'edit_project': {
1218
+ await budgetService.executeEditProject(auth, pending.data);
1219
+ return { sucesso: true, mensagem: 'Projeto editado com sucesso!' };
1220
+ }
1221
+ case 'delete_project': {
1222
+ await budgetService.executeDeleteProject(auth, pending.data);
1223
+ return { sucesso: true, mensagem: 'Projeto excluído com sucesso!' };
1224
+ }
1225
+ case 'link_budget_to_project': {
1226
+ await budgetService.executeLinkBudgetToProject(auth, pending.data);
1227
+ return { sucesso: true, mensagem: 'Orçamento vinculado ao projeto com sucesso!' };
1228
+ }
1229
+ default:
1230
+ return { erro: `Ação desconhecida: ${pending.action}` };
447
1231
  }
448
- default:
449
- return { erro: `Ação desconhecida: ${pending.action}` };
450
- }
1232
+ });
451
1233
  }
452
1234
  // === ANALYSIS ===
453
1235
  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
- };
1236
+ return executeWithLearning(`Buscar orçamentos similares`, async () => {
1237
+ const similar = await analysisService.findSimilarBudgets(auth, args.orcamento_id);
1238
+ return {
1239
+ total: similar.length,
1240
+ similares: similar.map(s => ({
1241
+ id: s.id,
1242
+ titulo: s.title,
1243
+ similaridade: `${s.similarity}%`,
1244
+ itens_em_comum: s.commonItems
1245
+ }))
1246
+ };
1247
+ });
464
1248
  }
465
1249
  case 'quanty_sugerir_itens': {
466
- const suggestions = await analysisService.suggestMissingItems(auth, args.orcamento_id);
1250
+ return executeWithLearning(`Sugerir itens`, async () => {
1251
+ const suggestions = await analysisService.suggestMissingItems(auth, args.orcamento_id);
1252
+ return {
1253
+ total: suggestions.length,
1254
+ sugestoes: suggestions.map(s => ({
1255
+ descricao: s.description,
1256
+ unidade: s.unit,
1257
+ custo_medio: `R$ ${s.avgUnitCost.toFixed(2)}`,
1258
+ frequencia: s.frequency,
1259
+ razao: s.reason
1260
+ }))
1261
+ };
1262
+ });
1263
+ }
1264
+ case 'quanty_custo_medio': {
1265
+ return executeWithLearning(`Calcular custo médio de ${args.driver}`, async () => {
1266
+ const metric = await analysisService.calculateCostPerUnit(auth, args.categoria || '', args.driver);
1267
+ return {
1268
+ driver: metric.driver,
1269
+ custo_medio: `R$ ${metric.avgCostPerUnit.toFixed(2)}`,
1270
+ custo_minimo: `R$ ${metric.minCost.toFixed(2)}`,
1271
+ custo_maximo: `R$ ${metric.maxCost.toFixed(2)}`,
1272
+ custo_p90: `R$ ${metric.p90Cost.toFixed(2)}`,
1273
+ amostra: `${metric.sampleSize} orçamentos analisados`
1274
+ };
1275
+ });
1276
+ }
1277
+ // === MANAGEMENT HANDLERS ===
1278
+ case 'quanty_ajustar_bdi': {
1279
+ return executeWithLearning(`Ajustar BDI`, async () => {
1280
+ const prepared = budgetService.prepareUpdateBdi(args.orcamento_id, args.bdi_principal, args.bdi_diferenciado);
1281
+ const pendingId = generatePendingId();
1282
+ pendingOperations.set(pendingId, {
1283
+ action: prepared.action,
1284
+ data: prepared.data,
1285
+ expiresAt: Date.now() + 5 * 60 * 1000
1286
+ });
1287
+ return {
1288
+ pending_id: pendingId,
1289
+ preview: prepared.preview,
1290
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1291
+ };
1292
+ });
1293
+ }
1294
+ case 'quanty_adicionar_item_bdi': {
1295
+ return executeWithLearning(`Adicionar item ao BDI ${args.tipo_bdi}`, async () => {
1296
+ const prepared = budgetService.prepareAddBdiItem(args.orcamento_id, args.tipo_bdi, {
1297
+ description: args.descricao,
1298
+ percentage: args.percentual,
1299
+ incidence: args.incidencia || 'Custo Direto'
1300
+ });
1301
+ const pendingId = generatePendingId();
1302
+ pendingOperations.set(pendingId, {
1303
+ action: prepared.action,
1304
+ data: prepared.data,
1305
+ expiresAt: Date.now() + 5 * 60 * 1000
1306
+ });
1307
+ return {
1308
+ pending_id: pendingId,
1309
+ preview: prepared.preview,
1310
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1311
+ };
1312
+ });
1313
+ }
1314
+ case 'quanty_editar_item_bdi': {
1315
+ return executeWithLearning(`Editar item do BDI ${args.tipo_bdi}`, async () => {
1316
+ const updates = {};
1317
+ if (args.descricao !== undefined)
1318
+ updates.description = args.descricao;
1319
+ if (args.percentual !== undefined)
1320
+ updates.percentage = args.percentual;
1321
+ if (args.incidencia !== undefined)
1322
+ updates.incidence = args.incidencia;
1323
+ const prepared = budgetService.prepareEditBdiItem(args.orcamento_id, args.tipo_bdi, args.item_id, updates);
1324
+ const pendingId = generatePendingId();
1325
+ pendingOperations.set(pendingId, {
1326
+ action: prepared.action,
1327
+ data: prepared.data,
1328
+ expiresAt: Date.now() + 5 * 60 * 1000
1329
+ });
1330
+ return {
1331
+ pending_id: pendingId,
1332
+ preview: prepared.preview,
1333
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1334
+ };
1335
+ });
1336
+ }
1337
+ case 'quanty_remover_item_bdi': {
1338
+ return executeWithLearning(`Remover item do BDI ${args.tipo_bdi}`, async () => {
1339
+ const prepared = budgetService.prepareRemoveBdiItem(args.orcamento_id, args.tipo_bdi, args.item_id, args.descricao_item);
1340
+ const pendingId = generatePendingId();
1341
+ pendingOperations.set(pendingId, {
1342
+ action: prepared.action,
1343
+ data: prepared.data,
1344
+ expiresAt: Date.now() + 5 * 60 * 1000
1345
+ });
1346
+ return {
1347
+ pending_id: pendingId,
1348
+ preview: prepared.preview,
1349
+ mensagem: 'Use quanty_executar com este pending_id para confirmar a remoção.'
1350
+ };
1351
+ });
1352
+ }
1353
+ case 'quanty_editar_item': {
1354
+ return executeWithLearning(`Editar item`, async () => {
1355
+ const updates = {};
1356
+ if (args.descricao !== undefined)
1357
+ updates.description = args.descricao;
1358
+ if (args.unidade !== undefined)
1359
+ updates.unit = args.unidade;
1360
+ if (args.quantidade !== undefined)
1361
+ updates.quantity = args.quantidade;
1362
+ if (args.custo_unitario !== undefined)
1363
+ updates.unitCost = args.custo_unitario;
1364
+ if (args.nivel !== undefined)
1365
+ updates.level = args.nivel;
1366
+ if (args.codigo !== undefined)
1367
+ updates.code = args.codigo;
1368
+ const prepared = budgetService.prepareEditItem(args.orcamento_id, args.item_id, updates);
1369
+ const pendingId = generatePendingId();
1370
+ pendingOperations.set(pendingId, {
1371
+ action: prepared.action,
1372
+ data: prepared.data,
1373
+ expiresAt: Date.now() + 5 * 60 * 1000
1374
+ });
1375
+ return {
1376
+ pending_id: pendingId,
1377
+ preview: prepared.preview,
1378
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1379
+ };
1380
+ });
1381
+ }
1382
+ case 'quanty_deletar_item': {
1383
+ return executeWithLearning(`Deletar item`, async () => {
1384
+ const prepared = budgetService.prepareDeleteItem(args.orcamento_id, args.item_id, args.descricao_item);
1385
+ const pendingId = generatePendingId();
1386
+ pendingOperations.set(pendingId, {
1387
+ action: prepared.action,
1388
+ data: prepared.data,
1389
+ expiresAt: Date.now() + 5 * 60 * 1000
1390
+ });
1391
+ return {
1392
+ pending_id: pendingId,
1393
+ preview: prepared.preview,
1394
+ mensagem: 'Use quanty_executar com este pending_id para confirmar a exclusão.'
1395
+ };
1396
+ });
1397
+ }
1398
+ case 'quanty_duplicar_orcamento': {
1399
+ return executeWithLearning(`Duplicar orçamento`, async () => {
1400
+ const prepared = budgetService.prepareDuplicateBudget(auth, args.orcamento_id, args.novo_titulo);
1401
+ const pendingId = generatePendingId();
1402
+ pendingOperations.set(pendingId, {
1403
+ action: prepared.action,
1404
+ data: prepared.data,
1405
+ expiresAt: Date.now() + 5 * 60 * 1000
1406
+ });
1407
+ return {
1408
+ pending_id: pendingId,
1409
+ preview: prepared.preview,
1410
+ mensagem: 'Use quanty_executar com este pending_id para confirmar a duplicação.'
1411
+ };
1412
+ });
1413
+ }
1414
+ case 'quanty_gerar_curva_abc': {
1415
+ return executeWithLearning(`Gerar Curva ABC`, async () => {
1416
+ const result = await analysisService.generateAbcCurve(auth, args.orcamento_id, args.limite_classe_a || 80, args.limite_classe_b || 95);
1417
+ return {
1418
+ valor_total: `R$ ${result.totalValue.toFixed(2)}`,
1419
+ resumo: {
1420
+ classe_a: `${result.classACount} itens (${result.classAThreshold}% do valor)`,
1421
+ classe_b: `${result.classBCount} itens (${result.classBThreshold - result.classAThreshold}% do valor)`,
1422
+ classe_c: `${result.classCCount} itens (${100 - result.classBThreshold}% do valor)`
1423
+ },
1424
+ itens: result.items.map(item => ({
1425
+ id: item.id,
1426
+ descricao: item.description,
1427
+ total: `R$ ${item.total.toFixed(2)}`,
1428
+ percentual: `${item.percentage}%`,
1429
+ acumulado: `${item.accumulated}%`,
1430
+ classe: item.class,
1431
+ tipo: item.resourceType
1432
+ }))
1433
+ };
1434
+ });
1435
+ }
1436
+ case 'quanty_mover_item': {
1437
+ return executeWithLearning(`Mover item de nível`, async () => {
1438
+ const prepared = budgetService.prepareMoveItem(args.orcamento_id, args.item_id, args.novo_nivel, args.descricao_item);
1439
+ const pendingId = generatePendingId();
1440
+ pendingOperations.set(pendingId, {
1441
+ action: prepared.action,
1442
+ data: prepared.data,
1443
+ expiresAt: Date.now() + 5 * 60 * 1000
1444
+ });
1445
+ return {
1446
+ pending_id: pendingId,
1447
+ preview: prepared.preview,
1448
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1449
+ };
1450
+ });
1451
+ }
1452
+ // === LEARNING ===
1453
+ case 'quanty_aprender': {
1454
+ if (!learningSystem)
1455
+ throw new Error('Sistema de aprendizado não inicializado');
1456
+ await learningSystem.learnFromExecution(args.capacidade, [{ type: 'explicit_learning', instructions: args.instrucoes }], { success: true }, 0);
467
1457
  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
- }))
1458
+ status: 'success',
1459
+ message: `Aprendi sobre: ${args.capacidade}. Este conhecimento será aplicado em futuras operações.`
476
1460
  };
477
1461
  }
478
- case 'quanty_custo_medio': {
479
- const metric = await analysisService.calculateCostPerUnit(auth, args.categoria || '', args.driver);
1462
+ case 'quanty_feedback': {
1463
+ if (!learningSystem)
1464
+ throw new Error('Sistema de aprendizado não inicializado');
1465
+ await learningSystem.processFeedback('last_operation', // Simplified for now
1466
+ args.tipo, args.detalhes);
1467
+ console.error(`[Feedback] Type: ${args.tipo}, Details: ${args.detalhes || 'N/A'}`);
480
1468
  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`
1469
+ status: 'success',
1470
+ message: 'Feedback registrado. Obrigado por ajudar a melhorar o sistema!'
487
1471
  };
488
1472
  }
1473
+ // === NEW HANDLERS - EDIT BUDGET ===
1474
+ case 'quanty_editar_orcamento': {
1475
+ return executeWithLearning(`Editar orçamento`, async () => {
1476
+ const updates = {};
1477
+ if (args.titulo !== undefined)
1478
+ updates.title = args.titulo;
1479
+ if (args.data_base !== undefined)
1480
+ updates.baseDate = args.data_base;
1481
+ if (args.bdi_principal !== undefined)
1482
+ updates.bdiPrincipal = args.bdi_principal;
1483
+ if (args.bdi_diferenciado !== undefined)
1484
+ updates.bdiDiferenciado = args.bdi_diferenciado;
1485
+ const prepared = budgetService.prepareEditBudget(args.orcamento_id, updates);
1486
+ const pendingId = generatePendingId();
1487
+ pendingOperations.set(pendingId, {
1488
+ action: prepared.action,
1489
+ data: prepared.data,
1490
+ expiresAt: Date.now() + 5 * 60 * 1000
1491
+ });
1492
+ return {
1493
+ pending_id: pendingId,
1494
+ preview: prepared.preview,
1495
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1496
+ };
1497
+ });
1498
+ }
1499
+ case 'quanty_definir_bdi_item': {
1500
+ return executeWithLearning(`Definir BDI de item`, async () => {
1501
+ const prepared = budgetService.prepareSetBdiType(args.orcamento_id, args.item_id, args.tipo_bdi, args.descricao_item || 'Item');
1502
+ const pendingId = generatePendingId();
1503
+ pendingOperations.set(pendingId, {
1504
+ action: prepared.action,
1505
+ data: prepared.data,
1506
+ expiresAt: Date.now() + 5 * 60 * 1000
1507
+ });
1508
+ return {
1509
+ pending_id: pendingId,
1510
+ preview: prepared.preview,
1511
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1512
+ };
1513
+ });
1514
+ }
1515
+ case 'quanty_classificar_abc': {
1516
+ return executeWithLearning(`Classificar ABC`, async () => {
1517
+ const prepared = budgetService.prepareSetAbcClassification(args.orcamento_id, args.chave_item, // Usa código ou descrição como chave
1518
+ args.classificacao, args.chave_item // Usa a própria chave como descrição
1519
+ );
1520
+ const pendingId = generatePendingId();
1521
+ pendingOperations.set(pendingId, {
1522
+ action: prepared.action,
1523
+ data: prepared.data,
1524
+ expiresAt: Date.now() + 5 * 60 * 1000
1525
+ });
1526
+ return {
1527
+ pending_id: pendingId,
1528
+ preview: prepared.preview,
1529
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1530
+ };
1531
+ });
1532
+ }
1533
+ // === NEW HANDLERS - COMPOSITION EDITING ===
1534
+ case 'quanty_editar_insumo_composicao': {
1535
+ return executeWithLearning(`Editar insumo de composição`, async () => {
1536
+ const updates = {};
1537
+ if (args.codigo !== undefined)
1538
+ updates.code = args.codigo;
1539
+ if (args.descricao !== undefined)
1540
+ updates.description = args.descricao;
1541
+ if (args.unidade !== undefined)
1542
+ updates.unit = args.unidade;
1543
+ if (args.quantidade !== undefined)
1544
+ updates.quantity = args.quantidade;
1545
+ if (args.custo_unitario !== undefined)
1546
+ updates.unitCost = args.custo_unitario;
1547
+ if (args.tipo_item !== undefined)
1548
+ updates.itemType = args.tipo_item;
1549
+ const prepared = budgetService.prepareEditCompositionInput(args.orcamento_id, args.composicao_id, args.insumo_id, updates, args.descricao_insumo || 'Insumo');
1550
+ const pendingId = generatePendingId();
1551
+ pendingOperations.set(pendingId, {
1552
+ action: prepared.action,
1553
+ data: prepared.data,
1554
+ expiresAt: Date.now() + 5 * 60 * 1000
1555
+ });
1556
+ return {
1557
+ pending_id: pendingId,
1558
+ preview: prepared.preview,
1559
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1560
+ };
1561
+ });
1562
+ }
1563
+ case 'quanty_adicionar_insumo_composicao': {
1564
+ return executeWithLearning(`Adicionar insumo a composição`, async () => {
1565
+ const input = {
1566
+ code: args.codigo || '',
1567
+ description: args.descricao,
1568
+ unit: args.unidade,
1569
+ quantity: args.quantidade,
1570
+ unitCost: args.custo_unitario,
1571
+ itemType: args.tipo_item || 'INSUMO'
1572
+ };
1573
+ const prepared = budgetService.prepareAddCompositionInput(args.orcamento_id, args.composicao_id, input);
1574
+ const pendingId = generatePendingId();
1575
+ pendingOperations.set(pendingId, {
1576
+ action: prepared.action,
1577
+ data: prepared.data,
1578
+ expiresAt: Date.now() + 5 * 60 * 1000
1579
+ });
1580
+ return {
1581
+ pending_id: pendingId,
1582
+ preview: prepared.preview,
1583
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1584
+ };
1585
+ });
1586
+ }
1587
+ case 'quanty_remover_insumo_composicao': {
1588
+ return executeWithLearning(`Remover insumo de composição`, async () => {
1589
+ const prepared = budgetService.prepareRemoveCompositionInput(args.orcamento_id, args.composicao_id, args.insumo_id, args.descricao_insumo || 'Insumo');
1590
+ const pendingId = generatePendingId();
1591
+ pendingOperations.set(pendingId, {
1592
+ action: prepared.action,
1593
+ data: prepared.data,
1594
+ expiresAt: Date.now() + 5 * 60 * 1000
1595
+ });
1596
+ return {
1597
+ pending_id: pendingId,
1598
+ preview: prepared.preview,
1599
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1600
+ };
1601
+ });
1602
+ }
1603
+ case 'quanty_converter_para_composicao': {
1604
+ return executeWithLearning(`Converter para composição`, async () => {
1605
+ const prepared = budgetService.prepareConvertToComposition(args.orcamento_id, args.item_id, args.descricao_item || 'Item');
1606
+ const pendingId = generatePendingId();
1607
+ pendingOperations.set(pendingId, {
1608
+ action: prepared.action,
1609
+ data: prepared.data,
1610
+ expiresAt: Date.now() + 5 * 60 * 1000
1611
+ });
1612
+ return {
1613
+ pending_id: pendingId,
1614
+ preview: prepared.preview,
1615
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1616
+ };
1617
+ });
1618
+ }
1619
+ case 'quanty_converter_para_custo': {
1620
+ return executeWithLearning(`Converter para custo`, async () => {
1621
+ const prepared = budgetService.prepareConvertToCost(args.orcamento_id, args.item_id, args.descricao_item || 'Item');
1622
+ const pendingId = generatePendingId();
1623
+ pendingOperations.set(pendingId, {
1624
+ action: prepared.action,
1625
+ data: prepared.data,
1626
+ expiresAt: Date.now() + 5 * 60 * 1000
1627
+ });
1628
+ return {
1629
+ pending_id: pendingId,
1630
+ preview: prepared.preview,
1631
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1632
+ };
1633
+ });
1634
+ }
1635
+ // === NEW HANDLERS - BANK MANAGEMENT ===
1636
+ case 'quanty_criar_banco': {
1637
+ return executeWithLearning(`Criar banco privado`, async () => {
1638
+ const bankId = await bankService.createPrivateBank(auth, args.nome, args.descricao);
1639
+ return {
1640
+ sucesso: true,
1641
+ banco_id: bankId,
1642
+ mensagem: `Banco "${args.nome}" criado com sucesso!`
1643
+ };
1644
+ });
1645
+ }
1646
+ case 'quanty_editar_banco': {
1647
+ return executeWithLearning(`Editar banco`, async () => {
1648
+ const updates = {};
1649
+ if (args.titulo !== undefined)
1650
+ updates.title = args.titulo;
1651
+ if (args.descricao !== undefined)
1652
+ updates.description = args.descricao;
1653
+ if (args.imagem_capa !== undefined)
1654
+ updates.background_image = args.imagem_capa;
1655
+ if (args.data_versao !== undefined)
1656
+ updates.versionDate = args.data_versao;
1657
+ await bankService.editBank(auth, args.banco_id, updates);
1658
+ return {
1659
+ sucesso: true,
1660
+ mensagem: 'Banco atualizado com sucesso!'
1661
+ };
1662
+ });
1663
+ }
1664
+ case 'quanty_adicionar_ao_banco': {
1665
+ return executeWithLearning(`Adicionar item ao banco`, async () => {
1666
+ const item = {
1667
+ code: args.codigo || '',
1668
+ description: args.descricao,
1669
+ unit: args.unidade,
1670
+ unitCost: args.custo_unitario,
1671
+ itemType: args.tipo_item || 'Insumo',
1672
+ isComposition: args.eh_composicao || false,
1673
+ composition: args.insumos || []
1674
+ };
1675
+ const result = await bankService.addItemToBank(auth, args.banco_id, args.categoria_id, item);
1676
+ return {
1677
+ sucesso: true,
1678
+ mensagem: result
1679
+ };
1680
+ });
1681
+ }
1682
+ // === MATCHING HANDLERS ===
1683
+ case 'quanty_buscar_similares': {
1684
+ return executeWithLearning(`Buscar itens similares: ${args.descricao.substring(0, 50)}...`, async () => {
1685
+ const results = await matchingService.findSimilarItems(auth, args.descricao, {
1686
+ searchBudgets: args.buscar_orcamentos !== false,
1687
+ searchBanks: args.buscar_bancos !== false,
1688
+ excludeBudgetId: args.excluir_orcamento_id,
1689
+ bankIds: args.banco_ids,
1690
+ limit: args.limite || 10,
1691
+ minSimilarity: args.similaridade_minima || 50
1692
+ });
1693
+ return {
1694
+ descricao_buscada: args.descricao,
1695
+ total_orcamentos: results.budgetResults.length,
1696
+ total_bancos: results.bankResults.length,
1697
+ melhores_correspondencias: results.topMatches.map(m => ({
1698
+ codigo: m.code,
1699
+ descricao: m.description,
1700
+ unidade: m.unit,
1701
+ custo_unitario: `R$ ${m.unitCost.toFixed(2)}`,
1702
+ fonte: m.source === 'budget' ? 'Orçamento' : 'Banco',
1703
+ fonte_nome: m.sourceName,
1704
+ similaridade: `${m.similarity}%`,
1705
+ detalhes: {
1706
+ fuzzy: `${m.similarityDetails.fuzzy}%`,
1707
+ palavras_chave: `${m.similarityDetails.keyword}%`,
1708
+ tfidf: `${m.similarityDetails.tfidf}%`
1709
+ }
1710
+ })),
1711
+ _info: 'Similaridade combina análise fuzzy, palavras-chave e TF-IDF. Itens com >70% são boas correspondências.'
1712
+ };
1713
+ });
1714
+ }
1715
+ case 'quanty_comparar_precos': {
1716
+ return executeWithLearning(`Comparar preços: ${args.descricao.substring(0, 50)}...`, async () => {
1717
+ const comparison = await matchingService.comparePrices(auth, args.descricao, args.preco_atual, {
1718
+ excludeBudgetId: args.excluir_orcamento_id,
1719
+ limit: args.limite || 20,
1720
+ minSimilarity: args.similaridade_minima || 60
1721
+ });
1722
+ return {
1723
+ descricao: args.descricao,
1724
+ preco_atual: `R$ ${comparison.currentPrice.toFixed(2)}`,
1725
+ itens_comparados: comparison.matches.length,
1726
+ estatisticas: {
1727
+ preco_minimo: `R$ ${comparison.statistics.minPrice.toFixed(2)}`,
1728
+ preco_maximo: `R$ ${comparison.statistics.maxPrice.toFixed(2)}`,
1729
+ preco_medio: `R$ ${comparison.statistics.avgPrice.toFixed(2)}`,
1730
+ preco_mediano: `R$ ${comparison.statistics.medianPrice.toFixed(2)}`,
1731
+ recomendacao: comparison.statistics.recommendation
1732
+ },
1733
+ comparacoes: comparison.matches.slice(0, 10).map(m => ({
1734
+ descricao: m.description,
1735
+ unidade: m.unit,
1736
+ preco: `R$ ${m.unitCost.toFixed(2)}`,
1737
+ diferenca: `${m.priceDiffPercent >= 0 ? '+' : ''}${m.priceDiffPercent}%`,
1738
+ fonte: m.sourceName,
1739
+ similaridade: `${m.similarity}%`
1740
+ }))
1741
+ };
1742
+ });
1743
+ }
1744
+ // === TAGS (RÓTULOS) HANDLERS ===
1745
+ case 'quanty_adicionar_tag': {
1746
+ return executeWithLearning(`Adicionar tag ao orçamento`, async () => {
1747
+ const prepared = budgetService.prepareAddTag(args.orcamento_id, args.nome_tag);
1748
+ const pendingId = generatePendingId();
1749
+ pendingOperations.set(pendingId, {
1750
+ action: prepared.action,
1751
+ data: prepared.data,
1752
+ expiresAt: Date.now() + 5 * 60 * 1000
1753
+ });
1754
+ return {
1755
+ pending_id: pendingId,
1756
+ preview: prepared.preview,
1757
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1758
+ };
1759
+ });
1760
+ }
1761
+ case 'quanty_editar_tag': {
1762
+ return executeWithLearning(`Editar tag`, async () => {
1763
+ const prepared = budgetService.prepareEditTag(args.tag_id, args.novo_nome, args.nome_atual || 'Tag');
1764
+ const pendingId = generatePendingId();
1765
+ pendingOperations.set(pendingId, {
1766
+ action: prepared.action,
1767
+ data: prepared.data,
1768
+ expiresAt: Date.now() + 5 * 60 * 1000
1769
+ });
1770
+ return {
1771
+ pending_id: pendingId,
1772
+ preview: prepared.preview,
1773
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1774
+ };
1775
+ });
1776
+ }
1777
+ case 'quanty_remover_tag': {
1778
+ return executeWithLearning(`Remover tag`, async () => {
1779
+ const prepared = budgetService.prepareRemoveTag(args.tag_id, args.nome_tag || 'Tag');
1780
+ const pendingId = generatePendingId();
1781
+ pendingOperations.set(pendingId, {
1782
+ action: prepared.action,
1783
+ data: prepared.data,
1784
+ expiresAt: Date.now() + 5 * 60 * 1000
1785
+ });
1786
+ return {
1787
+ pending_id: pendingId,
1788
+ preview: prepared.preview,
1789
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1790
+ };
1791
+ });
1792
+ }
1793
+ // === PROJETOS HANDLERS ===
1794
+ case 'quanty_listar_projetos': {
1795
+ return executeWithLearning(`Listar projetos`, async () => {
1796
+ const projects = await budgetService.getProjects(auth);
1797
+ return {
1798
+ total: projects.length,
1799
+ projetos: projects.map(p => ({
1800
+ id: p.id,
1801
+ codigo: p.code || '',
1802
+ nome: p.name,
1803
+ descricao: p.description || '',
1804
+ criado_em: p.created_at
1805
+ })),
1806
+ _info: 'Use quanty_criar_projeto para criar um novo projeto.'
1807
+ };
1808
+ });
1809
+ }
1810
+ case 'quanty_criar_projeto': {
1811
+ return executeWithLearning(`Criar projeto: ${args.nome}`, async () => {
1812
+ const prepared = budgetService.prepareCreateProject(args.nome, args.descricao, args.codigo);
1813
+ const pendingId = generatePendingId();
1814
+ pendingOperations.set(pendingId, {
1815
+ action: prepared.action,
1816
+ data: prepared.data,
1817
+ expiresAt: Date.now() + 5 * 60 * 1000
1818
+ });
1819
+ return {
1820
+ pending_id: pendingId,
1821
+ preview: prepared.preview,
1822
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1823
+ };
1824
+ });
1825
+ }
1826
+ case 'quanty_editar_projeto': {
1827
+ return executeWithLearning(`Editar projeto`, async () => {
1828
+ const updates = {};
1829
+ if (args.nome !== undefined)
1830
+ updates.name = args.nome;
1831
+ if (args.descricao !== undefined)
1832
+ updates.description = args.descricao;
1833
+ if (args.codigo !== undefined)
1834
+ updates.code = args.codigo;
1835
+ const prepared = budgetService.prepareEditProject(args.projeto_id, updates, args.nome_atual || 'Projeto');
1836
+ const pendingId = generatePendingId();
1837
+ pendingOperations.set(pendingId, {
1838
+ action: prepared.action,
1839
+ data: prepared.data,
1840
+ expiresAt: Date.now() + 5 * 60 * 1000
1841
+ });
1842
+ return {
1843
+ pending_id: pendingId,
1844
+ preview: prepared.preview,
1845
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1846
+ };
1847
+ });
1848
+ }
1849
+ case 'quanty_excluir_projeto': {
1850
+ return executeWithLearning(`Excluir projeto`, async () => {
1851
+ const prepared = budgetService.prepareDeleteProject(args.projeto_id, args.nome_projeto || 'Projeto');
1852
+ const pendingId = generatePendingId();
1853
+ pendingOperations.set(pendingId, {
1854
+ action: prepared.action,
1855
+ data: prepared.data,
1856
+ expiresAt: Date.now() + 5 * 60 * 1000
1857
+ });
1858
+ return {
1859
+ pending_id: pendingId,
1860
+ preview: prepared.preview,
1861
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1862
+ };
1863
+ });
1864
+ }
1865
+ case 'quanty_vincular_orcamento_projeto': {
1866
+ return executeWithLearning(`Vincular orçamento a projeto`, async () => {
1867
+ const prepared = budgetService.prepareLinkBudgetToProject(args.orcamento_id, args.projeto_id || null, args.titulo_orcamento || 'Orçamento', args.nome_projeto || null);
1868
+ const pendingId = generatePendingId();
1869
+ pendingOperations.set(pendingId, {
1870
+ action: prepared.action,
1871
+ data: prepared.data,
1872
+ expiresAt: Date.now() + 5 * 60 * 1000
1873
+ });
1874
+ return {
1875
+ pending_id: pendingId,
1876
+ preview: prepared.preview,
1877
+ mensagem: 'Use quanty_executar com este pending_id para confirmar.'
1878
+ };
1879
+ });
1880
+ }
489
1881
  default:
490
1882
  return { erro: `Tool não reconhecida: ${name}` };
491
1883
  }
@@ -497,13 +1889,18 @@ async function main() {
497
1889
  try {
498
1890
  auth = await initAuth();
499
1891
  console.error(`[Quanty MCP] Authenticated as ${auth.userName} (${auth.userEmail})`);
1892
+ // Initialize Learning System
1893
+ initLearningSystem(auth);
1894
+ learningSystem = getLearningSystem();
1895
+ contextBuilder = getContextBuilder();
1896
+ console.error('[Quanty MCP] Learning System initialized');
500
1897
  }
501
1898
  catch (error) {
502
1899
  console.error('[Quanty MCP] Auth failed:', error);
503
1900
  process.exit(1);
504
1901
  }
505
1902
  // Create MCP server with resources capability
506
- const server = new Server({ name: 'quanty-mcp-server', version: '1.0.0' }, { capabilities: { tools: {}, resources: {} } });
1903
+ const server = new Server({ name: 'quanty-mcp-server', version: '2.0.0' }, { capabilities: { tools: {}, resources: {} } });
507
1904
  // List resources handler - Manual do Quanty
508
1905
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
509
1906
  resources: [
@@ -512,14 +1909,35 @@ async function main() {
512
1909
  name: 'Manual do Quanty',
513
1910
  description: 'Documentação completa sobre orçamentação de engenharia, conceitos, terminologia e dicas de uso.',
514
1911
  mimeType: 'text/plain'
1912
+ },
1913
+ {
1914
+ uri: 'quanty://learning-stats',
1915
+ name: 'Estatísticas de Aprendizado',
1916
+ description: 'Métricas sobre o aprendizado do sistema, padrões identificados e eficiência.',
1917
+ mimeType: 'application/json'
515
1918
  }
516
1919
  ]
517
1920
  }));
518
1921
  // Read resource handler
519
1922
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
520
1923
  if (request.params.uri === 'quanty://manual') {
1924
+ let content = QUANTY_MANUAL;
1925
+ if (contextBuilder) {
1926
+ try {
1927
+ content = await contextBuilder.generateSystemPrompt(content);
1928
+ }
1929
+ catch (err) {
1930
+ console.error('Failed to enhance manual:', err);
1931
+ }
1932
+ }
1933
+ return {
1934
+ contents: [{ uri: 'quanty://manual', mimeType: 'text/plain', text: content }]
1935
+ };
1936
+ }
1937
+ if (request.params.uri === 'quanty://learning-stats') {
1938
+ const stats = learningSystem ? await learningSystem.getStatistics() : {};
521
1939
  return {
522
- contents: [{ uri: 'quanty://manual', mimeType: 'text/plain', text: QUANTY_MANUAL }]
1940
+ contents: [{ uri: 'quanty://learning-stats', mimeType: 'application/json', text: JSON.stringify(stats, null, 2) }]
523
1941
  };
524
1942
  }
525
1943
  throw new Error(`Resource not found: ${request.params.uri}`);