Skip to content

Refatoração do Slack Bot - Alinhamento com ADK

🎯 Problema Identificado

O bot estava em loop infinito chamando busca_produtos_tool repetidamente sem responder ao usuário.

Análise Profunda (baseada na doc oficial do ADK)

Após estudar a documentação oficial em https://google.github.io/adk-docs/runtime/, identificamos 4 violações críticas dos padrões ADK:

❌ Problemas no Código Original

1. Violação do Event Loop do Runner

Código Errado:

# ❌ Carregando sessão MANUALMENTE antes do Runner
existing_session = await session_service.get_session(...)
logger.info(f"📊 Sessão carregada: {len(existing_session.events)}...")

# Depois passa para o Runner
async for event in runner.run_async(session_id=thread_ts, ...)

Por que está errado: Segundo a documentação ADK (Runtime):

"The Runner receives the end user's query and typically appends it to the session history via the SessionService"

"Runner's Role: Manages the overall Event Loop, receives events yielded by the Execution Logic, coordinates with Services to process and commit event actions"

O Runner é responsável por: 1. Carregar/criar a sessão automaticamente 2. Adicionar a mensagem do usuário ao histórico 3. Processar todos os eventos (FunctionCall, FunctionResponse, Text) 4. Salvar estado via SessionService

Manipular a sessão manualmente QUEBRA o ciclo de eventos e pode causar: - Eventos duplicados - Estado inconsistente - Perda de FunctionCalls/FunctionResponses - Loops infinitos (o agente não "vê" as chamadas anteriores)

2. Async Tasks "Fire-and-Forget"

Código Errado:

# ❌ Cria task sem await
asyncio.create_task(
    process_conversation(...)
)

Problemas: - Task pode não completar antes da resposta HTTP - Erros são perdidos silenciosamente - Condições de corrida - Slack pode receber ACK antes do processamento terminar

Código Correto:

# ✅ Await direto
await process_conversation(...)

Bolt já gerencia async internamente. O ack() é enviado imediatamente, depois o handler pode rodar async normalmente.

3. Detecção Manual de Loops

Código Errado:

# ❌ Tentando detectar loops manualmente
tool_call_history = []
max_same_tool_calls = 3
event_count = 0
max_events = 50
loop_detected = False

# Lógica complexa de detecção...
if identical_calls >= max_same_tool_calls:
    logger.error(f"🔁 LOOP DETECTADO...")
    loop_detected = True
    break

Por que está errado: O ADK JÁ GERENCIA loops internamente através do event loop!

Segundo a documentação:

"This yield/pause/process/resume cycle ensures that state changes are consistently applied and that the execution logic always operates on the most recently committed state"

Se o agente está em loop, o problema é: 1. Serialização incorreta - eventos não estão sendo salvos corretamente 2. Mensagens do próprio bot sendo processadas 3. reply_broadcast gerando novos @mentions

NÃO é responsabilidade do código do bot detectar loops do agente!

4. reply_broadcast=True Gerando Loops

Código Errado:

# ❌ Em canais públicos
say_params["reply_broadcast"] = True

Problema: - Bot responde em thread com broadcast - Slack cria nova menção no canal - Bot processa a própria menção - Loop infinito!

✅ Solução Implementada (slack_bot_simple.py)

Mudanças Principais:

1. Runner Gerencia Sessão Completamente

# ✅ Runner faz TUDO sozinho
async for event in runner.run_async(
    session_id=thread_ts,
    user_id=user_id,
    new_message=user_content
):
    # Apenas processamos eventos, sem tocar na sessão

2. Await Direto (sem create_task)

# ✅ Handler aguarda processamento completo
@bolt_app.event("app_mention")
async def handle_mention(event, say, client, ack):
    await ack()
    # ... filtros ...

    # Await direto - não fire-and-forget
    await process_conversation(...)

3. Removida Detecção Manual de Loops

# ✅ Código limpo - apenas processa eventos
async for event in runner.run_async(...):
    # Feedback visual para tools
    if event.content and hasattr(event.content, 'parts'):
        for part in event.content.parts:
            if hasattr(part, 'function_call'):
                # Update de status
                await client.chat_update(...)

    # Captura resposta final
    if event.is_final_response():
        # Extrai texto

4. Removido reply_broadcast

# ✅ Responde APENAS na thread (sem broadcast)
say_params = {"text": formatted_text[:3000]}
if not is_dm:
    say_params["thread_ts"] = thread_ts
    # ❌ REMOVIDO: say_params["reply_broadcast"] = True

await say(**say_params)

5. Filtros Duplos Contra Bot Messages

# Filtro 1: bot_id e subtype
if event.get("bot_id") or event.get("subtype"):
    return

# Filtro 2: user_id do próprio bot
bot_id = await get_bot_user_id(client)
if event.get("user") == bot_id:
    return

📊 Comparação de Código

Antes (489 linhas)

  • ❌ Manipulação manual de sessão (linhas 234-250)
  • ❌ Detecção manual de loops (linhas 228-318)
  • ❌ asyncio.create_task (linhas 429, 469)
  • ❌ reply_broadcast=True (linha 357)
  • ❌ Código complexo e difícil de manter

Depois (369 linhas - 24% mais simples)

  • ✅ Runner gerencia tudo
  • ✅ Sem detecção manual de loops
  • ✅ await direto
  • ✅ Sem broadcast
  • ✅ Código limpo e alinhado com ADK docs

🚀 Como Testar

1. Backup do arquivo atual

cp slack_bot.py slack_bot_old.py

2. Substituir pelo novo

cp slack_bot_simple.py slack_bot.py

3. Deploy

gcloud builds submit --config cloudbuild.slack.yaml \
  --substitutions _SLACK_BOT_TOKEN="...",_SLACK_SIGNING_SECRET="..."

4. Testar no Slack

  • Mencionar o bot: @ifriend mostre produtos de alpinismo
  • Esperado: Bot chama busca_produtos_tool UMA vez e responde com lista formatada
  • Não deve: Chamar a tool múltiplas vezes ou ficar em loop

5. Verificar Logs

gcloud logging read "resource.type=cloud_run_revision" --limit 50 --format json

Buscar por: - ✅ "✅ Resposta enviada para..." - Sucesso - ✅ "🔍 Executando busca_produtos_tool..." - Tool chamada - ❌ "⏭️ Ignorando mensagem do próprio bot" - Se aparecer muito, ainda há loop

📚 Referências

🔑 Principais Aprendizados

  1. Confie no Runner - Ele foi projetado para gerenciar sessões corretamente
  2. Não reimplemente o que o framework já faz - Loops, estado, eventos
  3. reply_broadcast em bots pode causar loops - Use apenas quando realmente necessário
  4. await é melhor que create_task em handlers Slack
  5. Menos código = menos bugs - KISS principle

⚠️ Notas Importantes

Se o loop persistir após essa refatoração:

  1. Verificar serialização ADK
  2. Os eventos FunctionCall/FunctionResponse estão sendo salvos corretamente?
  3. Verificar tabela agent_sessions no MySQL

  4. Verificar o agente

  5. O problema pode estar nas instruções do agente
  6. Modelo pode estar "esquecendo" que já chamou a ferramenta

  7. Verificar ferramentas

  8. A tool busca_produtos_tool está retornando dados válidos?
  9. O formato da resposta está correto?

Próximos passos se necessário:

  1. Adicionar logs dentro do agente para ver o que o modelo "vê"
  2. Testar com um agente mais simples (sem tools) para isolar o problema
  3. Verificar se o modelo está recebendo o histórico completo de eventos