LangChain / LlamaIndex / Vercel AI SDK: Three Framework Integration Practice and Performance Comparison
Chapter 62: Slack/Teams Integration: AI Assistants in Enterprise Communication Platforms
62.1 The Value of Enterprise Communication Platform Integration
Slack and Microsoft Teams are the primary collaboration platforms in modern enterprises. Integrating Claude into either platform means AI capabilities are woven directly into employees' daily workflows:
- Instant response: Employees don't switch to another tool—they get AI help right in the chat interface
- Context awareness: The bot can read channel history and understand the team's context
- Permission management: AI capability boundaries are controlled by workspace, channel, and user role
- Audit trail: All AI interactions are recorded in platform logs to meet compliance requirements
This chapter focuses on using the Slack Bolt framework (Python) to integrate Claude, and Microsoft Teams Bot Framework integration.
62.2 Slack Bolt + Claude: Complete Integration
62.2.1 Environment Setup
Creating a Slack App:
- Visit https://api.slack.com/apps, click Create New App
- Choose From scratch, fill in App Name and workspace
- Under OAuth & Permissions, add Bot Token Scopes:
app_mentions:readchannels:historychat:writeim:historyim:writecommands
- Under Event Subscriptions, enable and add events:
app_mentionmessage.im
- Install App to workspace, get Bot Token (
xoxb-...) - Get Signing Secret from Basic Information
pip install slack-bolt anthropic python-dotenv
# .env file
SLACK_BOT_TOKEN=xoxb-xxxxxxxxxxxx
SLACK_SIGNING_SECRET=xxxxxxxxxxxxxxxx
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxx
62.2.2 Basic Bot: Responding to @Mentions
# slack_claude_bot.py
import os
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import anthropic
from dotenv import load_dotenv
load_dotenv()
app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"]
)
claude = anthropic.Anthropic()
# Conversation history (use Redis/database in production)
conversation_history = {}
@app.event("app_mention")
def handle_mention(event, say, client):
"""Respond to @Bot mentions in channels"""
channel_id = event["channel"]
user_id = event["user"]
thread_ts = event.get("thread_ts", event["ts"])
# Remove Bot mention marker (<@BOT_ID>)
text = event["text"]
bot_id = client.auth_test()["user_id"]
text = text.replace(f"<@{bot_id}>", "").strip()
if not text:
say("Hello! How can I help you?", thread_ts=thread_ts)
return
session_key = f"{channel_id}:{thread_ts}"
if session_key not in conversation_history:
conversation_history[session_key] = []
history = conversation_history[session_key]
history.append({"role": "user", "content": text})
client.reactions_add(
channel=channel_id,
name="hourglass_flowing_sand",
timestamp=event["ts"]
)
try:
response = claude.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system="""You are Aria, the company's AI assistant deployed in Slack.
Your role is to help employees answer questions, analyze data, draft documents, and explain technical concepts.
Use Slack's Markdown format (*bold*, `code`, ```code blocks```).
For requests involving private information, suggest users reach out via DM.""",
messages=history
)
reply_text = response.content[0].text
history.append({"role": "assistant", "content": reply_text})
if len(history) > 20:
conversation_history[session_key] = history[-20:]
say(reply_text, thread_ts=thread_ts)
except Exception as e:
say(f":warning: Error processing request: {str(e)}", thread_ts=thread_ts)
finally:
client.reactions_remove(
channel=channel_id,
name="hourglass_flowing_sand",
timestamp=event["ts"]
)
@app.event("message")
def handle_dm(event, say):
"""Respond to direct messages"""
if event.get("bot_id"):
return
if event.get("channel_type") != "im":
return
user_id = event["user"]
text = event.get("text", "").strip()
if not text:
return
session_key = f"dm:{user_id}"
if session_key not in conversation_history:
conversation_history[session_key] = []
history = conversation_history[session_key]
history.append({"role": "user", "content": text})
response = claude.messages.create(
model="claude-opus-4-5",
max_tokens=2048,
system="You are the employee's personal AI assistant. In DMs, you can handle more sensitive content such as performance feedback drafts and personal development plans.",
messages=history
)
reply = response.content[0].text
history.append({"role": "assistant", "content": reply})
say(reply)
if __name__ == "__main__":
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()
62.2.3 Slash Commands: Structured Feature Entry Points
@app.command("/summarize")
def handle_summarize(ack, body, client, respond):
"""Summarize channel or thread history"""
ack() # Must ack within 3 seconds
channel_id = body["channel_id"]
user_id = body["user_id"]
result = client.conversations_history(channel=channel_id, limit=50)
messages = result["messages"]
if not messages:
respond("No message history available in this channel.")
return
formatted_messages = []
for msg in reversed(messages):
if msg.get("bot_id") or msg.get("subtype"):
continue
user_info = client.users_info(user=msg.get("user", "unknown"))
username = user_info["user"]["real_name"] if user_info["ok"] else "Unknown"
formatted_messages.append(f"{username}: {msg.get('text', '')}")
conversation_text = "\n".join(formatted_messages)
response = claude.messages.create(
model="claude-opus-4-5",
max_tokens=512,
messages=[{
"role": "user",
"content": f"Summarize the following Slack channel conversation. Include:\n1. Main topics discussed\n2. Decisions made (if any)\n3. Open questions (if any)\n\nConversation:\n{conversation_text}"
}]
)
summary = response.content[0].text
respond({
"blocks": [
{
"type": "header",
"text": {"type": "plain_text", "text": "Channel Discussion Summary"}
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": summary}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": f"Triggered by <@{user_id}> · Based on last {len(formatted_messages)} messages"
}
]
}
]
})
@app.command("/draft")
def handle_draft(ack, body, respond):
"""Draft a specified type of document"""
ack()
text = body.get("text", "").strip()
if not text:
respond("Please provide content, e.g.: `/draft meeting-notes Discussed Q3 goals and resource allocation`")
return
parts = text.split(" ", 1)
doc_type = parts[0]
content = parts[1] if len(parts) > 1 else ""
DOC_TYPE_PROMPTS = {
"meeting-notes": "You are a professional meeting notes writer. Generate standard-format meeting notes with placeholders for date/attendees, discussion content, decisions, and action items.",
"email": "You are a professional business writing assistant. Draft a professional business email based on the provided key points.",
"announcement": "You are a company announcement writer. Draft a clear, formal internal announcement based on the provided information."
}
system_prompt = DOC_TYPE_PROMPTS.get(doc_type, f"You are a professional {doc_type} drafting assistant.")
response = claude.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system=system_prompt,
messages=[{"role": "user", "content": f"Please draft: {content}"}]
)
respond(f"*{doc_type} Draft*\n\n{response.content[0].text}")
62.2.4 Event Subscriptions: Listening for Specific Triggers
@app.event("reaction_added")
def handle_reaction(event, client):
"""Trigger Claude processing when users add a specific emoji"""
if event["reaction"] != "summarize":
return
channel_id = event["item"]["channel"]
message_ts = event["item"]["ts"]
result = client.conversations_history(
channel=channel_id,
latest=message_ts,
limit=1,
inclusive=True
)
if not result["messages"]:
return
message_text = result["messages"][0].get("text", "")
response = claude.messages.create(
model="claude-haiku-4-5",
max_tokens=256,
messages=[{
"role": "user",
"content": f"Summarize the core point of the following in one sentence:\n\n{message_text}"
}]
)
client.chat_postMessage(
channel=channel_id,
thread_ts=message_ts,
text=f":robot_face: *One-line summary*\n{response.content[0].text}"
)
62.3 Microsoft Teams Integration
62.3.1 Teams Bot Framework
pip install botbuilder-core botbuilder-integration-aiohttp anthropic aiohttp
# teams_claude_bot.py
from botbuilder.core import ActivityHandler, TurnContext, MessageFactory
from botbuilder.schema import Activity, ActivityTypes
import anthropic
import re
class ClaudeBot(ActivityHandler):
def __init__(self):
self.claude = anthropic.Anthropic()
self.conversation_histories = {}
async def on_message_activity(self, turn_context: TurnContext):
user_id = turn_context.activity.from_property.id
conversation_id = turn_context.activity.conversation.id
text = turn_context.activity.text.strip()
# Strip HTML tags (Teams messages may contain HTML)
text = re.sub(r'<[^>]+>', '', text).strip()
if not text:
return
session_key = f"{conversation_id}:{user_id}"
if session_key not in self.conversation_histories:
self.conversation_histories[session_key] = []
history = self.conversation_histories[session_key]
history.append({"role": "user", "content": text})
await turn_context.send_activity(Activity(type=ActivityTypes.typing))
response = self.claude.messages.create(
model="claude-opus-4-5",
max_tokens=2048,
system="""You are an enterprise AI assistant deployed in Microsoft Teams.
Use Markdown for formatting (Teams supports a subset of Markdown).
Use code blocks for code examples.""",
messages=history
)
reply_text = response.content[0].text
history.append({"role": "assistant", "content": reply_text})
if len(history) > 20:
self.conversation_histories[session_key] = history[-20:]
await turn_context.send_activity(MessageFactory.text(reply_text))
async def on_members_added_activity(self, members_added, turn_context: TurnContext):
for member in members_added:
if member.id != turn_context.activity.recipient.id:
await turn_context.send_activity(
MessageFactory.text(
"Hello! I'm Claude, your AI assistant. I can help answer questions, analyze documents, and draft content. Just send me a message to get started!"
)
)
62.3.2 Teams Incoming Webhook for Notifications
import anthropic
import requests
import json
claude = anthropic.Anthropic()
def analyze_and_notify_teams(data: dict, teams_webhook_url: str):
"""Analyze data with Claude and push results to Teams via webhook"""
response = claude.messages.create(
model="claude-haiku-4-5",
max_tokens=512,
messages=[{
"role": "user",
"content": f"Analyze this sales data, identify anomalies, and provide 3 key insights:\n{json.dumps(data, indent=2)}"
}]
)
insights = response.content[0].text
# Teams Adaptive Card format
card = {
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{"type": "TextBlock", "text": "AI Sales Data Analysis Report", "weight": "Bolder", "size": "Large"},
{"type": "TextBlock", "text": insights, "wrap": True},
{"type": "FactSet", "facts": [
{"title": "Model", "value": "Claude Haiku"},
{"title": "Generated", "value": "Automatically"}
]}
]
}
}
]
}
requests.post(teams_webhook_url, json=card)
62.4 Cross-Platform Claude Logic Reuse
When maintaining both Slack and Teams integrations, abstracting the Claude interaction logic into a shared service layer avoids code duplication:
# claude_service.py - Platform-agnostic Claude service layer
import anthropic
from dataclasses import dataclass
from typing import Optional
@dataclass
class ConversationContext:
platform: str # "slack" | "teams"
user_id: str
channel_id: str
thread_id: Optional[str] = None
class ClaudeService:
def __init__(self):
self.client = anthropic.Anthropic()
self._histories: dict[str, list] = {}
def _get_session_key(self, ctx: ConversationContext) -> str:
return f"{ctx.platform}:{ctx.channel_id}:{ctx.user_id}"
def _get_system_prompt(self, ctx: ConversationContext) -> str:
base = "You are an enterprise AI assistant helping employees work more efficiently."
if ctx.platform == "slack":
base += " Use Slack Markdown (*bold*, `code`)."
elif ctx.platform == "teams":
base += " Use Teams-compatible Markdown."
return base
def chat(self, message: str, ctx: ConversationContext,
model: str = "claude-opus-4-5", max_tokens: int = 1024) -> str:
key = self._get_session_key(ctx)
if key not in self._histories:
self._histories[key] = []
history = self._histories[key]
history.append({"role": "user", "content": message})
response = self.client.messages.create(
model=model, max_tokens=max_tokens,
system=self._get_system_prompt(ctx),
messages=history
)
reply = response.content[0].text
history.append({"role": "assistant", "content": reply})
if len(history) > 20:
self._histories[key] = history[-20:]
return reply
def clear_history(self, ctx: ConversationContext):
self._histories.pop(self._get_session_key(ctx), None)
62.5 Security and Compliance
62.5.1 Content Filtering
SENSITIVE_KEYWORDS = ["salary", "layoffs", "competitor secrets", "acquisition"]
def pre_process_message(text: str) -> tuple[str, bool]:
for keyword in SENSITIVE_KEYWORDS:
if keyword.lower() in text.lower():
return text, True # True = needs additional review
return text, False
@app.event("app_mention")
def handle_mention_with_filter(event, say):
text = event["text"]
processed_text, needs_review = pre_process_message(text)
if needs_review:
say(":lock: Sensitive topic detected. This conversation has been flagged for security review. I'll still try to help, but please be mindful of company information security policies.")
log_sensitive_interaction(event)
# Continue normal processing...
62.5.2 Rate Limiting
from collections import defaultdict
import time
class RateLimiter:
def __init__(self, max_requests: int, window_seconds: int):
self.max_requests = max_requests
self.window_seconds = window_seconds
self._requests: dict[str, list] = defaultdict(list)
def is_allowed(self, user_id: str) -> bool:
now = time.time()
self._requests[user_id] = [t for t in self._requests[user_id] if now - t < self.window_seconds]
if len(self._requests[user_id]) >= self.max_requests:
return False
self._requests[user_id].append(now)
return True
rate_limiter = RateLimiter(max_requests=20, window_seconds=3600)
@app.event("app_mention")
def handle_mention_with_rate_limit(event, say):
user_id = event["user"]
if not rate_limiter.is_allowed(user_id):
say(f"<@{user_id}> You've reached the hourly usage limit (20 requests). Please try again later.")
return
# Normal processing...
Summary
Slack Bolt integrates Claude through three mechanisms: event listeners (app_mention, message.im), Slash Commands, and Reaction events. Microsoft Teams provides similar capabilities through the Bot Framework SDK, with Adaptive Card formatted output. Best practice is to abstract Claude interaction logic into a platform-agnostic service layer, shared across multiple platforms. Pair this with content filtering and rate limiting for enterprise security compliance. In production, conversation history should be stored in Redis or another persistent store rather than process memory, ensuring session continuity across service restarts.