Pular para conteúdo

Debounce e anti-overlap

Paciente no WhatsApp geralmente envia múltiplas mensagens seguidas:

[10:00:01] "oi"
[10:00:02] "tudo bem?"
[10:00:03] "queria saber"
[10:00:05] "sobre ultrassom"

Se cada mensagem dispara o LLM, o agente responde 4 vezes e bagunça a conversa. Solução: debounce.

Parâmetros

Projeto MESSAGE_DEBOUNCE_SECONDS
MEDCENTER 2
CLINFETO 10

Clinfeto tem 10s porque pacientes idosas/grávidas digitam devagar. Medcenter 2s porque a conversa é mais objetiva (menu numerado).

Fluxo

flowchart TD
    M1[Mensagem recebida] --> E[INSERT fila_mensagens<br/>phone + message_id + texto]
    E --> S[sleep MESSAGE_DEBOUNCE_SECONDS]
    S --> O{anti-overlap:<br/>sou a última mensagem<br/>desse telefone?}
    O -->|não| SKIP[skip - outra thread processa]
    O -->|sim| F[SELECT + DELETE toda a fila do telefone]
    F --> C[Concatena textos com espaço]
    C --> AG[run_agent com texto agregado]

Implementação

# workflow.py (resumo)
def clinfeto_workflow(payload):
    # ... filtros iniciais ...

    phone = session["contactPhoneNumber"]
    message_id = payload["content"].get("messageId", "")
    text = processar_conteudo(payload["content"])   # mídia -> texto

    enqueue_message(phone, message_id, text)
    time.sleep(MESSAGE_DEBOUNCE_SECONDS)

    # Anti-overlap: se outra thread já está processando, a fila estará vazia
    pending = fetch_and_clear_queue(phone)
    if not pending:
        return "skipped: another thread took over"

    combined = " ".join(pending)
    response = run_agent(
        message=combined,
        session_id=session_id,
        phone_number=phone,
        contact_name=session.get("contactName", ""),
    )

    # envio em partes...

Por que DELETE RETURNING

A leitura e deleção precisam ser atômicas. Se duas threads chegarem no mesmo telefone ao mesmo tempo:

DELETE FROM n8n_fila_mensagens_clinfeto
WHERE telefone = %s
RETURNING mensagem
ORDER BY id;

A primeira thread pega todas as mensagens e as remove; a segunda thread faz o SELECT e recebe [] → retorna sem processar.

Anti-overlap detalhado

Há outro caso: paciente manda mensagem, agente está respondendo (5s), e paciente manda mais uma mensagem. Sem anti-overlap, poderíamos responder duas vezes.

Com o pattern acima, a segunda mensagem:

  1. Vai para a fila.
  2. sleep(debounce).
  3. Tenta fetch_and_clear_queue.
  4. A primeira thread ainda está no LLM — a fila tem só a mensagem 2.
  5. Processa a 2 isoladamente (ok — se fosse muito próxima, o debounce teria juntado).

Edge cases

Mensagem durante o turn do agente

Se o paciente manda "nova mensagem" enquanto o agente está processando:

  • Nova mensagem entra na fila.
  • Após debounce, é processada em novo invoke com o mesmo thread_id.
  • LangGraph pega o histórico e o agente continua.

Não há race no LLM — invoke é thread-safe (cada thread tem seu ctxvar).

Mensagem depois da transferência

Se o paciente manda após transfer_session:

  • ALLOWED_TEAM e status filtram no topo do workflow.
  • Sessão agora é da equipe humana; o agente ignora.

Debounce muito alto (> 30s)

Paciente pensa que ficou sem resposta e manda mensagens repetidas. Fila cresce. Não há problema técnico, mas UX é ruim. Não passar de ~15s.

Por que não usar fila Redis / Celery

Overkill para este volume. Postgres como fila é adequado até ~100 mensagens/minuto por instância. Se precisar escalar, aí sim.

Tabela de fila

CREATE TABLE IF NOT EXISTS n8n_fila_mensagens_<projeto> (
    id SERIAL PRIMARY KEY,
    id_mensagem TEXT NOT NULL DEFAULT '',
    telefone TEXT NOT NULL,
    mensagem TEXT NOT NULL,
    timestamp TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_fila_<projeto>_telefone ON n8n_fila_mensagens_<projeto>(telefone);