/** * ORQUESTRADOR INTELIGENTE (Versão Multi-MCP) * * Agora suporta múltiplos servidores MCP! * * O que mudou? * - Antes: Um único cliente MCP * - Agora: Gerenciador com múltiplos MCPs * - Seleção automática do MCP baseado em idioma/domínio */ import { buscarContexto, type ResultadoBuscaComContexto } from './rag'; import { gerenciadorMCP } from './mcp-manager'; // import { resolve } from 'bun'; /** * Enum para definir a estratégia de busca * * Por que usar enum? * - Tipagem forte (TypeScript nos avisa de erros) * - Autodocumentação (fica claro quais são as opções) * - Fácil de estender no futuro */ enum EstrategiaBusca { APENAS_RAG = 'apenas_rag', APENAS_MCP = 'apenas_mcp', HIBRIDO = 'hibrido', DIRETO = 'direto' // Sem contexto, chat direto } /** * Interface para o resultado da orquestração */ interface ResultadoOrquestracao { estrategia: EstrategiaBusca; contextoRAG?: string; contextoMCP?: string; documentosRAG?: Array<{ nome: string; similaridade: number }>; ferramentasMCP?: string[]; mcpUtilizado?: string; // NOVO: qual MCP foi usado promptFinal: string; } /** * BUSCADOR MCP (Versão Multi-MCP) * * Agora detecta automaticamente qual MCP usar! */ async function buscarDadosMCP(mensagem: string): Promise<{ conteudo: string; mcpId: string }> { const mensagemLower = mensagem.toLowerCase(); // Passo 1: Selecionar qual MCP usar const mcpId = gerenciadorMCP.selecionarMCP(mensagem); if (!mcpId) { console.warn('[ORQUESTRADOR] Nenhum MCP disponível'); return { conteudo: 'Sistema de dados de exercícios temporariamente indisponível.', mcpId: 'nenhum' }; } console.log(`[ORQUESTRADOR] Usando MCP: ${mcpId}`); try { // Passo 2: Decidir qual ferramenta chamar // As ferramentas podem ter nomes diferentes entre MCPs (pt vs en) const ferramentas = getFerramentaMapping(mcpId); // Estratégia 1: Busca por nome de exercício const nomesExercicios = ['supino', 'agachamento', 'rosca', 'levantamento', 'squat', 'bench']; for (const nome of nomesExercicios) { if (mensagemLower.includes(nome)) { try { const resultado = await gerenciadorMCP.chamarFerramenta( mcpId, ferramentas.buscarPorNome, { [ferramentas.paramNome]: nome } ); if (!resultado.includes('Nenhum') && !resultado.includes('No exercise')) { return { conteudo: resultado, mcpId }; } } catch (erro) { console.error('[ORQUESTRADOR] Erro ao buscar por nome:', erro); } } } // Estratégia 2: Busca por grupo muscular const grupos = detectarGrupoMuscular(mensagemLower); if (grupos.length > 0) { try { const resultado = await gerenciadorMCP.chamarFerramenta( mcpId, ferramentas.buscarPorGrupo, { [ferramentas.paramGrupo]: grupos[0] } ); return { conteudo: resultado, mcpId }; } catch (erro) { console.error('[ORQUESTRADOR] Erro ao buscar por grupo:', erro); } } // Estratégia 3: Listar todos const resultado = await gerenciadorMCP.chamarFerramenta( mcpId, ferramentas.listarTodos, {} ); return { conteudo: resultado, mcpId }; } catch (erro) { console.error('[ORQUESTRADOR] Erro ao buscar dados MCP:', erro); return { conteudo: 'Não foi possível acessar os dados de exercícios.', mcpId }; } } /** * Mapeia nomes de ferramentas para cada MCP * * Por que criar esse mapeamento? * - MCPs diferentes podem ter nomes diferentes * - v1 usa português, v2 usa inglês * - Centraliza a lógica de compatibilidade */ function getFerramentaMapping(mcpId: string): any { if (mcpId === 'academia-v1') { return { buscarPorNome: 'buscar_exercicio_por_nome', buscarPorGrupo: 'buscar_exercicios_por_grupo', listarTodos: 'listar_todos_exercicios', paramNome: 'nome', paramGrupo: 'grupo_muscular' }; } if (mcpId === 'academia-v2') { return { buscarPorNome: 'buscar_exercicio_por_nome', // mesmo nome buscarPorGrupo: 'search_exercises_by_group', listarTodos: 'list_all_exercises', paramNome: 'nome', paramGrupo: 'muscle_group' }; } // Padrão (v1) return { buscarPorNome: 'buscar_exercicio_por_nome', buscarPorGrupo: 'buscar_exercicios_por_grupo', listarTodos: 'listar_todos_exercicios', paramNome: 'nome', paramGrupo: 'grupo_muscular' }; } /** * Detecta grupos musculares na mensagem */ function detectarGrupoMuscular(mensagem: string): string[] { const grupos = [ { palavras: ['perna', 'pernas', 'leg', 'legs'], pt: 'Pernas', en: 'Legs' }, { palavras: ['peito', 'peitoral', 'chest'], pt: 'Peito (peitoral)', en: 'Chest' }, { palavras: ['costas', 'dorsal', 'back'], pt: 'Costas (dorsais, lombar)', en: 'Back' }, { palavras: ['ombro', 'ombros', 'shoulder'], pt: 'Ombros (deltoides)', en: 'Shoulders' }, { palavras: ['braço', 'braços', 'arm', 'arms'], pt: 'Braços (Bíceps, Tríceps, Antebraço)', en: 'Arms' } ]; const encontrados = []; for (const grupo of grupos) { if (grupo.palavras.some(p => mensagem.includes(p))) { // Retorna ambas as versões encontrados.push(grupo.pt, grupo.en); } } return encontrados; } function detectarPorSimilaridade(mensagem: string, exemplos: string[]): boolean { // Simples: verifica se alguma palavra da mensagem está em exemplos const palavrasMensagem = mensagem.toLowerCase().split(' '); return exemplos.some(ex => palavrasMensagem.some(p => ex.includes(p))); } /** * ANALISADOR DE INTENÇÃO * * Esta função analisa a pergunta do usuário e decide qual estratégia usar * * Como funciona? * 1. Procura por palavras-chave que indicam dados estruturados (MCP) * 2. Sempre tenta RAG para contexto documental * 3. Decide se usa ambos baseado na relevância * * Por que essa abordagem? * - Simples e eficaz (KISS) * - Pode evoluir para usar o próprio LLM para decidir * - Transparente para debug */ function analisarIntencao(mensagem: string): EstrategiaBusca { const mensagemLower = mensagem.toLowerCase(); console.log(`[ANALISADOR] Analisando: "${mensagem}"`); // Palavras que indicam necessidade de dados estruturados (MCP) const palavrasChaveMCP = [ 'meu', 'meus', 'minhas', 'minha', 'my', 'mine', 'cadastrado', 'cadastrados', 'registered', 'tenho', 'possuo', 'have', 'listar', 'liste', 'list', 'mostrar', 'mostre', 'show', // Novos: adicionar sinônimos para mais flexibilidade 'quais', 'qual', 'que', 'existe', 'disponível', 'disponíveis', 'exercícios', 'exercicio', 'treino', 'treinos', 'rotina' ]; // Palavras que indicam necessidade de conhecimento/técnica (RAG) const palavrasChaveRAG = [ 'como fazer', 'como executar', 'how to', 'o que é', 'what is', 'explique', 'explain', 'técnica', 'technique', 'forma correta', 'correct form', // Novos: adicionar variações 'dica', 'dicas', 'passo', 'passos', 'guia', 'tutorial', 'benefício', 'benefícios', 'vantagem', 'vantagens' ]; // Verifica indicadores MCP const indicadoresMCP = palavrasChaveMCP.filter(palavra => mensagemLower.includes(palavra) ); // Verifica indicadores RAG const indicadoresRAG = palavrasChaveRAG.filter(palavra => mensagemLower.includes(palavra) ); console.log(`[ANALISADOR] Indicadores MCP encontrados: [${indicadoresMCP.join(', ')}]`); console.log(`[ANALISADOR] Indicadores RAG encontrados: [${indicadoresRAG.join(', ')}]`); /* Este trecho de código visa tentar dar outra opção para a analise // Novo: exemplos de frases para detectar intenção const exemplosMCP = [ 'liste meus exercícios', 'quais treinos tenho', 'mostre dados cadastrados' ]; const exemplosRAG = [ 'como fazer supino', 'explique técnica de agachamento', 'dicas para musculação' ]; function detectarPorSimilaridade(mensagem: string, exemplos: string[]): boolean { // Simples: verifica se alguma palavra da mensagem está em exemplos const palavrasMensagem = mensagem.toLowerCase().split(' '); return exemplos.some(ex => palavrasMensagem.some(p => ex.includes(p))); } */ // Novo: exemplos de frases para detectar intenção const exemplosMCP = [ 'liste meus exercícios', 'quais treinos tenho', 'mostre dados cadastrados' ]; const exemplosRAG = [ 'como fazer supino', 'explique técnica de agachamento', 'dicas para musculação' ]; // const temIndicadorMCP = indicadoresMCP.length > 0; // const temIndicadorRAG = indicadoresRAG.length > 0; const temIndicadorMCP = indicadoresMCP.length > 0 || detectarPorSimilaridade(mensagemLower, exemplosMCP); const temIndicadorRAG = indicadoresRAG.length > 0 || detectarPorSimilaridade(mensagemLower, exemplosRAG); // Lógica de decisão if (temIndicadorMCP && temIndicadorRAG) { console.log('[ANALISADOR] Decisão: HÍBRIDO (ambos indicadores presentes)'); return EstrategiaBusca.HIBRIDO; } if (temIndicadorMCP) { console.log('[ANALISADOR] Decisão: APENAS MCP (dados estruturados)'); return EstrategiaBusca.APENAS_MCP; } if (temIndicadorRAG) { console.log('[ANALISADOR] Decisão: APENAS RAG (conhecimento/técnica)'); return EstrategiaBusca.APENAS_RAG; } // Padrão: Sem indicador console.log('[ANALISADOR] Decisão: sem parametrização'); return EstrategiaBusca.DIRETO; } /** * ORQUESTRADOR PRINCIPAL (Versão Multi-MCP) */ async function orquestrar(mensagem: string): Promise { console.log('\n[ORQUESTRADOR] Analisando pergunta...'); const estrategia = analisarIntencao(mensagem); console.log(`[ORQUESTRADOR] Estratégia: ${estrategia}`); let contextoRAG = ''; let contextoMCP = ''; let documentosRAG: Array<{ nome: string; similaridade: number }> = []; let ferramentasMCP: string[] = []; let mcpUtilizado: string | undefined; switch (estrategia) { case EstrategiaBusca.APENAS_RAG: const resultadoRAG = await buscarContexto(mensagem, 3, 0.3); if (resultadoRAG.resultados.length > 0) { contextoRAG = resultadoRAG.contexto; documentosRAG = resultadoRAG.resultados.map(doc => ({ nome: doc.nome, similaridade: doc.similaridade })); } break; case EstrategiaBusca.APENAS_MCP: const resultadoMCP = await buscarDadosMCP(mensagem); contextoMCP = resultadoMCP.conteudo; mcpUtilizado = resultadoMCP.mcpId; ferramentasMCP.push(mcpUtilizado); break; case EstrategiaBusca.HIBRIDO: const resultadoHibridoRAG = await buscarContexto(mensagem, 2, 0.3); if (resultadoHibridoRAG.resultados.length > 0) { contextoRAG = resultadoHibridoRAG.contexto; documentosRAG = resultadoHibridoRAG.resultados.map(doc => ({ nome: doc.nome, similaridade: doc.similaridade })); } const resultadoHibridoMCP = await buscarDadosMCP(mensagem); contextoMCP = resultadoHibridoMCP.conteudo; mcpUtilizado = resultadoHibridoMCP.mcpId; ferramentasMCP.push(mcpUtilizado); break; } const promptFinal = montarPromptFinal(mensagem, contextoRAG, contextoMCP); return { estrategia, contextoRAG: contextoRAG || undefined, contextoMCP: contextoMCP || undefined, documentosRAG, ferramentasMCP, mcpUtilizado, promptFinal }; } /** * Monta o prompt final para a LLM * * Por que essa função separada? * - Facilita testar diferentes formatos de prompt * - Mantém a lógica de prompt isolada * - Fácil de ajustar sem mexer na orquestração */ function montarPromptFinal( mensagem: string, contextoRAG: string, contextoMCP: string ): string { let prompt = 'Você é um assistente especializado em saúde, fitness e bem-estar.\n\n'; // Adiciona contexto RAG se existir if (contextoRAG) { prompt += contextoRAG + '\n'; } // Adiciona contexto MCP se existir if (contextoMCP) { prompt += 'DADOS ESTRUTURADOS:\n\n' + contextoMCP + '\n\n'; } // Instruções para a LLM prompt += 'INSTRUÇÕES:\n'; prompt += '- Use as informações fornecidas\n'; prompt += '- Seja claro e objetivo\n'; prompt += '- Responda no idioma da pergunta\n\n'; prompt += `PERGUNTA: ${mensagem}\n\nRESPOSTA:`; return prompt; } export { orquestrar, EstrategiaBusca, type ResultadoOrquestracao };