Skip to content

Ferramentas Manuais

Ferramentas Manuais são funções customizadas que pausam a execução do agente e delegam o processamento para sua aplicação. Diferente de outros tipos de ferramentas que executam automaticamente, ferramentas manuais param o fluxo da conversa e aguardam seu sistema fornecer o resultado.


Quando Usar Ferramentas Manuais

Ferramentas Manuais são ideais quando você precisa de:

CenárioPor Que Ferramentas Manuais
Aprovação humanaRequer confirmação humana antes de ações críticas
Fluxos de UI customizadosColetar dados através de sua própria interface (formulários de pagamento, upload de arquivos)
Consultas a sistemas externosConsultar seus próprios bancos de dados ou APIs internas
Lógica de negócio complexaExecutar processos de múltiplos passos em seu backend
Operações sensíveisManter lógica sensível inteiramente em sua infraestrutura

Manual vs. Outros Tipos de Ferramentas

AspectoIntegração com API / RAG / MCPFerramentas Manuais
ExecuçãoSipPulse executa automaticamenteSua aplicação executa
FluxoAgente continua imediatamenteAgente pausa e aguarda
ControleConfigurado no SipPulseControle total no seu código
Caso de usoIntegrações padrãoOperações customizadas/sensíveis

Quando Escolher Ferramentas Manuais

Use Ferramentas Manuais quando você precisa que o agente pare e aguarde por input externo. Isso é essencial para workflows human-in-the-loop, interações customizadas de UI, ou quando operações sensíveis devem permanecer em sua infraestrutura.


Como Ferramentas Manuais Funcionam

A diferença chave de outras ferramentas é que o workflow do agente para quando uma ferramenta manual é chamada. Sua aplicação deve capturar isso, processar a requisição e enviar o resultado de volta.

1. Usuário envia mensagem
   "Preciso de aprovação para processar um reembolso de R$500"

2. Agente decide chamar ferramenta manual
   Ferramenta: solicitar_aprovacao
   Argumentos: { acao: "reembolso", valor: 500 }

3. Workflow do agente PARA (retorna END)
   Status da thread vira "pending"

4. API retorna resposta com tool_calls
   finish_reason: "tool_use"
   tool_calls: [{ id: "call_xyz", name: "...", input: {...} }]

5. SUA APLICAÇÃO captura esta resposta
   Processa a tool call (mostra UI, consulta BD, etc.)

6. SUA APLICAÇÃO envia resultado de volta
   POST mensagem com role: "tool"

7. Agente continua com o resultado
   "O reembolso foi aprovado pelo gerente."

Conceito Crítico

Ferramentas manuais NÃO usam webhooks. SipPulse NÃO chama seu servidor. Em vez disso, sua aplicação faz polling ou recebe a resposta da API e processa a tool call externamente, então envia o resultado de volta via API de mensagens.


Criando uma Ferramenta Manual

Passo 1: Configure no SipPulse

Na configuração do Agente, adicione uma nova ferramenta com tipo Manual.

CampoDescrição
NomeNome da função que o agente vai chamar (ex: solicitar_aprovacao). Deve seguir ^[a-zA-Z0-9_-]{1,64}$
DescriçãoExplicação detalhada de quando e como o agente deve usar esta ferramenta
ParâmetrosJSON Schema definindo o input esperado

Passo 2: Escreva Descrições Eficazes

Fator Mais Importante

Descrições de ferramentas são o fator mais importante para a performance da tool. Descrições ruins levam à seleção errada de ferramenta, parâmetros faltando e comportamento inesperado.

Sua descrição deve incluir:

  1. O que a ferramenta faz — Explicação clara da funcionalidade
  2. Quando usar — Cenários específicos e gatilhos
  3. Quando NÃO usar — Evitar confusão com ferramentas similares
  4. O que retorna — Formato de saída esperado
  5. Limitações — O que a ferramenta não pode fazer

Tenha como objetivo pelo menos 3-4 frases por descrição de ferramenta.

Descrições Boas vs. Ruins

text
Processa reembolsos de pedidos.
text
Processa um reembolso para um pedido de cliente. O order_id deve ser um
número de pedido válido do nosso sistema. Use esta ferramenta quando um
cliente solicitar explicitamente um reembolso e fornecer o número do pedido.
Antes de chamar, verifique se o pedido existe. Retorna o ID do reembolso
e tempo estimado de conclusão. Não processa trocas ou crédito em loja —
use as ferramentas apropriadas para esses cenários. Reembolsos são limitados
à janela de 30 dias para devolução.

Passo 3: Defina o Schema de Parâmetros

Use JSON Schema para definir quais argumentos a ferramenta aceita. Siga estas melhores práticas:

json
{
  "type": "object",
  "properties": {
    "order_id": {
      "type": "string",
      "description": "Número do pedido para reembolsar (ex: PED-12345)"
    },
    "motivo": {
      "type": "string",
      "enum": ["danificado", "item_errado", "diferente_do_anunciado", "desisti", "outro"],
      "description": "O motivo da solicitação de reembolso"
    },
    "valor": {
      "type": "number",
      "description": "Valor do reembolso parcial em BRL. Omita para reembolso total."
    }
  },
  "required": ["order_id", "motivo"],
  "additionalProperties": false
}

Melhores Práticas de Schema:

  • Use nomes de propriedades claros e descritivos (order_id não oid)
  • Inclua exemplos nas descrições (ex: PED-12345)
  • Use enum para valores restritos para reduzir erros
  • Adicione additionalProperties: false para validação mais rigorosa
  • Marque campos realmente obrigatórios no array required

Integrando com Sua Aplicação

Entendendo a Resposta da API

Quando o agente chama uma ferramenta manual, a resposta da API contém tool_calls em vez de conteúdo regular:

json
{
  "id": "msg_abc123",
  "thread_id": "thread_xyz789",
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "toolu_01A09q90qw90lq917835",
        "type": "function",
        "name": "solicitar_aprovacao",
        "input": {
          "acao": "reembolso",
          "valor": 500.00,
          "motivo": "Solicitação do cliente"
        }
      }]
    },
    "finish_reason": "tool_use"
  }]
}

Campos chave para verificar:

CampoDescrição
finish_reason: "tool_use"Indica que uma ferramenta foi chamada
tool_calls[].idCrítico: Você precisa deste ID para enviar o resultado de volta
tool_calls[].nameA ferramenta que foi chamada
tool_calls[].inputObjeto contendo os parâmetros

Enviando Resultados de Volta

Após processar a tool call, envie o resultado como uma nova mensagem:

json
{
  "role": "user",
  "content": [{
    "type": "tool_result",
    "tool_call_id": "toolu_01A09q90qw90lq917835",
    "content": "{\"aprovado\": true, \"aprovado_por\": \"gerente@empresa.com\"}"
  }]
}

Regras Importantes:

  • O tool_call_id deve corresponder exatamente ao id do tool_calls
  • O content pode ser uma string, string JSON ou array de content blocks
  • Após enviar esta mensagem, o agente continuará a execução

Ferramentas Manuais São Sempre Sequenciais

Ferramentas manuais são sempre solicitadas e entregues uma chamada por resposta do agente. O agente nunca emite várias tool calls manuais de uma vez, e nunca mistura uma tool call manual com uma chamada server-executed (Integração com API, Base de Conhecimento, MCP Servers ou Funções Internas) na mesma resposta — isso inclui ferramentas de controle de chamada como end_dialog, transfer_call, send_dtmf e receive_dtmf. Na prática, tool_calls na resposta da API contém exatamente uma entrada sempre que inclui uma ferramenta manual.

Chamadas paralelas de ferramentas existem na plataforma, mas apenas para ferramentas server-executed, e apenas em entradas de modelo do catálogo que foram validadas para isso. É uma capacidade do par modelo/provedor, tratada internamente pela SipPulse — você não configura isso, e nunca fica disponível para ferramentas manuais, independentemente do modelo usado pelo agente.

Como uma tool call manual é sempre única, enviar o resultado de volta é sempre um array content com um único item:

json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_call_id": "toolu_01_clima_sp",
      "content": "São Paulo: 28°C, parcialmente nublado"
    }
  ]
}

O tool_call_id ainda deve corresponder exatamente ao id recebido em tool_calls.

Erro unsafe_parallel_tool_calls

Se um provedor ignora a solicitação de chamadas seriais e retorna várias tool calls em uma mesma resposta mesmo assim, enquanto ferramentas manuais estão habilitadas no agente, a SipPulse não as executa silenciosamente — a execução para com um erro unsafe_parallel_tool_calls. Para corrigir, troque o agente para um modelo validado para chamadas seriais, ou remova as ferramentas manuais da configuração daquele agente. Veja a Solução de Problemas abaixo para mais detalhes.

Tratando Erros

Quando a execução da ferramenta falha, retorne o erro com is_error: true:

json
{
  "type": "tool_result",
  "tool_call_id": "toolu_01A09q90qw90lq917835",
  "content": "Pedido #12345 não foi encontrado ou não é elegível para reembolso.",
  "is_error": true
}

Como o agente trata erros:

  1. Incorpora o erro em sua resposta ao usuário
  2. Pode tentar novamente com parâmetros corrigidos (tipicamente 2-3 tentativas)
  3. Eventualmente pede desculpas e explica o problema

Diretrizes para mensagens de erro:

text
ENOTFOUND em db.orders.findById - conexão recusada
text
Não consegui encontrar um pedido com esse número. Você poderia verificar novamente?

Tipos de Conteúdo para Resultados

Resultados de ferramentas podem retornar diferentes tipos de conteúdo:

Texto (Mais Comum)

json
{
  "type": "tool_result",
  "tool_call_id": "toolu_xxx",
  "content": "O status do pedido é: Enviado. Previsão de entrega: 20 de janeiro."
}

JSON Estruturado

json
{
  "type": "tool_result",
  "tool_call_id": "toolu_xxx",
  "content": "{\"status\": \"enviado\", \"rastreio\": \"BR123456789BR\", \"previsao\": \"2025-01-20\"}"
}

Imagens (Base64)

json
{
  "type": "tool_result",
  "tool_call_id": "toolu_xxx",
  "content": [
    { "type": "text", "text": "Aqui está a imagem do produto:" },
    {
      "type": "image",
      "source": {
        "type": "base64",
        "media_type": "image/jpeg",
        "data": "/9j/4AAQSkZJRg..."
      }
    }
  ]
}

Resultado Vazio

Para ferramentas que executam ações sem retornar dados:

json
{
  "type": "tool_result",
  "tool_call_id": "toolu_xxx"
}

Considerações de Segurança

O Modelo Sugere — Você Executa

O LLM sugere chamadas de ferramenta mas não as executa. Este padrão é intencional e seguro:

1. Modelo propõe: { name: "excluir_conta", input: { user_id: "123" } }
2. SEU CÓDIGO valida a requisição
3. SEU CÓDIGO executa (ou rejeita)
4. SEU CÓDIGO retorna o resultado

Esta arquitetura significa que você tem controle total sobre o que realmente acontece.

Checklist de Validação

Antes de executar qualquer ferramenta:

  • Valide todos os parâmetros de entrada — Não confie cegamente no input do modelo
  • Verifique autorização do usuário — Garanta que o usuário pode executar esta ação
  • Confirme operações destrutivas — Considere requerer aprovação humana
  • Implemente rate limiting — Previna abuso de chamadas repetidas
  • Adicione circuit breakers — Pare se muitos erros ocorrerem

Exemplo: Validando Antes da Execução

typescript
async function handleToolCall(toolCall: ToolCall, userId: string) {
  const { name, input } = toolCall;

  // Valida formato do input
  if (name === 'processar_reembolso') {
    if (!isValidOrderId(input.order_id)) {
      return { error: 'Formato de ID do pedido inválido', is_error: true };
    }

    // Verifica autorização do usuário
    const order = await getOrder(input.order_id);
    if (order.user_id !== userId) {
      return { error: 'Você só pode reembolsar seus próprios pedidos', is_error: true };
    }

    // Verifica regras de negócio
    if (order.created_at < trintaDiasAtras()) {
      return { error: 'Pedido está fora da janela de 30 dias para reembolso', is_error: true };
    }

    // Executa o reembolso de fato
    return await processRefund(order, input.valor);
  }
}
python
async def handle_tool_call(tool_call: dict, user_id: str) -> dict:
    name = tool_call["name"]
    input_data = tool_call["input"]

    if name == "processar_reembolso":
        # Valida formato do input
        if not is_valid_order_id(input_data.get("order_id")):
            return {"error": "Formato de ID do pedido inválido", "is_error": True}

        # Verifica autorização do usuário
        order = await get_order(input_data["order_id"])
        if order.user_id != user_id:
            return {"error": "Você só pode reembolsar seus próprios pedidos", "is_error": True}

        # Verifica regras de negócio
        if order.created_at < trinta_dias_atras():
            return {"error": "Pedido está fora da janela de 30 dias para reembolso", "is_error": True}

        # Executa o reembolso de fato
        return await process_refund(order, input_data.get("valor"))

Exemplo Completo de Integração

typescript
import axios from 'axios';

const API_BASE = 'https://api.sippulse.ai';
const API_KEY = 'sua_chave_api';

interface ToolCall {
  id: string;
  name: string;
  input: Record<string, unknown>;
}

interface ToolResult {
  tool_call_id: string;
  content: string;
  is_error?: boolean;
}

// Envia mensagem e verifica tool calls
async function sendMessage(threadId: string, content: string) {
  const response = await axios.post(
    `${API_BASE}/threads/${threadId}/messages`,
    { role: 'user', content },
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  );

  const choice = response.data.choices[0];

  if (choice.finish_reason === 'tool_use' && choice.message.tool_calls) {
    return { type: 'tool_calls', toolCalls: choice.message.tool_calls };
  }

  return { type: 'message', content: choice.message.content };
}

// Processa tool calls e retorna resultados
async function handleToolCalls(toolCalls: ToolCall[]): Promise<ToolResult[]> {
  const results: ToolResult[] = [];

  for (const toolCall of toolCalls) {
    try {
      const result = await executeToolCall(toolCall);
      results.push({
        tool_call_id: toolCall.id,
        content: JSON.stringify(result)
      });
    } catch (error) {
      results.push({
        tool_call_id: toolCall.id,
        content: error.message,
        is_error: true
      });
    }
  }

  return results;
}

// Executa uma única tool call
async function executeToolCall(toolCall: ToolCall): Promise<unknown> {
  switch (toolCall.name) {
    case 'solicitar_aprovacao':
      // Mostra UI de aprovação, aguarda decisão humana
      const approved = await showApprovalDialog(toolCall.input);
      return {
        aprovado: approved,
        aprovado_por: approved ? getCurrentUser() : null,
        timestamp: new Date().toISOString()
      };

    case 'verificar_estoque':
      // Consulta sistema interno de estoque
      return await queryInventorySystem(toolCall.input.product_id as string);

    default:
      throw new Error(`Ferramenta desconhecida: ${toolCall.name}`);
  }
}

// Envia todos os resultados de volta (DEVE ser em uma única mensagem!)
async function sendToolResults(threadId: string, results: ToolResult[]) {
  const response = await axios.post(
    `${API_BASE}/threads/${threadId}/messages`,
    {
      role: 'user',
      content: results.map(r => ({
        type: 'tool_result',
        tool_call_id: r.tool_call_id,
        content: r.content,
        ...(r.is_error && { is_error: true })
      }))
    },
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  );

  return response.data;
}

// Loop principal de conversa
async function conversationLoop(threadId: string, userMessage: string) {
  let result = await sendMessage(threadId, userMessage);

  // Continua processando até obter mensagem final (não tool calls)
  while (result.type === 'tool_calls') {
    // Processa TODAS as tool calls
    const toolResults = await handleToolCalls(result.toolCalls);

    // Envia TODOS os resultados em UMA ÚNICA mensagem
    const nextResponse = await sendToolResults(threadId, toolResults);
    const choice = nextResponse.choices[0];

    if (choice.finish_reason === 'tool_use' && choice.message.tool_calls) {
      result = { type: 'tool_calls', toolCalls: choice.message.tool_calls };
    } else {
      result = { type: 'message', content: choice.message.content };
    }
  }

  console.log('Agente:', result.content);
}
python
import requests
import json
from typing import Optional
from dataclasses import dataclass

API_BASE = "https://api.sippulse.ai"
API_KEY = "sua_chave_api"

@dataclass
class ToolResult:
    tool_call_id: str
    content: str
    is_error: bool = False

def send_message(thread_id: str, content: str) -> dict:
    """Envia uma mensagem e retorna a resposta."""
    response = requests.post(
        f"{API_BASE}/threads/{thread_id}/messages",
        json={"role": "user", "content": content},
        headers={"Authorization": f"Bearer {API_KEY}"}
    )
    return response.json()

async def handle_tool_calls(tool_calls: list) -> list[ToolResult]:
    """Processa todas as tool calls e retorna resultados."""
    results = []

    for tool_call in tool_calls:
        try:
            result = await execute_tool_call(tool_call)
            results.append(ToolResult(
                tool_call_id=tool_call["id"],
                content=json.dumps(result)
            ))
        except Exception as e:
            results.append(ToolResult(
                tool_call_id=tool_call["id"],
                content=str(e),
                is_error=True
            ))

    return results

async def execute_tool_call(tool_call: dict) -> dict:
    """Executa uma única tool call."""
    name = tool_call["name"]
    input_data = tool_call["input"]

    if name == "solicitar_aprovacao":
        # Mostra UI de aprovação, aguarda decisão humana
        approved = await show_approval_dialog(input_data)
        return {
            "aprovado": approved,
            "aprovado_por": get_current_user() if approved else None,
            "timestamp": datetime.now().isoformat()
        }

    elif name == "verificar_estoque":
        # Consulta sistema interno de estoque
        return await query_inventory_system(input_data["product_id"])

    else:
        raise ValueError(f"Ferramenta desconhecida: {name}")

def send_tool_results(thread_id: str, results: list[ToolResult]) -> dict:
    """Envia TODOS os resultados em UMA ÚNICA mensagem."""
    content = [
        {
            "type": "tool_result",
            "tool_call_id": r.tool_call_id,
            "content": r.content,
            **({"is_error": True} if r.is_error else {})
        }
        for r in results
    ]

    response = requests.post(
        f"{API_BASE}/threads/{thread_id}/messages",
        json={"role": "user", "content": content},
        headers={"Authorization": f"Bearer {API_KEY}"}
    )
    return response.json()

async def conversation_loop(thread_id: str, user_message: str):
    """Handler principal de conversa com loop de tool call."""
    response = send_message(thread_id, user_message)
    choice = response["choices"][0]

    # Continua processando até obter mensagem final
    while choice.get("finish_reason") == "tool_use":
        tool_calls = choice["message"].get("tool_calls", [])

        # Processa TODAS as tool calls
        results = await handle_tool_calls(tool_calls)

        # Envia TODOS os resultados em UMA ÚNICA mensagem
        response = send_tool_results(thread_id, results)
        choice = response["choices"][0]

    print("Agente:", choice["message"]["content"])

Resumo de Melhores Práticas

PráticaPor Que Importa
Escreva descrições detalhadasFator mais importante para performance da ferramenta
Use enums para valores restritosReduz erros de parâmetros
Escolha um modelo validado para chamadas seriais em ferramentas manuaisEvita erros unsafe_parallel_tool_calls
Use is_error: true para falhasAgente trata erros graciosamente
Retorne mensagens de erro amigáveisMelhor experiência do usuário
Valide inputs antes da execuçãoSegurança e integridade de dados
Registre tool calls para debuggingFacilita troubleshooting
Implemente timeoutsPrevine operações travadas

Solução de Problemas

Agente Não Chama a Ferramenta

  • Verifique a descrição — Está detalhada o suficiente? Explica quando usar a ferramenta?
  • Verifique se a ferramenta está habilitada — Certifique-se de que está ativa na configuração de ferramentas do agente
  • Teste com prompts explícitos — Tente "Use a ferramenta solicitar_aprovacao para aprovar este reembolso"

Erro "Invalid tool_call_id"

  • Certifique-se de que está usando o id exato do array tool_calls
  • Não modifique ou gere seus próprios IDs

Agente Recebe Resultado Vazio ou Errado

  • Verifique se seu campo content é uma string ou JSON válido
  • Verifique se role é exatamente "user" com tipo de conteúdo tool_result
  • Para resultados JSON, garanta encoding correto com JSON.stringify() / json.dumps()

Thread Travada em Status "pending"

  • Você ainda não enviou o resultado da tool
  • Verifique se seu código de integração está enviando corretamente a mensagem tool_result
  • Verifique se a chamada da API foi bem sucedida (verifique erros na resposta)

Erro unsafe_parallel_tool_calls

  • Significa que um provedor retornou várias tool calls em uma única resposta enquanto o agente tem ferramentas manuais habilitadas, em vez de respeitar a solicitação de chamadas seriais
  • A SipPulse não tenta adivinhar como parear e executá-las — a execução para com este erro em vez de executar silenciosamente
  • Correção: troque o agente para um modelo validado para chamadas seriais, ou remova as ferramentas manuais da configuração daquele agente
  • Isso não é algo que seu código de integração pode contornar — é comportamento do provedor/modelo, resolvido pela escolha do modelo ou pela configuração de ferramentas

Misturar Ferramentas Manuais e Server-Executed

  • Você não precisa prevenir isso por conta própria — o runtime já garante que ferramentas manuais são solicitadas uma de cada vez e nunca combinadas com uma chamada server-executed (Integração com API, Base de Conhecimento, MCP, Funções Internas) na mesma resposta
  • Se você algum dia ver mais de uma tool call, ou uma chamada manual junto de uma server-executed, no mesmo array tool_calls, é o mesmo problema de fundo e ele aparece como o erro unsafe_parallel_tool_calls acima

Documentação Relacionada