@oliverlessa/gemini-agent-lib
Version:
Uma biblioteca NodeJS para criar agentes de IA com Gemini LLM
519 lines (446 loc) • 28.7 kB
JavaScript
// hierarchical-agent-thinking-orchestrator.js
const debug = require('./debug').create('gemini-agent-lib:orchestrator:thinking');
const HierarchicalAgentOrchestrator = require('./hierarchical-agent-orchestrator');
const GenerativeAILLM = require('./generative-ai-llm');
/**
* @class HierarchicalAgentThinkingOrchestrator
* @description Orquestrador hierárquico que utiliza o modelo "thinking" para coordenar múltiplos agentes especialistas.
* Esta classe estende HierarchicalAgentOrchestrator e é configurada especificamente para trabalhar
* com o modelo gemini-2.5-pro-preview-03-25, que possui características específicas de resposta
* com raciocínio passo a passo, permitindo extrair tanto o processo de pensamento quanto a resposta final.
* @extends HierarchicalAgentOrchestrator
*/
class HierarchicalAgentThinkingOrchestrator extends HierarchicalAgentOrchestrator {
/**
* Cria uma instância do orquestrador hierárquico de agentes com modelo thinking.
*
* @param {Array} agents - Array de objetos de agentes especialistas. Cada agente deve ter propriedades 'role' e 'objective'.
* @param {Object} config - Objeto de configuração para o orquestrador thinking
* @param {string} config.apiKey - Chave de API para acessar o modelo Gemini
* @param {boolean} [config.useVertexAI=true] - Define se deve usar a Vertex AI (true) ou a API Gemini direta (false)
* @param {Object} [config.vertexConfig={}] - Configurações específicas para Vertex AI
* @param {string} [config.vertexConfig.credentialsPath] - Caminho para o arquivo de credenciais da Vertex AI
* @param {string} [config.vertexConfig.projectId] - ID do projeto na Google Cloud
* @param {string} [config.vertexConfig.location='us-central1'] - Região da Vertex AI
* @param {boolean} [config.includeThinkingSteps=false] - Define se o resultado final deve incluir os passos de raciocínio do LLM. Padrão: false (retorna apenas a resposta final).
* @throws {Error} Se o array de agentes estiver vazio ou não for um array (herdado da classe pai)
*/
constructor(agents, {
apiKey,
useVertexAI = true,
vertexConfig = {}, // Para configurações específicas do Vertex AI, se necessário
includeThinkingSteps = false // Nova opção de configuração
}) {
// Criar a instância LLM específica para o modelo thinking
let thinkingLLM;
if (useVertexAI) {
const VertexAILLM = require('./vertex-ai-llm');
thinkingLLM = new VertexAILLM({
apiKey: apiKey,
credentialsPath: vertexConfig.credentialsPath,
projectId: vertexConfig.projectId,
location: vertexConfig.location || 'us-central1',
modelName: "gemini-2.5-pro-preview-03-25",
mode: "oneshot"
});
} else {
thinkingLLM = new GenerativeAILLM({
apiKey: apiKey,
modelName: "gemini-2.5-pro-preview-03-25",
mode: "oneshot"
});
}
// Chamar o construtor da classe pai
super(agents, thinkingLLM);
// Propriedades específicas
this.isThinkingModel = true;
this.includeThinkingSteps = includeThinkingSteps; // Armazena a opção
}
/**
* Orquestra a execução de uma tarefa utilizando agentes especialistas com modelo thinking.
*
* O processo de orquestração segue três etapas principais, semelhantes à classe pai, mas com
* diferenças no processamento das respostas:
* 1. Determinar quais agentes especialistas são mais adequados para a tarefa usando o modelo thinking
* 2. Acionar cada agente selecionado e coletar suas respostas
* 3. Sintetizar as respostas dos especialistas em uma resposta final que inclui o raciocínio passo a passo
*
* A principal diferença em relação à classe pai é que este método, por padrão, processa a resposta
* bruta do modelo thinking para retornar apenas a resposta final. Opcionalmente, pode retornar
* a resposta bruta completa, incluindo o raciocínio, se configurado.
*
* @param {string} task - A descrição da tarefa a ser executada
* @returns {Promise<string>} Uma promessa que resolve para a resposta final (por padrão) ou a resposta bruta completa (se `includeThinkingSteps` for true).
* @override
*/
async orchestrate(task) {
debug("--- Iniciando Orquestração Hierárquica de Agentes com Modelo Thinking ---");
debug(`Tarefa Principal: ${task}`);
// Passo 1: Agente Orquestrador analisa a tarefa e decide quais especialistas acionar
const agentsToEngage = await this.determineAgentsForTask(task);
debug("Agentes Selecionados para a Tarefa: %o", agentsToEngage.map(agent => agent.role));
if (agentsToEngage.length === 0) {
debug("Nenhum agente especialista adequado encontrado para a tarefa.");
return "Nenhum agente especialista adequado encontrado para a tarefa.";
}
let expertResponses = {}; // Para armazenar as respostas dos agentes especialistas
// Passo 2: Acionar agentes especialistas e coletar respostas
for (const agent of agentsToEngage) {
debug(`**Agente Especialista: ${agent.role} - Iniciando tarefa... **`);
let agentTask;
if (agent.taskFormatter && typeof agent.taskFormatter === 'function') {
// Usar formatador personalizado se disponível
agentTask = agent.taskFormatter(task, agent);
} else if (agent.enableGoogleSearch === true) {
// Tarefa específica para agentes com Google Search habilitado
agentTask = `"${task}"`;
// agentTask = `Realize uma pesquisa no Google sobre: "${task}".
// Retorne apenas os resultados relevantes da pesquisa com suas respectivas fontes.
// Não elabore ou adicione informações além dos resultados encontrados.`;
} else {
// Comportamento padrão para outros agentes
agentTask = `
Como especialista no papel de ${agent.role} com o objetivo de "${agent.objective}", sua missão é contribuir para a seguinte tarefa principal:
"${task}"
Instruções específicas:
1. Analise a tarefa principal sob a perspectiva específica da sua expertise
2. Concentre-se nos aspectos da tarefa que mais se relacionam com seu papel e objetivo
3. Forneça insights, análises ou soluções que apenas um especialista em ${agent.role} poderia oferecer
4. Evite abordar aspectos que estejam fora do escopo da sua especialidade
Formato da resposta:
- Inicie com uma breve análise da tarefa sob sua perspectiva especializada
- Apresente sua contribuição principal de forma estruturada e objetiva
- Se aplicável, inclua recomendações específicas ou próximos passos
- Mantenha sua resposta concisa, direta e focada na sua área de expertise
Lembre-se: Sua resposta será integrada com as de outros especialistas para formar uma solução completa.
`;
}
agent.task = agentTask; // Define a tarefa específica para o agente especialista
try {
const response = await agent.executeTask();
debug(`**Agente Especialista: ${agent.role} - Tarefa Concluída.**`);
debug(`Resposta do Agente ${agent.role}: %o`, response);
expertResponses[agent.role] = response; // Armazena a resposta, indexada pelo papel do agente
} catch (error) {
console.error(`Erro ao executar Agente Especialista ${agent.role}:`, error);
expertResponses[agent.role] = `Erro ao executar tarefa: ${error.message}`;
}
}
// Passo 3: Agente Orquestrador processa as respostas dos especialistas e gera a resposta final
const finalResponse = await this.generateFinalResponse(task, expertResponses);
debug("--- Orquestração Hierárquica com Modelo Thinking Concluída ---");
debug("Resposta Final Orquestrada (Bruta): %o", finalResponse);
// Processar a resposta final se includeThinkingSteps for false (padrão)
if (!this.includeThinkingSteps) {
const processedResponse = this.processThinkingResponse(finalResponse);
debug("Resposta Final Processada (Apenas Resposta): %o", processedResponse.finalAnswer);
return processedResponse.finalAnswer;
} else {
// Retornar a resposta bruta completa se includeThinkingSteps for true
debug("Retornando Resposta Bruta Completa (com Raciocínio).");
return finalResponse;
}
}
/**
* Determina quais agentes especialistas são mais adequados para uma tarefa específica.
* Utiliza o modelo thinking para analisar a tarefa e selecionar os agentes mais relevantes,
* processando a resposta bruta para extrair os roles dos agentes selecionados.
*
* Este método sobrescreve o da classe pai para trabalhar com o modelo thinking, que fornece
* um raciocínio passo a passo antes de chegar à seleção final de agentes. O método inclui
* lógica adicional para extrair os roles dos agentes a partir da resposta em formato de texto.
*
* @private
* @param {string} task - A descrição da tarefa a ser analisada
* @returns {Promise<Array>} Uma promessa que resolve para um array de objetos de agentes selecionados
* @override
*/
async determineAgentsForTask(task) {
debug("--- Determinando Agentes Especialistas com Modelo Thinking ---");
const agentDescriptions = this.agents.map(agent => `- Role: ${agent.role}, Objective: ${agent.objective}`).join('\n');
const prompt = `
Você é um agente orquestrador hierárquico avançado, responsável por planejar e coordenar a execução de tarefas complexas através da seleção estratégica de agentes especialistas.
Tarefa Principal: "${task}"
Agentes Especialistas Disponíveis:
${agentDescriptions}
Instruções para Análise da Tarefa:
1. Decomponha a tarefa principal em subtarefas ou componentes essenciais
2. Identifique quais habilidades e conhecimentos são necessários para cada componente
3. Avalie cada agente especialista considerando:
- Relevância direta para componentes específicos da tarefa
- Capacidade de fornecer informações ou perspectivas únicas
- Complementaridade com outros agentes (evitando redundâncias)
- Potencial para resolver aspectos críticos da tarefa
Critérios de Seleção:
- Selecione APENAS agentes que contribuirão significativamente para o resultado
- Evite selecionar agentes com funções redundantes
- Considere a eficiência do conjunto (máximo valor com mínimo de agentes)
- Priorize agentes com expertise diretamente relacionada aos aspectos centrais da tarefa
Processo de Raciocínio:
1. Analise a tarefa principal e identifique seus componentes essenciais
2. Examine cada agente disponível e avalie sua relevância para cada componente
3. Considere as possíveis interações e complementaridades entre agentes
4. Selecione o conjunto ideal de agentes para maximizar a qualidade da resposta
Formato de Resposta:
Liste os roles dos agentes selecionados, um por linha.
Se nenhum agente for relevante, responda apenas: Nenhum agente relevante encontrado
Pense passo a passo, explicando seu raciocínio para cada decisão antes de fornecer a lista final de agentes selecionados.
`;
debug("Prompt para Seleção de Agentes: %o", prompt);
try {
// Obter a resposta bruta do modelo thinking
const response = await this.llm.generateContent({ prompt: prompt });
const rawResponse = response.text;
debug("Resposta Bruta do Modelo Thinking para Seleção de Agentes: %o", rawResponse);
// Processar a resposta bruta para extrair os roles dos agentes selecionados
if (rawResponse.toLowerCase().includes("nenhum agente relevante encontrado")) {
return []; // Retorna array vazio se nenhum agente for considerado relevante
}
// Extrair roles dos agentes selecionados da resposta do LLM
// Procurar por linhas que contenham apenas o nome do role, sem números ou marcadores
const lines = rawResponse.split('\n');
const potentialRoles = [];
for (const line of lines) {
const trimmedLine = line.trim();
// Verificar se a linha contém apenas o nome de um role (sem números, marcadores, etc.)
if (trimmedLine && !trimmedLine.match(/^[\d\.\-\*\#]/)) {
// Verificar se a linha corresponde exatamente a um dos roles disponíveis
const matchingAgent = this.agents.find(agent =>
trimmedLine === agent.role ||
trimmedLine.includes(agent.role)
);
if (matchingAgent) {
potentialRoles.push(matchingAgent.role);
}
}
}
// Se não encontrou roles usando o método acima, tentar extrair de forma mais agressiva
if (potentialRoles.length === 0) {
for (const agent of this.agents) {
if (rawResponse.includes(agent.role)) {
potentialRoles.push(agent.role);
}
}
}
debug("Roles de Agentes Potencialmente Selecionados: %o", potentialRoles);
// Encontrar os objetos Agent correspondentes aos roles selecionados
const selectedAgents = this.agents.filter(agent => potentialRoles.includes(agent.role));
debug("Agentes Selecionados (objetos Agent): %o", selectedAgents.map(agent => agent.role));
return selectedAgents;
} catch (error) {
console.error("Erro ao determinar agentes com Modelo Thinking:", error);
return []; // Em caso de erro, retorna array vazio (nenhum agente selecionado)
}
}
/**
* Gera uma resposta final sintetizada a partir das respostas dos agentes especialistas.
* Utiliza o modelo thinking para integrar as diferentes perspectivas em uma resposta coesa,
* incluindo o raciocínio passo a passo do processo de síntese.
*
* Este método sobrescreve o da classe pai para trabalhar com o modelo thinking, que fornece
* um raciocínio detalhado sobre como as informações dos especialistas foram analisadas,
* avaliadas e integradas para formar a resposta final.
*
* @private
* @param {string} task - A descrição da tarefa original
* @param {Object} expertResponses - Um objeto contendo as respostas dos agentes especialistas, indexadas por seus roles
* @returns {Promise<string>} Uma promessa que resolve para a resposta final bruta, incluindo o raciocínio
* @override
*/
async generateFinalResponse(task, expertResponses) {
debug("--- Gerando Resposta Final Orquestrada com Modelo Thinking ---");
const responsesText = Object.entries(expertResponses)
.map(([role, response]) => `- Agente ${role}: ${response}`)
.join('\n');
const prompt = `
Você é um agente orquestrador hierárquico avançado, responsável por sintetizar informações de múltiplas fontes especializadas em uma resposta coesa, precisa e abrangente.
Tarefa Principal: "${task}"
Respostas dos Agentes Especialistas:
${responsesText}
Instruções para Síntese:
1. Identifique os principais componentes/aspectos da tarefa que precisam ser abordados
2. Mapeie as contribuições de cada agente para esses componentes
3. Avalie a qualidade, relevância e confiabilidade de cada contribuição
4. Identifique padrões, conexões e insights que emergem do conjunto de respostas
5. Resolva contradições e preencha lacunas de informação quando possível
Princípios para Estruturação da Resposta:
- Priorize informações por relevância e confiabilidade
- Organize o conteúdo de forma lógica e progressiva
- Elimine redundâncias e informações tangenciais
- Mantenha um tom consistente e apropriado ao contexto da tarefa
- Apresente perspectivas alternativas quando relevante, com indicação clara das diferenças
Formato da Resposta Final:
- Introdução: Contextualize brevemente o problema/tarefa
- Corpo: Apresente as informações organizadas por tópicos ou aspectos relevantes
- Conclusão: Sintetize os principais pontos e forneça uma resposta direta à tarefa original
- (Quando aplicável) Recomendações ou próximos passos
Processo de Raciocínio:
1. Analise cada resposta de especialista e extraia os pontos-chave
2. Identifique áreas de concordância, complementaridade e contradição
3. Avalie a qualidade e relevância de cada contribuição
4. Desenvolva uma estrutura lógica para integrar as informações
5. Refine a resposta para garantir clareza, concisão e completude
A resposta final deve ser clara, concisa e completa, integrando as informações dos especialistas sem mencionar explicitamente os agentes.
Pense passo a passo, explicando seu processo de síntese antes de fornecer a resposta final.
`;
debug("Prompt para Geração da Resposta Final: %o", prompt);
try {
// Obter a resposta bruta do modelo thinking
const response = await this.llm.generateContent({ prompt: prompt });
const rawResponse = response.text;
debug("Resposta Bruta do Modelo Thinking para Geração da Resposta Final: %o", rawResponse);
// Retornar a resposta bruta, sem processamento
return rawResponse;
} catch (error) {
console.error("Erro ao gerar resposta final com Modelo Thinking:", error);
return "Erro ao gerar resposta final orquestrada."; // Em caso de erro, retorna mensagem de erro genérica
}
}
/**
* Processa a resposta bruta do modelo thinking para extrair informações estruturadas.
* Este método é exclusivo desta classe e não existe na classe pai.
*
* Analisa a resposta bruta do modelo thinking e tenta extrair:
* 1. A resposta final (conclusão)
* 2. Os passos de raciocínio que levaram à resposta
*
* O método utiliza várias estratégias para identificar estas partes:
* - Verifica se a resposta é um JSON válido com campos específicos
* - Procura por padrões de texto que indicam seções de "resposta final" ou "conclusão"
* - Identifica indicadores linguísticos de conclusão (como "portanto", "em conclusão", etc.)
* - Analisa a estrutura de parágrafos para separar o raciocínio da resposta final
*
* @param {string} rawResponse - A resposta bruta do modelo thinking
* @returns {Object} Um objeto contendo as partes estruturadas da resposta:
* - finalAnswer {string} - A resposta/conclusão final extraída
* - thinkingSteps {string} - Os passos de raciocínio que levaram à resposta
* - rawResponse {string} - A resposta bruta original
* - error {string} - Mensagem de erro, se ocorrer algum problema durante o processamento
*/
processThinkingResponse(rawResponse) {
debug("--- Processando Resposta Thinking ---");
debug("Resposta Bruta Recebida: %o", rawResponse);
try {
// Tentativa 1: Verificar se a resposta é um JSON válido
try {
const jsonResponse = JSON.parse(rawResponse);
debug("Resposta detectada como JSON válido.");
if (jsonResponse.final_answer || jsonResponse.answer) {
debug("JSON contém campos 'final_answer' ou 'answer'.");
return {
finalAnswer: jsonResponse.final_answer || jsonResponse.answer,
thinkingSteps: JSON.stringify(jsonResponse.thinking_steps || jsonResponse.steps || jsonResponse.reasoning || jsonResponse, null, 2), // Formata o resto como JSON
rawResponse: rawResponse
};
}
debug("JSON não contém campos esperados, retornando JSON completo como resposta final.");
// Se não encontrar campos específicos, retornar o JSON completo como string na resposta final
return {
finalAnswer: JSON.stringify(jsonResponse, null, 2),
thinkingSteps: "",
rawResponse: rawResponse
};
} catch (jsonError) {
// Não é JSON, continuar com processamento de texto
debug("Resposta não é JSON válido, prosseguindo com análise de texto.");
}
// Tentativa 2: Processamento baseado em texto (Refatorado)
// A ideia é encontrar o *último* marcador explícito da resposta final ou um separador '---'
// e considerar tudo *após* ele como a resposta final.
let thinkingSteps = rawResponse; // Inicialmente, tudo é considerado raciocínio
let finalAnswer = ""; // Inicialmente, a resposta final está vazia
// Padrões para identificar o início da *resposta final*, procurando do fim para o início.
// Inclui marcadores em markdown (**, ##) e a linha separadora '---'.
// Os padrões buscam o marcador/separador com quebras de linha antes e depois para maior precisão.
const finalAnswerMarkers = [
/\n\*\*(resposta final|conclusão|resultado|análise|summary|conclusion|final answer)\*\*\s*\n/i,
/\n##\s*(resposta final|conclusão|resultado|análise|summary|conclusion|final answer)\s*\n/i,
/\n-{3,}\s*\n/i // Linha separadora ---
];
let finalAnswerStartIndex = -1; // O índice no rawResponse onde a resposta final começa
// Iterar pelos padrões para encontrar a *última* ocorrência de um marcador/separador
for (const pattern of finalAnswerMarkers) {
let lastMatch = null;
let match;
// Usar regex global para encontrar todas as ocorrências
const regex = new RegExp(pattern.source, 'gi');
while ((match = regex.exec(rawResponse)) !== null) {
lastMatch = match; // Guarda a última correspondência encontrada
}
// Se encontramos uma correspondência para este padrão e ela ocorre *depois*
// de qualquer outra correspondência já encontrada, atualizamos o índice.
if (lastMatch && lastMatch.index > finalAnswerStartIndex) {
// O índice onde a resposta final começa é *após* o marcador/separador completo.
finalAnswerStartIndex = lastMatch.index + lastMatch[0].length;
debug(`Último marcador/separador candidato encontrado na posição ${lastMatch.index}: "${lastMatch[0].trim()}"`);
}
}
// Se encontramos um marcador/separador (finalAnswerStartIndex foi atualizado)
if (finalAnswerStartIndex !== -1 && finalAnswerStartIndex < rawResponse.length) {
// A resposta final é tudo a partir do índice encontrado.
finalAnswer = rawResponse.substring(finalAnswerStartIndex).trim();
// Os passos de raciocínio são tudo *antes* do início do marcador/separador que determinou o finalAnswerStartIndex.
// Precisamos encontrar o índice exato do *início* desse marcador específico.
let separatorStartIndex = -1;
for (const pattern of finalAnswerMarkers) {
let lastMatch = null;
let match;
const regex = new RegExp(pattern.source, 'gi');
while ((match = regex.exec(rawResponse)) !== null) {
// Verifica se esta correspondência é a que definiu o finalAnswerStartIndex
if (match.index + match[0].length === finalAnswerStartIndex) {
separatorStartIndex = match.index;
break; // Encontramos o marcador exato
}
}
if (separatorStartIndex !== -1) break; // Sai do loop externo se já encontrou
}
if (separatorStartIndex !== -1) {
// Se encontramos o início do separador, o raciocínio vai até ali.
thinkingSteps = rawResponse.substring(0, separatorStartIndex).trim();
debug(`Separador encontrado. Thinking steps extraídos até a posição ${separatorStartIndex}.`);
} else {
// Fallback: Se algo deu errado ao encontrar o início do separador (não deveria acontecer),
// pegamos tudo até o início da resposta final como raciocínio.
thinkingSteps = rawResponse.substring(0, finalAnswerStartIndex).trim();
debug(`Separador encontrado, mas índice inicial do separador não localizado (fallback). Thinking steps extraídos até ${finalAnswerStartIndex}.`);
}
} else {
// Nenhum marcador/separador claro foi encontrado.
// Comportamento seguro: Considerar a resposta inteira como final.
debug("Nenhum marcador/separador claro encontrado para a resposta final. Retornando resposta bruta como final.");
finalAnswer = rawResponse;
thinkingSteps = ""; // Nenhum passo de raciocínio claramente separado
}
// Limpeza final: Remover possíveis marcadores que ficaram no início da resposta final
finalAnswer = finalAnswer.replace(/^\s*(\*\*|##)\s*(resposta final|conclusão|resultado|análise|summary|conclusion|final answer)\s*(\*\*|##)?\s*\n?/i, '').trim();
finalAnswer = finalAnswer.replace(/^\s*-{3,}\s*\n?/,'').trim(); // Remover linha --- inicial
debug(`Final Answer extraída:\n---\n${finalAnswer}\n---`);
debug(`Thinking Steps extraídos:\n---\n${thinkingSteps}\n---`);
// Verificação de segurança: Se a resposta final extraída estiver vazia, mas havia algo antes (raciocínio),
// é mais seguro retornar a resposta bruta completa para evitar perder informação.
if (!finalAnswer && thinkingSteps) {
debug("AVISO: Resposta final extraída está vazia, mas passos de raciocínio foram encontrados. Retornando resposta bruta como final.");
return {
finalAnswer: rawResponse,
thinkingSteps: "", // Limpa os passos para evitar confusão
rawResponse: rawResponse
};
}
return {
finalAnswer: finalAnswer || rawResponse, // Retorna rawResponse se finalAnswer for vazio por algum motivo
thinkingSteps: thinkingSteps,
rawResponse: rawResponse
};
} catch (error) {
console.error("Erro crítico ao processar resposta do modelo thinking:", error);
return {
finalAnswer: rawResponse,
thinkingSteps: "",
rawResponse: rawResponse,
error: error.message
};
}
}
}
module.exports = HierarchicalAgentThinkingOrchestrator;