Skip to content

Commit 44abe30

Browse files
more
1 parent 3aad114 commit 44abe30

3 files changed

Lines changed: 347 additions & 277 deletions

File tree

docs/english/_sidebar.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@
3939
"tools/bolt-python/concepts/app-home"
4040
]
4141
},
42+
{
43+
"type": "category",
44+
"label": "Creating agents",
45+
"link": {
46+
"type": "doc",
47+
"id": "tools/bolt-python/concepts/ai-apps"
48+
},
49+
"items": [
50+
"tools/bolt-python/concepts/ai-apps",
51+
"tools/bolt-python/concepts/assistant-class"
52+
]
53+
},
4254
"tools/bolt-python/concepts/ai-apps",
4355
{
4456
"type": "category",

docs/english/concepts/ai-apps.md

Lines changed: 6 additions & 277 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ If you're unfamiliar with using these feature within Slack, you may want to read
77

88
## Listening for events
99

10-
1110
Agents can be invoked throughout Slack, such as @mentions in channels.
1211

12+
:::tip[Using the Assistant side panel]
13+
14+
The Assistant side panel requires additional setup. See the [Assistant class guide](/concepts/assistant-class).
15+
:::
16+
1317
```python
1418
import re
1519
from logging import Logger
@@ -59,283 +63,10 @@ def handle_app_mentioned(
5963
...
6064
```
6165

62-
### Via the Assistant class (side panel)
63-
64-
:::info[Some features within this guide require a paid plan]
65-
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.
66-
:::
67-
68-
The [`Assistant`](/tools/bolt-js/reference#the-assistantconfig-configuration-object) class handles incoming events from the Slack Agents & AI Apps feature. A typical flow:
69-
70-
1. [The user starts a thread](#handling-new-thread). The `Assistant` class handles the incoming [`assistant_thread_started`](/reference/events/assistant_thread_started) event.
71-
2. [The thread context may change](#handling-thread-context-changes). The `Assistant` class handles [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) events and automatically manages context.
72-
3. [The user responds](#handling-user-response). The `Assistant` class handles the incoming [`message.im`](/reference/events/message.im) event.
73-
74-
```python
75-
assistant = Assistant()
76-
77-
# This listener is invoked when a human user opened an assistant thread
78-
@assistant.thread_started
79-
def start_assistant_thread(
80-
say: Say,
81-
get_thread_context: GetThreadContext,
82-
set_suggested_prompts: SetSuggestedPrompts,
83-
logger: logging.Logger,
84-
):
85-
try:
86-
...
87-
88-
# This listener is invoked when the human user sends a reply in the assistant thread
89-
@assistant.user_message
90-
def respond_in_assistant_thread(
91-
client: WebClient,
92-
context: BoltContext,
93-
get_thread_context: GetThreadContext,
94-
logger: logging.Logger,
95-
payload: dict,
96-
say: Say,
97-
set_status: SetStatus,
98-
):
99-
try:
100-
...
101-
102-
# Enable this assistant middleware in your Bolt app
103-
app.use(assistant)
104-
```
105-
106-
:::info[Consider the following]
107-
You _could_ go it alone and [listen](/tools/bolt-python/concepts/event-listening) for the `assistant_thread_started`, `assistant_thread_context_changed`, and `message.im` events in order to implement the AI features in your app. That being said, using the `Assistant` class will streamline the process. And we already wrote this nice guide for you!
108-
:::
109-
110-
While the `assistant_thread_started` and `assistant_thread_context_changed` events do provide Slack-client thread context information, the `message.im` event does not. Any subsequent user message events won't contain thread context data. For that reason, Bolt not only provides a way to store thread context — the `threadContextStore` property — but it also provides a `DefaultThreadContextStore` instance that is utilized by default. This implementation relies on storing and retrieving [message metadata](/messaging/message-metadata/) as the user interacts with the app.
111-
112-
If you do provide your own `threadContextStore` property, it must feature `get` and `save` methods.
113-
114-
:::tip[Refer to the [reference docs](https://docs.slack.dev/tools/bolt-python/reference/kwargs_injection/args.html) to learn the available listener arguments.]
115-
:::
116-
117-
#### Configuring your app to support the `Assistant` class {#configuring-assistant-class}
118-
119-
1. Within [App Settings](https://api.slack.com/apps), enable the **Agents & AI Apps** feature.
120-
121-
2. Within the App Settings **OAuth & Permissions** page, add the following scopes:
122-
* [`assistant:write`](/reference/scopes/assistant.write)
123-
* [`chat:write`](/reference/scopes/chat.write)
124-
* [`im:history`](/reference/scopes/im.history)
125-
126-
3. Within the App Settings **Event Subscriptions** page, subscribe to the following events:
127-
* [`assistant_thread_started`](/reference/events/assistant_thread_started)
128-
* [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed)
129-
* [`message.im`](/reference/events/message.im)
130-
131-
#### Handling a new thread {#handling-new-thread}
132-
133-
When the user opens a new thread with your AI-enabled app, the [`assistant_thread_started`](/reference/events/assistant_thread_started) event will be sent to your app.
134-
135-
:::tip[When a user opens an app thread while in a channel, the channel info is stored as the thread's `AssistantThreadContext` data.]
136-
137-
You can grab that info by using the `get_thread_context` utility, as subsequent user message event payloads won't include the channel info.
138-
:::
139-
140-
```python
141-
assistant = Assistant()
142-
143-
@assistant.thread_started
144-
def start_assistant_thread(
145-
say: Say,
146-
get_thread_context: GetThreadContext,
147-
set_suggested_prompts: SetSuggestedPrompts,
148-
logger: logging.Logger,
149-
):
150-
try:
151-
say("How can I help you?")
152-
153-
prompts: List[Dict[str, str]] = [
154-
{
155-
"title": "Suggest names for my Slack app",
156-
"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.",
157-
},
158-
]
159-
160-
thread_context = get_thread_context()
161-
if thread_context is not None and thread_context.channel_id is not None:
162-
summarize_channel = {
163-
"title": "Summarize the referred channel",
164-
"message": "Can you generate a brief summary of the referred channel?",
165-
}
166-
prompts.append(summarize_channel)
167-
168-
set_suggested_prompts(prompts=prompts)
169-
except Exception as e:
170-
logger.exception(f"Failed to handle an assistant_thread_started event: {e}", e)
171-
say(f":warning: Something went wrong! ({e})")
172-
```
173-
174-
You can send more complex messages to the user — see [Sending Block Kit alongside messages](#block-kit-interactions) for more info.
175-
176-
#### Handling thread context changes {#handling-thread-context-changes}
177-
178-
When the user switches channels, the [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) event will be sent to your app.
179-
180-
If you use the built-in `Assistant` middleware without any custom configuration, the updated context data is automatically saved as [message metadata](/messaging/message-metadata/) of the first reply from the app.
181-
182-
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`).
183-
184-
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. 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`.
185-
186-
```python
187-
from slack_bolt import FileAssistantThreadContextStore
188-
assistant = Assistant(thread_context_store=FileAssistantThreadContextStore())
189-
```
190-
191-
#### Handling the user response {#handling-user-response}
192-
193-
When the user messages your app, the [`message.im`](/reference/events/message.im) event will be sent to your app.
194-
195-
Messages sent to the app do not contain a [subtype](/reference/events/message#subtypes) and must be deduced based on their shape and any provided [message metadata](/messaging/message-metadata/).
196-
197-
There are three utilities that are particularly useful in curating the user experience:
198-
* [`say`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.Say)
199-
* [`setTitle`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetTitle)
200-
* [`setStatus`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetStatus)
201-
202-
Within the `setStatus` utility, you can cycle through strings passed into a `loading_messages` array. See [Setting assistant status](#setting-assistant-status) for implementation examples.
203-
204-
#### Sending Block Kit alongside messages {#block-kit-interactions}
205-
206-
For advanced use cases, Block Kit buttons may be used instead of suggested prompts, as well as the sending of messages with structured [metadata](/messaging/message-metadata/) to trigger subsequent interactions with the user.
207-
208-
For example, an app can display a button such as "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, purpose of the summary, etc.), the app can handle that information and post a message that describes the request with structured metadata.
209-
210-
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:
211-
212-
```python
213-
app = App(
214-
token=os.environ["SLACK_BOT_TOKEN"],
215-
# This must be set to handle bot message events
216-
ignoring_self_assistant_message_events_enabled=False,
217-
)
218-
219-
assistant = Assistant()
220-
221-
@assistant.thread_started
222-
def start_assistant_thread(say: Say):
223-
say(
224-
text=":wave: Hi, how can I help you today?",
225-
blocks=[
226-
{
227-
"type": "section",
228-
"text": {"type": "mrkdwn", "text": ":wave: Hi, how can I help you today?"},
229-
},
230-
{
231-
"type": "actions",
232-
"elements": [
233-
# You can have multiple buttons here
234-
{
235-
"type": "button",
236-
"action_id": "assistant-generate-random-numbers",
237-
"text": {"type": "plain_text", "text": "Generate random numbers"},
238-
"value": "clicked",
239-
},
240-
],
241-
},
242-
],
243-
)
244-
245-
# This listener is invoked when the above button is clicked
246-
@app.action("assistant-generate-random-numbers")
247-
def configure_random_number_generation(ack: Ack, client: WebClient, body: dict):
248-
ack()
249-
client.views_open(
250-
trigger_id=body["trigger_id"],
251-
view={
252-
"type": "modal",
253-
"callback_id": "configure_assistant_summarize_channel",
254-
"title": {"type": "plain_text", "text": "My Assistant"},
255-
"submit": {"type": "plain_text", "text": "Submit"},
256-
"close": {"type": "plain_text", "text": "Cancel"},
257-
# Relay the assistant thread information to app.view listener
258-
"private_metadata": json.dumps(
259-
{
260-
"channel_id": body["channel"]["id"],
261-
"thread_ts": body["message"]["thread_ts"],
262-
}
263-
),
264-
"blocks": [
265-
{
266-
"type": "input",
267-
"block_id": "num",
268-
"label": {"type": "plain_text", "text": "# of outputs"},
269-
# You can have this kind of predefined input from a user instead of parsing human text
270-
"element": {
271-
"type": "static_select",
272-
"action_id": "input",
273-
"placeholder": {"type": "plain_text", "text": "How many numbers do you need?"},
274-
"options": [
275-
{"text": {"type": "plain_text", "text": "5"}, "value": "5"},
276-
{"text": {"type": "plain_text", "text": "10"}, "value": "10"},
277-
{"text": {"type": "plain_text", "text": "20"}, "value": "20"},
278-
],
279-
"initial_option": {"text": {"type": "plain_text", "text": "5"}, "value": "5"},
280-
},
281-
}
282-
],
283-
},
284-
)
285-
286-
# This listener is invoked when the above modal is submitted
287-
@app.view("configure_assistant_summarize_channel")
288-
def receive_random_number_generation_details(ack: Ack, client: WebClient, payload: dict):
289-
ack()
290-
num = payload["state"]["values"]["num"]["input"]["selected_option"]["value"]
291-
thread = json.loads(payload["private_metadata"])
292-
293-
# Post a bot message with structured input data
294-
# The following assistant.bot_message will continue processing
295-
# If you prefer processing this request within this listener, it also works!
296-
# If you don't need bot_message listener, no need to set ignoring_self_assistant_message_events_enabled=False
297-
client.chat_postMessage(
298-
channel=thread["channel_id"],
299-
thread_ts=thread["thread_ts"],
300-
text=f"OK, you need {num} numbers. I will generate it shortly!",
301-
metadata={
302-
"event_type": "assistant-generate-random-numbers",
303-
"event_payload": {"num": int(num)},
304-
},
305-
)
306-
307-
# This listener is invoked whenever your app's bot user posts a message
308-
@assistant.bot_message
309-
def respond_to_bot_messages(logger: logging.Logger, set_status: SetStatus, say: Say, payload: dict):
310-
try:
311-
if payload.get("metadata", {}).get("event_type") == "assistant-generate-random-numbers":
312-
# Handle the above random-number-generation request
313-
set_status("is generating an array of random numbers...")
314-
time.sleep(1)
315-
nums: Set[str] = set()
316-
num = payload["metadata"]["event_payload"]["num"]
317-
while len(nums) < num:
318-
nums.add(str(random.randint(1, 100)))
319-
say(f"Here you are: {', '.join(nums)}")
320-
else:
321-
# nothing to do for this bot message
322-
# If you want to add more patterns here, be careful not to cause infinite loop messaging
323-
pass
324-
325-
except Exception as e:
326-
logger.exception(f"Failed to respond to an inquiry: {e}")
327-
...
328-
```
329-
330-
See the [_Adding and handling feedback_](#adding-and-handling-feedback) section for adding feedback buttons with Block Kit.
331-
33266
## Setting assistant status {#setting-assistant-status}
33367

33468
Your app can show users action is happening behind the scenes by setting the thread status.
33569

336-
<Tabs>
337-
<TabItem value="app_listener" label="App listener">
338-
33970
```python
34071
def handle_app_mentioned(
34172
set_status: SetStatus,
@@ -374,9 +105,6 @@ def respond_in_assistant_thread(
374105
)
375106
```
376107

377-
</TabItem>
378-
</Tabs>
379-
380108
## Streaming messages {#text-streaming}
381109

382110
You can have your app's messages stream in to replicate conventional agent behavior. Bolt for Python provides a `say_stream` utility as a listener argument available for `app.event` and `app.message` listeners.
@@ -795,4 +523,5 @@ def handle_app_mentioned(
795523

796524
---
797525

526+
For a dedicated AI assistant experience with a side panel, see the [Assistant class guide](./assistant-class.md).
798527

0 commit comments

Comments
 (0)