From 43e0089b19ed85f298374233f08119c1a610c011 Mon Sep 17 00:00:00 2001 From: George Murray Date: Fri, 18 Aug 2023 16:46:41 -0700 Subject: [PATCH 01/16] Slack thread support --- .../agents/mixins/transports/slack.py | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index 180715e4a..a890f8c5a 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -5,13 +5,15 @@ import requests from pydantic import BaseModel, Field - from steamship import Block, Steamship +from steamship import DocTag from steamship.agents.llms import OpenAI from steamship.agents.mixins.transports.transport import Transport from steamship.agents.schema import EmitFunc, Metadata from steamship.agents.service.agent_service import AgentService from steamship.agents.utils import with_llm +from steamship.data import TagValueKey +from steamship.data.block import get_tag_value_key from steamship.invocable import Config, InvocableResponse, get, post from steamship.utils.kv_store import KeyValueStore @@ -19,6 +21,14 @@ SETTINGS_KVSTORE_KEY = "slack-transport" +def get_block_thread_ts(block: Block) -> Optional[str]: + return get_tag_value_key(block.tags, TagValueKey.STRING_VALUE, kind=DocTag.CHAT, name="slack-threadts") + + +def set_block_thread_ts(block: Block, thread_ts) -> None: + block._one_time_set_tag(tag_kind=DocTag.CHAT, tag_name="slack-threadid", string_value=thread_ts) + + class SlackElement(BaseModel): """An element of a Slack Block.""" @@ -95,6 +105,9 @@ class SlackEvent(BaseModel): ts: Optional[str] = Field( description="Timestamp of the message. A string, but is a floating point number within that." ) + thread_ts: Optional[str] = Field( + description="Timestamp of the thread this message is a part of, if any. Same format as `ts`." + ) item: Optional[str] = Field( description="Data specific to the underlying object type being described." ) @@ -120,8 +133,16 @@ def to_blocks(self) -> Optional[List[Block]]: ret = [] for slack_block in self.blocks or []: for block in slack_block.to_blocks() or []: + # TODO (george): Having some sort of wrapper for Slack-specific Blocks might be nice + # Or I guess the metadata block? if self.channel: block.set_chat_id(str(self.channel)) + if self.ts: + block.set_message_id(str(self.ts)) + if self.user: + block._one_time_set_tag(tag_kind=DocTag.CHAT, tag_name="userid", string_value=self.user) + if self.thread_ts: + set_block_thread_ts(block, self.thread_ts) # TODO: Do we want to encode other things like the tab, user, etc? ret.append(block) return ret @@ -282,6 +303,7 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 text = None slack_blocks = [] chat_id = None + thread_ts = None for block in blocks: # This is required for the public_url creation below. @@ -290,6 +312,8 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 if block.chat_id: chat_id = block.chat_id + thread_ts = get_block_thread_ts(block) + if block.is_text() or block.text: if not text: # This is the fallback for mobile notifications @@ -344,6 +368,8 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 "text": text, # This is for mobile previews. The "block" key has the real content. "channel": chat_id, } + if thread_ts: + body["thread_ts"] = thread_ts post_url = f"{self.config.slack_api_base}chat.postMessage" @@ -353,12 +379,14 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 json=body, ) - def build_emit_func(self, chat_id: str) -> EmitFunc: + def build_emit_func(self, chat_id: str, thread_ts: Optional[str]) -> EmitFunc: """Return an EmitFun that sends messages to the appropriate Slack channel.""" def new_emit_func(blocks: List[Block], metadata: Metadata): for block in blocks: block.set_chat_id(chat_id) + if thread_ts: + set_block_thread_ts(block, thread_ts) return self.send(blocks, metadata) return new_emit_func @@ -367,14 +395,18 @@ def _respond_to_block(self, incoming_message: Block): """Respond to a single inbound message from Slack, posting the response back to Slack.""" try: chat_id = incoming_message.chat_id - context = self.agent_service.build_default_context(context_id=chat_id) + thread_ts = get_block_thread_ts(incoming_message) + # TODO (george) not sure this is always what we want, unfortunately. Some bots may want to keep a + # conversation going through threads, others may not? + context_id = chat_id if not thread_ts else f"{chat_id}-{thread_ts}" + context = self.agent_service.build_default_context(context_id=context_id) context.chat_history.append_user_message( text=incoming_message.text, tags=incoming_message.tags ) # TODO: For truly async support, this emit fn will need to be wired in at the Agent level. - context.emit_funcs = [self.build_emit_func(chat_id=chat_id)] + context.emit_funcs = [self.build_emit_func(chat_id=chat_id, thread_ts=thread_ts)] # Add an LLM to the context, using the Agent's if it exists. llm = None @@ -411,9 +443,10 @@ def respond_to_webhook(self, **kwargs) -> InvocableResponse[str]: # noqa: C901 if slack_request.event: if slack_request.event.bot_id is None: if slack_request.event.is_message(): - logging.info( - f"User {slack_request.event.user} sent message in channel {slack_request.event.channel}" - ) + log_message = f"User {slack_request.event.user} sent message in channel {slack_request.event.channel}" + if slack_request.event.thread_ts: + log_message += f" from within thread {slack_request.event.thread_ts}" + logging.info(log_message) incoming_messages = slack_request.event.to_blocks() for incoming_message in incoming_messages: if incoming_message is not None: From db4ad727c660f06e7ccc8a0657e50e0a0ddb55ef Mon Sep 17 00:00:00 2001 From: George Murray Date: Sat, 19 Aug 2023 13:02:26 -0700 Subject: [PATCH 02/16] logging --- src/steamship/agents/mixins/transports/slack.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index a890f8c5a..591371ee8 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -370,6 +370,7 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 } if thread_ts: body["thread_ts"] = thread_ts + logging.info(f"Post to slack: {body}") post_url = f"{self.config.slack_api_base}chat.postMessage" @@ -406,6 +407,7 @@ def _respond_to_block(self, incoming_message: Block): ) # TODO: For truly async support, this emit fn will need to be wired in at the Agent level. + logging.info("Context: {context_id}") context.emit_funcs = [self.build_emit_func(chat_id=chat_id, thread_ts=thread_ts)] # Add an LLM to the context, using the Agent's if it exists. From 40707d0bab68dce78d881656f92922dbb2250b39 Mon Sep 17 00:00:00 2001 From: George Murray Date: Sat, 19 Aug 2023 13:07:44 -0700 Subject: [PATCH 03/16] Fix logs --- src/steamship/agents/mixins/transports/slack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index 591371ee8..48b0aa9dc 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -407,7 +407,7 @@ def _respond_to_block(self, incoming_message: Block): ) # TODO: For truly async support, this emit fn will need to be wired in at the Agent level. - logging.info("Context: {context_id}") + logging.info(f"Context: {context_id}") context.emit_funcs = [self.build_emit_func(chat_id=chat_id, thread_ts=thread_ts)] # Add an LLM to the context, using the Agent's if it exists. From 83c814b2f38a376f1c7264b23a5c1312056a28d2 Mon Sep 17 00:00:00 2001 From: George Murray Date: Sat, 19 Aug 2023 13:11:15 -0700 Subject: [PATCH 04/16] Fix the tag settings/retrieval --- src/steamship/agents/mixins/transports/slack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index 48b0aa9dc..d4a959aff 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -26,7 +26,7 @@ def get_block_thread_ts(block: Block) -> Optional[str]: def set_block_thread_ts(block: Block, thread_ts) -> None: - block._one_time_set_tag(tag_kind=DocTag.CHAT, tag_name="slack-threadid", string_value=thread_ts) + block._one_time_set_tag(tag_kind=DocTag.CHAT, tag_name="slack-threadts", string_value=thread_ts) class SlackElement(BaseModel): From 04d34931edb1cd0bbc76fab25764484ca3ce961b Mon Sep 17 00:00:00 2001 From: George Murray Date: Sun, 20 Aug 2023 07:10:37 -0700 Subject: [PATCH 05/16] Threads in the agent context --- src/steamship/agents/mixins/transports/slack.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index d4a959aff..3936a9054 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -406,6 +406,10 @@ def _respond_to_block(self, incoming_message: Block): text=incoming_message.text, tags=incoming_message.tags ) + context.metadata["slack-channel"] = chat_id + context.metadata["slack-threadts"] = thread_ts + context.metadata["slack-messagets"] = incoming_message.message_id + # TODO: For truly async support, this emit fn will need to be wired in at the Agent level. logging.info(f"Context: {context_id}") context.emit_funcs = [self.build_emit_func(chat_id=chat_id, thread_ts=thread_ts)] From a90d7606e3a39e7d1647e3784c4d6a3003670b31 Mon Sep 17 00:00:00 2001 From: George Murray Date: Mon, 21 Aug 2023 09:26:30 -0700 Subject: [PATCH 06/16] Remove extraneous logging --- src/steamship/agents/mixins/transports/slack.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index c9c0d826b..2c636e1d0 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -372,7 +372,6 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 } if thread_ts: body["thread_ts"] = thread_ts - logging.info(f"Post to slack: {body}") post_url = f"{self.config.slack_api_base}chat.postMessage" @@ -413,7 +412,6 @@ def _respond_to_block(self, incoming_message: Block): context.metadata["slack-messagets"] = incoming_message.message_id # TODO: For truly async support, this emit fn will need to be wired in at the Agent level. - logging.info(f"Context: {context_id}") context.emit_funcs = [self.build_emit_func(chat_id=chat_id, thread_ts=thread_ts)] # Add an LLM to the context, using the Agent's if it exists. From 914d594b4c02ca47cb817649ee602e0376763176 Mon Sep 17 00:00:00 2001 From: George Murray Date: Mon, 21 Aug 2023 14:57:25 -0700 Subject: [PATCH 07/16] Tags for user id and thread id --- .../agents/mixins/transports/slack.py | 29 +++++-------------- src/steamship/data/block.py | 22 ++++++++++++++ src/steamship/data/tags/tag_constants.py | 6 ++++ 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index 2c636e1d0..68223ceca 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -6,14 +6,11 @@ import requests from pydantic import BaseModel, Field from steamship import Block, Steamship -from steamship import DocTag from steamship.agents.llms import OpenAI from steamship.agents.mixins.transports.transport import Transport from steamship.agents.schema import EmitFunc, Metadata from steamship.agents.service.agent_service import AgentService from steamship.agents.utils import with_llm -from steamship.data import TagValueKey -from steamship.data.block import get_tag_value_key from steamship.invocable import Config, InvocableResponse, get, post from steamship.utils.kv_store import KeyValueStore @@ -21,14 +18,6 @@ SETTINGS_KVSTORE_KEY = "slack-transport" -def get_block_thread_ts(block: Block) -> Optional[str]: - return get_tag_value_key(block.tags, TagValueKey.STRING_VALUE, kind=DocTag.CHAT, name="slack-threadts") - - -def set_block_thread_ts(block: Block, thread_ts) -> None: - block._one_time_set_tag(tag_kind=DocTag.CHAT, tag_name="slack-threadts", string_value=thread_ts) - - class SlackElement(BaseModel): """An element of a Slack Block.""" @@ -133,17 +122,14 @@ def to_blocks(self) -> Optional[List[Block]]: ret = [] for slack_block in self.blocks or []: for block in slack_block.to_blocks() or []: - # TODO (george): Having some sort of wrapper for Slack-specific Blocks might be nice - # Or I guess the metadata block? if self.channel: - block.set_chat_id(str(self.channel)) + block.set_chat_id(self.channel) if self.ts: - block.set_message_id(str(self.ts)) + block.set_message_id(self.ts) if self.user: - block._one_time_set_tag(tag_kind=DocTag.CHAT, tag_name="userid", string_value=self.user) + block.set_user_id(self.user) if self.thread_ts: - set_block_thread_ts(block, self.thread_ts) - # TODO: Do we want to encode other things like the tab, user, etc? + block.set_thread_id(self.thread_ts) ret.append(block) return ret @@ -314,7 +300,8 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 if block.chat_id: chat_id = block.chat_id - thread_ts = get_block_thread_ts(block) + if block.thread_id: + thread_ts = block.thread_id if block.is_text() or block.text: if not text: @@ -388,7 +375,7 @@ def new_emit_func(blocks: List[Block], metadata: Metadata): for block in blocks: block.set_chat_id(chat_id) if thread_ts: - set_block_thread_ts(block, thread_ts) + block.set_thread_id(thread_ts) return self.send(blocks, metadata) return new_emit_func @@ -397,7 +384,7 @@ def _respond_to_block(self, incoming_message: Block): """Respond to a single inbound message from Slack, posting the response back to Slack.""" try: chat_id = incoming_message.chat_id - thread_ts = get_block_thread_ts(incoming_message) + thread_ts = incoming_message.thread_id # TODO (george) not sure this is always what we want, unfortunately. Some bots may want to keep a # conversation going through threads, others may not? context_id = chat_id if not thread_ts else f"{chat_id}-{thread_ts}" diff --git a/src/steamship/data/block.py b/src/steamship/data/block.py index 4247f706d..e477f1300 100644 --- a/src/steamship/data/block.py +++ b/src/steamship/data/block.py @@ -283,6 +283,28 @@ def set_chat_id(self, chat_id: str): tag_kind=DocTag.CHAT, tag_name=ChatTag.CHAT_ID, string_value=chat_id ) + @property + def thread_id(self) -> Optional[str]: + return get_tag_value_key( + self.tags, TagValueKey.STRING_VALUE, kind=DocTag.CHAT, name=ChatTag.THREAD_ID + ) + + def set_thread_id(self, thread_id: str) -> None: + return self._one_time_set_tag( + tag_kind=DocTag.CHAT, tag_name=ChatTag.THREAD_ID, string_value=thread_id + ) + + @property + def user_id(self) -> Optional[str]: + return get_tag_value_key( + self.tags, TagValueKey.STRING_VALUE, kind=DocTag.CHAT, name=ChatTag.USER_ID + ) + + def set_user_id(self, user_id: str) -> None: + return self._one_time_set_tag( + tag_kind=DocTag.CHAT, tag_name=ChatTag.USER_ID, string_value=user_id + ) + def _one_time_set_tag(self, tag_kind: str, tag_name: str, string_value: str): existing = get_tag_value_key( self.tags, TagValueKey.STRING_VALUE, kind=tag_kind, name=tag_name diff --git a/src/steamship/data/tags/tag_constants.py b/src/steamship/data/tags/tag_constants.py index 6cfe2700d..34fe4b6b8 100644 --- a/src/steamship/data/tags/tag_constants.py +++ b/src/steamship/data/tags/tag_constants.py @@ -267,6 +267,12 @@ class ChatTag(str, Enum): # The message id of a message MESSAGE_ID = "message-id" + # In environments which support threading, the thread id where the message occurred + THREAD_ID = "thread-id" + + # In multiuser environments, the ID of the user who created the message + USER_ID = "user-id" + # The role of a message ROLE = "role" From c6c8de584b4946b230a3578d39d3de3489abfdab Mon Sep 17 00:00:00 2001 From: George Murray Date: Mon, 21 Aug 2023 15:01:49 -0700 Subject: [PATCH 08/16] Consolidate metadata in context --- src/steamship/agents/mixins/transports/slack.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index 68223ceca..5f9347e79 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -394,9 +394,12 @@ def _respond_to_block(self, incoming_message: Block): text=incoming_message.text, tags=incoming_message.tags ) - context.metadata["slack-channel"] = chat_id - context.metadata["slack-threadts"] = thread_ts - context.metadata["slack-messagets"] = incoming_message.message_id + context.metadata["slack"] = { + "channel": chat_id, + "message_ts": incoming_message.message_id + } + if thread_ts: + context.metadata["slack"]["thread"] = thread_ts # TODO: For truly async support, this emit fn will need to be wired in at the Agent level. context.emit_funcs = [self.build_emit_func(chat_id=chat_id, thread_ts=thread_ts)] From d52384779ebf9e50fe58704fb94dfa20084b2162 Mon Sep 17 00:00:00 2001 From: George Murray Date: Mon, 21 Aug 2023 15:24:55 -0700 Subject: [PATCH 09/16] Advanced configuration options --- .../agents/mixins/transports/slack.py | 64 +++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index 5f9347e79..c9b247865 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -1,6 +1,7 @@ import json import logging import urllib.parse +from enum import Enum from typing import List, Optional import requests @@ -18,6 +19,42 @@ SETTINGS_KVSTORE_KEY = "slack-transport" +class SlackContextBehavior(Enum): + """Defines how history between agent and users is tracked. + + These specifications are specifically in regard to how the agent interacts with Slack as it pertains to Agent + Context. + """ + + ENTIRE_CHANNEL = 0, + """ + Agent context is per channel as a whole, which includes bot mentions sent to the top level channel, and across *any* + thread in that channel. + """ + + THREADS_ARE_NEW_CONVERSATIONS = 1, + """ + Agent context is thread-aware. The top level channel is treated as its own context, and threads have their own + contexts. + """ + + +class SlackThreadingBehavior(Enum): + """Defines how responses from the agent will be delivered in response to user mentions.""" + + FOLLOW_THREADS = 0, + """ + If the bot is mentioned from the top-level channel, the response will be in the channel. If the bot is mentioned + from within a thread, the response will be to that thread. + """ + + ALWAYS_THREADED = 1 + """ + Responses from the bot will always be threaded. If the bot was mentioned at the top level of the channel, a new + thread will be created for the response. + """ + + class SlackElement(BaseModel): """An element of a Slack Block.""" @@ -166,6 +203,14 @@ class SlackTransportConfig(Config): slack_api_base: str = Field( SLACK_API_BASE, description="Slack API base URL. If blank defaults to production Slack." ) + threading_behavior: SlackThreadingBehavior = Field( + SlackThreadingBehavior.FOLLOW_THREADS, + description="Whether the bot will always respond in threads, or only if the invocation was threaded" + ) + context_behavior: SlackContextBehavior = Field( + SlackContextBehavior.ENTIRE_CHANNEL, + description="Whether the bot will be provided conversation context from the channel as a whole, or per thread." + ) class SlackTransport(Transport): @@ -385,9 +430,12 @@ def _respond_to_block(self, incoming_message: Block): try: chat_id = incoming_message.chat_id thread_ts = incoming_message.thread_id - # TODO (george) not sure this is always what we want, unfortunately. Some bots may want to keep a - # conversation going through threads, others may not? - context_id = chat_id if not thread_ts else f"{chat_id}-{thread_ts}" + if self.config.context_behavior == SlackContextBehavior.ENTIRE_CHANNEL: + context_id = chat_id + elif self.config.context_behavior == SlackContextBehavior.THREADS_ARE_NEW_CONVERSATIONS: + context_id = chat_id if not thread_ts else f"{chat_id}-{thread_ts}" + else: + raise ValueError(f"Unhandled context behavior: {self.config.context_behavior}") context = self.agent_service.build_default_context(context_id=context_id) context.chat_history.append_user_message( @@ -399,10 +447,16 @@ def _respond_to_block(self, incoming_message: Block): "message_ts": incoming_message.message_id } if thread_ts: - context.metadata["slack"]["thread"] = thread_ts + context.metadata["slack"]["thread_ts"] = thread_ts # TODO: For truly async support, this emit fn will need to be wired in at the Agent level. - context.emit_funcs = [self.build_emit_func(chat_id=chat_id, thread_ts=thread_ts)] + if self.config.threading_behavior == SlackThreadingBehavior.FOLLOW_THREADS: + reply_thread_ts = thread_ts + elif self.config.threading_behavior == SlackThreadingBehavior.ALWAYS_THREADED: + reply_thread_ts = thread_ts or incoming_message.message_id + else: + raise ValueError(f"Unhandled threading behavior: {self.config.threading_behavior}") + context.emit_funcs = [self.build_emit_func(chat_id=chat_id, thread_ts=reply_thread_ts)] # Add an LLM to the context, using the Agent's if it exists. llm = None From 6f625bccd555384d0c15c92bbb2ba4de85d4af31 Mon Sep 17 00:00:00 2001 From: George Murray Date: Mon, 21 Aug 2023 16:14:13 -0700 Subject: [PATCH 10/16] lint --- .../agents/mixins/transports/slack.py | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index c9b247865..fcc60a286 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -6,6 +6,7 @@ import requests from pydantic import BaseModel, Field + from steamship import Block, Steamship from steamship.agents.llms import OpenAI from steamship.agents.mixins.transports.transport import Transport @@ -26,13 +27,13 @@ class SlackContextBehavior(Enum): Context. """ - ENTIRE_CHANNEL = 0, + ENTIRE_CHANNEL = (0,) """ Agent context is per channel as a whole, which includes bot mentions sent to the top level channel, and across *any* thread in that channel. """ - THREADS_ARE_NEW_CONVERSATIONS = 1, + THREADS_ARE_NEW_CONVERSATIONS = (1,) """ Agent context is thread-aware. The top level channel is treated as its own context, and threads have their own contexts. @@ -42,13 +43,13 @@ class SlackContextBehavior(Enum): class SlackThreadingBehavior(Enum): """Defines how responses from the agent will be delivered in response to user mentions.""" - FOLLOW_THREADS = 0, + FOLLOW_THREADS = (0,) """ If the bot is mentioned from the top-level channel, the response will be in the channel. If the bot is mentioned from within a thread, the response will be to that thread. """ - ALWAYS_THREADED = 1 + ALWAYS_THREADED = (1,) """ Responses from the bot will always be threaded. If the bot was mentioned at the top level of the channel, a new thread will be created for the response. @@ -205,11 +206,11 @@ class SlackTransportConfig(Config): ) threading_behavior: SlackThreadingBehavior = Field( SlackThreadingBehavior.FOLLOW_THREADS, - description="Whether the bot will always respond in threads, or only if the invocation was threaded" + description="Whether the bot will always respond in threads, or only if the invocation was threaded", ) context_behavior: SlackContextBehavior = Field( SlackContextBehavior.ENTIRE_CHANNEL, - description="Whether the bot will be provided conversation context from the channel as a whole, or per thread." + description="Whether the bot will be provided conversation context from the channel as a whole, or per thread.", ) @@ -413,29 +414,40 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 json=body, ) - def build_emit_func(self, chat_id: str, thread_ts: Optional[str]) -> EmitFunc: + def build_emit_func( + self, chat_id: str, incoming_message_ts: str, thread_ts: Optional[str] + ) -> EmitFunc: """Return an EmitFun that sends messages to the appropriate Slack channel.""" + if self.config.threading_behavior == SlackThreadingBehavior.FOLLOW_THREADS: + reply_thread_ts = thread_ts + elif self.config.threading_behavior == SlackThreadingBehavior.ALWAYS_THREADED: + reply_thread_ts = thread_ts or incoming_message_ts + else: + raise ValueError(f"Unhandled threading behavior: {self.config.threading_behavior}") def new_emit_func(blocks: List[Block], metadata: Metadata): for block in blocks: block.set_chat_id(chat_id) if thread_ts: - block.set_thread_id(thread_ts) + block.set_thread_id(reply_thread_ts) return self.send(blocks, metadata) return new_emit_func + def _get_context_id_for_response(self, channel: str, thread_ts: Optional[str]) -> str: + if self.config.context_behavior == SlackContextBehavior.ENTIRE_CHANNEL: + return channel + elif self.config.context_behavior == SlackContextBehavior.THREADS_ARE_NEW_CONVERSATIONS: + return channel if not thread_ts else f"{channel}-{thread_ts}" + else: + raise ValueError(f"Unhandled context behavior: {self.config.context_behavior}") + def _respond_to_block(self, incoming_message: Block): """Respond to a single inbound message from Slack, posting the response back to Slack.""" try: chat_id = incoming_message.chat_id thread_ts = incoming_message.thread_id - if self.config.context_behavior == SlackContextBehavior.ENTIRE_CHANNEL: - context_id = chat_id - elif self.config.context_behavior == SlackContextBehavior.THREADS_ARE_NEW_CONVERSATIONS: - context_id = chat_id if not thread_ts else f"{chat_id}-{thread_ts}" - else: - raise ValueError(f"Unhandled context behavior: {self.config.context_behavior}") + context_id = self._get_context_id_for_response(chat_id, thread_ts) context = self.agent_service.build_default_context(context_id=context_id) context.chat_history.append_user_message( @@ -444,19 +456,19 @@ def _respond_to_block(self, incoming_message: Block): context.metadata["slack"] = { "channel": chat_id, - "message_ts": incoming_message.message_id + "message_ts": incoming_message.message_id, } if thread_ts: context.metadata["slack"]["thread_ts"] = thread_ts # TODO: For truly async support, this emit fn will need to be wired in at the Agent level. - if self.config.threading_behavior == SlackThreadingBehavior.FOLLOW_THREADS: - reply_thread_ts = thread_ts - elif self.config.threading_behavior == SlackThreadingBehavior.ALWAYS_THREADED: - reply_thread_ts = thread_ts or incoming_message.message_id - else: - raise ValueError(f"Unhandled threading behavior: {self.config.threading_behavior}") - context.emit_funcs = [self.build_emit_func(chat_id=chat_id, thread_ts=reply_thread_ts)] + context.emit_funcs = [ + self.build_emit_func( + chat_id=chat_id, + incoming_message_ts=incoming_message.message_id, + thread_ts=thread_ts, + ) + ] # Add an LLM to the context, using the Agent's if it exists. llm = None From 53013d6b49adac5e616acd989c2dc2265a9a4b86 Mon Sep 17 00:00:00 2001 From: George Murray Date: Tue, 22 Aug 2023 16:05:34 -0700 Subject: [PATCH 11/16] Fix enum checks (didn't realize config strips them into values), make them strings for ease of reading plaintext --- .../agents/mixins/transports/slack.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index fcc60a286..71d309c2f 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -1,7 +1,7 @@ import json import logging import urllib.parse -from enum import Enum +from enum import StrEnum from typing import List, Optional import requests @@ -20,36 +20,36 @@ SETTINGS_KVSTORE_KEY = "slack-transport" -class SlackContextBehavior(Enum): +class SlackContextBehavior(StrEnum): """Defines how history between agent and users is tracked. These specifications are specifically in regard to how the agent interacts with Slack as it pertains to Agent Context. """ - ENTIRE_CHANNEL = (0,) + ENTIRE_CHANNEL = "entire-channel" """ Agent context is per channel as a whole, which includes bot mentions sent to the top level channel, and across *any* thread in that channel. """ - THREADS_ARE_NEW_CONVERSATIONS = (1,) + THREADS_ARE_NEW_CONVERSATIONS = "threads-are-new-conversations" """ Agent context is thread-aware. The top level channel is treated as its own context, and threads have their own contexts. """ -class SlackThreadingBehavior(Enum): +class SlackThreadingBehavior(StrEnum): """Defines how responses from the agent will be delivered in response to user mentions.""" - FOLLOW_THREADS = (0,) + FOLLOW_THREADS = "follow-threads" """ If the bot is mentioned from the top-level channel, the response will be in the channel. If the bot is mentioned from within a thread, the response will be to that thread. """ - ALWAYS_THREADED = (1,) + ALWAYS_THREADED = "always-threaded" """ Responses from the bot will always be threaded. If the bot was mentioned at the top level of the channel, a new thread will be created for the response. @@ -418,9 +418,9 @@ def build_emit_func( self, chat_id: str, incoming_message_ts: str, thread_ts: Optional[str] ) -> EmitFunc: """Return an EmitFun that sends messages to the appropriate Slack channel.""" - if self.config.threading_behavior == SlackThreadingBehavior.FOLLOW_THREADS: + if self.config.threading_behavior == SlackThreadingBehavior.FOLLOW_THREADS.value: reply_thread_ts = thread_ts - elif self.config.threading_behavior == SlackThreadingBehavior.ALWAYS_THREADED: + elif self.config.threading_behavior == SlackThreadingBehavior.ALWAYS_THREADED.value: reply_thread_ts = thread_ts or incoming_message_ts else: raise ValueError(f"Unhandled threading behavior: {self.config.threading_behavior}") @@ -435,9 +435,11 @@ def new_emit_func(blocks: List[Block], metadata: Metadata): return new_emit_func def _get_context_id_for_response(self, channel: str, thread_ts: Optional[str]) -> str: - if self.config.context_behavior == SlackContextBehavior.ENTIRE_CHANNEL: + if self.config.context_behavior == SlackContextBehavior.ENTIRE_CHANNEL.value: return channel - elif self.config.context_behavior == SlackContextBehavior.THREADS_ARE_NEW_CONVERSATIONS: + elif ( + self.config.context_behavior == SlackContextBehavior.THREADS_ARE_NEW_CONVERSATIONS.value + ): return channel if not thread_ts else f"{channel}-{thread_ts}" else: raise ValueError(f"Unhandled context behavior: {self.config.context_behavior}") From 4cd8d73def92766b1175d31d3869316e227a0f7d Mon Sep 17 00:00:00 2001 From: George Murray Date: Tue, 22 Aug 2023 16:30:52 -0700 Subject: [PATCH 12/16] Logging for slack threading behavior --- src/steamship/agents/mixins/transports/slack.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index 71d309c2f..2c9760aad 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -405,6 +405,7 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 } if thread_ts: body["thread_ts"] = thread_ts + logging.info(f"Post mesage: {body}") post_url = f"{self.config.slack_api_base}chat.postMessage" @@ -417,6 +418,7 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 def build_emit_func( self, chat_id: str, incoming_message_ts: str, thread_ts: Optional[str] ) -> EmitFunc: + logging.info(f"Logging behavior: {self.config.threading_behavior}") """Return an EmitFun that sends messages to the appropriate Slack channel.""" if self.config.threading_behavior == SlackThreadingBehavior.FOLLOW_THREADS.value: reply_thread_ts = thread_ts @@ -424,6 +426,7 @@ def build_emit_func( reply_thread_ts = thread_ts or incoming_message_ts else: raise ValueError(f"Unhandled threading behavior: {self.config.threading_behavior}") + logging.info(f"reply_thread_ts: {reply_thread_ts}") def new_emit_func(blocks: List[Block], metadata: Metadata): for block in blocks: From 1936af2de88f1bdc37bc3ab83908b76bfe48fd46 Mon Sep 17 00:00:00 2001 From: George Murray Date: Tue, 22 Aug 2023 16:34:35 -0700 Subject: [PATCH 13/16] Fix strenum not available --- src/steamship/agents/mixins/transports/slack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index 2c9760aad..4a948ee9b 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -1,7 +1,7 @@ import json import logging import urllib.parse -from enum import StrEnum +from enum import Enum from typing import List, Optional import requests @@ -20,7 +20,7 @@ SETTINGS_KVSTORE_KEY = "slack-transport" -class SlackContextBehavior(StrEnum): +class SlackContextBehavior(Enum): """Defines how history between agent and users is tracked. These specifications are specifically in regard to how the agent interacts with Slack as it pertains to Agent @@ -40,7 +40,7 @@ class SlackContextBehavior(StrEnum): """ -class SlackThreadingBehavior(StrEnum): +class SlackThreadingBehavior(Enum): """Defines how responses from the agent will be delivered in response to user mentions.""" FOLLOW_THREADS = "follow-threads" From 38555dc9834b8335d8df59d2b910011f15434986 Mon Sep 17 00:00:00 2001 From: George Murray Date: Tue, 22 Aug 2023 16:40:54 -0700 Subject: [PATCH 14/16] Whoooooops easy fix --- src/steamship/agents/mixins/transports/slack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index 4a948ee9b..5560f5082 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -431,7 +431,7 @@ def build_emit_func( def new_emit_func(blocks: List[Block], metadata: Metadata): for block in blocks: block.set_chat_id(chat_id) - if thread_ts: + if reply_thread_ts: block.set_thread_id(reply_thread_ts) return self.send(blocks, metadata) From 7f00228d6c98caf4ba1c3c2a959a5399fb3e9ea1 Mon Sep 17 00:00:00 2001 From: George Murray Date: Tue, 22 Aug 2023 19:09:25 -0700 Subject: [PATCH 15/16] Fix reply_thread_ts vs thread_ts --- src/steamship/agents/mixins/transports/slack.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index 5560f5082..4fc6f82ae 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -405,7 +405,6 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 } if thread_ts: body["thread_ts"] = thread_ts - logging.info(f"Post mesage: {body}") post_url = f"{self.config.slack_api_base}chat.postMessage" @@ -418,7 +417,6 @@ def _send(self, blocks: List[Block], metadata: Metadata): # noqa: C901 def build_emit_func( self, chat_id: str, incoming_message_ts: str, thread_ts: Optional[str] ) -> EmitFunc: - logging.info(f"Logging behavior: {self.config.threading_behavior}") """Return an EmitFun that sends messages to the appropriate Slack channel.""" if self.config.threading_behavior == SlackThreadingBehavior.FOLLOW_THREADS.value: reply_thread_ts = thread_ts @@ -426,7 +424,6 @@ def build_emit_func( reply_thread_ts = thread_ts or incoming_message_ts else: raise ValueError(f"Unhandled threading behavior: {self.config.threading_behavior}") - logging.info(f"reply_thread_ts: {reply_thread_ts}") def new_emit_func(blocks: List[Block], metadata: Metadata): for block in blocks: From ff36a0d6fd62e6cae30d7cd43619c5f46c679541 Mon Sep 17 00:00:00 2001 From: George Murray Date: Wed, 23 Aug 2023 09:40:31 -0700 Subject: [PATCH 16/16] Confusing pydantic behavior --- src/steamship/agents/mixins/transports/slack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/steamship/agents/mixins/transports/slack.py b/src/steamship/agents/mixins/transports/slack.py index 4fc6f82ae..0869c0e7c 100644 --- a/src/steamship/agents/mixins/transports/slack.py +++ b/src/steamship/agents/mixins/transports/slack.py @@ -205,11 +205,11 @@ class SlackTransportConfig(Config): SLACK_API_BASE, description="Slack API base URL. If blank defaults to production Slack." ) threading_behavior: SlackThreadingBehavior = Field( - SlackThreadingBehavior.FOLLOW_THREADS, + SlackThreadingBehavior.FOLLOW_THREADS.value, description="Whether the bot will always respond in threads, or only if the invocation was threaded", ) context_behavior: SlackContextBehavior = Field( - SlackContextBehavior.ENTIRE_CHANNEL, + SlackContextBehavior.ENTIRE_CHANNEL.value, description="Whether the bot will be provided conversation context from the channel as a whole, or per thread.", )