Skip to content

Commit 3c993d5

Browse files
seratchmisscodedfilmaj
authored
Add Agents & Assistants document page (#1175)
Co-authored-by: Alissa Renz <[email protected]> Co-authored-by: Fil Maj <[email protected]>
1 parent a57619e commit 3c993d5

File tree

3 files changed

+478
-0
lines changed

3 files changed

+478
-0
lines changed

docs/content/basic/assistant.md

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
---
2+
title: Agents & Assistants
3+
lang: en
4+
slug: /concepts/assistant
5+
---
6+
7+
This guide focuses on how to implement Agents & Assistants using Bolt. For general information about the feature, please refer to the [API documentation](https://api.slack.com/docs/apps/ai).
8+
9+
To get started, enable the **Agents & Assistants** feature on [the app configuration page](https://api.slack.com/apps). Add [`assistant:write`](https://api.slack.com/scopes/assistant:write), [`chat:write`](https://api.slack.com/scopes/chat:write), and [`im:history`](https://api.slack.com/scopes/im:history) to the **bot** scopes on the **OAuth & Permissions** page. Make sure to subscribe to [`assistant_thread_started`](https://api.slack.com/events/assistant_thread_started), [`assistant_thread_context_changed`](https://api.slack.com/events/assistant_thread_context_changed), and [`message.im`](https://api.slack.com/events/message.im) events on the **Event Subscriptions** page.
10+
11+
Please note that this feature requires a paid plan. If you don't have a paid workspace for development, you can join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free.
12+
13+
To handle assistant thread interactions with humans, although you can implement your agents [using `app.event(...)` listeners](event-listening) for `assistant_thread_started`, `assistant_thread_context_changed`, and `message` events, Bolt offers a simpler approach. You just need to create an `Assistant` instance, attach the needed event handlers to it, and then add the assistant to your `App` instance.
14+
15+
```python
16+
assistant = Assistant()
17+
18+
# This listener is invoked when a human user opened an assistant thread
19+
@assistant.thread_started
20+
def start_assistant_thread(say: Say, set_suggested_prompts: SetSuggestedPrompts):
21+
# Send the first reply to the human who started chat with your app's assistant bot
22+
say(":wave: Hi, how can I help you today?")
23+
24+
# Setting suggested prompts is optional
25+
set_suggested_prompts(
26+
prompts=[
27+
# If the suggested prompt is long, you can use {"title": "short one to display", "message": "full prompt"} instead
28+
"What does SLACK stand for?",
29+
"When Slack was released?",
30+
],
31+
)
32+
33+
# This listener is invoked when the human user sends a reply in the assistant thread
34+
@assistant.user_message
35+
def respond_in_assistant_thread(
36+
payload: dict,
37+
logger: logging.Logger,
38+
context: BoltContext,
39+
set_status: SetStatus,
40+
say: Say,
41+
):
42+
try:
43+
# Tell the human user the assistant bot acknowledges the request and is working on it
44+
set_status("is typing...")
45+
46+
# Collect the conversation history with this user
47+
replies_in_thread = client.conversations_replies(
48+
channel=context.channel_id,
49+
ts=context.thread_ts,
50+
oldest=context.thread_ts,
51+
limit=10,
52+
)
53+
messages_in_thread: List[Dict[str, str]] = []
54+
for message in replies_in_thread["messages"]:
55+
role = "user" if message.get("bot_id") is None else "assistant"
56+
messages_in_thread.append({"role": role, "content": message["text"]})
57+
58+
# Pass the latest prompt and chat history to the LLM (call_llm is your own code)
59+
returned_message = call_llm(messages_in_thread)
60+
61+
# Post the result in the assistant thread
62+
say(text=returned_message)
63+
64+
except Exception as e:
65+
logger.exception(f"Failed to respond to an inquiry: {e}")
66+
# Don't forget sending a message telling the error
67+
# Without this, the status 'is typing...' won't be cleared, therefore the end-user is unable to continue the chat
68+
say(f":warning: Sorry, something went wrong during processing your request (error: {e})")
69+
70+
# Enable this assistant middleware in your Bolt app
71+
app.use(assistant)
72+
```
73+
74+
Refer to [the module document](https://tools.slack.dev/bolt-python/api-docs/slack_bolt/kwargs_injection/args.html) to learn the available listener arguments.
75+
76+
When a user opens an Assistant thread while in a channel, the channel information is stored as the thread's `AssistantThreadContext` data. You can access this information by using the `get_thread_context` utility. The reason Bolt provides this utility is that the most recent thread context information is not included in the subsequent user message event payload data. Therefore, an app must store the context data when it is changed so that the app can refer to the data in message event listeners.
77+
78+
When the user switches channels, the `assistant_thread_context_changed` event will be sent to your app. If you use the built-in `Assistant` middleware without any custom configuration (like the above code snippet does), the updated context data is automatically saved as message metadata of the first reply from the assistant bot.
79+
80+
As long as you use the built-in approach, you don't need to store the context data within a datastore. The downside of this default behavior is the overhead of additional calls to the Slack API. These calls include those to `conversations.history` which are used to look up the stored message metadata that contains the thread context (via `get_thread_context`).
81+
82+
To store context elsewhere, pass a custom `AssistantThreadContextStore` implementation to the `Assistant` constructor. We provide `FileAssistantThreadContextStore`, which is a reference implementation that uses the local file system:
83+
84+
```python
85+
# You can use your own thread_context_store if you want
86+
from slack_bolt import FileAssistantThreadContextStore
87+
assistant = Assistant(thread_context_store=FileAssistantThreadContextStore())
88+
```
89+
90+
Since this reference implementation relies on local files, it's not advised for use in production. For production apps, we recommend creating a class that inherits `AssistantThreadContextStore`.
91+
92+
<details>
93+
94+
<summary>
95+
Block Kit interactions in the assistant thread
96+
</summary>
97+
98+
For advanced use cases, Block Kit buttons may be used instead of suggested prompts, as well as the sending of messages with structured [metadata](https://api.slack.com/metadata) to trigger subsequent interactions with the user.
99+
100+
For example, an app can display a button like "Summarize the referring channel" in the initial reply. When the user clicks the button and submits detailed information (such as the number of messages, days to check, the purpose of the summary, etc.), the app can handle that information and post a message that describes the request with structured metadata.
101+
102+
By default, apps can't respond to their own bot messages (Bolt prevents infinite loops by default). However, if you pass `ignoring_self_assistant_message_events_enabled=False` to the `App` constructor and add a `bot_message` listener to your `Assistant` middleware, your app can continue processing the request as shown below:
103+
104+
```python
105+
app = App(
106+
token=os.environ["SLACK_BOT_TOKEN"],
107+
# This must be set to handle bot message events
108+
ignoring_self_assistant_message_events_enabled=False,
109+
)
110+
111+
assistant = Assistant()
112+
113+
# Refer to https://tools.slack.dev/bolt-python/api-docs/slack_bolt/kwargs_injection/args.html to learn available listener arguments
114+
115+
@assistant.thread_started
116+
def start_assistant_thread(say: Say):
117+
say(
118+
text=":wave: Hi, how can I help you today?",
119+
blocks=[
120+
{
121+
"type": "section",
122+
"text": {"type": "mrkdwn", "text": ":wave: Hi, how can I help you today?"},
123+
},
124+
{
125+
"type": "actions",
126+
"elements": [
127+
# You can have multiple buttons here
128+
{
129+
"type": "button",
130+
"action_id": "assistant-generate-random-numbers",
131+
"text": {"type": "plain_text", "text": "Generate random numbers"},
132+
"value": "clicked",
133+
},
134+
],
135+
},
136+
],
137+
)
138+
139+
# This listener is invoked when the above button is clicked
140+
@app.action("assistant-generate-random-numbers")
141+
def configure_random_number_generation(ack: Ack, client: WebClient, body: dict):
142+
ack()
143+
client.views_open(
144+
trigger_id=body["trigger_id"],
145+
view={
146+
"type": "modal",
147+
"callback_id": "configure_assistant_summarize_channel",
148+
"title": {"type": "plain_text", "text": "My Assistant"},
149+
"submit": {"type": "plain_text", "text": "Submit"},
150+
"close": {"type": "plain_text", "text": "Cancel"},
151+
# Relay the assistant thread information to app.view listener
152+
"private_metadata": json.dumps(
153+
{
154+
"channel_id": body["channel"]["id"],
155+
"thread_ts": body["message"]["thread_ts"],
156+
}
157+
),
158+
"blocks": [
159+
{
160+
"type": "input",
161+
"block_id": "num",
162+
"label": {"type": "plain_text", "text": "# of outputs"},
163+
# You can have this kind of predefined input from a user instead of parsing human text
164+
"element": {
165+
"type": "static_select",
166+
"action_id": "input",
167+
"placeholder": {"type": "plain_text", "text": "How many numbers do you need?"},
168+
"options": [
169+
{"text": {"type": "plain_text", "text": "5"}, "value": "5"},
170+
{"text": {"type": "plain_text", "text": "10"}, "value": "10"},
171+
{"text": {"type": "plain_text", "text": "20"}, "value": "20"},
172+
],
173+
"initial_option": {"text": {"type": "plain_text", "text": "5"}, "value": "5"},
174+
},
175+
}
176+
],
177+
},
178+
)
179+
180+
# This listener is invoked when the above modal is submitted
181+
@app.view("configure_assistant_summarize_channel")
182+
def receive_random_number_generation_details(ack: Ack, client: WebClient, payload: dict):
183+
ack()
184+
num = payload["state"]["values"]["num"]["input"]["selected_option"]["value"]
185+
thread = json.loads(payload["private_metadata"])
186+
187+
# Post a bot message with structured input data
188+
# The following assistant.bot_message will continue processing
189+
# If you prefer processing this request within this listener, it also works!
190+
# If you don't need bot_message listener, no need to set ignoring_self_assistant_message_events_enabled=False
191+
client.chat_postMessage(
192+
channel=thread["channel_id"],
193+
thread_ts=thread["thread_ts"],
194+
text=f"OK, you need {num} numbers. I will generate it shortly!",
195+
metadata={
196+
"event_type": "assistant-generate-random-numbers",
197+
"event_payload": {"num": int(num)},
198+
},
199+
)
200+
201+
# This listener is invoked whenever your app's bot user posts a message
202+
@assistant.bot_message
203+
def respond_to_bot_messages(logger: logging.Logger, set_status: SetStatus, say: Say, payload: dict):
204+
try:
205+
if payload.get("metadata", {}).get("event_type") == "assistant-generate-random-numbers":
206+
# Handle the above random-number-generation request
207+
set_status("is generating an array of random numbers...")
208+
time.sleep(1)
209+
nums: Set[str] = set()
210+
num = payload["metadata"]["event_payload"]["num"]
211+
while len(nums) < num:
212+
nums.add(str(random.randint(1, 100)))
213+
say(f"Here you are: {', '.join(nums)}")
214+
else:
215+
# nothing to do for this bot message
216+
# If you want to add more patterns here, be careful not to cause infinite loop messaging
217+
pass
218+
219+
except Exception as e:
220+
logger.exception(f"Failed to respond to an inquiry: {e}")
221+
222+
# This listener is invoked when the human user posts a reply
223+
@assistant.user_message
224+
def respond_to_user_messages(logger: logging.Logger, set_status: SetStatus, say: Say):
225+
try:
226+
set_status("is typing...")
227+
say("Please use the buttons in the first reply instead :bow:")
228+
except Exception as e:
229+
logger.exception(f"Failed to respond to an inquiry: {e}")
230+
say(f":warning: Sorry, something went wrong during processing your request (error: {e})")
231+
232+
233+
# Enable this assistant middleware in your Bolt app
234+
app.use(assistant)
235+
```
236+
237+
</details>
238+
239+
240+
Lastly, if you want to check full working example app, you can check [our sample repository](https://github.com/slack-samples/bolt-python-assistant-template) on GitHub.

0 commit comments

Comments
 (0)