Files
instrucoes_gerais/clube67/newwhats.local/plugins/secretaria/brain.js
T

730 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
/**
* ProtocolEngine — Cérebro Stateful da Secretária IA
*
* Princípios de economia de tokens:
* 1. Lê o ESTADO atual do protocolo (summary), não o histórico completo
* 2. Carrega apenas as últimas N mensagens (context_window)
* 3. Sumarização ativa a cada 10 trocas para manter o resumo atualizado
* 4. Nós do cérebro são compostos apenas com os ativos
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProtocolEngine = void 0;
const tools_1 = require("./tools");
class ProtocolEngine {
constructor(db, config) {
this.db = db;
this.config = config;
}
// ── Chat ─────────────────────────────────────────────────────────────────
async chat(conversationId, userMessage, opts) {
const conversation = await this.db('sec_conversations').where({ id: conversationId }).first();
if (!conversation)
throw new Error('Conversa não encontrada');
const agent = await this.db('sec_agents').where({ id: conversation.agent_id }).first();
if (!agent)
throw new Error('Agente não encontrado');
// Monta system prompt a partir dos nós ativos + contexto externo
const systemPrompt = await this.buildSystemPrompt(agent, conversation, opts);
// Carrega apenas as últimas N mensagens (não o histórico completo)
const contextWindow = agent.context_window ?? 8;
const recentMessages = await this.db('sec_messages')
.where({ conversation_id: conversationId })
.orderBy('created_at', 'desc')
.limit(contextWindow)
.then((rows) => rows.reverse());
// Salva a mensagem do usuário
await this.db('sec_messages').insert({
id: this.uuid(),
conversation_id: conversationId,
role: 'user',
content: userMessage,
created_at: new Date(),
});
// Chama a IA — usa node_model do nó persona se definido (sobrepõe o agente)
const personaNode = await this.db('sec_brain_nodes')
.where({ agent_id: conversation.agent_id, type: 'persona', active: true })
.orderBy('sort_order')
.first();
const agentOverride = personaNode?.node_model
? { ...agent, model: personaNode.node_model }
: agent;
const messages = [
...recentMessages.map((m) => ({ role: m.role, content: m.content })),
{ role: 'user', content: userMessage },
];
// Resolve tools: usa lista passada por opts, ou todas as builtins por padrão
const toolNames = opts?.tools ?? tools_1.ALL_TOOL_NAMES;
const toolDefs = (0, tools_1.resolveTools)(toolNames);
const toolCtx = {
db: this.db,
conversationId,
extChatId: conversation.ext_chat_id ?? undefined,
tenantId: opts?.tenantId,
hooks: opts?.hooks,
};
let response;
let usageInfo = null;
let providerUsed = null;
let modelUsed = null;
try {
if (toolDefs.length > 0) {
response = await this.callAIWithTools(agentOverride, systemPrompt, messages, toolDefs, toolCtx);
// Telemetria escrita pelos tool loops via side channel (toolCtx._telemetry)
if (toolCtx._telemetry) {
usageInfo = toolCtx._telemetry.usage;
providerUsed = toolCtx._telemetry.provider;
modelUsed = toolCtx._telemetry.model;
}
}
else {
const result = await this.callAI(agentOverride, systemPrompt, messages);
response = result.text;
usageInfo = result.usage;
providerUsed = result.provider;
modelUsed = result.model;
}
}
catch (err) {
response = `[Erro ao chamar IA: ${err.message}. Verifique a API Key nas configurações do plugin.]`;
}
// Salva resposta da IA com telemetria de tokens
await this.db('sec_messages').insert({
id: this.uuid(),
conversation_id: conversationId,
role: 'assistant',
content: response,
usage_tokens: usageInfo ? JSON.stringify(usageInfo) : null,
provider_used: providerUsed,
model_used: modelUsed,
created_at: new Date(),
});
// Atualiza conversa + sumariza a cada 10 trocas
const totalMsgs = await this.db('sec_messages')
.where({ conversation_id: conversationId })
.count('id as c')
.first()
.then((r) => Number(r?.c ?? 0));
let summary = conversation.summary;
if (totalMsgs > 0 && (totalMsgs % 10 === 0 || totalMsgs === 5)) {
summary = await this.summarize(agent, recentMessages, userMessage, response);
}
await this.db('sec_conversations')
.where({ id: conversationId })
.update({ updated_at: new Date(), summary });
return response;
}
// ── Protocol Number ───────────────────────────────────────────────────────
static generateProtocolNumber() {
const now = new Date();
const p = (n, d = 2) => String(n).padStart(d, '0');
return `${p(now.getDate())}${p(now.getMonth() + 1)}${String(now.getFullYear()).slice(-2)}${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
}
// ── System Prompt Builder ─────────────────────────────────────────────────
async buildSystemPrompt(agent, conversation, opts) {
const nodes = await this.db('sec_brain_nodes')
.where({ agent_id: agent.id, active: true })
.orderBy('sort_order');
let prompt = '';
// Data/hora real — impede o modelo de alucinar a data
const nowReal = new Date();
const dtStr = nowReal.toLocaleString('pt-BR', {
timeZone: 'America/Sao_Paulo',
weekday: 'long', day: '2-digit', month: 'long', year: 'numeric',
hour: '2-digit', minute: '2-digit',
});
prompt += `=== DATA E HORA ATUAL ===\n${dtStr} (horário de Brasília)\n\n`;
// Cabeçalho do protocolo — sempre presente, leve (3 linhas)
const protocolHeader = [
`=== PROTOCOLO ATIVO ===`,
`Número: ${conversation.protocol_number || '—'}`,
`Contato: ${conversation.contact_name}`,
`Status: ${conversation.status}`,
``,
].join('\n');
prompt += protocolHeader;
for (const node of nodes) {
switch (node.type) {
case 'persona':
prompt += `${node.content}\n\n`;
break;
case 'knowledge':
prompt += `=== BASE DE CONHECIMENTO ===\n${node.content}\n\n`;
break;
case 'rules':
prompt += `=== REGRAS ===\n${node.content}\n\n`;
break;
case 'calendar': {
const calCtx = await this.getCalendarContext();
prompt += `=== AGENDA DISPONÍVEL (próximos 7 dias) ===\n${calCtx}\n\nInstruções: ${node.content}\n\n`;
break;
}
case 'escalation':
prompt += `=== REGRAS DE ESCALADA ===\n${node.content}\n\n`;
break;
default:
prompt += `${node.content}\n\n`;
}
}
// Injeta contexto local do projeto (enviado pelo plugin satélite)
if (opts?.contextData && Object.keys(opts.contextData).length > 0) {
const ctx = JSON.stringify(opts.contextData, null, 2);
prompt += `=== CONTEXTO DO CLIENTE (dados reais do projeto) ===\n${ctx}\n\n`;
}
// Prompt extra do plugin (instruções específicas da chamada)
if (opts?.systemExtra?.trim()) {
prompt += `=== INSTRUÇÕES ADICIONAIS ===\n${opts.systemExtra.trim()}\n\n`;
}
// Injeta resumo do estado atual (economia de tokens — evita reler o histórico)
if (conversation.summary) {
prompt += `=== ESTADO ATUAL DA CONVERSA ===\n${conversation.summary}\n\n`;
}
return prompt.trim();
}
// ── Finalize Protocol ─────────────────────────────────────────────────────
async finalizeProtocol(conversationId) {
const conversation = await this.db('sec_conversations').where({ id: conversationId }).first();
if (!conversation)
throw new Error('Conversa não encontrada');
if (conversation.status === 'closed') {
return { summary: conversation.summary ?? '', protocol_number: conversation.protocol_number };
}
const agent = await this.db('sec_agents').where({ id: conversation.agent_id }).first();
if (!agent)
throw new Error('Agente não encontrado');
// Carrega todas as mensagens para gerar resumo completo
const messages = await this.db('sec_messages')
.where({ conversation_id: conversationId })
.orderBy('created_at');
let summary = conversation.summary ?? '';
if (messages.length > 0) {
const transcript = messages
.map((m) => `${m.role === 'user' ? 'Cliente' : 'Ana'}: ${m.content}`)
.join('\n');
const summaryPrompt = `Gere um resumo estruturado desta conversa de atendimento para uso futuro como contexto rápido.\nInclua: motivo do contato, o que foi resolvido, próximos passos pendentes (se houver).\nMáximo 5 linhas. Seja objetivo.\n\nProtocolo: ${conversation.protocol_number}\nContato: ${conversation.contact_name}\n\n${transcript}`;
const cheapModel = {
openai: 'gpt-4o-mini', anthropic: 'claude-3-5-haiku-20241022',
gemini: 'gemini-2.0-flash', ollama: agent.model ?? 'llama3',
};
const finalAgent = {
...agent, temperature: 0.2, max_tokens: 200,
model: cheapModel[agent.provider] ?? agent.model,
};
try {
const result = await this.callAI(finalAgent, '', [{ role: 'user', content: summaryPrompt }]);
summary = result.text;
}
catch {
summary = conversation.summary ?? `Protocolo ${conversation.protocol_number} encerrado.`;
}
}
// Apaga mensagens — contexto comprimido no resumo (economia de tokens)
await this.db('sec_messages').where({ conversation_id: conversationId }).delete();
// Fecha o protocolo com resumo persistido
await this.db('sec_conversations')
.where({ id: conversationId })
.update({ status: 'closed', summary, updated_at: new Date() });
return { summary, protocol_number: conversation.protocol_number };
}
// ── AI Call ───────────────────────────────────────────────────────────────
buildFallbackChain(agent, cfg) {
const chainStr = cfg.fallback_chain ?? 'openai,gemini,anthropic,ollama';
const order = chainStr.split(',').map((s) => s.trim()).filter(Boolean);
const defaults = {
openai: 'gpt-4o-mini',
anthropic: 'claude-3-5-haiku-20241022',
gemini: 'gemini-2.0-flash',
ollama: 'llama3',
};
const hasKey = (p) => {
if (p === 'openai')
return !!cfg.openai_key;
if (p === 'anthropic')
return !!cfg.anthropic_key;
if (p === 'gemini')
return !!cfg.gemini_key;
if (p === 'ollama')
return true; // local, sempre disponível
return false;
};
const agentProvider = agent.provider ?? 'openai';
const agentModel = agent.model ?? defaults[agentProvider] ?? 'gpt-4o-mini';
const chain = [{ provider: agentProvider, model: agentModel }];
for (const p of order) {
if (p === agentProvider)
continue;
if (!hasKey(p))
continue;
chain.push({ provider: p, model: defaults[p] ?? p });
}
return chain;
}
isRecoverableError(err) {
const msg = err.message.toLowerCase();
return (msg.includes('quota') ||
msg.includes('rate limit') ||
msg.includes('limite da api') ||
msg.includes('exceeded') ||
msg.includes('billing') ||
msg.includes('insufficient') ||
msg.includes('invalid_api_key') ||
msg.includes('econnrefused') ||
msg.includes('enotfound') ||
msg.includes('não configurada'));
}
async callAI(agent, systemPrompt, messages) {
const cfg = await this.config.get('secretaria');
const chain = this.buildFallbackChain(agent, cfg);
let lastError = new Error('Nenhum provider disponível');
for (const entry of chain) {
try {
return await this.callProvider(entry.provider, entry.model, agent, cfg, systemPrompt, messages);
}
catch (err) {
lastError = err;
if (this.isRecoverableError(err))
continue;
throw err;
}
}
throw new Error(`Todos os providers falharam. Último erro: ${lastError.message}`);
}
/**
* Chama o provider e retorna { text, usage, provider, model }.
* usage: { input, output, cached?, total } — chars/tokens consumidos.
* Reads agent.max_tokens (default 250 — adequado a WhatsApp).
*/
async callProvider(provider, model, agent, cfg, systemPrompt, messages) {
const maxTokens = agent.max_tokens ?? 250;
const temperature = agent.temperature ?? 0.7;
// ── OpenAI ────────────────────────────────────────────────────────────────
if (provider === 'openai') {
const apiKey = cfg.openai_key ?? '';
if (!apiKey)
throw new Error('OpenAI API Key não configurada. Acesse Admin → Plugins → Secretária IA.');
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(25000),
body: JSON.stringify({
model,
temperature,
max_tokens: maxTokens,
messages: [{ role: 'system', content: systemPrompt }, ...messages],
}),
});
const data = (await res.json());
if (!res.ok)
throw new Error(data.error?.message ?? `OpenAI ${res.status}`);
const text = data.choices[0].message.content;
const usage = {
input: data.usage?.prompt_tokens ?? 0,
output: data.usage?.completion_tokens ?? 0,
cached: data.usage?.prompt_tokens_details?.cached_tokens ?? 0,
total: data.usage?.total_tokens ?? 0,
};
return { text, usage, provider, model };
}
// ── Anthropic (com prompt caching ephemeral no system) ───────────────────
if (provider === 'anthropic') {
const apiKey = cfg.anthropic_key ?? '';
if (!apiKey)
throw new Error('Anthropic API Key não configurada. Acesse Admin → Plugins → Secretária IA.');
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
// Header necessário até GA do prompt caching
'anthropic-beta': 'prompt-caching-2024-07-31',
},
signal: AbortSignal.timeout(25000),
body: JSON.stringify({
model,
max_tokens: maxTokens,
// System como array com cache_control: trecho fica em cache 5min
// Próximas chamadas com mesmo systemPrompt pagam ~10% pelo trecho cacheado.
system: [
{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } },
],
messages,
}),
});
const data = (await res.json());
if (!res.ok)
throw new Error(data.error?.message ?? `Anthropic ${res.status}`);
const text = data.content[0].text;
const usage = {
input: data.usage?.input_tokens ?? 0,
output: data.usage?.output_tokens ?? 0,
cache_create: data.usage?.cache_creation_input_tokens ?? 0,
cache_read: data.usage?.cache_read_input_tokens ?? 0,
total: (data.usage?.input_tokens ?? 0) + (data.usage?.output_tokens ?? 0),
};
return { text, usage, provider, model };
}
// ── Google Gemini ─────────────────────────────────────────────────────────
if (provider === 'gemini') {
const apiKey = cfg.gemini_key ?? '';
if (!apiKey)
throw new Error('Google Gemini API Key não configurada. Acesse Admin → Plugins → Secretária IA.');
const geminiModel = model.startsWith('gemini') ? model : 'gemini-2.0-flash';
const geminiMessages = messages.map((m) => ({
role: m.role === 'assistant' ? 'model' : 'user',
parts: [{ text: m.content }],
}));
const geminiBody = JSON.stringify({
systemInstruction: { parts: [{ text: systemPrompt }] },
contents: geminiMessages,
generationConfig: { temperature, maxOutputTokens: maxTokens },
});
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${apiKey}`;
const doGeminiCall = async () => fetch(geminiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(25000),
body: geminiBody,
});
let res = await doGeminiCall();
let data = (await res.json());
if (!res.ok && (res.status === 429 || String(data.error?.message ?? '').toLowerCase().includes('quota'))) {
const msg = data.error?.message ?? '';
const match = msg.match(/retry in ([\d.]+)s/i);
// Espera no máximo 8s (era 30s) e mínimo 1.5s (era 5s) — limita impacto de quota no tempo total
const waitMs = match ? Math.min(Math.ceil(parseFloat(match[1])) * 1000, 8000) : 1500;
await new Promise((r) => setTimeout(r, waitMs));
res = await doGeminiCall();
data = (await res.json());
}
if (!res.ok) {
const errMsg = data.error?.message ?? `Gemini ${res.status}`;
if (errMsg.toLowerCase().includes('quota') || res.status === 429) {
throw new Error('Limite da API Gemini atingido. Aguarde alguns instantes e tente novamente.');
}
throw new Error(errMsg);
}
const text = data.candidates[0].content.parts[0].text;
const usage = {
input: data.usageMetadata?.promptTokenCount ?? 0,
output: data.usageMetadata?.candidatesTokenCount ?? 0,
total: data.usageMetadata?.totalTokenCount ?? 0,
};
return { text, usage, provider, model: geminiModel };
}
// ── Ollama (local) ────────────────────────────────────────────────────────
if (provider === 'ollama') {
const baseUrl = cfg.ollama_url ?? 'http://localhost:11434';
const ollamaModel = model || 'llama3';
const res = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(25000),
body: JSON.stringify({
model: ollamaModel,
stream: false,
messages: [{ role: 'system', content: systemPrompt }, ...messages],
options: { temperature, num_predict: maxTokens },
}),
});
const data = (await res.json());
if (!res.ok)
throw new Error(data.error ?? `Ollama ${res.status}`);
const text = data.message.content;
const usage = {
input: data.prompt_eval_count ?? 0,
output: data.eval_count ?? 0,
total: (data.prompt_eval_count ?? 0) + (data.eval_count ?? 0),
};
return { text, usage, provider, model: ollamaModel };
}
throw new Error(`Provider "${provider}" não suportado. Use: openai, anthropic, gemini, ollama`);
}
// ── Calendar Context ──────────────────────────────────────────────────────
async getCalendarContext() {
const today = new Date().toISOString().split('T')[0];
const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);
const nextWeekStr = nextWeek.toISOString().split('T')[0];
const slots = await this.db('sec_calendar')
.whereIn('status', ['available', 'booked'])
.whereBetween('date', [today, nextWeekStr])
.orderBy('date')
.orderBy('time_start')
.limit(30);
if (slots.length === 0)
return 'Nenhum horário nos próximos 7 dias.';
const lines = slots.map((s) => {
const time = `${s.date} ${s.time_start.slice(0, 5)}${s.time_end.slice(0, 5)}`;
if (s.status === 'booked') {
const who = s.attendee_name ? ` | Paciente: ${s.attendee_name}` : '';
const phone = s.attendee_phone ? ` (${s.attendee_phone})` : '';
return `• [AGENDADO] ${time}: ${s.title}${who}${phone}`;
}
return `• [DISPONÍVEL] ${time}: ${s.title}`;
});
return lines.join('\n');
}
// ── Summarization (token economy) ────────────────────────────────────────
/**
* Sumarização com modelo barato (M1.5).
* Força modelo "mini/haiku/flash" mesmo que o agente principal use modelo caro.
* Sumário é tarefa simples — não precisa do modelo de produção.
*/
async summarize(agent, recentMsgs, lastUser, lastAssistant) {
const excerpt = [
...recentMsgs.slice(-6).map((m) => `${m.role}: ${m.content}`),
`user: ${lastUser}`,
`assistant: ${lastAssistant}`,
].join('\n');
const prompt = `Resuma em no máximo 2 frases curtas o estado atual desta conversa de atendimento, focando no tema e próximo passo:\n\n${excerpt}`;
// Modelo barato por provider
const cheapModel = {
openai: 'gpt-4o-mini',
anthropic: 'claude-3-5-haiku-20241022',
gemini: 'gemini-2.0-flash',
ollama: agent.model ?? 'llama3',
};
const summaryAgent = {
...agent,
temperature: 0.3,
max_tokens: 120,
model: cheapModel[agent.provider] ?? agent.model,
};
try {
const result = await this.callAI(summaryAgent, '', [{ role: 'user', content: prompt }]);
return result.text;
}
catch {
return '';
}
}
// ── Tool Calling ──────────────────────────────────────────────────────────
async callAIWithTools(agent, systemPrompt, inputMessages, tools, toolCtx) {
const cfg = await this.config.get('secretaria');
const chain = this.buildFallbackChain(agent, cfg);
// Prefere provider com suporte a tool calling; Ollama cai em modo texto
const TOOL_PROVIDERS = ['openai', 'anthropic', 'gemini'];
const entry = chain.find(e => TOOL_PROVIDERS.includes(e.provider));
if (!entry) {
// Nenhum provider com tool calling disponível — usa modo texto normal
return (await this.callAI(agent, systemPrompt, inputMessages)).text;
}
try {
switch (entry.provider) {
case 'openai':
return await this.openAIToolLoop(entry.model, agent, cfg, systemPrompt, inputMessages, tools, toolCtx);
case 'anthropic':
return await this.anthropicToolLoop(entry.model, agent, cfg, systemPrompt, inputMessages, tools, toolCtx);
case 'gemini':
return await this.geminiToolLoop(entry.model, agent, cfg, systemPrompt, inputMessages, tools, toolCtx);
default:
return (await this.callAI(agent, systemPrompt, inputMessages)).text;
}
}
catch (err) {
if (this.isRecoverableError(err)) {
// Provider com tools falhou — tenta sem tools no próximo da chain
return (await this.callAI(agent, systemPrompt, inputMessages)).text;
}
throw err;
}
}
async executeTool(name, rawArgs, tools, toolCtx) {
const tool = tools.find(t => t.name === name);
if (!tool)
return { error: `Tool "${name}" não encontrada.` };
const args = typeof rawArgs === 'string' ? JSON.parse(rawArgs || '{}') : rawArgs;
try {
return await tool.execute(args, toolCtx);
}
catch (e) {
return { error: e.message };
}
}
// ── Telemetria helper para tool loops ─────────────────────────────────────
accumTelemetry(toolCtx, provider, model, incremental) {
if (!toolCtx._telemetry) {
toolCtx._telemetry = {
usage: { input: 0, output: 0, total: 0, cache_read: 0, cached: 0 },
provider, model, iterations: 0,
};
}
toolCtx._telemetry.iterations += 1;
toolCtx._telemetry.usage.input += incremental.input;
toolCtx._telemetry.usage.output += incremental.output;
toolCtx._telemetry.usage.total += incremental.input + incremental.output;
if (incremental.cache_read)
toolCtx._telemetry.usage.cache_read = (toolCtx._telemetry.usage.cache_read ?? 0) + incremental.cache_read;
if (incremental.cached)
toolCtx._telemetry.usage.cached = (toolCtx._telemetry.usage.cached ?? 0) + incremental.cached;
}
// ── OpenAI tool loop ───────────────────────────────────────────────────────
async openAIToolLoop(model, agent, cfg, systemPrompt, inputMessages, tools, toolCtx) {
const apiKey = cfg.openai_key ?? '';
if (!apiKey)
throw new Error('OpenAI API Key não configurada');
const oaiTools = tools.map(t => ({
type: 'function',
function: { name: t.name, description: t.description, parameters: t.parameters },
}));
let msgs = [...inputMessages];
const MAX_ITER = 5;
for (let i = 0; i < MAX_ITER; i++) {
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(25000),
body: JSON.stringify({
model,
temperature: agent.temperature ?? 0.7,
max_tokens: agent.max_tokens ?? 250,
messages: [{ role: 'system', content: systemPrompt }, ...msgs],
tools: oaiTools,
tool_choice: 'auto',
}),
});
const data = (await res.json());
if (!res.ok)
throw new Error(data.error?.message ?? `OpenAI ${res.status}`);
// Telemetria (M1.4)
this.accumTelemetry(toolCtx, 'openai', model, {
input: data.usage?.prompt_tokens ?? 0,
output: data.usage?.completion_tokens ?? 0,
cached: data.usage?.prompt_tokens_details?.cached_tokens ?? 0,
});
const choice = data.choices[0];
const assistantMsg = choice.message;
if (choice.finish_reason !== 'tool_calls' || !assistantMsg.tool_calls?.length) {
return (assistantMsg.content ?? '');
}
// Execute tools in parallel
msgs.push(assistantMsg);
const toolResults = await Promise.all(assistantMsg.tool_calls.map(async (tc) => {
const result = await this.executeTool(tc.function.name, tc.function.arguments, tools, toolCtx);
return { role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) };
}));
msgs.push(...toolResults);
}
throw new Error('Tool calling: limite de iterações atingido');
}
// ── Anthropic tool loop ────────────────────────────────────────────────────
async anthropicToolLoop(model, agent, cfg, systemPrompt, inputMessages, tools, toolCtx) {
const apiKey = cfg.anthropic_key ?? '';
if (!apiKey)
throw new Error('Anthropic API Key não configurada');
const anthropicTools = tools.map(t => ({
name: t.name, description: t.description, input_schema: t.parameters,
}));
let msgs = [...inputMessages];
const MAX_ITER = 5;
for (let i = 0; i < MAX_ITER; i++) {
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
},
signal: AbortSignal.timeout(25000),
body: JSON.stringify({
model, max_tokens: agent.max_tokens ?? 250, system: systemPrompt,
messages: msgs, tools: anthropicTools,
}),
});
const data = (await res.json());
if (!res.ok)
throw new Error(data.error?.message ?? `Anthropic ${res.status}`);
// Telemetria (M1.4)
this.accumTelemetry(toolCtx, 'anthropic', model, {
input: data.usage?.input_tokens ?? 0,
output: data.usage?.output_tokens ?? 0,
cache_read: data.usage?.cache_read_input_tokens ?? 0,
});
// Texto puro
if (data.stop_reason !== 'tool_use') {
const textBlock = data.content.find(b => b.type === 'text');
return (textBlock?.text ?? '');
}
// Tool calls
msgs.push({ role: 'assistant', content: data.content });
const toolResults = await Promise.all(data.content
.filter(b => b.type === 'tool_use')
.map(async (b) => {
const result = await this.executeTool(b.name, b.input, tools, toolCtx);
return { type: 'tool_result', tool_use_id: b.id, content: JSON.stringify(result) };
}));
msgs.push({ role: 'user', content: toolResults });
}
throw new Error('Tool calling (Anthropic): limite de iterações atingido');
}
// ── Gemini tool loop ───────────────────────────────────────────────────────
async geminiToolLoop(model, agent, cfg, systemPrompt, inputMessages, tools, toolCtx) {
const apiKey = cfg.gemini_key ?? '';
if (!apiKey)
throw new Error('Gemini API Key não configurada');
const geminiModel = model.startsWith('gemini') ? model : 'gemini-2.0-flash';
const url = `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${apiKey}`;
const geminiTools = [{
functionDeclarations: tools.map(t => ({
name: t.name, description: t.description, parameters: t.parameters,
})),
}];
// Converte msgs para formato Gemini
let contents = inputMessages.map(m => ({
role: m.role === 'assistant' ? 'model' : 'user',
parts: [{ text: m.content }],
}));
const MAX_ITER = 5;
for (let i = 0; i < MAX_ITER; i++) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(25000),
body: JSON.stringify({
systemInstruction: { parts: [{ text: systemPrompt }] },
contents,
tools: geminiTools,
generationConfig: { temperature: agent.temperature ?? 0.7, maxOutputTokens: agent.max_tokens ?? 250 },
}),
});
const data = (await res.json());
if (!res.ok)
throw new Error(data.error?.message ?? `Gemini ${res.status}`);
// Telemetria (M1.4)
this.accumTelemetry(toolCtx, 'gemini', geminiModel, {
input: data.usageMetadata?.promptTokenCount ?? 0,
output: data.usageMetadata?.candidatesTokenCount ?? 0,
});
const candidate = data.candidates?.[0];
const parts = candidate?.content?.parts ?? [];
// Verifica se há function calls
const fnCalls = parts.filter(p => p.functionCall);
if (!fnCalls.length) {
const textPart = parts.find(p => p.text);
return (textPart?.text ?? '');
}
// Adiciona resposta do modelo ao histórico
contents.push({ role: 'model', parts });
// Executa tools e injeta resultados
const resultParts = await Promise.all(fnCalls.map(async (p) => {
const result = await this.executeTool(p.functionCall.name, p.functionCall.args ?? {}, tools, toolCtx);
return { functionResponse: { name: p.functionCall.name, response: result } };
}));
contents.push({ role: 'user', parts: resultParts });
}
throw new Error('Tool calling (Gemini): limite de iterações atingido');
}
// ── Utils ─────────────────────────────────────────────────────────────────
uuid() {
// Node 14.17+ tem crypto.randomUUID globalmente; fallback para Date-based
try {
return crypto.randomUUID();
}
catch {
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
}
}
exports.ProtocolEngine = ProtocolEngine;
//# sourceMappingURL=brain.js.map