Skip to content

Commit

Permalink
Delay sending of chat UI messages until reactive graph is flushed (#1593
Browse files Browse the repository at this point in the history
)
  • Loading branch information
cpsievert authored Aug 28, 2024
1 parent 32b9761 commit 52e60e8
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* A handful of fixes for `ui.Chat()`, including:
* A fix for use inside Shiny modules. (#1582)
* `.messages(format="google")` now returns the correct role. (#1622)
* `ui.Chat(messages)` are no longer dropped when dynamically rendered. (#1593)
* `transform_assistant_response` can now return `None` and correctly handles change of content on the last chunk. (#1641)

* An empty `ui.input_date()` value no longer crashes Shiny. (#1528)
Expand Down
32 changes: 21 additions & 11 deletions shiny/ui/_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ async def append_message_stream(self, message: Iterable[Any] | AsyncIterable[Any
async def _stream_task():
await self._append_message_stream(message)

_stream_task()
self._session.on_flushed(_stream_task, once=True)

# Since the task runs in the background (outside/beyond the current context,
# if any), we need to manually raise any exceptions that occur
Expand Down Expand Up @@ -642,7 +642,9 @@ async def _send_append_message(

# print(msg)

await self._send_custom_message(msg_type, msg)
# When streaming (i.e., chunk is truthy), we can send messages immediately
# since we already waited for the flush in order to start the stream
await self._send_custom_message(msg_type, msg, on_flushed=chunk is False)
# TODO: Joe said it's a good idea to yield here, but I'm not sure why?
# await asyncio.sleep(0)

Expand Down Expand Up @@ -994,15 +996,23 @@ def destroy(self):
async def _remove_loading_message(self):
await self._send_custom_message("shiny-chat-remove-loading-message", None)

async def _send_custom_message(self, handler: str, obj: ClientMessage | None):
await self._session.send_custom_message(
"shinyChatMessage",
{
"id": self.id,
"handler": handler,
"obj": obj,
},
)
async def _send_custom_message(
self, handler: str, obj: ClientMessage | None, on_flushed: bool = True
):
async def _do_send():
await self._session.send_custom_message(
"shinyChatMessage",
{
"id": self.id,
"handler": handler,
"obj": obj,
},
)

if on_flushed:
self._session.on_flushed(_do_send, once=True)
else:
await _do_send()


@add_example(ex_dir="../api-examples/chat")
Expand Down
8 changes: 8 additions & 0 deletions tests/playwright/shiny/components/chat/dynamic_ui/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from shiny.express import render, ui

chat = ui.Chat(id="chat", messages=["A starting message"])


@render.ui
def chat_output():
return chat.ui()
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from playwright.sync_api import Page, expect
from utils.deploy_utils import skip_on_webkit

from shiny.playwright import controller
from shiny.run import ShinyAppProc


@skip_on_webkit
def test_validate_chat_basic(page: Page, local_app: ShinyAppProc) -> None:
page.goto(local_app.url)

chat = controller.Chat(page, "chat")

expect(chat.loc).to_be_visible(timeout=30 * 1000)
chat.expect_latest_message("A starting message", timeout=30 * 1000)
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ def test_validate_chat(page: Page, local_app: ShinyAppProc) -> None:
expect(chat.loc_input_button).to_be_disabled()

messages = [
"FIRST FIRST FIRST",
"SECOND SECOND SECOND",
"THIRD THIRD THIRD",
"FOURTH FOURTH FOURTH",
"FIRST FIRST FIRST",
"THIRD THIRD THIRD",
"FIFTH FIFTH FIFTH",
]
# Allow for any whitespace between messages
Expand Down

0 comments on commit 52e60e8

Please sign in to comment.