diff --git a/CHANGELOG.md b/CHANGELOG.md index e773c7010..65e3edf97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/shiny/ui/_chat.py b/shiny/ui/_chat.py index 7f8adbeb5..1125c2e0c 100644 --- a/shiny/ui/_chat.py +++ b/shiny/ui/_chat.py @@ -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 @@ -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, diff --git a/tests/playwright/shiny/components/chat/stream-result/app.py b/tests/playwright/shiny/components/chat/stream-result/app.py index 34e254408..b16a1b213 100644 --- a/tests/playwright/shiny/components/chat/stream-result/app.py +++ b/tests/playwright/shiny/components/chat/stream-result/app.py @@ -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() diff --git a/tests/playwright/shiny/components/chat/stream-result/test_chat_stream_result.py b/tests/playwright/shiny/components/chat/stream-result/test_chat_stream_result.py index 8d6c216da..eaa08fc57 100644 --- a/tests/playwright/shiny/components/chat/stream-result/test_chat_stream_result.py +++ b/tests/playwright/shiny/components/chat/stream-result/test_chat_stream_result.py @@ -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) @@ -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")