diff --git a/bolt-socket-mode/src/test/java/samples/AssistantEventListenerApp.java b/bolt-socket-mode/src/test/java/samples/AssistantEventListenerApp.java index 397250929..8a9534f49 100644 --- a/bolt-socket-mode/src/test/java/samples/AssistantEventListenerApp.java +++ b/bolt-socket-mode/src/test/java/samples/AssistantEventListenerApp.java @@ -6,6 +6,7 @@ import com.slack.api.model.assistant.SuggestedPrompt; import com.slack.api.model.event.*; +import java.util.Arrays; import java.util.Collections; public class AssistantEventListenerApp { @@ -96,13 +97,14 @@ public static void main(String[] args) throws Exception { ctx.client().assistantThreadsSetStatus(r -> r .channelId(channelId) .threadTs(threadTs) - .status("is analyzing the files...") + .status("is downloading the files...") ); Thread.sleep(500L); ctx.client().assistantThreadsSetStatus(r -> r .channelId(channelId) .threadTs(threadTs) - .status("is still checking the files...") + .status("is analyzing the files...") + .loadingMessages(Arrays.asList("Reading bytes...", "Confirming hashes...")) ); Thread.sleep(500L); ctx.client().chatPostMessage(r -> r diff --git a/bolt-socket-mode/src/test/java/samples/AssistantInteractionApp.java b/bolt-socket-mode/src/test/java/samples/AssistantInteractionApp.java index e205883da..00ecf8566 100644 --- a/bolt-socket-mode/src/test/java/samples/AssistantInteractionApp.java +++ b/bolt-socket-mode/src/test/java/samples/AssistantInteractionApp.java @@ -35,7 +35,7 @@ public static void main(String[] args) throws Exception { ctx.say(r -> r .text("Hi, how can I help you today?") .blocks(Arrays.asList( - section(s -> s.text(plainText("Hi, how I can I help you today?"))), + section(s -> s.text(plainText("Hi, how can I help you today?"))), actions(a -> a.elements(Collections.singletonList( button(b -> b.actionId("assistant-generate-numbers").text(plainText("Generate numbers"))) ))) @@ -105,7 +105,7 @@ public static void main(String[] args) throws Exception { }); app.event(AppMentionEvent.class, (req, ctx) -> { - ctx.say("You can help you at our 1:1 DM!"); + ctx.say("I can help you at our 1:1 DM!"); return ctx.ack(); }); diff --git a/bolt-socket-mode/src/test/java/samples/AssistantSimpleApp.java b/bolt-socket-mode/src/test/java/samples/AssistantSimpleApp.java index 993be2fc9..87c2bb47d 100644 --- a/bolt-socket-mode/src/test/java/samples/AssistantSimpleApp.java +++ b/bolt-socket-mode/src/test/java/samples/AssistantSimpleApp.java @@ -8,6 +8,7 @@ import com.slack.api.model.event.AppMentionEvent; import com.slack.api.model.event.MessageEvent; +import java.util.Arrays; import java.util.Collections; public class AssistantSimpleApp { @@ -37,9 +38,9 @@ public static void main(String[] args) throws Exception { // ctx.setStatus(r -> r.status("is typing...")); works too ctx.setStatus("is typing..."); Thread.sleep(500L); - if (ctx.getThreadContext() != null) { + if (ctx.getThreadContext() != null && ctx.getThreadContext().getChannelId() != null) { String contextChannel = ctx.getThreadContext().getChannelId(); - ctx.say("I am ware of the channel context: <#" + contextChannel + ">"); + ctx.say("I am aware of the channel context: <#" + contextChannel + ">"); } else { ctx.say("Here you are!"); } @@ -55,9 +56,9 @@ public static void main(String[] args) throws Exception { assistant.userMessageWithFiles((req, ctx) -> { try { - ctx.setStatus("is analyzing the files..."); + ctx.setStatus("is downloading the files..."); Thread.sleep(500L); - ctx.setStatus("is still checking the files..."); + ctx.setStatus("is analyzing the files...", Arrays.asList("Reading bytes...", "Confirming hashes...")); Thread.sleep(500L); ctx.say("Your files do not have any issues!"); } catch (Exception e) { @@ -77,7 +78,7 @@ public static void main(String[] args) throws Exception { }); app.event(AppMentionEvent.class, (req, ctx) -> { - ctx.say("You can help you at our 1:1 DM!"); + ctx.say("I can help you at our 1:1 DM!"); return ctx.ack(); }); diff --git a/bolt/src/main/java/com/slack/api/bolt/context/builtin/EventContext.java b/bolt/src/main/java/com/slack/api/bolt/context/builtin/EventContext.java index 836012d9b..72f9d2c86 100644 --- a/bolt/src/main/java/com/slack/api/bolt/context/builtin/EventContext.java +++ b/bolt/src/main/java/com/slack/api/bolt/context/builtin/EventContext.java @@ -116,6 +116,19 @@ public AssistantThreadsSetStatusResponse setStatus(String status) throws IOExcep } } + public AssistantThreadsSetStatusResponse setStatus(String status, List loadingMessages) throws IOException, SlackApiException { + if (isAssistantThreadEvent()) { + return this.client().assistantThreadsSetStatus(r -> r + .channelId(this.getChannelId()) + .threadTs(this.getThreadTs()) + .status(status) + .loadingMessages(loadingMessages) + ); + } else { + throw new IllegalStateException("This utility is only available for Assistant feature enabled app!"); + } + } + public AssistantThreadsSetStatusResponse setStatus(RequestConfigurator req) throws IOException, SlackApiException { if (isAssistantThreadEvent()) { return this.client().assistantThreadsSetStatus(req.configure(AssistantThreadsSetStatusRequest.builder() diff --git a/bolt/src/test/java/test_locally/app/EventAssistantTest.java b/bolt/src/test/java/test_locally/app/EventAssistantTest.java index b4e759758..495a06b81 100644 --- a/bolt/src/test/java/test_locally/app/EventAssistantTest.java +++ b/bolt/src/test/java/test_locally/app/EventAssistantTest.java @@ -56,6 +56,10 @@ public void test() throws Exception { ctx.setTitle("title"); ctx.setTitle(r -> r.title("title")); ctx.setStatus("is typing..."); + ctx.setStatus( + "is typing...", + Arrays.asList("Teaching hamsters...", "Untangling cables...") + ); ctx.setStatus(r -> r.status("is typing...")); ctx.setSuggestedPrompts(new ArrayList<>()); ctx.setSuggestedPrompts(r -> r.prompts(new ArrayList<>())); diff --git a/docs/english/guides/ai-apps.md b/docs/english/guides/ai-apps.md index 6e230aa9e..64c2cb795 100644 --- a/docs/english/guides/ai-apps.md +++ b/docs/english/guides/ai-apps.md @@ -15,15 +15,17 @@ The Agents & AI Apps feature comprises a unique messaging experience for Slack. 1. Within [app settings](https://api.slack.com/apps), enable the **Agents & AI Apps** feature. -2. Within the app settings **OAuth & Permissions** page, add the following scopes: - * [`assistant:write`](/reference/scopes/assistant.write) - * [`chat:write`](/reference/scopes/chat.write) - * [`im:history`](/reference/scopes/im.history) +2. Within the app settings **OAuth & Permissions** page, add the following scopes: -3. Within the app settings **Event Subscriptions** page, subscribe to the following events: - * [`assistant_thread_started`](/reference/events/assistant_thread_started) - * [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) - * [`message.im`](/reference/events/message.im) +- [`assistant:write`](/reference/scopes/assistant.write) +- [`chat:write`](/reference/scopes/chat.write) +- [`im:history`](/reference/scopes/im.history) + +3. Within the app settings **Event Subscriptions** page, subscribe to the following events: + +- [`assistant_thread_started`](/reference/events/assistant_thread_started) +- [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) +- [`message.im`](/reference/events/message.im) ## The `Assistant` class instance {#assistant-class} @@ -31,7 +33,7 @@ The [`Assistant`](/tools/java-slack-sdk/reference#the-assistantconfig-configurat 1. [The user starts a thread](#handling-a-new-thread). The `Assistant` class handles the incoming [`assistant_thread_started`](/reference/events/assistant_thread_started) event. 2. [The thread context may change at any point](#handling-thread-context-changes). The `Assistant` class can handle any incoming [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) events. The class also provides a default `context` store to keep track of thread context changes as the user moves through Slack. -3. [The user responds](#handling-user-response). The `Assistant` class handles the incoming [`message.im`](/reference/events/message.im) event. +3. [The user responds](#handling-user-response). The `Assistant` class handles the incoming [`message.im`](/reference/events/message.im) event. ```java App app = new App(); @@ -52,9 +54,9 @@ assistant.userMessage((req, ctx) -> { try { ctx.setStatus("is typing..."); Thread.sleep(500L); - if (ctx.getThreadContext() != null) { + if (ctx.getThreadContext() != null && ctx.getThreadContext().getChannelId() != null) { String contextChannel = ctx.getThreadContext().getChannelId(); - ctx.say(r -> r.text("I am ware of the channel context: <#" + contextChannel + ">")); + ctx.say(r -> r.text("I am aware of the channel context: <#" + contextChannel + ">")); } else { ctx.say(r -> r.text("Here you are!")); } @@ -108,7 +110,7 @@ assistant.threadStarted((req, ctx) -> { ctx.say(r -> r .text("Hi, how can I help you today?") .blocks(Arrays.asList( - section(s -> s.text(plainText("Hi, how I can I help you today?"))), + section(s -> s.text(plainText("Hi, how can I help you today?"))), actions(a -> a.elements(Collections.singletonList( button(b -> b.actionId("assistant-generate-numbers").text(plainText("Generate numbers"))) ))) @@ -175,9 +177,9 @@ app.assistant(assistant); ## Handling thread context changes {#handling-thread-context-changes} -When the user switches channels, the [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) event will be sent to your app. +When the user switches channels, the [`assistant_thread_context_changed`](/reference/events/assistant_thread_context_changed) event will be sent to your app. -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 assistant bot. +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 assistant bot. 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 `context.getThreadContextService().findCurrentContext(channelId, threadTs)`). @@ -194,9 +196,10 @@ When the user messages your app, the [`message.im`](/reference/events/message.im Messages sent to the app do not contain a [subtype](/reference/events/message) and must be deduced based on their shape and any provided [message metadata](/messaging/message-metadata/). There are three utilities that are particularly useful in curating the user experience: -* [`say`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.Say) -* [`setTitle`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetTitle) -* [`setStatus`](https://docs.slack.dev/tools/bolt-python/reference/#slack_bolt.SetStatus) + +- [`say`]() +- [`setTitle`]() +- [`setStatus`]() ## Full example: Assistant Simple App {#full-example} @@ -213,6 +216,7 @@ import com.slack.api.model.assistant.SuggestedPrompt; import com.slack.api.model.event.AppMentionEvent; import com.slack.api.model.event.MessageEvent; +import java.util.Arrays; import java.util.Collections; public class AssistantSimpleApp { @@ -242,9 +246,9 @@ public class AssistantSimpleApp { // ctx.setStatus(r -> r.status("is typing...")); works too ctx.setStatus("is typing..."); Thread.sleep(500L); - if (ctx.getThreadContext() != null) { + if (ctx.getThreadContext() != null && ctx.getThreadContext().getChannelId() != null) { String contextChannel = ctx.getThreadContext().getChannelId(); - ctx.say("I am ware of the channel context: <#" + contextChannel + ">"); + ctx.say("I am aware of the channel context: <#" + contextChannel + ">"); } else { ctx.say("Here you are!"); } @@ -260,9 +264,9 @@ public class AssistantSimpleApp { assistant.userMessageWithFiles((req, ctx) -> { try { - ctx.setStatus("is analyzing the files..."); + ctx.setStatus("is downloading the files..."); Thread.sleep(500L); - ctx.setStatus("is still checking the files..."); + ctx.setStatus("is analyzing the files...", Arrays.asList("Reading bytes...", "Confirming hashes...")); Thread.sleep(500L); ctx.say("Your files do not have any issues!"); } catch (Exception e) { @@ -282,7 +286,7 @@ public class AssistantSimpleApp { }); app.event(AppMentionEvent.class, (req, ctx) -> { - ctx.say("You can help you at our 1:1 DM!"); + ctx.say("I can help you at our 1:1 DM!"); return ctx.ack(); }); diff --git a/slack-api-client/src/main/java/com/slack/api/methods/RequestFormBuilder.java b/slack-api-client/src/main/java/com/slack/api/methods/RequestFormBuilder.java index 33af10f29..08b38a8fd 100644 --- a/slack-api-client/src/main/java/com/slack/api/methods/RequestFormBuilder.java +++ b/slack-api-client/src/main/java/com/slack/api/methods/RequestFormBuilder.java @@ -1140,6 +1140,9 @@ public static FormBody.Builder toForm(AssistantThreadsSetStatusRequest req) { setIfNotNull("channel_id", req.getChannelId(), form); setIfNotNull("thread_ts", req.getThreadTs(), form); setIfNotNull("status", req.getStatus(), form); + if (req.getLoadingMessages() != null) { + setIfNotNull("loading_messages", req.getLoadingMessages().stream().collect(joining(",")), form); + } return form; } diff --git a/slack-api-client/src/main/java/com/slack/api/methods/request/assistant/threads/AssistantThreadsSetStatusRequest.java b/slack-api-client/src/main/java/com/slack/api/methods/request/assistant/threads/AssistantThreadsSetStatusRequest.java index 868e949b1..95256617f 100644 --- a/slack-api-client/src/main/java/com/slack/api/methods/request/assistant/threads/AssistantThreadsSetStatusRequest.java +++ b/slack-api-client/src/main/java/com/slack/api/methods/request/assistant/threads/AssistantThreadsSetStatusRequest.java @@ -4,6 +4,8 @@ import lombok.Builder; import lombok.Data; +import java.util.List; + /** * https://docs.slack.dev/reference/methods/assistant.threads.setStatus */ @@ -27,4 +29,9 @@ public class AssistantThreadsSetStatusRequest implements SlackApiRequest { * Status of the specified bot user, e.g. 'is thinking...' */ private String status; -} \ No newline at end of file + + /** + * The list of messages to rotate through as a loading indicator. + */ + private List loadingMessages; +}