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.

Configuração de Ferramentas Manuais

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

Tratando Chamadas Paralelas de Ferramentas

O agente pode chamar múltiplas ferramentas simultaneamente em uma única resposta. Isso é comum quando:

  • Usuário pede informações de diferentes fontes
  • Múltiplas operações independentes são necessárias
  • Ganhos de eficiência são possíveis

Regra Crítica

TODOS os resultados das ferramentas devem ser enviados em UMA ÚNICA mensagem. Enviá-los separadamente "ensina" o modelo a evitar chamadas paralelas no futuro.

Correto — Todos os resultados em uma mensagem:

json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_call_id": "toolu_01_clima_sp",
      "content": "São Paulo: 28°C, parcialmente nublado"
    },
    {
      "type": "tool_result",
      "tool_call_id": "toolu_02_clima_rj",
      "content": "Rio de Janeiro: 32°C, céu limpo"
    }
  ]
}

Errado — Mensagens separadas (causa problemas):

json
// ❌ Mensagem 1
{ "role": "user", "content": [{ "type": "tool_result", "tool_call_id": "toolu_01...", ... }] }

// ❌ Mensagem 2 - Isso quebra o padrão!
{ "role": "user", "content": [{ "type": "tool_result", "tool_call_id": "toolu_02...", ... }] }

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
Retorne todos os resultados em uma mensagemHabilita chamadas paralelas de ferramentas
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
  • Para chamadas paralelas, combine cada resultado com seu ID correspondente

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)

Chamadas Paralelas de Ferramentas Não Funcionam

  • Você está enviando todos os resultados em uma única mensagem?
  • Mensagens separadas "ensinam" o modelo a evitar chamadas paralelas
  • Verifique se o formato da mensagem corresponde aos exemplos acima

Documentação Relacionada