Integrating PySpur Chatbots with Slack

This guide explains how to connect your PySpur chatbots to Slack, allowing users to interact with your chatbot directly through Slack channels and threads.

Understanding Slack Integration Options

PySpur offers two distinct ways to work with Slack:

  1. Workflow Output to Slack - Using the SlackNotifyNode to send one-time results from a workflow to a Slack channel
  2. Interactive Chatbot in Slack - Creating a fully interactive chatbot that users can converse with through Slack

It’s important to understand the difference between these approaches:

FeatureSlackNotifyNodeInteractive Chatbot
InteractionOne-way (workflow → Slack)Two-way conversation
Session ManagementNoneFull conversation history
Use CaseSending alerts, summaries, notificationsQ&A, support, interactive assistance
ImplementationSimple workflow nodeCustom Slack app with API integration

Setting Up an Interactive Chatbot in Slack

Follow these steps to integrate your PySpur chatbot with Slack for interactive conversations:

1. Create a Slack App

  1. Go to api.slack.com/apps and click “Create New App”
  2. Choose “From scratch” and provide a name and workspace
  3. In the app settings, enable the following:
    • Socket Mode (under “Socket Mode”)
    • Event Subscriptions (under “Event Subscriptions”)
    • Bot Token Scopes (under “OAuth & Permissions”):
      • app_mentions:read
      • channels:history
      • chat:write
      • groups:history
      • im:history
      • mpim:history

2. Collect Required Tokens

You’ll need three tokens from your Slack app:

  • Bot Token (SLACK_BOT_TOKEN): Found under “OAuth & Permissions” → “Bot User OAuth Token”
  • Signing Secret (SLACK_SIGNING_SECRET): Found under “Basic Information” → “App Credentials”
  • App Token (SLACK_APP_TOKEN): Generate under “Basic Information” → “App-Level Tokens” (with connections:write scope)

3. Create Your Integration Script

Create a Python script similar to the example below. This script:

  • Initializes a Slack Bolt app
  • Listens for mentions and thread replies
  • Creates PySpur sessions for users
  • Forwards messages to your PySpur chatbot workflow
  • Returns responses back to Slack
# slack_integration.py
import logging
import os
import sys
from logging import getLogger

import requests
from dotenv import load_dotenv
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

# Load environment variables from .env file
load_dotenv()

# Replace with your bot's user ID and workflow ID
BOT_USER_ID = "U08HMJ15AHF"  # Your bot's user ID in Slack
WORKFLOW_ID = "S58"          # Your chatbot workflow ID in PySpur
PYSPUR_API_URL = "http://localhost:6080/api"  # Change to your PySpur API URL

# Configure logger
logger = getLogger(__name__)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(
    logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
)
logger.addHandler(handler)

# Initialize Slack app
app = App(
    token=os.environ.get("SLACK_BOT_TOKEN"),
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
    logger=logger,
)

# Handler for @mentions
@app.event("app_mention")
def handle_app_mention(event, say, logger):
    logger.info(f"Received mention: {event}")
    thread_ts = event.get("thread_ts") or event.get("ts")
    user_external_id = event.get("user")

    try:
        # Create or get user
        user_data = {
            "external_id": user_external_id,
            "user_metadata": {"platform": "slack"}
        }
        user_response = requests.post(f"{PYSPUR_API_URL}/user/", json=user_data)
        if user_response.status_code not in [200, 409]:  # 409 means user already exists
            logger.error(f"Failed to create user: {user_response.text}")
            say(text="Sorry, I encountered an error processing your request.", thread_ts=thread_ts)
            return

        user_id = user_response.json().get("id")

        # Create a new session or get existing one
        session_data = {
            "workflow_id": WORKFLOW_ID,
            "user_id": user_id,
            "external_id": thread_ts
        }
        session_response = requests.post(f"{PYSPUR_API_URL}/session/", json=session_data)
        if session_response.status_code != 200:
            logger.error(f"Failed to create session: {session_response.text}")
            say(text="Sorry, I encountered an error processing your request.", thread_ts=thread_ts)
            return

        # Get the message text
        message = app.client.conversations_history(
            channel=event["channel"], ts=thread_ts
        )

        # Call the workflow API
        url = f"{PYSPUR_API_URL}/wf/{WORKFLOW_ID}/run/?run_type=blocking"
        data = {
            "initial_inputs": {
                "input_node": {
                    "user_message": message["messages"][0]["text"],
                    "session_id": session_response.json()["id"],
                    "message_history": []
                }
            }
        }

        response = requests.post(url, json=data)
        response_data = response.json()

        # Get the assistant's message from the output node
        assistant_message = response_data.get("output_node", {}).get("assistant_message", "")

        # Send the response back to Slack
        if assistant_message:
            say(text=assistant_message, thread_ts=thread_ts)
        else:
            say(text="I encountered an issue processing your request.", thread_ts=thread_ts)

    except Exception as e:
        logger.error(f"Error processing request: {e}")
        say(text="Sorry, I encountered an error processing your request.", thread_ts=thread_ts)

# Handler for thread replies
@app.event("message")
def handle_thread_replies(event, say, logger):
    # Only process thread replies (not the first message) and ignore bot messages
    if (
        "thread_ts" in event
        and event.get("ts") != event.get("thread_ts")
        and not event.get("bot_id")
    ):
        thread_ts = event["thread_ts"]
        channel_id = event["channel"]
        user_external_id = event.get("user")

        try:
            # Create or get user
            user_data = {
                "external_id": user_external_id,
                "user_metadata": {"platform": "slack"}
            }
            user_response = requests.post(f"{PYSPUR_API_URL}/user/", json=user_data)
            if user_response.status_code not in [200, 409]:
                logger.error(f"Failed to create user: {user_response.text}")
                say(text="Sorry, I encountered an error processing your request.", thread_ts=thread_ts)
                return

            user_id = user_response.json().get("id")

            # Create a new session or get existing one
            session_data = {
                "workflow_id": WORKFLOW_ID,
                "user_id": user_id,
                "external_id": thread_ts
            }
            session_response = requests.post(f"{PYSPUR_API_URL}/session/", json=session_data)
            if session_response.status_code != 200:
                logger.error(f"Failed to create session: {session_response.text}")
                say(text="Sorry, I encountered an error processing your request.", thread_ts=thread_ts)
                return

            # Get all replies in the thread
            result = app.client.conversations_replies(channel=channel_id, ts=thread_ts)

            # Format messages as a conversation history
            chat_messages = []
            for message in result["messages"]:
                role = "assistant" if message.get("user") == BOT_USER_ID else "user"
                chat_messages.append({"role": role, "content": message.get("text", "")})

            # Get message history and current message
            message_history = chat_messages[:-1] if len(chat_messages) > 1 else []
            user_message = chat_messages[-1]["content"] if chat_messages else ""

            # Call the workflow API
            url = f"{PYSPUR_API_URL}/wf/{WORKFLOW_ID}/run/?run_type=blocking"
            data = {
                "initial_inputs": {
                    "input_node": {
                        "user_message": user_message,
                        "session_id": session_response.json()["id"],
                        "message_history": message_history,
                    }
                }
            }

            response = requests.post(url, json=data)
            response_data = response.json()

            # Get the assistant's message
            assistant_message = response_data.get("output_node", {}).get("assistant_message", "")

            # Send the response back to Slack
            if assistant_message:
                say(text=assistant_message, thread_ts=thread_ts)
            else:
                say(text="I encountered an issue processing your request.", thread_ts=thread_ts)

        except Exception as e:
            logger.error(f"Error processing thread reply: {e}")
            say(text="Sorry, I had trouble processing your message.", thread_ts=thread_ts)

if __name__ == "__main__":
    # Start the app using Socket Mode
    handler = SocketModeHandler(app, os.environ.get("SLACK_APP_TOKEN"))
    handler.start()

4. Set Up Environment Variables

Create a .env file with your tokens:

SLACK_BOT_TOKEN=xoxb-your-token
SLACK_SIGNING_SECRET=your-signing-secret
SLACK_APP_TOKEN=xapp-your-app-token

5. Run Your Integration

  1. Install required packages:

    pip install slack-bolt python-dotenv requests
    
  2. Make sure your PySpur server is running

  3. Start your Slack integration:

    python slack_integration.py
    

How It Works

This integration creates a bidirectional connection between Slack and your PySpur chatbot:

  1. User Input: When a user mentions your bot or replies in a thread, the Slack app captures this input.

  2. Session Management: The script creates or retrieves a PySpur session for the user, using the thread timestamp as the external ID to track conversations.

  3. Message History: For thread replies, the script retrieves the entire conversation history and formats it in the proper structure for your chatbot.

  4. PySpur API Call: The user message and conversation history are sent to your PySpur workflow through the API.

  5. Response: The assistant’s response from your workflow is posted back to the Slack thread.

Key Differences from SlackNotifyNode

This approach differs significantly from the SlackNotifyNode:

  • SlackNotifyNode is a one-way communication channel where your workflow sends a message to Slack when it completes. It’s ideal for notifications, alerts, or sharing results of automated processes.

  • Interactive Slack Integration creates a two-way communication channel where users can have ongoing conversations with your chatbot. The integration manages sessions, tracks conversation history, and maintains context across multiple interactions.

Example Use Cases

  • Support Bot: Create a support chatbot that answers user questions about your product
  • Data Query Assistant: Allow users to query company data through natural language in Slack
  • Task Manager: Build a bot that helps users create and track tasks through conversation
  • Knowledge Base: Develop a bot that retrieves and explains information from your documentation

Troubleshooting

  • Bot not responding: Ensure your bot has been invited to the channel and has the necessary permissions
  • Missing messages: Check your log output for any API errors
  • Session errors: Verify that your PySpur server is running and accessible
  • Token issues: Double-check that all environment variables are set correctly