Slack
Give a Slack app a memory of each person who talks to it. Slack sends every mention and direct message to your server; your server puts what is known about the person in front of Claude, replies in the thread, and saves the exchange. The next question starts knowing this one.
Install
pip install fastapi uvicorn httpx anthropic geniffyuv add fastapi uvicorn httpx anthropic geniffySet GENIFFY_API_KEY from API keys in the Geniffy app and ANTHROPIC_API_KEY. From your Slack app, set
SLACK_BOT_TOKEN (under OAuth & Permissions, the xoxb- one) and SLACK_SIGNING_SECRET (under Basic
Information).
Your server
import hashlib
import hmac
import json
import os
import re
import time
import httpx
from anthropic import AsyncAnthropic
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
from geniffy import AsyncGeniffy
geniffy = AsyncGeniffy() # reads GENIFFY_API_KEY
claude = AsyncAnthropic() # reads ANTHROPIC_API_KEY
slack = httpx.AsyncClient(base_url="https://slack.com/api",
headers={"Authorization": f"Bearer {os.environ['SLACK_BOT_TOKEN']}"})
app = FastAPI()
seen: dict[str, None] = {} # recent event ids: Slack sends an event again when it has to wait
def space_for(team_id: str, user_id: str) -> str:
"""The person, by their workspace and Slack id. If you know them as customers, use your customer id."""
return f"slack_{team_id}_{user_id}"
def check(request: Request, body: bytes) -> None:
"""Slack signs every request with your signing secret; one older than five minutes is a replay."""
stamp = request.headers.get("X-Slack-Request-Timestamp", "0")
if not stamp.isdigit() or abs(time.time() - int(stamp)) > 300:
raise HTTPException(401)
base = f"v0:{stamp}:".encode() + body
signed = "v0=" + hmac.new(os.environ["SLACK_SIGNING_SECRET"].encode(), base, hashlib.sha256).hexdigest()
if not hmac.compare_digest(request.headers.get("X-Slack-Signature", ""), signed):
raise HTTPException(401)
@app.post("/slack/events")
async def events(request: Request, background: BackgroundTasks):
body = await request.body()
check(request, body)
payload = json.loads(body)
if payload.get("type") == "url_verification": # Slack checks the address once, when you add it
return {"challenge": payload["challenge"]}
event = payload.get("event") or {}
if payload.get("event_id") in seen or event.get("bot_id") or event.get("subtype") or not event.get("user"):
return {} # a retry, a bot (this one included), or an edit: nothing to answer
if event.get("type") == "app_mention" or (event.get("type") == "message" and event.get("channel_type") == "im"):
seen[payload["event_id"]] = None
if len(seen) > 10_000:
seen.pop(next(iter(seen)))
background.add_task(answer, payload["team_id"], event)
return {} # at once: Slack sends an event again after three seconds
async def answer(team_id: str, event: dict) -> None:
text = re.sub(r"<@[A-Z0-9]+>\s*", "", event.get("text") or "").strip() # without the app's own mention
if not text:
return
mem = geniffy.space(space_for(team_id, event["user"]))
turn = {"role": "user", "content": text}
context = await mem.context(text) # what is known that bears on the message
response = await claude.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
system=f"You are a helpful assistant in Slack. Keep replies short.\n\n<memory>\n{context}\n</memory>",
messages=[turn],
)
reply = "".join(block.text for block in response.content if block.type == "text")
sent = await slack.post("/chat.postMessage", json={"channel": event["channel"], "text": reply,
"thread_ts": event.get("thread_ts") or event["ts"]})
sent.raise_for_status()
await mem.memories.add(messages=[turn, {"role": "assistant", "content": reply}])Run it with uvicorn server:app, somewhere Slack can reach over HTTPS.
The server answers each event at once and replies after, and it keeps the event ids it has seen in memory, which is right for one process; with more than one, keep them where every process can see them. Each person is a space of their own, so a busy channel keeps everyone's memory apart. The reply goes in the thread of the message it answers.
In Slack
- Under Event Subscriptions, turn events on and set the Request URL to
https://your-server/slack/events. Slack checks it at once. - Under Subscribe to bot events, add
app_mentionandmessage.im. - Under OAuth & Permissions, give the bot the
app_mentions:read,im:historyandchat:writescopes, then install the app to your workspace.
When nothing is known about the person, the memory says so in one sentence, so Claude says it doesn't know instead of guessing. Each exchange is added as a conversation, so what the person said becomes a fact about them, and what your app said stays the app's.