Chapter 62

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:

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:

  1. Visit https://api.slack.com/apps, click Create New App
  2. Choose From scratch, fill in App Name and workspace
  3. Under OAuth & Permissions, add Bot Token Scopes:
    • app_mentions:read
    • channels:history
    • chat:write
    • im:history
    • im:write
    • commands
  4. Under Event Subscriptions, enable and add events:
    • app_mention
    • message.im
  5. Install App to workspace, get Bot Token (xoxb-...)
  6. 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.

Rate this chapter
4.6  / 5  (3 ratings)

๐Ÿ’ฌ Comments