bolt-python icon indicating copy to clipboard operation
bolt-python copied to clipboard

get_thread_context within a thread - way to get thread_ts?

Open rayterrill opened this issue 9 months ago • 12 comments

I'm building an AI app in Slack, and I'm looking to have the bot help with thread summarization. If I open the AI widget when I have a thread open and have the bot call "get_thread_context", I get back the following:

{'channel_id': 'C08MY3JQDRN', 'team_id': 'T08MY3H9SGG', 'enterprise_id': 'E08MLKPKWCR', 'thread_entry_point': 'sunroof', 'force_search': False}

Note that that doesn't include the thread_ts or any real "handle" into the thread that I'm looking at in my main panee - it's just the channel at large. Is there a way to use the "get_thread_context" or something similar to get a handle into the thread so my AI bot can perform a thread summarization?

rayterrill avatar May 08 '25 16:05 rayterrill

Got a response from support that this likely isn't possible today - would love to have this ability though!

rayterrill avatar May 08 '25 18:05 rayterrill

Hi @rayterrill thanks for bringing this up 💯

I'm assuming you want to get all the messages for the given thread and summarize them, from what I understand you should be able to do this with client.conversations_replies and the thread_ts value in the handler bolt context

Something like this should work

@assistant.user_message
def respond_in_assistant_thread(
    logger: logging.Logger,
    context: BoltContext,
    set_status: SetStatus,
    client: WebClient,
    say: Say,
):
    try:
        set_status("is typing...")

        replies = client.conversations_replies(
            channel=context.channel_id,
            ts=context.thread_ts,
            oldest=context.thread_ts,
            limit=10,
        )
        messages_in_thread: List[Dict[str, str]] = []
        for message in replies["messages"]:
            role = "user" if message.get("bot_id") is None else "assistant"
            messages_in_thread.append({"role": role, "content": message["text"]})
        logger.info(json.dumps(messages_in_thread))

    except Exception as e:
        say(f":warning: Something went wrong! ({e})")

Take a look at our assistant sample app, I believe it does something similar to what you are doing and may help you

WilliamBergamin avatar May 08 '25 19:05 WilliamBergamin

@WilliamBergamin I tried that, but that context appears to be the context of the assistant window itself, not of the thread I'm trying to summarize. For example if I use that context to get the replies like you've done in the example, I end up seeing my bot's initial "How can I help you?" message as well as the request to summarize the thread - its not pointing to the context of the thread I'm trying to summarize.

I do have something very similar to the assistant sample app working, and that works great for things like summarizing a channel or even a channel's messages and any replies container within those messages.

I'm just not sure how to get the context of a thread to summarize it using the assistant logic. I might be missing something here though.

rayterrill avatar May 08 '25 20:05 rayterrill

I'm not sure I fully understand what you want to achieve 🤔 Are you trying to get all the replies for a random message sent to a random channel?

You may not need to use the "assistant" features of bolt, you may be able to accomplish this with the "general" bolt features

WilliamBergamin avatar May 09 '25 19:05 WilliamBergamin

I'm not 100% sure how to explain it either :)

I have the channel summarization feature working great, works as expected.

I would expect though, and maybe I'm misunderstanding, that when I have a thread open in the "main pane" as well as the "assistant window", that the context the assistant sees is the thread - but it's not. It's still the context of the channel the thread is in. I attached a screenshot with some garbage data from a throwaway tenant to try to explain it better.

Image

I definitely can accomplish it with the general bolt features, but I was really hoping to be able to do it with the assistant features. The "assistant pane" (or whatever it's actually called) is a really powerful way of allowing team members to converse with the AI and get answers to things without cluttering up the interface with other messages. Historically, prior to the introduction of the assistant pane - people would do things like "at" the bot and ask for a summary in the channel, and the summary would be posted in-channel or in DM depending on the app. I'm hoping to avoid that.

rayterrill avatar May 09 '25 22:05 rayterrill

Thanks a lot for this extra context 💯 especially the screenshot, this definitely helps me understand the issue better, its also great to hear that a feature a useful!

The get_thread_context assistant utility should return an AssistantThreadContext whenever the event payload contains an assistant_thread object, the assistant_thread object is present in assistant_thread_started and assistant_thread_context_changed events.

As you pointed out AssistantThreadContext does not contain a thread_ts field but assistant_thread field may still have a thread_ts field that is useful in your use case.

Something like the following should allow you to get the thread_ts from the assistant_thread object whenever it is present,

def assistant_thread(
    get_thread_context: GetThreadContext,
    payload: dict,
    logger: logging.Logger
):
    thread_context = get_thread_context()
    logger.info(f"Context: {json.dumps(thread_context, indent=2)}")
    thread_ts = payload.get("assistant_thread", {}).get("thread_ts")
    if thread_ts is not None:
        logger.info(f"Bingo: {thread_ts}")

assistant.thread_started(assistant_thread)
assistant.thread_context_changed(assistant_thread)

Let me know if this helps you out, if the thread_ts field is wrong then this might be a backend issue

WilliamBergamin avatar May 12 '25 15:05 WilliamBergamin

Makes sense, unfortunately I'm trying to do the summarize piece in an @assistant.user_message like from the example. That's where I'm struggling to understand how to get that context. Is there any way to get that info inside the user_message event?

rayterrill avatar May 12 '25 17:05 rayterrill

I see 🤔 This may be hard to do @assistant.user_message is intended to be used to listen for reply in the assistant thread, it specifically filters out message events that are not direct messages, listening to all message_replied events would make it hard for your app do know when it is actually being invoked

As a workaround you could ask users to send links to relevant threaded messages and your app could detect whenever a link is present, look up the replies and use them as context for the assistant

Messages that contain links show have blocks that resemble this

"blocks": [
    {
      "type": "rich_text",
      "block_id": "M3nhM",
      "elements": [
        {
          "type": "rich_text_section",
          "elements": [
            {
              "type": "text",
              "text": "Hello I need a summary of "
            },
            {
              "type": "link",
              "url": "https://my-team.slack.com/archives/C03KFSZM6CS/p1719935446855499",
              "text": "this thread"
            }
          ]
        }
      ]
    }
  ]

Something like the following should work but I haven't tested it out yet

def is_slack_url(url: str) -> bool:
    parsed_url = urlparse(url)
    if not parsed_url.netloc.endswith(".slack.com"):
        return False

    path_parts = [part for part in parsed_url.path.split("/") if part]
    channel_id = path_parts[-2]
    if not channel_id.startswith("C"):
        return False
    thread_ts = path_parts[-1]
    if not thread_ts.startswith("p"):
        return False
    return True

def extract_links(block: dict):
    links = []
    block_type = block.get("type")
    if block_type == "rich_text" or block_type == "rich_text_section":
        for element in block.get("elements", []):
            links.extend(extract_links(element))
    if block_type == "link" and "url" in block:
        links.append(block["url"])
    return links

found_links = []
for block in blocks:
    found_links.extend(extract_links(block))

for slack_thread_url in filter(is_slack_url, found_links):
    parsed_url = urlparse(url)
    path_parts = [part for part in parsed_url.path.split("/") if part]
    channel_id = path_parts[-2]
    thread_ts = int(''.join(filter(lambda x: x.isdigit(), path_parts[-1])))/100000

    replies = client.conversations_replies(
            channel=channel_id,
            ts=thread_ts,
            oldest=thread_ts,
            limit=10,
        )
    print(f"Replies for {url}")
    print(json.dumps(replies["messages"]))

WilliamBergamin avatar May 12 '25 19:05 WilliamBergamin

I'll try to check that out - this is the piece I wish just included the thread_ts - maybe I'm just misunderstanding how that works.

My understanding of that line is basically it gives you a handle to the channel that's open in the main pane, it just doesn't have the ability to return the thread_ts at the moment (it does return 'thread_entry_point': 'sunroof' but i have no idea what that means - can't find that in the docs anywhere).

It sounds like this just isn't really possible at the moment.

rayterrill avatar May 12 '25 20:05 rayterrill

it just doesn't have the ability to return the thread_ts at the moment

That is correct, the information returned by get_thread_context is obtained from the request your app receives from Slack and it does not contain any information regarding the thread opened on the "main panel", this is a server side limitation. I can raise this internally as a feature request you can also request the /feedback command

it does return 'thread_entry_point': 'sunroof'

I think this is related to how a user started the assistant_thread, but since it is not publicly documented it may be intended for internal use and should be considered unstable

WilliamBergamin avatar May 13 '25 14:05 WilliamBergamin

Raising it as a feature request would be great! Thank you!

rayterrill avatar May 13 '25 15:05 rayterrill

👋 It looks like this issue has been open for 30 days with no activity. We'll mark this as stale for now, and wait 10 days for an update or for further comment before closing this issue out. If you think this issue needs to be prioritized, please comment to get the thread going again! Maintainers also review issues marked as stale on a regular basis and comment or adjust status if the issue needs to be reprioritized.

github-actions[bot] avatar Jun 16 '25 00:06 github-actions[bot]