Features Reutilizáveis — Gita Agents¶
Conjunto de capabilities construídas durante o projeto Janaina Ortiga que ficam disponíveis para qualquer novo cliente. Cada feature abaixo é opt-in via configuração do agente, sem mudar código.
Esse documento é a fonte de verdade para decidir o que ativar em um cliente novo. Veja também janaina-ortiga-agente.md para o uso real dessas features em produção.
Índice¶
- Pausa por atendente humano
- Pausa via label no Chatwoot
- Follow-up multi-estágio com IA
- Tool
registrar_compra - Tool
reportar→ ClickUp Chat - Reclassificação entre agentes do cluster
- Histórico compartilhado por tenant
- Múltiplos webhooks Uazapi
- Modo dry_run para testes
- Distributed lock no follow-up
- Endpoint
seed-leadpara automações externas - Endpoint
chatwoot/inbound-message— captura durante pausa
1. Pausa por atendente humano (block_ia)¶
Quando um atendente responde manualmente via WhatsApp, a IA para de responder por um tempo configurável.
Como funciona: o filtro own_message (fromMe) detecta mensagens enviadas pela própria instância. Ao detectar, grava uma chave Redis block:{agentId}:{remoteJid} com TTL de block_ia_ttl_minutes minutos. Enquanto essa chave existir, toda mensagem do contato é ignorada com reason: block_ia_human.
Ativação (por agente):
{
"block_ia_enabled": true,
"block_ia_ttl_minutes": 360
}
Onde está: packages/engine/src/pipeline/filters.ts — filtro checkBlockIA.
Quando usar: todo cliente com atendimento humano simultâneo à IA. Padrão recomendado: 6 horas.
Limite: o follow-up também respeita essa chave (verificado em follow-up.ts antes de disparar).
2. Pausa via label no Chatwoot (pausar-ia)¶
Operador humano adiciona a label pausar-ia em uma conversa no Chatwoot e a IA para de responder imediatamente. Remove a label e a IA volta.
Como funciona: antes de processar cada mensagem, o filtro consulta o Chatwoot e verifica se alguma conversa do contato tem a label configurada. Se tiver, bloqueia silenciosamente (reason: crm_pausar_ia).
Ativação (por agente):
{
"crm_provider": "chatwoot",
"crm_config": {
"api_url": "https://atendimento.gita.work",
"token": "<api_access_token>",
"account_id": "3",
"block_label": "pausar-ia"
}
}
Onde está:
- packages/engine/src/integrations/crm/chatwoot.ts — hasBlockTag() e addBlockTag()
- packages/engine/src/pipeline/filters.ts — cache CRM de 30s
- packages/engine/src/pipeline/follow-up.ts — respeita a label antes de disparar
Cache: 30s por contato. Pausa/retomada reflete em até 30s.
Pontos de atenção:
- Busca tenta JID completo ([email protected]) antes do número puro — necessário quando o contato não tem phone_number cadastrado
- Cache só é escrito se a chamada ao Chatwoot for bem-sucedida (falha silenciosa não polui cache com false positives)
Quando usar: qualquer cliente com Chatwoot. Recomendado para todos com operação comercial ativa.
3. Follow-up multi-estágio com IA¶
Agente reengaja leads inativos com N mensagens sequenciais. Cada mensagem é gerada pelo Gemini lendo o histórico da conversa — adapta ao contexto, não é copy fixa.
Como funciona: loop a cada 60s (com distributed lock) busca leads elegíveis onde follow_up_stage < total_etapas E o tempo decorrido desde updated_at ou follow_up_last_at excede o intervalo da próxima etapa. Gera mensagem via Gemini e envia.
Ativação (por agente):
{
"follow_up_enabled": true,
"follow_up_intervals": [120, 1440, 4320],
"follow_up_prompts": [
{ "stage": 1, "mode": "ai", "prompt": "Instrução da primeira mensagem..." },
{ "stage": 2, "mode": "ai", "prompt": "Instrução da segunda..." },
{ "stage": 3, "mode": "ai", "prompt": "Encerramento gentil..." }
]
}
follow_up_intervals: minutos entre etapas (ex: 120 = 2h; 1440 = 1d)mode:"ai"gera com Gemini usando histórico;"fixed"envia o prompt literal
Onde está: packages/engine/src/pipeline/follow-up.ts.
Quando para automaticamente:
- Lead responde → stage reseta pra 0
- Tool registrar_compra → stage vira -1 (para permanente)
- Lead completa todas as etapas
- Label pausar-ia presente no Chatwoot
Exemplo real: Janaina ortiga-conversao usa 2h / 1d / 3d. Cada prompt orienta a Isis a ler o histórico e adaptar (retomar onde parou, tratar objeção mencionada, etc).
4. Tool registrar_compra¶
Tool que o agente chama quando a lojista confirma que comprou. Efeitos:
leads.follow_up_stage = -1em todos os agentes do tenant (não só o atual)leads.status = 'converted'- Label
pausar-iaadicionada no Chatwoot em todas conversas abertas do contato - Log
lead.convertedcomagents_updated(quantos leads paralelos foram pausados)
Ativação: INSERT na tabela tools para o agente:
INSERT INTO tools (agent_id, name, is_active, handler_type, function_declaration)
SELECT id, 'registrar_compra', true, 'builtin',
'{"name":"registrar_compra","description":"Chame quando a lojista confirmar que concluiu a compra. Encerra o follow-up automático.","parameters":{"type":"object","properties":{"forma_pagamento":{"type":"string"}},"required":[]}}'::jsonb
FROM ai_agents WHERE slug = '<slug>';
Prompt: adicionar instrução como:
Após enviar o link, pergunte "Conseguiu finalizar a inscrição?". Se ela confirmar, chame
registrar_comprae responda com boas-vindas.
Onde está: packages/engine/src/services/tools.ts — handleRegistrarCompra().
Quando usar: agentes de conversão/vendas que precisam saber fechar o ciclo.
5. Tool reportar → ClickUp Chat¶
Tool de escalonamento para humano. A Isis identifica situações que não deve resolver sozinha (desconto fora do padrão, problema técnico, pedido de NF, etc.) e chama reportar. O engine posta mensagem formatada no canal ClickUp configurado, com link direto pra conversa no Chatwoot.
Formato da notificação:
🔔 Lead reportado — Isis — Conversão Janaina Ortiga
Contato: +556196701770
Resumo: {"motivo":"desconto_fora_padrao","resumo":"..."}
Chatwoot: https://atendimento.gita.work/app/accounts/3/conversations/23895
Ativação (por agente):
{
"notification_clickup_channel_id": "8cgqa6x-12233"
}
Env vars do engine:
CLICKUP_TOKEN=pk_...
CLICKUP_WORKSPACE_ID=9010129117
Onde está:
- packages/engine/src/integrations/clickup/chat.ts — postChannelMessage()
- packages/engine/src/services/tools.ts — handleReportar()
- packages/engine/src/integrations/crm/chatwoot.ts — getConversationUrl()
Retrocompat: se o agente tem notification_number preenchido, continua mandando WhatsApp em paralelo. Se quiser só ClickUp, deixa notification_number: null.
Quando usar: qualquer cliente onde o time comercial opera via ClickUp Chat. Para Slack/outros, precisa nova integração seguindo o mesmo padrão.
6. Reclassificação entre agentes do cluster¶
Permite que a lojista comece conversando com um agente e, ao sinalizar outra intenção, seja redirecionada automaticamente para o agente correto — mantendo o histórico da conversa.
Como funciona: a cada mensagem, o router:
1. Classifica a mensagem com as regex
2. Se matched=true e o slug classificado ≠ sticky atual → troca (router.reclassified)
3. Se matched=false → mantém sticky (router.sticky)
4. Sem lead → usa resultado do classifier (default do tenant)
Ativação: automática para todos os clientes com router. Nenhuma configuração por cliente.
Onde está: packages/engine/src/pipeline/router-classifier.ts — resolveTargetSlug().
Ordenação: o sticky busca o lead com maior updated_at (não created_at) dentro do tenant. Ao reclassificar, o lead do novo agente tem updated_at tocado — vira o "mais recente". Toda interação também toca updated_at (em orchestrator.ts após getOrCreateLead).
Logs:
- router.reclassified: {from, to, textSample} — troca aconteceu
- router.sticky: {resolvedSlug, via: "lead"} — mantém agente anterior
- router.classified: sem lead, usa classifier
Quando usar: clientes com múltiplos agentes cobrindo funções diferentes (captação, conversão, suporte). Se o cliente tem só um agente, a feature não é exercitada mas não atrapalha.
7. Histórico compartilhado por tenant¶
Quando a reclassificação troca de agente no meio da conversa, o novo agente vê todo o histórico anterior — evitando perda de contexto.
Como funciona: getContents(agentId, remoteJid, clientName?) aceita clientName opcional. Quando passado, a query busca mensagens de todos os agentes do tenant. Para mensagens de outros agentes, filtra só parts com text — function calls (ex: registrar_compra) e thought_signature ficam isoladas no agente de origem para não confundir o Gemini do agente novo (que não declara essa tool).
Ativação: automática — o orchestrator.ts e follow-up.ts passam agent.client_name para getContents.
Onde está: packages/engine/src/services/history.ts — getContents().
Trade-off considerado: incluir function calls seria mais fiel à conversa, mas tool não declarada vira erro no Gemini. Filtrar só texto mantém o fio da conversa humana sem contaminar prompts.
Limite: 40 mensagens mais recentes (o mesmo do modo single-agent).
8. Múltiplos webhooks Uazapi (Chatwoot + IA coexistindo)¶
A instância Uazapi de um cliente pode enviar eventos para múltiplos webhooks em paralelo. Isso permite que o Chatwoot (via n8n) e o engine da IA convivam na mesma instância sem conflito.
Endpoint (documentação da Uazapi):
- GET https://grupogita.uazapi.com/webhook — lista webhooks da instância
- POST https://grupogita.uazapi.com/webhook — gerencia webhooks (add/update/delete via action)
Adicionar novo webhook sem tocar existente:
curl -X POST "https://grupogita.uazapi.com/webhook" \
-H "Content-Type: application/json" \
-H "token: <TOKEN_INSTANCIA>" \
-d '{
"action": "add",
"url": "https://<engine-host>/webhook/whatsapp/<slug-router>",
"events": ["messages"],
"excludeMessages": ["wasSentByApi"],
"enabled": true
}'
Pontos críticos:
- action: "add" não sobrescreve webhooks existentes. O modo simples (sem action) sobrescreve — evitar.
- excludeMessages: ["wasSentByApi"] é obrigatório para evitar loop infinito (IA envia pela API, receberia a própria mensagem como evento).
- events: ["messages"] limita ao essencial. Outros eventos (messages_update, connection) podem gerar ruído no engine.
Resposta bem-sucedida traz o ID gerado — guardar para delete/update futuro.
Quando usar: todo cliente migrando do N8N que já tem um webhook Chatwoot ativo. Não remover o do Chatwoot.
9. Modo dry_run para testes automatizados¶
Permite simular conversas completas com agentes reais sem enviar mensagens via Uazapi, sem postar no ClickUp, sem aplicar labels no Chatwoot. O pipeline roda integralmente: router, classifier, Gemini, tools, história — só os efeitos externos são bloqueados.
Requisitos de ativação (todos obrigatórios):
1. Header X-Gita-Dry-Run: 1
2. Header X-Test-Secret: <valor-de-GITA_TEST_SECRET>
3. remote_jid (chatid no payload) deve começar com test_
Se qualquer um falhar, o webhook é rejeitado.
Env var do engine:
GITA_TEST_SECRET=<random hex 32>
Onde está:
- packages/engine/src/services/request-context.ts — AsyncLocalStorage pra propagar o flag sem mudar assinaturas
- packages/engine/src/index.ts — validação no webhook handler
- packages/engine/src/pipeline/sender.ts — skip de Uazapi + delays de digitação
- packages/engine/src/services/tools.ts — skip de ClickUp e addBlockTag no Chatwoot
- scripts/test-chat.ts — CLI para rodar conversas de teste
Script de teste (rodar local):
export ENGINE_URL="https://<engine-host>"
export GITA_TEST_SECRET="<secret>"
export SUPABASE_URL="https://<project>.supabase.co"
export SUPABASE_SERVICE_KEY="<service-key>"
npx tsx scripts/test-chat.ts \
--slug janaina-router \
--jid test_556100000001 \
--reset \
"olá" \
"quero fazer parte da Formação" \
"tem aula ao vivo?"
O script posta cada mensagem, aguarda pipeline.completed no Supabase, e imprime qual agente respondeu + eventos de roteamento + function calls.
Efeito em produção: zero quando o header não vem — AsyncLocalStorage.getStore() retorna undefined e todos os checks são no-op.
Quando usar: validar mudanças em prompts, regex de classificação, comportamento de tools, antes de deployar.
10. Distributed lock no follow-up¶
Quando o engine roda com múltiplas réplicas (escala horizontal no Easypanel), todas tentam processar follow-up a cada 60s. Sem coordenação, cada lead receberia N mensagens (uma por réplica). O lock distribuído garante exclusão mútua.
Como funciona: no início de cada ciclo, o engine tenta SET NX EX 55 no Redis com chave lock:followup:cycle. Só quem consegue setar executa o ciclo. TTL de 55s < intervalo de 60s — se a instância crashar no meio, o lock expira antes do próximo tick e outra réplica assume.
Ativação: automática. Nenhuma configuração.
Onde está:
- packages/engine/src/services/distributed-lock.ts — acquireLock()
- packages/engine/src/pipeline/follow-up.ts — processFollowUp() envolvido no lock
Quando importa: qualquer deploy com 2+ réplicas. Também garante que deploys seguidos (container novo sobe antes do antigo terminar) não gerem mensagens duplicadas.
11. Endpoint seed-lead para automações externas¶
Quando uma automação externa (N8N, Zapier, formulário) dispara uma mensagem inicial de WhatsApp antes da IA entrar, o lead responde sem a IA saber o que foi dito. Esse endpoint resolve: a automação chama o engine, que envia a mensagem e salva no histórico — a IA vê tudo como se tivesse respondido.
Endpoint: POST /webhook/seed-lead/:agentSlug
Body:
{
"phone": "61987654321",
"name": "Maria Lojista",
"copy": "Oi! Tudo bem? Aqui é a Ísis, da equipe da Janaina. Vi que você entrou na lista de espera...",
"qualified_data": {
"fase_funil": "lista_espera",
"origem": "formulario_site"
}
}
phone(obrigatório): com ou sem DDI.61987654321→[email protected]copy(obrigatório): texto que será enviado via Uazapi e salvo comorole=modelemmessagesname(opcional): nome do leadqualified_data(opcional): objeto livre gravado emleads.qualified_data. Útil pro prompt usar (ex:{{fase_funil}})
Auth: X-Webhook-Secret: <provider_config.webhook_secret do agente> — o mesmo secret que o webhook principal usa.
Fluxo:
1. getOrCreateLead — cria lead se não existir, devolve existente
2. Se qualified_data veio, atualiza via updateLeadQualification (status vira qualified)
3. Envia a copy via sendMessage (provider do agente — Uazapi, Evolution, etc)
4. Só se o send deu certo: salva em messages com role=model
5. Loga lead.seeded com {isNew, qualified_data, copyLength}
Resposta:
{ "leadId": "uuid", "jid": "[email protected]", "isNew": true }
Onde está: packages/engine/src/index.ts — handler /webhook/seed-lead/:agentSlug.
Exemplo de uso (N8N):
Google Sheets Trigger (novo row no formulário de lista de espera)
│
▼
HTTP Request: POST https://<engine>/webhook/seed-lead/janaina-captacao
Headers: X-Webhook-Secret: <secret>
Body: { phone, name, copy, qualified_data: {fase_funil: "lista_espera"} }
Substitui o workflow antigo de 2 nodes (send + eventual save manual) por 1 node só. Se o lead responder, a IA continua de onde parou.
Quando usar: - Qualquer cliente que tenha automação externa disparando mensagens iniciais (formulários, import de lista, gatilho de tráfego pago) - Migração de base de leads de outro sistema (disparar copy + deixar IA continuar)
12. Endpoint chatwoot/inbound-message — captura durante pausa¶
Quando a label pausar-ia bloqueia a IA, mensagens trocadas entre lead e atendente humano ficam fora do contexto. Ao remover a label, a IA voltava "cega". Esse endpoint resolve: o Chatwoot dispara webhook a cada mensagem em conversa com a label pausar-ia, o engine salva no histórico do lead.
Endpoint: POST /webhook/chatwoot/inbound-message
Como configurar no Chatwoot:
1. Settings → Automation → New automation
2. Condição: Label is pausar-ia + Event: Message Created
3. Action: Send a webhook → URL: https://<engine>/webhook/chatwoot/inbound-message
Mapeamento:
- message_type: 0 (incoming, lead) → role: user
- message_type: 1 (outgoing, atendente) → role: model
- private: true → ignorado (nota interna)
Como identifica o tenant: busca ai_agents com crm_config.account_id igual ao account_id do payload. Se múltiplos agentes (normal), usa o lead mais recente do contato dentro do tenant — mesma lógica do sticky routing.
Formatos aceitos:
- Payload direto do Chatwoot
- Array-wrapper do n8n ([{ body: <chatwoot_payload> }]) — caso passe por n8n antes
Onde está: packages/engine/src/index.ts — handler /webhook/chatwoot/inbound-message.
Efeitos colaterais: toca updated_at do lead → sticky continua coerente após a pausa.
Logs:
- chatwoot.message_saved — tudo ok
- chatwoot.message_skipped com reason: no_lead — contato não tem lead no tenant
Quando usar: todo cliente que usa label pausar-ia. Complementa a feature #2 — sem esse endpoint, o contexto fica truncado entre as mensagens anterior/posterior à pausa.
Checklist de adoção em cliente novo¶
Ao migrar um cliente novo do N8N, considere quais features ativar:
| Feature | Recomendado para... | Configuração necessária |
|---|---|---|
block_ia |
Todo cliente com atendimento humano | block_ia_enabled: true, block_ia_ttl_minutes: 360 |
Label pausar-ia |
Todo cliente com Chatwoot | crm_provider: "chatwoot", crm_config: {...} |
| Follow-up IA | Agentes de conversão/venda | follow_up_enabled: true, intervals, prompts |
registrar_compra |
Agentes que fecham venda | INSERT na tools + instrução no prompt |
reportar ClickUp |
Times que operam via ClickUp | notification_clickup_channel_id |
| Reclassificação | Clientes com 2+ agentes especializados | Automático |
| Histórico compartilhado | Clientes com 2+ agentes especializados | Automático |
| Múltiplos webhooks | Clientes com Chatwoot+IA convivendo | Rodar action: add na Uazapi |
dry_run |
Todo cliente (ajuda em testes) | Env var GITA_TEST_SECRET no engine |
| Distributed lock | Automático | Nenhuma |
seed-lead endpoint |
Cliente com automação externa disparando copy inicial | Rodar via N8N ou similar com X-Webhook-Secret |
chatwoot/inbound-message |
Cliente com label pausar-ia ativa |
Automation no Chatwoot disparando webhook |
Onde cada feature vive no código¶
packages/engine/src/
├── config.ts # env vars (CLICKUP_TOKEN, GITA_TEST_SECRET, etc)
├── index.ts # webhook handler (dry_run check)
├── integrations/
│ ├── clickup/chat.ts # (5) postChannelMessage
│ └── crm/chatwoot.ts # (2) hasBlockTag, addBlockTag, getConversationUrl
├── pipeline/
│ ├── filters.ts # (1)(2) block_ia + CRM tag + cache
│ ├── follow-up.ts # (3)(10) follow-up + distributed lock
│ ├── orchestrator.ts # processWebhook, toca updated_at
│ ├── router-classifier.ts # (6) sticky + reclassificação
│ └── sender.ts # (9) skip em dry_run
└── services/
├── distributed-lock.ts # (10) acquireLock
├── history.ts # (7) getContents com clientName
├── request-context.ts # (9) AsyncLocalStorage pra dry_run
└── tools.ts # (4)(5) handleRegistrarCompra, handleReportar