Skip to content

Conversation

zimeg
Copy link
Member

@zimeg zimeg commented Oct 1, 2025

Summary

This PR adds a ChatStream class that creates chat streams with .chat_stream() 🤖

Preview

streamer = client.chat_stream(
    channel="C0123456789",
    thread_ts="123.000",
    recipient_team_id="T0123456789",
    recipient_user_id="U0123456789",
)
streamer.append(markdown_text="nice!")
streamer.stop()

Testing

WIP-

Notes

Category

  • slack_sdk.web.WebClient (sync/async) (Web API client)
  • /docs (Documents)
  • tests/integration_tests (Automated tests for this library)

Requirements

  • I've read and understood the Contributing Guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've run python3 -m venv .venv && source .venv/bin/activate && ./scripts/run_validation.sh after making the changes.

@zimeg zimeg self-assigned this Oct 1, 2025
@zimeg zimeg added enhancement M-T: A feature request for new functionality semver:minor web-client Version: 3x area:async labels Oct 1, 2025
Copy link

codecov bot commented Oct 1, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (ai-apps@bd1857f). Learn more about missing BASE report.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             ai-apps    #1755   +/-   ##
==========================================
  Coverage           ?   85.12%           
==========================================
  Files              ?      115           
  Lines              ?    13068           
  Branches           ?        0           
==========================================
  Hits               ?    11124           
  Misses             ?     1944           
  Partials           ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@zimeg zimeg marked this pull request as ready for review October 1, 2025 23:32
Copy link
Member Author

@zimeg zimeg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔏 A few fast jots for the wonderful reviewers!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📚 note: The docs/reference is autogenerated with the latest code changes. I'll follow up with separate PRs that both requires this and hides these from the preview since regular review ought happen with the comments elsewhere IMHO-

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔐 note: Ongoing discussion is happening for this - #1759.

Comment on lines +122 to +123
legacy_source = re.sub(r"^from slack_sdk.web.chat_stream import ChatStream\n", "", legacy_source, flags=re.MULTILINE)
legacy_source = re.sub(r"(?s)def chat_stream.*?(?=def)", "", legacy_source)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌲 note: The LegacyWebClient won't find this chat_stream since responses from the various APIs can't be parsed quick - these are either async or sync. Instead we might recommend updating to the latest version.

🔗 https://docs.slack.dev/tools/python-slack-sdk/v3-migration

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 note: Planning to include this script as part of the generate_api_docs.sh scripts in follow up to guarantee the latest changes are used. This'll match how unit tests are setup IIRC.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📚 note: Similar changes instead prefer to generate docs before tests in #1759.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📠 note: This file is autogenerated from slack_sdk/web/client.py! However, kind review that checks for correct async usage is so appreciated!

Comment on lines +160 to +161
if markdown_text:
self._buffer += markdown_text
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 note: Planning to follow up in the node implementation to match this. I find adding to the buffer to be more clear here than below when stopping the stream.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎁 note: The node implementation is in progress here: slackapi/node-slack-sdk#2388

Copy link
Member Author

@zimeg zimeg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📌 And a question on overall implementations I forgot!

Comment on lines +66 to +71
def append(
self,
*,
markdown_text: str,
**kwargs,
) -> Optional[SlackResponse]:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗣️ question: Are these arguments flexible enough for future changes? It's not obvious how kwargs might be buffered if the call to append doesn't send a request.

🐝 ramble: We can of course follow up with updates in later releases, but I'm wanting to avoid strangeness in implementations in the meantime...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 I think this is flexible for future changes.

Imagine in the future that the chat.appendStream method supports other content, aside from markdown_text. If that happens, we can update it to be markdown_text: Optional[str]. This is a non-breaking change for existing apps and allows us to add new arguments such as new_things: Optional[str] or new_things: Optional[Dict[str, str]].

Today, the kwargs would only work as long as markdown_text is a required field. But I think that's the right choice right now, since it is a required field to append text.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks Wow thanks for calling out that changing "required" to "optional" isn't breaking... That might've been catching me earlier!

I think we'll have to explore additional buffer configuration if those later changes arise, but I now feel confident in this approach 🙏 ✨

Copy link
Member

@mwbrooks mwbrooks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Very solid and well done, @zimeg! I feel like I'm short changing you by not providing deeper feedback, but this came out exactly as I was hoping! 🙌🏻

🧠 This is aligned very well with the Node SDK implementation and I've noticed that you've noted a small improvement to align both around appending text.

🔮 Thinking about the future, our concern is whether this can expand with the future changes to chat_stream. I've left a comment below, but I can see a path that's non-breaking and flexible for us. So I'm happy with it!

🧪 I've manually tested this by writing it up to our AI Sample App. I was able to adjust the buffer size successfully and it simnifically reduced the code and overhead.

Comment on lines +49 to +70
async_source = re.sub(
"from slack_sdk.web.chat_stream import ChatStream",
"from slack_sdk.web.async_chat_stream import AsyncChatStream",
async_source,
)
async_source = re.sub(r"ChatStream:", "AsyncChatStream:", async_source)
async_source = re.sub(r"ChatStream\(", "AsyncChatStream(", async_source)
async_source = re.sub(
r" client.chat_stream\(",
" await client.chat_stream(",
async_source,
)
async_source = re.sub(
r" streamer.append\(",
" await streamer.append(",
async_source,
)
async_source = re.sub(
r" streamer.stop\(",
" await streamer.stop(",
async_source,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: oof, you definitely had to push the boundaries of this script with that helper!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks Agreed. This wasn't a favorite change to make but I think it'll let us land this with autogenerated changes 🤖

Comment on lines +122 to +123
legacy_source = re.sub(r"^from slack_sdk.web.chat_stream import ChatStream\n", "", legacy_source, flags=re.MULTILINE)
legacy_source = re.sub(r"(?s)def chat_stream.*?(?=def)", "", legacy_source)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Can we revert this since the ChatStream isn't landing into the legacy client?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks Let's keep these for now - this removes the chat_stream implementation from the legacy client altogether after the original source file is copied over!

Comment on lines +88 to +99
Example:
```python
streamer = client.chat_stream(
channel="C0123456789",
thread_ts="1700000001.123456",
recipient_team_id="T0123456789",
recipient_user_id="U0123456789",
)
streamer.append(markdown_text="**hello wo")
streamer.append(markdown_text="rld!**")
streamer.stop()
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: very nice 👌🏻

recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
buffer_size: The length of markdown_text to buffer in-memory before calling a method. Increasing this value
decreases the number of method calls made for the same amount of text, which is useful to avoid rate limits.
Default: 256.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Technically, this class doesn't have a default buffer size, right? The 256 default is set by the web-client's client.chat_stream constructor.

Comment on lines +100 to +104
start_request = self.thread.server.chat_stream_requests.get("/chat.startStream", {})
self.assertEqual(start_request.get("channel"), "C0123456789")
self.assertEqual(start_request.get("thread_ts"), "123.000")
self.assertEqual(start_request.get("recipient_team_id"), "T0123456789")
self.assertEqual(start_request.get("recipient_user_id"), "U0123456789")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: This is so nice!

await streamer.append(markdown_text="e is", token="xoxb-chat_stream_test_token1")
await streamer.append(markdown_text=" bold!")
await streamer.append(markdown_text="*")
await streamer.stop(markdown_text="*", token="xoxb-chat_stream_test_token2")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I noticed that we don't have a test for .stop(blocks="...", ...). Since this is an important feature for the new feedback buttons, we should test that blocks are passed through to the API.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks Fantastic call! We should be serializing these blocks to a consistent string that we now test in 38ec262 👾 ✨

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The node tests are also updated in slackapi/node-slack-sdk#2388 to match!

await streamer.stop()

@async_test
async def test_streams_errors_when_appending_to_a_completed_stream(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Great use-case!

Comment on lines +66 to +71
def append(
self,
*,
markdown_text: str,
**kwargs,
) -> Optional[SlackResponse]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 I think this is flexible for future changes.

Imagine in the future that the chat.appendStream method supports other content, aside from markdown_text. If that happens, we can update it to be markdown_text: Optional[str]. This is a non-breaking change for existing apps and allows us to add new arguments such as new_things: Optional[str] or new_things: Optional[Dict[str, str]].

Today, the kwargs would only work as long as markdown_text is a required field. But I think that's the right choice right now, since it is a required field to append text.

Comment on lines +11 to +12
class ChatStreamMockHandler(MockHandler):
"""Extended mock handler that captures request bodies for chat stream methods"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(non-blocking, future): This is very impressive that you've build your own mock server for handling the tests. We may want to talk to @WilliamBergamin to see if he has opinions or ideas around this approach. But from my perspective, this is contained to our chat_stream tests and builds upon the existing mock server work, so I think we should move forward with it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks Kind thanks. I found this to be decent for confirming expected arguments and headers are being sent with streaming requests, but I support other approaches!

For now let's use this as a starting point, but we should revisit this with suggestions otherwise 🤖

@zimeg
Copy link
Member Author

zimeg commented Oct 2, 2025

@mwbrooks I appreciate your reviews so much! Thanks for sharing kind suggestions and thoughts of the future too 👾 ✨

I'll merge this for a release that we can test with, but I remain so open to improving both the organization of files and tests in follow up changes. Let's hope to keep the implementations similar in node and python otherwise 🙏

@zimeg zimeg merged commit 64a995c into ai-apps Oct 2, 2025
12 checks passed
@zimeg zimeg deleted the zimeg-feat-web-chat-stream branch October 2, 2025 17:10
@zimeg
Copy link
Member Author

zimeg commented Oct 2, 2025

Sample app reference WIP: slack-samples/bolt-python-assistant-template#16 🧪 ✨

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants