~ / tutoriais /criar-bot-no-slack-com-claude $ _

Como criar um bot no Slack com Claude: um agente de dados no seu workspace

Lucas Souza Lucas Souza 11 min de leitura Tutoriais
Como criar um bot no Slack com Claude: um agente de dados no seu workspace

O melhor lugar pra colocar um agente de dados não é um dashboard novo que ninguém abre. É onde o time já vive o dia inteiro: o Slack.

Pensa no fluxo real. Alguém do comercial quer saber por que as vendas caíram na última semana. Hoje isso vira um pedido pro time de dados, que abre um notebook, roda um groupby, exporta um gráfico e cola no canal. Três pessoas, meia hora, pra responder uma pergunta que o dado já sabia.

Neste tutorial você vai criar um bot no Slack que encurta esse caminho pra uma mensagem. O usuário marca o bot com uma pergunta e um CSV anexado. O bot responde, no próprio thread, com a análise feita por um agente rodando em Claude Managed Agents — o runtime hospedado da Anthropic pra agentes que usam ferramentas e mantêm estado. É o caminho do cookbook oficial da Anthropic, traduzido pra dor de produção.

TL;DR

  • O que é: um bot de Slack que recebe um CSV + pergunta e devolve análise de dados no thread, com follow-ups na mesma sessão.
  • Stack: Python, Bolt for Python em Socket Mode, SDK da Anthropic (Managed Agents), markdown-to-mrkdwn.
  • Modelo: claude-sonnet-4-6 rodando como agente com toolset agent_toolset_20260401 (bash, read, write, edit).
  • Custo/Acesso: chave da Anthropic com créditos + um app no Slack (gratuito). Managed Agents está em beta.
  • Repositório: anthropics/claude-cookbooks → managed_agents.

O contexto: por que o Slack e não mais um painel

Ferramenta de dados boa morre por falta de uso, não por falta de feature. Você constrói o dashboard perfeito, manda o link no canal, e em duas semanas ninguém lembra que ele existe. O dado fica numa aba que compete com vinte outras.

O Slack inverte isso. A interface já está aberta, o time já está logado, e perguntar é tão barato quanto digitar. Colocar o agente ali não é conveniência — é onde a barreira de uso vai a zero.

E tem uma diferença técnica que importa: não é um chatbot que responde com texto bonito. É um agente. Ele recebe o CSV, escreve código Python de verdade (pandas, plotly), roda esse código num container isolado, lê o resultado e só então te responde. A análise não é alucinada a partir do nome das colunas. Ela é computada.

No cookbook, o agente de dados pega um CSV e devolve um relatório HTML narrativo com gráficos interativos e recomendações — usando bash pra rodar scripts e read/write pra manipular arquivos. O que a gente vai fazer aqui é vestir esse agente numa interface de Slack.

Pré-requisitos e ferramentas

Antes de escrever uma linha, você precisa de:

  • [ ] Chave de API da Anthropic com créditos (ANTHROPIC_API_KEY).
  • [ ] Um app criado no Slack API dashboard com Socket Mode habilitado.
  • [ ] Python 3.11+ e os pacotes: slack-bolt, anthropic, requests, markdown-to-mrkdwn.
  • [ ] O agente de dados já criado (a parte 1 do cookbook — o notebook data_analyst_agent.ipynb).

O bot deste tutorial não cria o agente. Ele assume que o agente e o environment já existem — você os cria uma vez, rodando o notebook do cookbook, e guarda os IDs. Esse é o padrão correto de Managed Agents: agente é recurso persistente e versionado, criado uma vez; sessão é efêmera, criada a cada pergunta.

No Slack, configure o app token com o scope connections:write (é o que liga o Socket Mode) e habilite os eventos app_mention e message. Os scopes de bot precisam cobrir leitura de menções, escrita de mensagens e acesso a arquivos — o bot baixa o CSV anexado e sobe os gráficos de volta.

Mão na massa: criar o bot no Slack

Passo 1: o agente de dados (a base que já existe)

O agente é criado uma vez, fora do bot. Vale entender como ele é montado, porque é isso que o bot vai invocar. O environment pré-instala as libs de análise:

env = client.beta.environments.create(
    name="cookbook-data-analyst-env",
    config={
        "type": "cloud",
        "networking": {"type": "unrestricted"},
        "packages": {"type": "packages", "pip": ["pandas", "plotly"]},
    },
)

E o agente fixa o modelo, o system prompt e o toolset:

agent = client.beta.agents.create(
    name="cookbook-data-analyst",
    model="claude-sonnet-4-6",
    system=ANALYST_SYSTEM_PROMPT,
    tools=[{
        "type": "agent_toolset_20260401",
        "default_config": {"enabled": True, "permission_policy": {"type": "always_allow"}},
        "configs": [
            {"name": "web_search", "enabled": False},
            {"name": "web_fetch", "enabled": False},
        ],
    }],
)

Guarde agent.id, agent.version e env.id. O bot lê esses três do .env. Pronto: a inteligência já está de pé. O resto é encanamento de Slack.

Passo 2: o handler de menção

Com Bolt, responder a uma menção é um decorator. A regra de ouro aqui é o ack(): o Slack exige uma resposta em até 3 segundos, e análise de dados leva mais que isso. Então você confirma na hora e joga o trabalho pesado pra uma thread de background.

@app.event("app_mention")
def on_mention(event, say, ack):
    ack()  # responde ao Slack em < 3s
    channel = event["channel"]
    thread_ts = event.get("thread_ts") or event["ts"]
    question = event["text"].split(">", 1)[-1].strip()
    slack_file = (event.get("files") or [None])[0]

    say(text="Beleza. Analisando agora.", thread_ts=thread_ts)
    threading.Thread(
        target=start_analysis,
        args=(channel, thread_ts, question, slack_file),
    ).start()

Repara no thread_ts. Ele é a chave de tudo: é o identificador do thread, e vai ser o que amarra cada conversa a uma sessão do agente.

Passo 3: subir o CSV e abrir a sessão

Aqui o bot faz a ponte entre dois mundos. Baixa o arquivo do Slack (autenticado com o token do bot), sobe pra Files API da Anthropic, e monta esse arquivo no filesystem da sessão:

def start_analysis(channel, thread_ts, question, slack_file):
    resources = []
    if slack_file:
        resp = requests.get(
            slack_file["url_private"],
            headers={"Authorization": f"Bearer {app.client.token}"},
            timeout=30,
        )
        uploaded = client.beta.files.upload(
            file=(slack_file["name"], io.BytesIO(resp.content), mime)
        )
        mount = "/mnt/session/uploads/data.csv"
        resources.append({"type": "file", "file_id": uploaded.id, "mount_path": mount})
        question += f"\n\nThe data is mounted at {mount}."

    session = client.beta.sessions.create(
        environment_id=ANALYST_ENV_ID,
        agent={"type": "agent", **ANALYST_AGENT},
        resources=resources,
        metadata={"slack_channel": channel, "slack_thread_ts": thread_ts},
    )
    thread_sessions[thread_ts] = session.id

    client.beta.sessions.events.send(
        session.id,
        events=[{"type": "user.message", "content": [{"type": "text", "text": question}]}],
    )
    relay_stream(session.id, channel, thread_ts)

A linha que faz a mágica de continuidade é thread_sessions[thread_ts] = session.id. É um dicionário simples mapeando thread → sessão. Guarde isso, porque é o que vai permitir que o usuário continue a conversa.

Dica de ouro: note que a gente avisa o agente onde o arquivo está (The data is mounted at {mount}). O modelo não adivinha o path. Você diz, em texto, onde os dados foram montados — e o agente abre o arquivo a partir dali.

Passo 4: streamar a resposta de volta pro thread

O agente trabalha em etapas: pensa, roda código, lê o resultado, escreve a conclusão. O bot escuta esse stream de eventos e traduz pro Slack — um aviso de "rodando" no primeiro uso de ferramenta, e o texto final quando a sessão fica ociosa.

def relay_stream(session_id, channel, thread_ts):
    summary = ""
    posted_progress = False
    for ev in client.beta.sessions.events.stream(session_id):
        if ev.type == "agent.message":
            for b in ev.content:
                if b.type == "text" and b.text.strip():
                    summary = b.text
        elif ev.type == "agent.tool_use" and not posted_progress:
            app.client.chat_postMessage(
                channel=channel, thread_ts=thread_ts, text="Rodando a análise..."
            )
            posted_progress = True
        elif ev.type == "session.status_idle":
            break

    if summary:
        text = mrkdwn.convert(summary)
        app.client.chat_postMessage(channel=channel, thread_ts=thread_ts, text=text[:3900])

O session.status_idle é o sinal de "terminei, sua vez". Quebrar o loop nele é o que evita ficar escutando pra sempre — critério de parada é o que separa um agente de um loop infinito. E os gráficos que o agente gerou? Ficam em /mnt/session/outputs/. Você lista os arquivos da sessão com client.beta.files.list(scope_id=session_id, ...), baixa, e sobe de volta pro thread com files_upload_v2.

Passo 5: follow-ups na mesma sessão

Essa é a parte que transforma um comando num assistente. Quando alguém responde no thread — "e separa isso por região?" —, o bot não abre uma sessão nova. Ele continua a que já existe, com todo o contexto do CSV e da análise anterior intactos.

@app.event("message")
def on_thread_reply(event, ack):
    ack()
    thread_ts = event.get("thread_ts")
    if event.get("subtype") or event.get("bot_id"):
        return  # ignora edições, deletes e o próprio bot
    if not thread_ts or thread_ts not in thread_sessions:
        return  # só responde em threads que já têm sessão

    threading.Thread(
        target=continue_session,
        args=(thread_sessions[thread_ts], event["channel"], thread_ts, event["text"]),
    ).start()

O continue_session é quase idêntico ao start_analysis, sem a parte de criar sessão: ele só manda um novo user.message pra sessão existente e chama o mesmo relay_stream. O agente lembra do dataset, lembra do que já calculou, e responde a pergunta nova em cima disso.

Para rodar tudo, é uma linha — Socket Mode dispensa URL pública:

SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()

Limitações e pontos de atenção

Esse código é um tutorial, não um deploy. Onde ele vai te queimar se você subir do jeito que está:

  • thread_sessions mora na memória. Reiniciou o processo, perdeu todos os mapeamentos thread → sessão. Em produção isso vira Redis ou um banco. Sem isso, todo follow-up depois de um restart abre sessão nova e perde o contexto.
  • Sem limpeza de sessões. Cada thread deixa uma sessão aberta. Sessão segura um container. Você precisa arquivar (client.beta.sessions.archive) as antigas, ou o custo escala silenciosamente.
  • Dado sensível vai pra um container hospedado. O CSV é enviado pra Files API da Anthropic e montado numa sandbox. Para dados de cliente, isso é uma decisão de compliance, não um detalhe técnico. Mascare colunas sensíveis antes de subir, ou avalie um sandbox self-hosted.
  • Limite de tamanho da mensagem. O Slack corta mensagens longas. Por isso o [:3900] — relatório grande vai como arquivo anexo, não como texto.

Esse bloco não é opcional. É a diferença entre uma demo que funciona na sua máquina e algo que aguenta o time inteiro mandando CSV.

FAQ rápido

Preciso expor um servidor com URL pública? Não. Socket Mode usa WebSocket — o bot abre a conexão de dentro pra fora. Você só precisa do SLACK_APP_TOKEN com scope connections:write. Bom pra rodar local enquanto desenvolve.

Dá pra trocar o claude-sonnet-4-6 por Opus? Dá. O modelo é fixado na criação do agente, não no bot. Para análises mais pesadas, vale testar Opus 4.8 — só recriar o agente apontando pro outro modelo e atualizar o ID no .env.

Por que Managed Agents e não chamar a API direto? Porque o agente precisa rodar código (pandas, plotly) num ambiente isolado e manter estado entre turnos. Managed Agents hospeda esse loop e o container pra você. Dava pra fazer na mão com tool use, mas você assumiria a sandbox e a orquestração.

Funciona com mais de um CSV? A versão do cookbook monta um arquivo por sessão. Para múltiplos, é só adicionar mais entradas em resources com mount paths diferentes — e dizer ao agente onde cada um está.

Onde isso te leva

Você saiu de "abrir um notebook pra responder uma pergunta" pra "marcar um bot no canal". O agente de dados deixou de ser uma ferramenta separada e virou parte do lugar onde o time já trabalha. E o pulo conceitual aqui não é o Slack — é entender que um agente em produção é encanamento: handler, sessão, stream, estado, limites. O modelo é só uma peça.

Esse caminho do código de exemplo até o harness que aguenta produção é exatamente o que a gente destrincha no workshop Do Prompt ao Harness: construindo um agente de vendas — saindo do prompt e indo até o agente rodando de verdade. Se este tutorial fez sentido, é o próximo degrau natural.

O próximo passo prático é trocar o thread_sessions em memória por um banco e arquivar as sessões antigas. Aí você tem algo que não morre no primeiro restart. O agente já sabe analisar. Agora é fazer ele sobreviver à produção.

Lucas Souza
Lucas Souza

{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.

Você também pode gostar

VirguIA

beer & code assistant

conectando…

Não foi possível iniciar o chat agora.

tocando