Skip to content

Commit

Permalink
Replace .get_latest_stream_result() with a more general `.get_messa…
Browse files Browse the repository at this point in the history
…ge_stream()` (#1880)

Co-authored-by: Garrick Aden-Buie <[email protected]>
  • Loading branch information
cpsievert and gadenbuie authored Mar 3, 2025
1 parent c65ca17 commit ee998f9
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 26 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* The `ui.Chat()` component also gains the following:
* The `.on_user_submit()` decorator method now passes the user input to the decorated function. This makes it a bit easier to access the user input. See the new templates (mentioned below) for examples. (#1801)
* The assistant icon is now configurable via `ui.chat_ui()` (or the `ui.Chat.ui()` method in Shiny Express) or for individual messages in the `.append_message()` and `.append_message_stream()` methods of `ui.Chat()`. (#1853)
* A new `get_latest_stream_result()` method was added for an easy way to access the final result of the stream when it completes. (#1846)
* A new `latest_message_stream` property was added for an easy way to reactively read the stream's status, result, and also cancel an in progress stream. (#1846)
* The `.append_message_stream()` method now returns the `reactive.extended_task` instance that it launches. (#1846)
* The `ui.Chat()` component's `.update_user_input()` method gains `submit` and `focus` options that allow you to submit the input on behalf of the user and to choose whether the input receives focus after the update. (#1851)

Expand Down
48 changes: 27 additions & 21 deletions shiny/ui/_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,13 @@ def __init__(
reactive.Value(None)
)

self._latest_stream: reactive.Value[
reactive.ExtendedTask[[], str] | None
] = reactive.Value(None)
@reactive.extended_task
async def _mock_task() -> str:
return ""

self._latest_stream: reactive.Value[reactive.ExtendedTask[[], str]] = (
reactive.Value(_mock_task)
)

# TODO: deprecate messages once we start promoting managing LLM message
# state through other means
Expand Down Expand Up @@ -669,32 +673,34 @@ async def _handle_error():

return _stream_task

def get_latest_stream_result(self) -> str | None:
@property
def latest_message_stream(self) -> reactive.ExtendedTask[[], str]:
"""
Reactively read the latest message stream result.
React to changes in the latest message stream.
Reactively reads for the :class:`~shiny.reactive.ExtendedTask` behind the
latest message stream.
This method reads a reactive value containing the result of the latest
`.append_message_stream()`. Therefore, this method must be called in a reactive
context (e.g., a render function, a :func:`~shiny.reactive.calc`, or a
:func:`~shiny.reactive.effect`).
From the return value (i.e., the extended task), you can then:
1. Reactively read for the final `.result()`.
2. `.cancel()` the stream.
3. Check the `.status()` of the stream.
Returns
-------
:
The result of the latest stream (a string).
An extended task that represents the streaming task. The `.result()` method
of the task can be called in a reactive context to get the final state of the
stream.
Raises
------
:
A silent exception if no stream has completed yet.
Note
----
If no stream has yet been started when this method is called, then it returns an
extended task with `.status()` of `"initial"` and that it status doesn't change
state until a message is streamed.
"""
stream = self._latest_stream()
if stream is None:
from .. import req

req(False)
else:
return stream.result()
return self._latest_stream()

async def _append_message_stream(
self,
Expand Down
4 changes: 2 additions & 2 deletions tests/playwright/shiny/components/chat/stream-result/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ async def _(message: str):


@render.code
async def stream_result_ui():
return chat.get_latest_stream_result()
async def stream_result():
return chat.latest_message_stream.result()
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_validate_chat_stream_result(page: Page, local_app: ShinyAppProc) -> Non
page.goto(local_app.url)

chat = controller.Chat(page, "chat")
stream_result_ui = controller.OutputCode(page, "stream_result_ui")
stream_result = controller.OutputCode(page, "stream_result")

expect(chat.loc).to_be_visible(timeout=10 * 1000)

Expand All @@ -34,4 +34,4 @@ def test_validate_chat_stream_result(page: Page, local_app: ShinyAppProc) -> Non
chat.expect_messages(re.compile(r"\s*".join(messages)), timeout=30 * 1000)

# Verify that the stream result is as expected
stream_result_ui.expect.to_contain_text("Message 9")
stream_result.expect.to_contain_text("Message 9")

0 comments on commit ee998f9

Please sign in to comment.