From dd5197ddb72c95162c4168876e167ee852452560 Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 28 Feb 2025 16:28:27 -0600 Subject: [PATCH 1/5] Follow up to #1846: replace .get_latest_stream_result() with a more general .get_message_stream() --- CHANGELOG.md | 2 +- shiny/ui/_chat.py | 48 ++++++++++++++++++++++++++--------------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e773c7010..b0c5f0a20 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 `get_message_stream()` method was added for an easy way to reactive 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..c5430b71b 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: + def get_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 latest :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 a "mock" task + is returned. This mock task behaves much like a stream that hasn't yet completed, + except it has a `.status()` of "initial" instead of "running", and `.cancel()` + is a no-op. """ - 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, From c6af40242d9f0beb77e6e4b79c4793fe3050162f Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 28 Feb 2025 18:09:17 -0600 Subject: [PATCH 2/5] Update test --- tests/playwright/shiny/components/chat/stream-result/app.py | 4 ++-- .../components/chat/stream-result/test_chat_stream_result.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/playwright/shiny/components/chat/stream-result/app.py b/tests/playwright/shiny/components/chat/stream-result/app.py index 34e254408..bbf2b6e69 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.get_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") From 6a1bb73baabcc5d832eda12ef1ed993c55c4f1d6 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 3 Mar 2025 11:20:35 -0600 Subject: [PATCH 3/5] Update CHANGELOG.md Co-authored-by: Garrick Aden-Buie --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c5f0a20..04f00af46 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_message_stream()` method was added for an easy way to reactive read the stream's status, result, and also cancel an in progress stream. (#1846) + * A new `get_message_stream()` method 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) From f26501f4dc509dc3f6862bde42f0ce319ced963d Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 3 Mar 2025 11:50:12 -0600 Subject: [PATCH 4/5] Change to property named latest_message_stream --- CHANGELOG.md | 2 +- shiny/ui/_chat.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04f00af46..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_message_stream()` method was added for an easy way to reactively read the stream's status, result, and also cancel an in progress stream. (#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 c5430b71b..1125c2e0c 100644 --- a/shiny/ui/_chat.py +++ b/shiny/ui/_chat.py @@ -673,11 +673,12 @@ async def _handle_error(): return _stream_task - def get_message_stream(self) -> reactive.ExtendedTask[[], str]: + @property + def latest_message_stream(self) -> reactive.ExtendedTask[[], str]: """ React to changes in the latest message stream. - Reactively reads for the latest :class:`~shiny.reactive.ExtendedTask` behind the + Reactively reads for the :class:`~shiny.reactive.ExtendedTask` behind the latest message stream. From the return value (i.e., the extended task), you can then: @@ -695,10 +696,9 @@ def get_message_stream(self) -> reactive.ExtendedTask[[], str]: Note ---- - If no stream has yet been started when this method is called, then a "mock" task - is returned. This mock task behaves much like a stream that hasn't yet completed, - except it has a `.status()` of "initial" instead of "running", and `.cancel()` - is a no-op. + 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. """ return self._latest_stream() From b8c6644ead90e2a11188e44c50c6433f4ffa5b76 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 3 Mar 2025 11:51:40 -0600 Subject: [PATCH 5/5] Update tests/playwright/shiny/components/chat/stream-result/app.py --- tests/playwright/shiny/components/chat/stream-result/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/playwright/shiny/components/chat/stream-result/app.py b/tests/playwright/shiny/components/chat/stream-result/app.py index bbf2b6e69..b16a1b213 100644 --- a/tests/playwright/shiny/components/chat/stream-result/app.py +++ b/tests/playwright/shiny/components/chat/stream-result/app.py @@ -21,4 +21,4 @@ async def _(message: str): @render.code async def stream_result(): - return chat.get_message_stream().result() + return chat.latest_message_stream.result()