Skip to content

Commit

Permalink
Initial submission (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch authored Sep 23, 2024
1 parent 95c64f2 commit e243e7d
Show file tree
Hide file tree
Showing 25 changed files with 376 additions and 305 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[flake8]
max-line-length = 125
max-line-length = 200
exclude = .gitignore,venv
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ tmp.txt
.DS_Store
logs/
*.db
.pytype/
.pytype/
.idea/
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Bolt for Python Template App
# App Assistant Sample (Bolt for Python)

This is a generic Bolt for Python template app used to build out Slack apps.
This Bolt for Python sample app demonstrates how to use [Agents & Assistants](https://api.slack.com/docs/apps/ai) in Slack.

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).
## Installation
Expand All @@ -22,15 +22,17 @@ Before you can run the app, you'll need to store some environment variables.
# Replace with your app token and bot token
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>
```

### Setup Your Local Project
```zsh
# Clone this project onto your machine
git clone https://github.com/slack-samples/bolt-python-starter-template.git
git clone https://github.com/slack-samples/bolt-python-app-assistant.git

# Change into this project directory
cd bolt-python-starter-template
cd bolt-python-app-assistant

# Setup your python virtual environment
python3 -m venv .venv
Expand Down
2 changes: 1 addition & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from listeners import register_listeners

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

# Register Listeners
register_listeners(app)
Expand Down
9 changes: 8 additions & 1 deletion app_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ def failure(args: FailureArgs) -> BoltResponse:
oauth_settings=OAuthSettings(
client_id=os.environ.get("SLACK_CLIENT_ID"),
client_secret=os.environ.get("SLACK_CLIENT_SECRET"),
scopes=["channels:history", "chat:write", "commands"],
scopes=[
"assistant:write",
"im:history",
"chat:write",
"channels:join", # required only for the channel summary
"channels:history", # required only for the channel summary
"groups:history", # required only for the channel summary
],
user_scopes=[],
redirect_uri=None,
install_path="/slack/install",
Expand Down
10 changes: 0 additions & 10 deletions listeners/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
from listeners import actions
from listeners import commands
from listeners import events
from listeners import messages
from listeners import shortcuts
from listeners import views


def register_listeners(app):
actions.register(app)
commands.register(app)
events.register(app)
messages.register(app)
shortcuts.register(app)
views.register(app)
6 changes: 0 additions & 6 deletions listeners/actions/__init__.py

This file was deleted.

64 changes: 0 additions & 64 deletions listeners/actions/sample_action.py

This file was deleted.

6 changes: 0 additions & 6 deletions listeners/commands/__init__.py

This file was deleted.

10 changes: 0 additions & 10 deletions listeners/commands/sample_command.py

This file was deleted.

27 changes: 25 additions & 2 deletions listeners/events/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
from typing import Dict, Any

from slack_bolt import App
from .app_home_opened import app_home_opened_callback
from slack_bolt.request.payload_utils import is_event

from .assistant_thread_started import start_thread_with_suggested_prompts
from .asssistant_thread_context_changed import save_new_thread_context
from .user_message import respond_to_user_message


def register(app: App):
app.event("app_home_opened")(app_home_opened_callback)
app.event("assistant_thread_started")(start_thread_with_suggested_prompts)
app.event("assistant_thread_context_changed")(save_new_thread_context)
app.event("message", matchers=[is_user_message_event_in_assistant_thread])(respond_to_user_message)
app.event("message", matchers=[is_message_event_in_assistant_thread])(just_ack)


def is_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
if is_event(body):
return body["event"]["type"] == "message" and body["event"].get("channel_type") == "im"
return False


def is_user_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
return is_message_event_in_assistant_thread(body) and body["event"].get("subtype") in (None, "file_share")


def just_ack():
pass
33 changes: 0 additions & 33 deletions listeners/events/app_home_opened.py

This file was deleted.

64 changes: 64 additions & 0 deletions listeners/events/assistant_thread_started.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from typing import List, Dict
from logging import Logger

from slack_sdk import WebClient


def start_thread_with_suggested_prompts(
payload: dict,
client: WebClient,
logger: Logger,
):
thread = payload["assistant_thread"]
channel_id, thread_ts = thread["channel_id"], thread["thread_ts"]
try:
thread_context = thread.get("context")
message_metadata = (
{
"event_type": "assistant_thread_context",
"event_payload": thread_context,
}
if bool(thread_context) is True # the dict is not empty
else None
)
client.chat_postMessage(
text="How can I help you?",
channel=channel_id,
thread_ts=thread_ts,
metadata=message_metadata,
)

prompts: List[Dict[str, str]] = [
{
"title": "What does Slack stand for?",
"message": "Slack, a business communication service, was named after an acronym. Can you guess what it stands for?",
},
{
"title": "Write a draft announcement",
"message": "Can you write a draft announcement about a new feature my team just released? It must include how impactful it is.",
},
{
"title": "Suggest names for my Slack app",
"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.",
},
]
if message_metadata is not None:
prompts.append(
{
"title": "Summarize the referred channel",
"message": "Can you generate a brief summary of the referred channel?",
}
)

client.assistant_threads_setSuggestedPrompts(
channel_id=channel_id,
thread_ts=thread_ts,
prompts=prompts,
)
except Exception as e:
logger.exception(f"Failed to handle an assistant_thread_started event: {e}", e)
client.chat_postMessage(
channel=channel_id,
thread_ts=thread_ts,
text=f":warning: Something went wrong! ({e})",
)
19 changes: 19 additions & 0 deletions listeners/events/asssistant_thread_context_changed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from slack_sdk import WebClient
from slack_bolt import BoltContext

from .thread_context_store import save_thread_context


def save_new_thread_context(
payload: dict,
client: WebClient,
context: BoltContext,
):
thread = payload["assistant_thread"]
save_thread_context(
context=context,
client=client,
channel_id=thread["channel_id"],
thread_ts=thread["thread_ts"],
new_context=thread.get("context"),
)
56 changes: 56 additions & 0 deletions listeners/events/llm_caller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os
import re
from typing import List, Dict

import openai

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) -> str:
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.chat.completions.create(
model="gpt-4o-mini",
n=1,
messages=messages,
max_tokens=16384,
)
return markdown_to_slack(response.choices[0].message.content)


# Conversion from OpenAI markdown to Slack mrkdwn
# See also: https://api.slack.com/reference/surfaces/formatting#basics
def markdown_to_slack(content: str) -> str:
# Split the input string into parts based on code blocks and inline code
parts = re.split(r"(?s)(```.+?```|`[^`\n]+?`)", content)

# Apply the bold, italic, and strikethrough formatting to text not within code
result = ""
for part in parts:
if part.startswith("```") or part.startswith("`"):
result += part
else:
for o, n in [
(
r"\*\*\*(?!\s)([^\*\n]+?)(?<!\s)\*\*\*",
r"_*\1*_",
), # ***bold italic*** to *_bold italic_*
(
r"(?<![\*_])\*(?!\s)([^\*\n]+?)(?<!\s)\*(?![\*_])",
r"_\1_",
), # *italic* to _italic_
(r"\*\*(?!\s)([^\*\n]+?)(?<!\s)\*\*", r"*\1*"), # **bold** to *bold*
(r"__(?!\s)([^_\n]+?)(?<!\s)__", r"*\1*"), # __bold__ to *bold*
(r"~~(?!\s)([^~\n]+?)(?<!\s)~~", r"~\1~"), # ~~strike~~ to ~strike~
]:
part = re.sub(o, n, part)
result += part
return result
Loading

0 comments on commit e243e7d

Please sign in to comment.