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
Runnerreceives the end user's query and typically appends it to the session history via theSessionService""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_toolUMA 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¶
- Confie no Runner - Ele foi projetado para gerenciar sessões corretamente
- Não reimplemente o que o framework já faz - Loops, estado, eventos
- reply_broadcast em bots pode causar loops - Use apenas quando realmente necessário
- await é melhor que create_task em handlers Slack
- Menos código = menos bugs - KISS principle
⚠️ Notas Importantes¶
Se o loop persistir após essa refatoração:¶
- Verificar serialização ADK
- Os eventos FunctionCall/FunctionResponse estão sendo salvos corretamente?
-
Verificar tabela
agent_sessionsno MySQL -
Verificar o agente
- O problema pode estar nas instruções do agente
-
Modelo pode estar "esquecendo" que já chamou a ferramenta
-
Verificar ferramentas
- A tool
busca_produtos_toolestá retornando dados válidos? - O formato da resposta está correto?
Próximos passos se necessário:¶
- Adicionar logs dentro do agente para ver o que o modelo "vê"
- Testar com um agente mais simples (sem tools) para isolar o problema
- Verificar se o modelo está recebendo o histórico completo de eventos