Skip to content

Commit 722b982

Browse files
authored
feat(pkg-py): Improvements for statically rendered messages (#131)
* fix: ChatMessage() can now be constructed outside a Shiny session context * feat: Chat.chat_ui() can now render any type that message_contents() supports * Update changelog * Always return dependencies * html_deps needs to be a dictionary * Simplify * Avoid deprecated API in tests * Provider more type hints * Various fixes * Minimize diff; add missing docstrings * Fix lints
1 parent e2a74c8 commit 722b982

File tree

8 files changed

+38
-55
lines changed

8 files changed

+38
-55
lines changed

pkg-py/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [UNRELEASED]
99

10+
### New features
1011

12+
* `ChatMessage()` can now be constructed outside of a Shiny session. (#131)
13+
* `Chat.chat_ui(messages=...)` now supports any type also supported by `message_content()`. (#131)
1114

1215
## [0.2.0] - 2025-09-10
1316

pkg-py/src/shinychat/_chat.py

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
from htmltools import (
2424
HTML,
25-
RenderedHTML,
25+
HTMLDependency,
2626
Tag,
2727
TagAttrValue,
2828
TagChild,
@@ -595,6 +595,7 @@ async def append_message(
595595
* A dictionary with `content` and `role` keys. The `content` key can contain
596596
content as described above, and the `role` key can be "assistant" or
597597
"user".
598+
* More generally, any type registered with :func:`shinychat.message_content`.
598599
599600
**NOTE:** content may include specially formatted **input suggestion** links
600601
(see note below).
@@ -829,6 +830,7 @@ async def append_message_stream(
829830
* A dictionary with `content` and `role` keys. The `content` key can contain
830831
content as described above, and the `role` key can be "assistant" or
831832
"user".
833+
* More generally, any type registered with :func:`shinychat.message_content_chunk`.
832834
833835
**NOTE:** content may include specially formatted **input suggestion** links
834836
(see note below).
@@ -1588,7 +1590,9 @@ class ChatExpress(Chat):
15881590
def ui(
15891591
self,
15901592
*,
1591-
messages: Optional[Sequence[TagChild | ChatMessageDict]] = None,
1593+
messages: Optional[
1594+
Iterable[str | TagChild | ChatMessageDict | ChatMessage | Any]
1595+
] = None,
15921596
placeholder: str = "Enter a message...",
15931597
width: CssUnit = "min(680px, 100%)",
15941598
height: CssUnit = "auto",
@@ -1690,12 +1694,14 @@ def enable_bookmarking(
16901694
def chat_ui(
16911695
id: str,
16921696
*,
1693-
messages: Optional[Sequence[TagChild | ChatMessageDict]] = None,
1697+
messages: Optional[
1698+
Iterable[str | TagChild | ChatMessageDict | ChatMessage | Any]
1699+
] = None,
16941700
placeholder: str = "Enter a message...",
16951701
width: CssUnit = "min(680px, 100%)",
16961702
height: CssUnit = "auto",
16971703
fill: bool = True,
1698-
icon_assistant: HTML | Tag | TagList | None = None,
1704+
icon_assistant: Optional[HTML | Tag | TagList] = None,
16991705
**kwargs: TagAttrValue,
17001706
) -> Tag:
17011707
"""
@@ -1722,6 +1728,7 @@ def chat_ui(
17221728
interpreted as markdown as long as they're not inside HTML.
17231729
* A dictionary with `content` and `role` keys. The `content` key can contain a
17241730
content as described above, and the `role` key can be "assistant" or "user".
1731+
* More generally, any type registered with :func:`shinychat.message_content`.
17251732
17261733
**NOTE:** content may include specially formatted **input suggestion** links
17271734
(see :method:`~shiny.ui.Chat.append_message` for more info).
@@ -1755,34 +1762,14 @@ def chat_ui(
17551762
if messages is None:
17561763
messages = []
17571764
for x in messages:
1758-
role = "assistant"
1759-
content: TagChild = None
1760-
if not isinstance(x, dict):
1761-
content = x
1762-
else:
1763-
if "content" not in x:
1764-
raise ValueError(
1765-
"Each message dictionary must have a 'content' key."
1766-
)
1767-
1768-
content = x["content"]
1769-
if "role" in x:
1770-
role = x["role"]
1771-
1772-
# `content` is most likely a string, so avoid overhead in that case
1773-
# (it's also important that we *don't escape HTML* here).
1774-
if isinstance(content, str):
1775-
ui: RenderedHTML = {"html": content, "dependencies": []}
1776-
else:
1777-
ui = TagList(content).render()
1778-
1765+
msg = message_content(x)
17791766
message_tags.append(
17801767
Tag(
17811768
"shiny-chat-message",
1782-
ui["dependencies"],
1783-
content=ui["html"],
1769+
*[HTMLDependency(**d) for d in msg.html_deps],
1770+
content=msg.content,
17841771
icon=icon_attr,
1785-
data_role=role,
1772+
data_role=msg.role,
17861773
)
17871774
)
17881775

pkg-py/src/shinychat/_chat_types.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Literal, TypedDict
55

66
from htmltools import HTML, Tag, TagChild, Tagifiable, TagList
7-
from shiny.session import require_active_session
7+
from shiny.session import get_current_session
88

99
from ._typing_extensions import NotRequired
1010

@@ -32,10 +32,14 @@ def __init__(
3232
# markdown), so only process it if it's not a string.
3333
deps = []
3434
if not isinstance(content, str):
35-
session = require_active_session(None)
36-
res = session._process_ui(content)
35+
session = get_current_session()
36+
if session and not session.is_stub_session():
37+
res = session._process_ui(content)
38+
deps = res["deps"]
39+
else:
40+
res = TagList(content).render()
41+
deps = [d.as_dict() for d in res["dependencies"]]
3742
content = res["html"]
38-
deps = res["deps"]
3943

4044
if is_html:
4145
# Code blocks with `{=html}` infostrings are rendered as-is by a

pkg-py/tests/playwright/chat/basic/app.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,10 @@
55
ui.page_opts(title="Hello Chat")
66

77
# Create a chat instance, with an initial message
8-
chat = Chat(
9-
id="chat",
10-
messages=[
11-
{"content": "Hello! How can I help you today?", "role": "assistant"},
12-
],
13-
)
8+
chat = Chat(id="chat")
149

1510
# Display the chat
16-
chat.ui()
11+
chat.ui(messages=["Hello! How can I help you today?"])
1712

1813

1914
# Define a callback to run when the user submits a message

pkg-py/tests/playwright/chat/basic/test_chat_basic.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ def test_validate_chat_basic(page: Page, local_app: ShinyAppProc) -> None:
4242
message_state = controller.OutputCode(page, "message_state")
4343
message_state_expected = tuple(
4444
[
45-
{"content": initial_message, "role": "assistant"},
4645
{"content": f"\n{user_message}", "role": "user"},
4746
{"content": f"You said: \n{user_message}", "role": "assistant"},
4847
{"content": f"{user_message2}", "role": "user"},

pkg-py/tests/playwright/chat/icon/app.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,14 @@
1111

1212
with ui.layout_columns():
1313
# Default Bot ---------------------------------------------------------------------
14-
chat_default = Chat(
15-
id="chat_default",
16-
messages=[
17-
{
18-
"content": "Hello! I'm Default Bot. How can I help you today?",
19-
"role": "assistant",
20-
},
21-
],
22-
)
14+
chat_default = Chat(id="chat_default")
2315

2416
with ui.div():
2517
ui.h2("Default Bot")
26-
chat_default.ui(icon_assistant=None)
18+
chat_default.ui(
19+
messages=["Hello! I'm Default Bot. How can I help you today?"],
20+
icon_assistant=None,
21+
)
2722

2823
@chat_default.on_user_submit
2924
async def handle_user_input_default(user_input: str):

pkg-py/tests/playwright/chat/input-suggestion/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
And <span id="fifth" data-suggestion="another suggestion" data-suggestion-submit="true">this suggestion will also auto-submit</span>.</p>
1313
"""
1414

15-
chat = Chat("chat", messages=[suggestion2])
15+
chat = Chat("chat")
1616

17-
chat.ui(messages=[suggestions1])
17+
chat.ui(messages=[suggestions1, suggestion2])
1818

1919

2020
@chat.on_user_submit

pkg-py/tests/playwright/chat/shiny_input/app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
),
2020
)
2121

22-
chat = Chat(
23-
id="chat",
22+
chat = Chat(id="chat")
23+
chat.ui(
2424
messages=[welcome],
25+
class_="mb-5",
2526
)
26-
chat.ui(class_="mb-5")
2727

2828

2929
@reactive.effect

0 commit comments

Comments
 (0)