├── CODEOWNERS ├── tests └── __init__.py ├── listeners ├── actions │ ├── __init__.py │ └── actions.py ├── events │ ├── __init__.py │ └── app_mentioned.py ├── __init__.py ├── assistant │ ├── __init__.py │ ├── assistant_thread_started.py │ └── message.py └── views │ └── feedback_block.py ├── requirements.txt ├── .env.sample ├── pyproject.toml ├── .github ├── dependabot.yml ├── CODEOWNERS └── workflows │ └── ruff.yml ├── .gitignore ├── app.py ├── ai └── llm_caller.py ├── LICENSE ├── manifest.json ├── app_oauth.py └── README.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | 404: Not Found#ECCN:Open Source 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022, Slack Technologies, LLC. All rights reserved. 2 | -------------------------------------------------------------------------------- /listeners/actions/__init__.py: -------------------------------------------------------------------------------- 1 | from slack_bolt import App 2 | 3 | from .actions import handle_feedback 4 | 5 | 6 | def register(app: App): 7 | app.action("feedback")(handle_feedback) 8 | -------------------------------------------------------------------------------- /listeners/events/__init__.py: -------------------------------------------------------------------------------- 1 | from slack_bolt import App 2 | 3 | from .app_mentioned import app_mentioned_callback 4 | 5 | 6 | def register(app: App): 7 | app.event("app_mention")(app_mentioned_callback) 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | slack-sdk==3.39.0 2 | slack-bolt==1.27.0 3 | 4 | # If you use a different LLM vendor, replace this dependency 5 | openai==2.12.0 6 | 7 | pytest==8.4.2 8 | ruff==0.14.7 9 | python-dotenv==1.2.1 10 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | SLACK_APP_TOKEN=YOUR_SLACK_APP_TOKEN 2 | SLACK_BOT_TOKEN=YOUR_SLACK_BOT_TOKEN 3 | # SLACK_API_URL=YOUR_SLACK_API_URL 4 | # This template uses OpenAI, but you can use any other provider! 5 | OPENAI_API_KEY=YOUR_OPENAI_API_KEY 6 | -------------------------------------------------------------------------------- /listeners/__init__.py: -------------------------------------------------------------------------------- 1 | from slack_bolt import App 2 | 3 | from listeners import actions, assistant, events 4 | 5 | 6 | def register_listeners(app: App): 7 | actions.register(app) 8 | assistant.register(app) 9 | events.register(app) 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | [tool.ruff.lint] 3 | [tool.ruff.format] 4 | 5 | [tool.pytest.ini_options] 6 | testpaths = ["tests"] 7 | log_file = "logs/pytest.log" 8 | log_file_level = "DEBUG" 9 | log_format = "%(asctime)s %(levelname)s %(message)s" 10 | log_date_format = "%Y-%m-%d %H:%M:%S" 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | labels: 8 | - "pip" 9 | - "dependencies" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "monthly" 14 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners are the default owners for everything in 2 | # this repository. The owners listed below will be requested for 3 | # review when a pull request is opened. 4 | # To add code owner(s), uncomment the line below and 5 | # replace the @global-owner users with their GitHub username(s). 6 | # * @global-owner1 @global-owner2 7 | -------------------------------------------------------------------------------- /listeners/assistant/__init__.py: -------------------------------------------------------------------------------- 1 | from slack_bolt import App, Assistant 2 | 3 | from .assistant_thread_started import assistant_thread_started 4 | from .message import message 5 | 6 | 7 | # Refer to https://docs.slack.dev/tools/bolt-python/concepts/ai-apps#assistant for more details on the Assistant class 8 | def register(app: App): 9 | assistant = Assistant() 10 | 11 | assistant.thread_started(assistant_thread_started) 12 | assistant.user_message(message) 13 | 14 | app.assistant(assistant) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general things to ignore 2 | build/ 3 | dist/ 4 | docs/_sources/ 5 | docs/.doctrees 6 | .eggs/ 7 | *.egg-info/ 8 | *.egg 9 | *.py[cod] 10 | __pycache__/ 11 | *.so 12 | *~ 13 | 14 | # virtualenv 15 | env*/ 16 | venv/ 17 | .venv* 18 | .env* 19 | !.env.sample 20 | 21 | # codecov / coverage 22 | .coverage 23 | cov_* 24 | coverage.xml 25 | 26 | # due to using tox and pytest 27 | .tox 28 | .cache 29 | .pytest_cache/ 30 | .python-version 31 | pip 32 | .mypy_cache/ 33 | 34 | # misc 35 | tmp.txt 36 | .DS_Store 37 | logs/ 38 | *.db 39 | .pytype/ 40 | .idea/ 41 | -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | name: Linting and formatting validation using ruff 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 5 12 | strategy: 13 | matrix: 14 | python-version: ["3.13"] 15 | 16 | steps: 17 | - uses: actions/checkout@v6 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v6 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | pip install -U pip 25 | pip install -r requirements.txt 26 | - name: Lint and check formatting with ruff 27 | run: | 28 | ruff check . 29 | ruff format --check . -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from dotenv import load_dotenv 5 | 6 | from slack_bolt import App 7 | from slack_bolt.adapter.socket_mode import SocketModeHandler 8 | from slack_sdk import WebClient 9 | 10 | from listeners import register_listeners 11 | 12 | # Load environment variables 13 | load_dotenv(dotenv_path=".env", override=False) 14 | 15 | # Initialization 16 | logging.basicConfig(level=logging.DEBUG) 17 | 18 | app = App( 19 | token=os.environ.get("SLACK_BOT_TOKEN"), 20 | client=WebClient( 21 | base_url=os.environ.get("SLACK_API_URL", "https://slack.com/api"), 22 | token=os.environ.get("SLACK_BOT_TOKEN"), 23 | ), 24 | ) 25 | # Register Listeners 26 | register_listeners(app) 27 | 28 | # Start Bolt app 29 | if __name__ == "__main__": 30 | SocketModeHandler(app, os.environ.get("SLACK_APP_TOKEN")).start() 31 | -------------------------------------------------------------------------------- /ai/llm_caller.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, List 3 | 4 | import openai 5 | from openai import Stream 6 | from openai.types.responses import ResponseStreamEvent 7 | 8 | DEFAULT_SYSTEM_CONTENT = """ 9 | You're an assistant in a Slack workspace. 10 | Users in the workspace will ask you to help them write something or to think better about a specific topic. 11 | You'll respond to those questions in a professional way. 12 | When you include markdown text, convert them to Slack compatible ones. 13 | When a prompt has Slack's special syntax like <@USER_ID> or <#CHANNEL_ID>, you must keep them as-is in your response. 14 | """ 15 | 16 | 17 | def call_llm( 18 | messages_in_thread: List[Dict[str, str]], 19 | system_content: str = DEFAULT_SYSTEM_CONTENT, 20 | ) -> Stream[ResponseStreamEvent]: 21 | openai_client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY")) 22 | messages = [{"role": "system", "content": system_content}] 23 | messages.extend(messages_in_thread) 24 | response = openai_client.responses.create( 25 | model="gpt-4o-mini", input=messages, stream=True 26 | ) 27 | return response 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Slack Technologies, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_information": { 3 | "name": "Bolt Python AI Agent" 4 | }, 5 | "features": { 6 | "app_home": { 7 | "home_tab_enabled": false, 8 | "messages_tab_enabled": true, 9 | "messages_tab_read_only_enabled": false 10 | }, 11 | "bot_user": { 12 | "display_name": "Bolt Python AI Agent", 13 | "always_online": false 14 | }, 15 | "assistant_view": { 16 | "assistant_description": "Hi, I am an agent built using Bolt for Python. I am here to help you out!", 17 | "suggested_prompts": [] 18 | } 19 | }, 20 | "oauth_config": { 21 | "scopes": { 22 | "bot": [ 23 | "app_mentions:read", 24 | "assistant:write", 25 | "im:history", 26 | "chat:write" 27 | ] 28 | } 29 | }, 30 | "settings": { 31 | "event_subscriptions": { 32 | "bot_events": [ 33 | "assistant_thread_started", 34 | "message.im", 35 | "app_mention" 36 | ] 37 | }, 38 | "interactivity": { 39 | "is_enabled": false 40 | }, 41 | "org_deploy_enabled": true, 42 | "socket_mode_enabled": true, 43 | "token_rotation_enabled": false 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /listeners/views/feedback_block.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from slack_sdk.models.blocks import ( 4 | Block, 5 | ContextActionsBlock, 6 | FeedbackButtonObject, 7 | FeedbackButtonsElement, 8 | ) 9 | 10 | 11 | def create_feedback_block() -> List[Block]: 12 | """ 13 | Create feedback block with thumbs up/down buttons 14 | 15 | Returns: 16 | Block Kit context_actions block 17 | """ 18 | blocks: List[Block] = [ 19 | ContextActionsBlock( 20 | elements=[ 21 | FeedbackButtonsElement( 22 | action_id="feedback", 23 | positive_button=FeedbackButtonObject( 24 | text="Good Response", 25 | accessibility_label="Submit positive feedback on this response", 26 | value="good-feedback", 27 | ), 28 | negative_button=FeedbackButtonObject( 29 | text="Bad Response", 30 | accessibility_label="Submit negative feedback on this response", 31 | value="bad-feedback", 32 | ), 33 | ) 34 | ] 35 | ) 36 | ] 37 | return blocks 38 | -------------------------------------------------------------------------------- /listeners/actions/actions.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | 3 | from slack_bolt import Ack 4 | from slack_sdk import WebClient 5 | 6 | 7 | def handle_feedback(ack: Ack, body: dict, client: WebClient, logger: Logger): 8 | """ 9 | Handles user feedback on AI-generated responses via thumbs up/down buttons. 10 | 11 | Args: 12 | ack: Function to acknowledge the action request 13 | body: Action payload containing feedback details (message, channel, user, action value) 14 | client: Slack WebClient for making API calls 15 | logger: Logger instance for debugging and error tracking 16 | """ 17 | try: 18 | ack() 19 | message_ts = body["message"]["ts"] 20 | channel_id = body["channel"]["id"] 21 | feedback_type = body["actions"][0]["value"] 22 | is_positive = feedback_type == "good-feedback" 23 | 24 | if is_positive: 25 | client.chat_postEphemeral( 26 | channel=channel_id, 27 | user=body["user"]["id"], 28 | thread_ts=message_ts, 29 | text="We're glad you found this useful.", 30 | ) 31 | else: 32 | client.chat_postEphemeral( 33 | channel=channel_id, 34 | user=body["user"]["id"], 35 | thread_ts=message_ts, 36 | 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.", 37 | ) 38 | 39 | logger.debug(f"Handled feedback: type={feedback_type}, message_ts={message_ts}") 40 | except Exception as error: 41 | logger.error(f":warning: Something went wrong! {error}") 42 | -------------------------------------------------------------------------------- /listeners/assistant/assistant_thread_started.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | from typing import Dict, List 3 | 4 | from slack_bolt import Say, SetSuggestedPrompts 5 | 6 | 7 | def assistant_thread_started( 8 | say: Say, 9 | set_suggested_prompts: SetSuggestedPrompts, 10 | logger: Logger, 11 | ): 12 | """ 13 | Handle the assistant thread start event by greeting the user and setting suggested prompts. 14 | 15 | Args: 16 | say: Function to send messages to the thread from the app 17 | set_suggested_prompts: Function to configure suggested prompt options 18 | logger: Logger instance for error tracking 19 | """ 20 | try: 21 | say("How can I help you?") 22 | 23 | prompts: List[Dict[str, str]] = [ 24 | { 25 | "title": "What does Slack stand for?", 26 | "message": "Slack, a business communication service, was named after an acronym. Can you guess what it stands for?", 27 | }, 28 | { 29 | "title": "Write a draft announcement", 30 | "message": "Can you write a draft announcement about a new feature my team just released? It must include how impactful it is.", 31 | }, 32 | { 33 | "title": "Suggest names for my Slack app", 34 | "message": "Can you suggest a few names for my Slack app? The app helps my teammates better organize information and plan priorities and action items.", 35 | }, 36 | ] 37 | 38 | set_suggested_prompts(prompts=prompts) 39 | except Exception as e: 40 | logger.exception(f"Failed to handle an assistant_thread_started event: {e}", e) 41 | say(f":warning: Something went wrong! ({e})") 42 | -------------------------------------------------------------------------------- /app_oauth.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from slack_bolt import App, BoltResponse 5 | from slack_bolt.oauth.callback_options import CallbackOptions, FailureArgs, SuccessArgs 6 | from slack_bolt.oauth.oauth_settings import OAuthSettings 7 | from slack_sdk.oauth.installation_store import FileInstallationStore 8 | from slack_sdk.oauth.state_store import FileOAuthStateStore 9 | 10 | from listeners import register_listeners 11 | 12 | logging.basicConfig(level=logging.DEBUG) 13 | 14 | 15 | # Callback to run on successful installation 16 | def success(args: SuccessArgs) -> BoltResponse: 17 | # Call default handler to return an HTTP response 18 | return args.default.success(args) 19 | # return BoltResponse(status=200, body="Installation successful!") 20 | 21 | 22 | # Callback to run on failed installation 23 | def failure(args: FailureArgs) -> BoltResponse: 24 | return args.default.failure(args) 25 | # return BoltResponse(status=args.suggested_status_code, body=args.reason) 26 | 27 | 28 | # Initialization 29 | app = App( 30 | signing_secret=os.environ.get("SLACK_SIGNING_SECRET"), 31 | installation_store=FileInstallationStore(), 32 | oauth_settings=OAuthSettings( 33 | client_id=os.environ.get("SLACK_CLIENT_ID"), 34 | client_secret=os.environ.get("SLACK_CLIENT_SECRET"), 35 | scopes=[ 36 | "assistant:write", 37 | "im:history", 38 | "chat:write", 39 | "channels:join", # required only for the channel summary 40 | "channels:history", # required only for the channel summary 41 | "groups:history", # required only for the channel summary 42 | ], 43 | user_scopes=[], 44 | redirect_uri=None, 45 | install_path="/slack/install", 46 | redirect_uri_path="/slack/oauth_redirect", 47 | state_store=FileOAuthStateStore(expiration_seconds=600), 48 | callback_options=CallbackOptions(success=success, failure=failure), 49 | ), 50 | ) 51 | 52 | # Register Listeners 53 | register_listeners(app) 54 | 55 | # Start Bolt app 56 | if __name__ == "__main__": 57 | app.start(3000) 58 | -------------------------------------------------------------------------------- /listeners/events/app_mentioned.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | 3 | from slack_bolt import Say 4 | from slack_sdk import WebClient 5 | 6 | from ai.llm_caller import call_llm 7 | from ..views.feedback_block import create_feedback_block 8 | 9 | 10 | def app_mentioned_callback(client: WebClient, event: dict, logger: Logger, say: Say): 11 | """ 12 | Handles the event when the app is mentioned in a Slack conversation 13 | and generates an AI response. 14 | 15 | Args: 16 | client: Slack WebClient for making API calls 17 | event: Event payload containing mention details (channel, user, text, etc.) 18 | logger: Logger instance for error tracking 19 | say: Function to send messages to the thread from the app 20 | """ 21 | try: 22 | channel_id = event.get("channel") 23 | team_id = event.get("team") 24 | text = event.get("text") 25 | thread_ts = event.get("thread_ts") or event.get("ts") 26 | user_id = event.get("user") 27 | 28 | client.assistant_threads_setStatus( 29 | channel_id=channel_id, 30 | thread_ts=thread_ts, 31 | status="thinking...", 32 | loading_messages=[ 33 | "Teaching the hamsters to type faster…", 34 | "Untangling the internet cables…", 35 | "Consulting the office goldfish…", 36 | "Polishing up the response just for you…", 37 | "Convincing the AI to stop overthinking…", 38 | ], 39 | ) 40 | 41 | returned_message = call_llm([{"role": "user", "content": text}]) 42 | 43 | streamer = client.chat_stream( 44 | channel=channel_id, 45 | recipient_team_id=team_id, 46 | recipient_user_id=user_id, 47 | thread_ts=thread_ts, 48 | ) 49 | 50 | # Loop over OpenAI response stream 51 | # https://platform.openai.com/docs/api-reference/responses/create 52 | for event in returned_message: 53 | if event.type == "response.output_text.delta": 54 | streamer.append(markdown_text=f"{event.delta}") 55 | else: 56 | continue 57 | 58 | feedback_block = create_feedback_block() 59 | streamer.stop(blocks=feedback_block) 60 | except Exception as e: 61 | logger.exception(f"Failed to handle a user message event: {e}") 62 | say(f":warning: Something went wrong! ({e})") 63 | -------------------------------------------------------------------------------- /listeners/assistant/message.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | from typing import Dict, List 3 | 4 | from slack_bolt import BoltContext, Say, SetStatus 5 | from slack_sdk import WebClient 6 | 7 | from ai.llm_caller import call_llm 8 | 9 | from ..views.feedback_block import create_feedback_block 10 | 11 | 12 | def message( 13 | client: WebClient, 14 | context: BoltContext, 15 | logger: Logger, 16 | payload: dict, 17 | say: Say, 18 | set_status: SetStatus, 19 | ): 20 | """ 21 | Handles when users send messages or select a prompt in an assistant thread and generate AI responses: 22 | 23 | Args: 24 | client: Slack WebClient for making API calls 25 | context: Bolt context containing channel and thread information 26 | logger: Logger instance for error tracking 27 | payload: Event payload with message details (channel, user, text, etc.) 28 | say: Function to send messages to the thread 29 | set_status: Function to update the assistant's status 30 | """ 31 | try: 32 | channel_id = payload["channel"] 33 | team_id = context.team_id 34 | thread_ts = payload["thread_ts"] 35 | user_id = context.user_id 36 | 37 | set_status( 38 | status="thinking...", 39 | loading_messages=[ 40 | "Teaching the hamsters to type faster…", 41 | "Untangling the internet cables…", 42 | "Consulting the office goldfish…", 43 | "Polishing up the response just for you…", 44 | "Convincing the AI to stop overthinking…", 45 | ], 46 | ) 47 | 48 | replies = client.conversations_replies( 49 | channel=context.channel_id, 50 | ts=context.thread_ts, 51 | oldest=context.thread_ts, 52 | limit=10, 53 | ) 54 | messages_in_thread: List[Dict[str, str]] = [] 55 | for message in replies["messages"]: 56 | role = "user" if message.get("bot_id") is None else "assistant" 57 | messages_in_thread.append({"role": role, "content": message["text"]}) 58 | 59 | returned_message = call_llm(messages_in_thread) 60 | 61 | streamer = client.chat_stream( 62 | channel=channel_id, 63 | recipient_team_id=team_id, 64 | recipient_user_id=user_id, 65 | thread_ts=thread_ts, 66 | ) 67 | 68 | # Loop over OpenAI response stream 69 | # https://platform.openai.com/docs/api-reference/responses/create 70 | for event in returned_message: 71 | if event.type == "response.output_text.delta": 72 | streamer.append(markdown_text=f"{event.delta}") 73 | else: 74 | continue 75 | 76 | feedback_block = create_feedback_block() 77 | streamer.stop(blocks=feedback_block) 78 | 79 | except Exception as e: 80 | logger.exception(f"Failed to handle a user message event: {e}") 81 | say(f":warning: Something went wrong! ({e})") 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Agent App Template (Bolt for Python) 2 | 3 | This Bolt for Python template demonstrates how to build [AI Apps](https://docs.slack.dev/ai/) in Slack. 4 | 5 | Models from [OpenAI](https://openai.com) are used and can be customized for prompts of all kinds. 6 | 7 | ## Setup 8 | 9 | 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). 10 | 11 | ### Developer Program 12 | 13 | Join the [Slack Developer Program](https://api.slack.com/developer-program) for exclusive access to sandbox environments for building and testing your apps, tooling, and resources created to help you build and grow. 14 | 15 | ## Installation 16 | 17 | Add this app to your workspace using either the Slack CLI or other development tooling, then read ahead to configuring LLM responses in the **[Providers](#providers)** section. 18 | 19 | ### Using Slack CLI 20 | 21 | Install the latest version of the Slack CLI for your operating system: 22 | 23 | - [Slack CLI for macOS & Linux](https://docs.slack.dev/tools/slack-cli/guides/installing-the-slack-cli-for-mac-and-linux/) 24 | - [Slack CLI for Windows](https://docs.slack.dev/tools/slack-cli/guides/installing-the-slack-cli-for-windows/) 25 | 26 | You'll also need to log in if this is your first time using the Slack CLI. 27 | 28 | ```sh 29 | slack login 30 | ``` 31 | 32 | #### Initializing the project 33 | 34 | ```sh 35 | slack create my-bolt-python-assistant --template slack-samples/bolt-python-assistant-template 36 | cd my-bolt-python-assistant 37 | ``` 38 | 39 | #### Creating the Slack app 40 | 41 | Use the following command to add your new Slack app to your development workspace. Choose a "local" app environment for upcoming development: 42 | 43 | ```sh 44 | slack install 45 | ``` 46 | 47 | After the Slack app has been created you're all set to configure the LLM provider! 48 | 49 | ### Using Terminal 50 | 51 | 1. Open [https://api.slack.com/apps/new](https://api.slack.com/apps/new) and choose "From an app manifest" 52 | 2. Choose the workspace you want to install the application to 53 | 3. Copy the contents of [manifest.json](./manifest.json) into the text box that says `*Paste your manifest code here*` (within the JSON tab) and click _Next_ 54 | 4. Review the configuration and click _Create_ 55 | 5. Click _Install to Workspace_ and _Allow_ on the screen that follows. You'll then be redirected to the App Configuration dashboard. 56 | 57 | #### Environment Variables 58 | 59 | Before you can run the app, you'll need to store some environment variables. 60 | 61 | 1. Rename `.env.sample` to `.env`. 62 | 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`. 63 | 64 | ```sh 65 | SLACK_BOT_TOKEN=YOUR_SLACK_BOT_TOKEN 66 | ``` 67 | 68 | 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`. 69 | 70 | ```sh 71 | SLACK_APP_TOKEN=YOUR_SLACK_APP_TOKEN 72 | ``` 73 | 74 | #### Initializing the project 75 | 76 | ```sh 77 | git clone https://github.com/slack-samples/bolt-python-assistant-template.git my-bolt-python-assistant 78 | cd my-bolt-python-assistant 79 | ``` 80 | 81 | #### Setup your python virtual environment 82 | 83 | ```sh 84 | python3 -m venv .venv 85 | source .venv/bin/activate # for Windows OS, .\.venv\Scripts\Activate instead should work 86 | ``` 87 | 88 | #### Install dependencies 89 | 90 | ```sh 91 | pip install -r requirements.txt 92 | ``` 93 | 94 | ## Providers 95 | 96 | ### OpenAI Setup 97 | 98 | Unlock the OpenAI models from your OpenAI account dashboard by clicking [create a new secret key](https://platform.openai.com/api-keys), then save your OpenAI key into the `.env` file as `OPENAI_API_KEY` like so: 99 | 100 | ```zsh 101 | OPENAI_API_KEY=YOUR_OPEN_API_KEY 102 | ``` 103 | 104 | ## Development 105 | 106 | ### Starting the app 107 | 108 | #### Slack CLI 109 | 110 | ```sh 111 | slack run 112 | ``` 113 | 114 | #### Terminal 115 | 116 | ```sh 117 | python3 app.py 118 | ``` 119 | 120 | Start talking to the bot! Start a new DM or thread and click the feedback button when it responds. 121 | 122 | ### Linting 123 | 124 | ```sh 125 | # Run ruff check from root directory for linting 126 | ruff check 127 | 128 | # Run ruff format from root directory for code formatting 129 | ruff format 130 | ``` 131 | 132 | ## Project Structure 133 | 134 | ### `manifest.json` 135 | 136 | `manifest.json` is a configuration for Slack apps. With a manifest, you can create an app with a pre-defined configuration, or adjust the configuration of an existing app. 137 | 138 | ### `app.py` 139 | 140 | `app.py` is the entry point for the application and is the file you'll run to start the server. This project aims to keep this file as thin as possible, primarily using it as a way to route inbound requests. 141 | 142 | ### `/listeners` 143 | 144 | 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. 145 | 146 | **`/listeners/assistant`** 147 | 148 | Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This module includes: 149 | 150 | - The `assistant_thread_started.py` file, which responds to new app threads with a list of suggested prompts. 151 | - The `message.py` file, which responds to user messages sent to app threads or from the **Chat** and **History** tab with an LLM generated response. 152 | 153 | ### `/ai` 154 | 155 | The `llm_caller.py` file, which handles OpenAI API integration and message formatting. It includes the `call_llm()` function that sends conversation threads to OpenAI's models. 156 | 157 | ## App Distribution / OAuth 158 | 159 | 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. 160 | 161 | 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. 162 | 163 | Start `ngrok` to access the app on an external network and create a redirect URL for OAuth. 164 | 165 | ``` 166 | ngrok http 3000 167 | ``` 168 | 169 | This output should include a forwarding address for `http` and `https` (we'll use `https`). It should look something like the following: 170 | 171 | ``` 172 | Forwarding https://3cb89939.ngrok.io -> http://localhost:3000 173 | ``` 174 | 175 | Navigate to **OAuth & Permissions** in your app configuration and click **Add a Redirect URL**. The redirect URL should be set to your `ngrok` forwarding address with the `slack/oauth_redirect` path appended. For example: 176 | 177 | ``` 178 | https://3cb89939.ngrok.io/slack/oauth_redirect 179 | ``` 180 | --------------------------------------------------------------------------------