Pular para conteúdo

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

  1. Pausa por atendente humano
  2. Pausa via label no Chatwoot
  3. Follow-up multi-estágio com IA
  4. Tool registrar_compra
  5. Tool reportar → ClickUp Chat
  6. Reclassificação entre agentes do cluster
  7. Histórico compartilhado por tenant
  8. Múltiplos webhooks Uazapi
  9. Modo dry_run para testes
  10. Distributed lock no follow-up
  11. Endpoint seed-lead para automações externas
  12. 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.tshasBlockTag() 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:

  1. leads.follow_up_stage = -1 em todos os agentes do tenant (não só o atual)
  2. leads.status = 'converted'
  3. Label pausar-ia adicionada no Chatwoot em todas conversas abertas do contato
  4. Log lead.converted com agents_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_compra e responda com boas-vindas.

Onde está: packages/engine/src/services/tools.tshandleRegistrarCompra().

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.tspostChannelMessage() - packages/engine/src/services/tools.tshandleReportar() - packages/engine/src/integrations/crm/chatwoot.tsgetConversationUrl()

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.tsresolveTargetSlug().

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.tsgetContents().

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.tsAsyncLocalStorage 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.tsacquireLock() - packages/engine/src/pipeline/follow-up.tsprocessFollowUp() 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 como role=model em messages
  • name (opcional): nome do lead
  • qualified_data (opcional): objeto livre gravado em leads.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