Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial submission #1

Merged
merged 5 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For gen AI apps, this length is too small, thus I've changed it this time

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/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for PyCharm users

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't already have this in place in the main template, can we open a PR to update it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate you! 🙇🏼‍♀️

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
# Agent & Assistant Sample (Bolt for Python)

This is a generic Bolt for Python template app used to build out Slack apps.
This is a 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-agent-assistant.git

# Change into this project directory
cd bolt-python-starter-template
cd bolt-python-agent-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
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other message events such as message_changed, message_deleted etc. are produced by an assistant thread's behavior. Just acknowledging them.



def is_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These is_xxx methods will be unnecessary once bolt-python's full support is released: slackapi/bolt-python#1162

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