Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add messages parameter to chat_ui() and Chat.ui(); revert #1593 #1736

Merged
merged 3 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Added [narwhals](https://posit-dev.github.io/py-narwhals) support for `@render.table`. This allows for any eager data frame supported by narwhals to be returned from a `@render.table` output method. (#1570)

* `chat_ui()` and `Chat.ui()` gain a `messages` parameter for providing starting messages. (#1736)

### Other changes

* Incorporated `orjson` for faster data serialization in `@render.data_frame` outputs. (#1570)
Expand Down Expand Up @@ -100,7 +102,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* A few 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
9 changes: 1 addition & 8 deletions js/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,7 @@ class ChatContainer extends LightElement {
private resizeObserver!: ResizeObserver;

render(): ReturnType<LitElement["render"]> {
const input_id = this.id + "_user_input";
return html`
<shiny-chat-messages></shiny-chat-messages>
<shiny-chat-input
id=${input_id}
placeholder=${this.placeholder}
></shiny-chat-input>
`;
return html``;
}

firstUpdated(): void {
Expand Down
72 changes: 50 additions & 22 deletions shiny/ui/_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Callable,
Iterable,
Literal,
Optional,
Sequence,
Tuple,
Union,
Expand Down Expand Up @@ -193,7 +194,8 @@ def __init__(
reactive.Value(None)
)

# Initialize the chat with the provided messages
# TODO: deprecate messages once we start promoting managing LLM message
# state through other means
@reactive.effect
async def _init_chat():
for msg in messages:
Expand Down Expand Up @@ -233,6 +235,7 @@ async def _on_user_input():
def ui(
self,
*,
messages: Optional[Sequence[str | ChatMessage]] = None,
placeholder: str = "Enter a message...",
width: CssUnit = "min(680px, 100%)",
height: CssUnit = "auto",
Expand All @@ -247,6 +250,11 @@ def ui(

Parameters
----------
messages
A sequence of messages to display in the chat. Each message can be either a
string or a dictionary with `content` and `role` keys. The `content` key
should contain the message text, and the `role` key can be "assistant" or
"user".
placeholder
Placeholder text for the chat input.
width
Expand All @@ -261,6 +269,7 @@ def ui(
"""
return chat_ui(
id=self.id,
messages=messages,
placeholder=placeholder,
width=width,
height=height,
Expand Down Expand Up @@ -572,7 +581,7 @@ async def append_message_stream(self, message: Iterable[Any] | AsyncIterable[Any
async def _stream_task():
await self._append_message_stream(message)

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

# 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 @@ -643,9 +652,7 @@ async def _send_append_message(

# print(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)
await self._send_custom_message(msg_type, msg)
# 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 @@ -1012,29 +1019,22 @@ 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, 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()
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,
},
)


@add_example(ex_dir="../api-examples/chat")
def chat_ui(
id: str,
*,
messages: Optional[Sequence[str | ChatMessage]] = None,
placeholder: str = "Enter a message...",
width: CssUnit = "min(680px, 100%)",
height: CssUnit = "auto",
Expand All @@ -1052,6 +1052,10 @@ def chat_ui(
----------
id
A unique identifier for the chat UI.
messages
A sequence of messages to display in the chat. Each message can be either a string
or a dictionary with a `content` and `role` key. The `content` key should contain
the message text, and the `role` key can be "assistant" or "user".
placeholder
Placeholder text for the chat input.
width
Expand All @@ -1066,8 +1070,32 @@ def chat_ui(

id = resolve_id(id)

message_tags: list[Tag] = []
if messages is None:
messages = []
for msg in messages:
if isinstance(msg, str):
msg = {"content": msg, "role": "assistant"}
elif isinstance(msg, dict):
if "content" not in msg:
raise ValueError("Each message must have a 'content' key.")
if "role" not in msg:
raise ValueError("Each message must have a 'role' key.")
else:
raise ValueError("Each message must be a string or a dictionary.")

message_tags.append(
Tag("shiny-chat-message", content=msg["content"], role=msg["role"])
)

res = Tag(
"shiny-chat-container",
Tag("shiny-chat-messages", *message_tags),
Tag(
"shiny-chat-input",
id=f"{id}_user_input",
placeholder=placeholder,
),
chat_deps(),
{
"style": css(
Expand Down
8 changes: 1 addition & 7 deletions shiny/www/py-shiny/chat/chat.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions shiny/www/py-shiny/chat/chat.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions tests/playwright/shiny/components/chat/dynamic_ui/app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from shiny.express import render, ui

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


@render.ui
def chat_output():
return chat.ui()
return chat.ui(messages=["A starting message"])
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 = [
"SECOND SECOND SECOND",
"FOURTH FOURTH FOURTH",
"FIRST FIRST FIRST",
"SECOND SECOND SECOND",
"THIRD THIRD THIRD",
"FOURTH FOURTH FOURTH",
"FIFTH FIFTH FIFTH",
]
# Allow for any whitespace between messages
Expand Down
Loading