feat: sincronização de desenvolvimento local da VPS 4 para o Gitea

This commit is contained in:
VPS 4 Deploy Agent
2026-05-10 07:38:54 +02:00
parent 621fbfba64
commit 3396fa0598
402 changed files with 234697 additions and 0 deletions
@@ -0,0 +1,730 @@
"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