orquestrador.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. /**
  2. * ORQUESTRADOR INTELIGENTE (Versão Multi-MCP)
  3. *
  4. * Agora suporta múltiplos servidores MCP!
  5. *
  6. * O que mudou?
  7. * - Antes: Um único cliente MCP
  8. * - Agora: Gerenciador com múltiplos MCPs
  9. * - Seleção automática do MCP baseado em idioma/domínio
  10. */
  11. import { buscarContexto, type ResultadoBuscaComContexto } from './rag';
  12. import { gerenciadorMCP } from './mcp-manager';
  13. // import { resolve } from 'bun';
  14. /**
  15. * Enum para definir a estratégia de busca
  16. *
  17. * Por que usar enum?
  18. * - Tipagem forte (TypeScript nos avisa de erros)
  19. * - Autodocumentação (fica claro quais são as opções)
  20. * - Fácil de estender no futuro
  21. */
  22. enum EstrategiaBusca {
  23. APENAS_RAG = 'apenas_rag',
  24. APENAS_MCP = 'apenas_mcp',
  25. HIBRIDO = 'hibrido',
  26. DIRETO = 'direto' // Sem contexto, chat direto
  27. }
  28. /**
  29. * Interface para o resultado da orquestração
  30. */
  31. interface ResultadoOrquestracao {
  32. estrategia: EstrategiaBusca;
  33. contextoRAG?: string;
  34. contextoMCP?: string;
  35. documentosRAG?: Array<{ nome: string; similaridade: number }>;
  36. ferramentasMCP?: string[];
  37. mcpUtilizado?: string; // NOVO: qual MCP foi usado
  38. promptFinal: string;
  39. }
  40. /**
  41. * BUSCADOR MCP (Versão Multi-MCP)
  42. *
  43. * Agora detecta automaticamente qual MCP usar!
  44. */
  45. async function buscarDadosMCP(mensagem: string): Promise<{ conteudo: string; mcpId: string }> {
  46. const mensagemLower = mensagem.toLowerCase();
  47. // Passo 1: Selecionar qual MCP usar
  48. const mcpId = gerenciadorMCP.selecionarMCP(mensagem);
  49. if (!mcpId) {
  50. console.warn('[ORQUESTRADOR] Nenhum MCP disponível');
  51. return {
  52. conteudo: 'Sistema de dados de exercícios temporariamente indisponível.',
  53. mcpId: 'nenhum'
  54. };
  55. }
  56. console.log(`[ORQUESTRADOR] Usando MCP: ${mcpId}`);
  57. try {
  58. // Passo 2: Decidir qual ferramenta chamar
  59. // As ferramentas podem ter nomes diferentes entre MCPs (pt vs en)
  60. const ferramentas = getFerramentaMapping(mcpId);
  61. // Estratégia 1: Busca por nome de exercício
  62. const nomesExercicios = ['supino', 'agachamento', 'rosca', 'levantamento', 'squat', 'bench'];
  63. for (const nome of nomesExercicios) {
  64. if (mensagemLower.includes(nome)) {
  65. try {
  66. const resultado = await gerenciadorMCP.chamarFerramenta(
  67. mcpId,
  68. ferramentas.buscarPorNome,
  69. { [ferramentas.paramNome]: nome }
  70. );
  71. if (!resultado.includes('Nenhum') && !resultado.includes('No exercise')) {
  72. return { conteudo: resultado, mcpId };
  73. }
  74. } catch (erro) {
  75. console.error('[ORQUESTRADOR] Erro ao buscar por nome:', erro);
  76. }
  77. }
  78. }
  79. // Estratégia 2: Busca por grupo muscular
  80. const grupos = detectarGrupoMuscular(mensagemLower);
  81. if (grupos.length > 0) {
  82. try {
  83. const resultado = await gerenciadorMCP.chamarFerramenta(
  84. mcpId,
  85. ferramentas.buscarPorGrupo,
  86. { [ferramentas.paramGrupo]: grupos[0] }
  87. );
  88. return { conteudo: resultado, mcpId };
  89. } catch (erro) {
  90. console.error('[ORQUESTRADOR] Erro ao buscar por grupo:', erro);
  91. }
  92. }
  93. // Estratégia 3: Listar todos
  94. const resultado = await gerenciadorMCP.chamarFerramenta(
  95. mcpId,
  96. ferramentas.listarTodos,
  97. {}
  98. );
  99. return { conteudo: resultado, mcpId };
  100. } catch (erro) {
  101. console.error('[ORQUESTRADOR] Erro ao buscar dados MCP:', erro);
  102. return {
  103. conteudo: 'Não foi possível acessar os dados de exercícios.',
  104. mcpId
  105. };
  106. }
  107. }
  108. /**
  109. * Mapeia nomes de ferramentas para cada MCP
  110. *
  111. * Por que criar esse mapeamento?
  112. * - MCPs diferentes podem ter nomes diferentes
  113. * - v1 usa português, v2 usa inglês
  114. * - Centraliza a lógica de compatibilidade
  115. */
  116. function getFerramentaMapping(mcpId: string): any {
  117. if (mcpId === 'academia-v1') {
  118. return {
  119. buscarPorNome: 'buscar_exercicio_por_nome',
  120. buscarPorGrupo: 'buscar_exercicios_por_grupo',
  121. listarTodos: 'listar_todos_exercicios',
  122. paramNome: 'nome',
  123. paramGrupo: 'grupo_muscular'
  124. };
  125. }
  126. if (mcpId === 'academia-v2') {
  127. return {
  128. buscarPorNome: 'buscar_exercicio_por_nome', // mesmo nome
  129. buscarPorGrupo: 'search_exercises_by_group',
  130. listarTodos: 'list_all_exercises',
  131. paramNome: 'nome',
  132. paramGrupo: 'muscle_group'
  133. };
  134. }
  135. // Padrão (v1)
  136. return {
  137. buscarPorNome: 'buscar_exercicio_por_nome',
  138. buscarPorGrupo: 'buscar_exercicios_por_grupo',
  139. listarTodos: 'listar_todos_exercicios',
  140. paramNome: 'nome',
  141. paramGrupo: 'grupo_muscular'
  142. };
  143. }
  144. /**
  145. * Detecta grupos musculares na mensagem
  146. */
  147. function detectarGrupoMuscular(mensagem: string): string[] {
  148. const grupos = [
  149. { palavras: ['perna', 'pernas', 'leg', 'legs'], pt: 'Pernas', en: 'Legs' },
  150. { palavras: ['peito', 'peitoral', 'chest'], pt: 'Peito (peitoral)', en: 'Chest' },
  151. { palavras: ['costas', 'dorsal', 'back'], pt: 'Costas (dorsais, lombar)', en: 'Back' },
  152. { palavras: ['ombro', 'ombros', 'shoulder'], pt: 'Ombros (deltoides)', en: 'Shoulders' },
  153. { palavras: ['braço', 'braços', 'arm', 'arms'], pt: 'Braços (Bíceps, Tríceps, Antebraço)', en: 'Arms' }
  154. ];
  155. const encontrados = [];
  156. for (const grupo of grupos) {
  157. if (grupo.palavras.some(p => mensagem.includes(p))) {
  158. // Retorna ambas as versões
  159. encontrados.push(grupo.pt, grupo.en);
  160. }
  161. }
  162. return encontrados;
  163. }
  164. function detectarPorSimilaridade(mensagem: string, exemplos: string[]): boolean {
  165. // Simples: verifica se alguma palavra da mensagem está em exemplos
  166. const palavrasMensagem = mensagem.toLowerCase().split(' ');
  167. return exemplos.some(ex => palavrasMensagem.some(p => ex.includes(p)));
  168. }
  169. /**
  170. * ANALISADOR DE INTENÇÃO
  171. *
  172. * Esta função analisa a pergunta do usuário e decide qual estratégia usar
  173. *
  174. * Como funciona?
  175. * 1. Procura por palavras-chave que indicam dados estruturados (MCP)
  176. * 2. Sempre tenta RAG para contexto documental
  177. * 3. Decide se usa ambos baseado na relevância
  178. *
  179. * Por que essa abordagem?
  180. * - Simples e eficaz (KISS)
  181. * - Pode evoluir para usar o próprio LLM para decidir
  182. * - Transparente para debug
  183. */
  184. function analisarIntencao(mensagem: string): EstrategiaBusca {
  185. const mensagemLower = mensagem.toLowerCase();
  186. console.log(`[ANALISADOR] Analisando: "${mensagem}"`);
  187. // Palavras que indicam necessidade de dados estruturados (MCP)
  188. const palavrasChaveMCP = [
  189. 'meu', 'meus', 'minhas', 'minha',
  190. 'my', 'mine',
  191. 'cadastrado', 'cadastrados', 'registered',
  192. 'tenho', 'possuo', 'have',
  193. 'listar', 'liste', 'list', 'mostrar', 'mostre', 'show',
  194. // Novos: adicionar sinônimos para mais flexibilidade
  195. 'quais', 'qual', 'que', 'existe', 'disponível', 'disponíveis',
  196. 'exercícios', 'exercicio', 'treino', 'treinos', 'rotina'
  197. ];
  198. // Palavras que indicam necessidade de conhecimento/técnica (RAG)
  199. const palavrasChaveRAG = [
  200. 'como fazer', 'como executar', 'how to',
  201. 'o que é', 'what is',
  202. 'explique', 'explain',
  203. 'técnica', 'technique',
  204. 'forma correta', 'correct form',
  205. // Novos: adicionar variações
  206. 'dica', 'dicas', 'passo', 'passos', 'guia', 'tutorial',
  207. 'benefício', 'benefícios', 'vantagem', 'vantagens'
  208. ];
  209. // Verifica indicadores MCP
  210. const indicadoresMCP = palavrasChaveMCP.filter(palavra =>
  211. mensagemLower.includes(palavra)
  212. );
  213. // Verifica indicadores RAG
  214. const indicadoresRAG = palavrasChaveRAG.filter(palavra =>
  215. mensagemLower.includes(palavra)
  216. );
  217. console.log(`[ANALISADOR] Indicadores MCP encontrados: [${indicadoresMCP.join(', ')}]`);
  218. console.log(`[ANALISADOR] Indicadores RAG encontrados: [${indicadoresRAG.join(', ')}]`);
  219. /*
  220. Este trecho de código visa
  221. tentar dar outra opção para a analise
  222. // Novo: exemplos de frases para detectar intenção
  223. const exemplosMCP = [
  224. 'liste meus exercícios',
  225. 'quais treinos tenho',
  226. 'mostre dados cadastrados'
  227. ];
  228. const exemplosRAG = [
  229. 'como fazer supino',
  230. 'explique técnica de agachamento',
  231. 'dicas para musculação'
  232. ];
  233. function detectarPorSimilaridade(mensagem: string, exemplos: string[]): boolean {
  234. // Simples: verifica se alguma palavra da mensagem está em exemplos
  235. const palavrasMensagem = mensagem.toLowerCase().split(' ');
  236. return exemplos.some(ex => palavrasMensagem.some(p => ex.includes(p)));
  237. }
  238. */
  239. // Novo: exemplos de frases para detectar intenção
  240. const exemplosMCP = [
  241. 'liste meus exercícios',
  242. 'quais treinos tenho',
  243. 'mostre dados cadastrados'
  244. ];
  245. const exemplosRAG = [
  246. 'como fazer supino',
  247. 'explique técnica de agachamento',
  248. 'dicas para musculação'
  249. ];
  250. // const temIndicadorMCP = indicadoresMCP.length > 0;
  251. // const temIndicadorRAG = indicadoresRAG.length > 0;
  252. const temIndicadorMCP = indicadoresMCP.length > 0 || detectarPorSimilaridade(mensagemLower, exemplosMCP);
  253. const temIndicadorRAG = indicadoresRAG.length > 0 || detectarPorSimilaridade(mensagemLower, exemplosRAG);
  254. // Lógica de decisão
  255. if (temIndicadorMCP && temIndicadorRAG) {
  256. console.log('[ANALISADOR] Decisão: HÍBRIDO (ambos indicadores presentes)');
  257. return EstrategiaBusca.HIBRIDO;
  258. }
  259. if (temIndicadorMCP) {
  260. console.log('[ANALISADOR] Decisão: APENAS MCP (dados estruturados)');
  261. return EstrategiaBusca.APENAS_MCP;
  262. }
  263. if (temIndicadorRAG) {
  264. console.log('[ANALISADOR] Decisão: APENAS RAG (conhecimento/técnica)');
  265. return EstrategiaBusca.APENAS_RAG;
  266. }
  267. // Padrão: Sem indicador
  268. console.log('[ANALISADOR] Decisão: sem parametrização');
  269. return EstrategiaBusca.DIRETO;
  270. }
  271. /**
  272. * ORQUESTRADOR PRINCIPAL (Versão Multi-MCP)
  273. */
  274. async function orquestrar(mensagem: string): Promise<ResultadoOrquestracao> {
  275. console.log('\n[ORQUESTRADOR] Analisando pergunta...');
  276. const estrategia = analisarIntencao(mensagem);
  277. console.log(`[ORQUESTRADOR] Estratégia: ${estrategia}`);
  278. let contextoRAG = '';
  279. let contextoMCP = '';
  280. let documentosRAG: Array<{ nome: string; similaridade: number }> = [];
  281. let ferramentasMCP: string[] = [];
  282. let mcpUtilizado: string | undefined;
  283. switch (estrategia) {
  284. case EstrategiaBusca.APENAS_RAG:
  285. const resultadoRAG = await buscarContexto(mensagem, 3, 0.3);
  286. if (resultadoRAG.resultados.length > 0) {
  287. contextoRAG = resultadoRAG.contexto;
  288. documentosRAG = resultadoRAG.resultados.map(doc => ({ nome: doc.nome, similaridade: doc.similaridade }));
  289. }
  290. break;
  291. case EstrategiaBusca.APENAS_MCP:
  292. const resultadoMCP = await buscarDadosMCP(mensagem);
  293. contextoMCP = resultadoMCP.conteudo;
  294. mcpUtilizado = resultadoMCP.mcpId;
  295. ferramentasMCP.push(mcpUtilizado);
  296. break;
  297. case EstrategiaBusca.HIBRIDO:
  298. const resultadoHibridoRAG = await buscarContexto(mensagem, 2, 0.3);
  299. if (resultadoHibridoRAG.resultados.length > 0) {
  300. contextoRAG = resultadoHibridoRAG.contexto;
  301. documentosRAG = resultadoHibridoRAG.resultados.map(doc => ({ nome: doc.nome, similaridade: doc.similaridade }));
  302. }
  303. const resultadoHibridoMCP = await buscarDadosMCP(mensagem);
  304. contextoMCP = resultadoHibridoMCP.conteudo;
  305. mcpUtilizado = resultadoHibridoMCP.mcpId;
  306. ferramentasMCP.push(mcpUtilizado);
  307. break;
  308. }
  309. const promptFinal = montarPromptFinal(mensagem, contextoRAG, contextoMCP);
  310. return {
  311. estrategia,
  312. contextoRAG: contextoRAG || undefined,
  313. contextoMCP: contextoMCP || undefined,
  314. documentosRAG,
  315. ferramentasMCP,
  316. mcpUtilizado,
  317. promptFinal
  318. };
  319. }
  320. /**
  321. * Monta o prompt final para a LLM
  322. *
  323. * Por que essa função separada?
  324. * - Facilita testar diferentes formatos de prompt
  325. * - Mantém a lógica de prompt isolada
  326. * - Fácil de ajustar sem mexer na orquestração
  327. */
  328. function montarPromptFinal(
  329. mensagem: string,
  330. contextoRAG: string,
  331. contextoMCP: string
  332. ): string {
  333. let prompt = 'Você é um assistente especializado em saúde, fitness e bem-estar.\n\n';
  334. // Adiciona contexto RAG se existir
  335. if (contextoRAG) {
  336. prompt += contextoRAG + '\n';
  337. }
  338. // Adiciona contexto MCP se existir
  339. if (contextoMCP) {
  340. prompt += 'DADOS ESTRUTURADOS:\n\n' + contextoMCP + '\n\n';
  341. }
  342. // Instruções para a LLM
  343. prompt += 'INSTRUÇÕES:\n';
  344. prompt += '- Use as informações fornecidas\n';
  345. prompt += '- Seja claro e objetivo\n';
  346. prompt += '- Responda no idioma da pergunta\n\n';
  347. prompt += `PERGUNTA: ${mensagem}\n\nRESPOSTA:`;
  348. return prompt;
  349. }
  350. export {
  351. orquestrar,
  352. EstrategiaBusca,
  353. type ResultadoOrquestracao
  354. };