Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
SLACK_APP_TOKEN=YOUR_SLACK_APP_TOKEN
SLACK_BOT_TOKEN=YOUR_SLACK_BOT_TOKEN
# SLACK_API_URL=YOUR_SLACK_API_URL
# This template uses OpenAI, but you can use any other provider!
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ env*/
venv/
.venv*
.env*
!.env.sample

# codecov / coverage
.coverage
Expand Down
43 changes: 30 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# App Agent & Assistant Template (Bolt for Python)
# AI Agent App Template (Bolt for Python)

This Bolt for Python template demonstrates how to build [Agents & Assistants](https://api.slack.com/docs/apps/ai) in Slack.
This Bolt for Python template demonstrates how to build [AI Apps](https://docs.slack.dev/ai/) in Slack.

## Setup
Before getting started, make sure you have a development workspace where you have permissions to install apps. If you don’t have one setup, go ahead and [create one](https://slack.com/create).
Expand All @@ -17,20 +17,25 @@ Join the [Slack Developer Program](https://api.slack.com/developer-program) for
4. Review the configuration and click *Create*
5. Click *Install to Workspace* and *Allow* on the screen that follows. You'll then be redirected to the App Configuration dashboard.

#### Environment Variables
### Environment Variables

Before you can run the app, you'll need to store some environment variables.

1. Open your app configuration page from this list, click **OAuth & Permissions** in the left hand menu, then copy the Bot User OAuth Token. You will store this in your environment as `SLACK_BOT_TOKEN`.
2. Click **Basic Information** from the left hand menu and follow the steps in the App-Level Tokens section to create an app-level token with the `connections:write` scope. Copy this token. You will store this in your environment as `SLACK_APP_TOKEN`.

1. Rename `.env.sample` to `.env`.
2. Open your apps setting page from [this list](https://api.slack.com/apps), click _OAuth & Permissions_ in the left hand menu, then copy the _Bot User OAuth Token_ into your `.env` file under `SLACK_BOT_TOKEN`.
```zsh
SLACK_BOT_TOKEN=YOUR_SLACK_BOT_TOKEN
```
3. Click _Basic Information_ from the left hand menu and follow the steps in the _App-Level Tokens_ section to create an app-level token with the `connections:write` scope. Copy that token into your `.env` as `SLACK_APP_TOKEN`.
```zsh
# Replace with your app token and bot token
# For Windows OS, env:SLACK_BOT_TOKEN = <your-bot-token> works
export SLACK_BOT_TOKEN=<your-bot-token>
export SLACK_APP_TOKEN=<your-app-token>
# This sample uses OpenAI's API by default, but you can switch to any other solution!
export OPENAI_API_KEY=<your-openai-api-key>
SLACK_APP_TOKEN=YOUR_SLACK_APP_TOKEN
```
4. Save your OpenAI key into `.env` under `OPENAI_API_KEY`.
```zsh
OPENAI_API_KEY=YOUR_OPEN_API_KEY
```


### Setup Your Local Project
```zsh
Expand All @@ -51,6 +56,8 @@ pip install -r requirements.txt
python3 app.py
```

Start talking to the bot! Start a new DM or thread and click the feedback button when it responds.

#### Linting
```zsh
# Run flake8 from root directory for linting
Expand All @@ -72,15 +79,25 @@ black .

### `/listeners`

Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events, `/listeners/shortcuts` would handle incoming [Shortcuts](https://api.slack.com/interactivity/shortcuts) requests, and so on.
Every incoming request is routed to a "listener". This directory groups each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events, `/listeners/shortcuts` would handle incoming [Shortcuts](https://docs.slack.dev/interactivity/implementing-shortcuts/) requests, and so on.

**`/listeners/assistant`**

Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This module includes:

`assistant.py`, which contains two listeners:
* The `@assistant.thread_started` listener receives an event when users start new app thread.
* The `@assistant.user_message` listener processes user messages in app threads or from the app **Chat** and **History** tab.

`ai/llm_caller.py`, which handles OpenAI API integration and message formatting. It includes the `call_llm()` function that sends conversation threads to OpenAI's models.

## App Distribution / OAuth

Only implement OAuth if you plan to distribute your application across multiple workspaces. A separate `app_oauth.py` file can be found with relevant OAuth settings.

When using OAuth, Slack requires a public URL where it can send requests. In this template app, we've used [`ngrok`](https://ngrok.com/download). Checkout [this guide](https://ngrok.com/docs#getting-started-expose) for setting it up.

Start `ngrok` to access the app on an external network and create a redirect URL for OAuth.
Start `ngrok` to access the app on an external network and create a redirect URL for OAuth.

```
ngrok http 3000
Expand Down
25 changes: 25 additions & 0 deletions ai/llm_caller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os
from typing import Dict, List

import openai
from openai import Stream
from openai.types.responses import ResponseStreamEvent

DEFAULT_SYSTEM_CONTENT = """
You're an assistant in a Slack workspace.
Users in the workspace will ask you to help them write something or to think better about a specific topic.
You'll respond to those questions in a professional way.
When you include markdown text, convert them to Slack compatible ones.
When a prompt has Slack's special syntax like <@USER_ID> or <#CHANNEL_ID>, you must keep them as-is in your response.
"""


def call_llm(
messages_in_thread: List[Dict[str, str]],
system_content: str = DEFAULT_SYSTEM_CONTENT,
) -> Stream[ResponseStreamEvent]:
openai_client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
messages = [{"role": "system", "content": system_content}]
messages.extend(messages_in_thread)
response = openai_client.responses.create(model="gpt-4o-mini", input=messages, stream=True)
return response
16 changes: 14 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import os
import logging
import os

from dotenv import load_dotenv

from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
from slack_sdk import WebClient

from listeners import register_listeners

# Load environment variables
load_dotenv(dotenv_path=".env", override=False)

# Initialization
logging.basicConfig(level=logging.DEBUG)
app = App(token=os.environ.get("SLACK_BOT_TOKEN"))

app = App(
token=os.environ.get("SLACK_BOT_TOKEN"),
client=WebClient(
base_url=os.environ.get("SLACK_API_URL", "https://slack.com/api"),
token=os.environ.get("SLACK_BOT_TOKEN"),
),
)
# Register Listeners
register_listeners(app)

Expand Down
4 changes: 2 additions & 2 deletions app_oauth.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
import os

from slack_bolt import App, BoltResponse
from slack_bolt.oauth.callback_options import CallbackOptions, SuccessArgs, FailureArgs
from slack_bolt.oauth.callback_options import CallbackOptions, FailureArgs, SuccessArgs
from slack_bolt.oauth.oauth_settings import OAuthSettings

from slack_sdk.oauth.installation_store import FileInstallationStore
from slack_sdk.oauth.state_store import FileOAuthStateStore

Expand Down
13 changes: 6 additions & 7 deletions listeners/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from .assistant import assistant
from slack_bolt import App

from listeners import actions, assistant, events

def register_listeners(app):
# Using assistant middleware is the recommended way.
app.assistant(assistant)

# The following event listeners demonstrate how to implement the same on your own.
# from listeners import events
# events.register(app)
def register_listeners(app: App):
actions.register(app)
assistant.register(app)
events.register(app)
7 changes: 7 additions & 0 deletions listeners/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from slack_bolt import App

from .actions import handle_feedback


def register(app: App):
app.action("feedback")(handle_feedback)
41 changes: 41 additions & 0 deletions listeners/actions/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from logging import Logger

from slack_bolt import Ack
from slack_sdk import WebClient


def handle_feedback(ack: Ack, body: dict, client: WebClient, logger: Logger):
"""
Handles user feedback on AI-generated responses via thumbs up/down buttons.

Args:
ack: Function to acknowledge the action request
body: Action payload containing feedback details (message, channel, user, action value)
client: Slack WebClient for making API calls
logger: Logger instance for debugging and error tracking
"""
try:
ack()
message_ts = body["message"]["ts"]
channel_id = body["channel"]["id"]
feedback_type = body["actions"][0]["value"]
is_positive = feedback_type == "good-feedback"

if is_positive:
client.chat_postEphemeral(
channel=channel_id,
user=body["user"]["id"],
thread_ts=message_ts,
text="We're glad you found this useful.",
)
else:
client.chat_postEphemeral(
channel=channel_id,
user=body["user"]["id"],
thread_ts=message_ts,
text="Sorry to hear that response wasn't up to par :slightly_frowning_face: Starting a new chat may help with AI mistakes and hallucinations.",
)

logger.debug(f"Handled feedback: type={feedback_type}, message_ts={message_ts}")
except Exception as error:
logger.error(f":warning: Something went wrong! {error}")
109 changes: 0 additions & 109 deletions listeners/assistant.py

This file was deleted.

7 changes: 7 additions & 0 deletions listeners/assistant/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from slack_bolt import App

from .assistant import assistant


def register(app: App):
app.assistant(assistant)
Loading