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ário | Por Que Ferramentas Manuais |
|---|---|
| Aprovação humana | Requer confirmação humana antes de ações críticas |
| Fluxos de UI customizados | Coletar dados através de sua própria interface (formulários de pagamento, upload de arquivos) |
| Consultas a sistemas externos | Consultar seus próprios bancos de dados ou APIs internas |
| Lógica de negócio complexa | Executar processos de múltiplos passos em seu backend |
| Operações sensíveis | Manter lógica sensível inteiramente em sua infraestrutura |
Manual vs. Outros Tipos de Ferramentas
| Aspecto | Integração com API / RAG / MCP | Ferramentas Manuais |
|---|---|---|
| Execução | SipPulse executa automaticamente | Sua aplicação executa |
| Fluxo | Agente continua imediatamente | Agente pausa e aguarda |
| Controle | Configurado no SipPulse | Controle total no seu código |
| Caso de uso | Integrações padrão | Operaçõ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.
| Campo | Descrição |
|---|---|
| Nome | Nome da função que o agente vai chamar (ex: solicitar_aprovacao). Deve seguir ^[a-zA-Z0-9_-]{1,64}$ |
| Descrição | Explicação detalhada de quando e como o agente deve usar esta ferramenta |
| Parâmetros | JSON 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:
- O que a ferramenta faz — Explicação clara da funcionalidade
- Quando usar — Cenários específicos e gatilhos
- Quando NÃO usar — Evitar confusão com ferramentas similares
- O que retorna — Formato de saída esperado
- 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
Processa reembolsos de pedidos.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:
{
"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_idnãooid) - Inclua exemplos nas descrições (
ex: PED-12345) - Use
enumpara valores restritos para reduzir erros - Adicione
additionalProperties: falsepara 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:
{
"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:
| Campo | Descrição |
|---|---|
finish_reason: "tool_use" | Indica que uma ferramenta foi chamada |
tool_calls[].id | Crítico: Você precisa deste ID para enviar o resultado de volta |
tool_calls[].name | A ferramenta que foi chamada |
tool_calls[].input | Objeto contendo os parâmetros |
Enviando Resultados de Volta
Após processar a tool call, envie o resultado como uma nova mensagem:
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_call_id": "toolu_01A09q90qw90lq917835",
"content": "{\"aprovado\": true, \"aprovado_por\": \"gerente@empresa.com\"}"
}]
}Regras Importantes:
- O
tool_call_iddeve corresponder exatamente aoiddotool_calls - O
contentpode 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:
{
"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):
// ❌ 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:
{
"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:
- Incorpora o erro em sua resposta ao usuário
- Pode tentar novamente com parâmetros corrigidos (tipicamente 2-3 tentativas)
- Eventualmente pede desculpas e explica o problema
Diretrizes para mensagens de erro:
ENOTFOUND em db.orders.findById - conexão recusadaNã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)
{
"type": "tool_result",
"tool_call_id": "toolu_xxx",
"content": "O status do pedido é: Enviado. Previsão de entrega: 20 de janeiro."
}JSON Estruturado
{
"type": "tool_result",
"tool_call_id": "toolu_xxx",
"content": "{\"status\": \"enviado\", \"rastreio\": \"BR123456789BR\", \"previsao\": \"2025-01-20\"}"
}Imagens (Base64)
{
"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:
{
"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 resultadoEsta 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
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);
}
}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
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);
}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ática | Por Que Importa |
|---|---|
| Escreva descrições detalhadas | Fator mais importante para performance da ferramenta |
| Use enums para valores restritos | Reduz erros de parâmetros |
| Retorne todos os resultados em uma mensagem | Habilita chamadas paralelas de ferramentas |
Use is_error: true para falhas | Agente trata erros graciosamente |
| Retorne mensagens de erro amigáveis | Melhor experiência do usuário |
| Valide inputs antes da execução | Segurança e integridade de dados |
| Registre tool calls para debugging | Facilita troubleshooting |
| Implemente timeouts | Previne 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
idexato do arraytool_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údotool_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
- Visão Geral de Ferramentas — Entendendo ferramentas de agentes
- Integração com API — Chamadas de API externas auto-executadas
- Deploy via API — Deploy e integração via API
