17
17
)
18
18
from weakref import WeakValueDictionary
19
19
20
- from htmltools import HTML , Tag , TagAttrValue , css
20
+ from htmltools import HTML , Tag , TagAttrValue , TagList , css
21
21
22
22
from .. import _utils , reactive
23
23
from .._deprecated import warn_deprecated
@@ -493,7 +493,12 @@ def messages(
493
493
494
494
return tuple (res )
495
495
496
- async def append_message (self , message : Any ) -> None :
496
+ async def append_message (
497
+ self ,
498
+ message : Any ,
499
+ * ,
500
+ icon : HTML | Tag | TagList | None = None ,
501
+ ):
497
502
"""
498
503
Append a message to the chat.
499
504
@@ -506,10 +511,14 @@ async def append_message(self, message: Any) -> None:
506
511
Content strings are interpreted as markdown and rendered to HTML on the
507
512
client. Content may also include specially formatted **input suggestion**
508
513
links (see note below).
514
+ icon
515
+ An optional icon to display next to the message, currently only used for
516
+ assistant messages. The icon can be any HTML element (e.g., an
517
+ :func:`~shiny.ui.img` tag) or a string of HTML.
509
518
510
519
Note
511
520
----
512
- `` {.callout-note title="Input suggestions"}
521
+ ::: {.callout-note title="Input suggestions"}
513
522
Input suggestions are special links that send text to the user input box when
514
523
clicked (or accessed via keyboard). They can be created in the following ways:
515
524
@@ -528,17 +537,22 @@ async def append_message(self, message: Any) -> None:
528
537
529
538
Note that a user may also opt-out of submitting a suggestion by holding the
530
539
`Alt/Option` key while clicking the suggestion link.
531
- ```
540
+ :::
532
541
533
- ``` {.callout-note title="Streamed messages"}
542
+ ::: {.callout-note title="Streamed messages"}
534
543
Use `.append_message_stream()` instead of this method when `stream=True` (or
535
544
similar) is specified in model's completion method.
536
- ```
545
+ :::
537
546
"""
538
- await self ._append_message (message )
547
+ await self ._append_message (message , icon = icon )
539
548
540
549
async def _append_message (
541
- self , message : Any , * , chunk : ChunkOption = False , stream_id : str | None = None
550
+ self ,
551
+ message : Any ,
552
+ * ,
553
+ chunk : ChunkOption = False ,
554
+ stream_id : str | None = None ,
555
+ icon : HTML | Tag | TagList | None = None ,
542
556
) -> None :
543
557
# If currently we're in a stream, handle other messages (outside the stream) later
544
558
if not self ._can_append_message (stream_id ):
@@ -568,9 +582,18 @@ async def _append_message(
568
582
if msg is None :
569
583
return
570
584
self ._store_message (msg , chunk = chunk )
571
- await self ._send_append_message (msg , chunk = chunk )
585
+ await self ._send_append_message (
586
+ msg ,
587
+ chunk = chunk ,
588
+ icon = icon ,
589
+ )
572
590
573
- async def append_message_stream (self , message : Iterable [Any ] | AsyncIterable [Any ]):
591
+ async def append_message_stream (
592
+ self ,
593
+ message : Iterable [Any ] | AsyncIterable [Any ],
594
+ * ,
595
+ icon : HTML | Tag | None = None ,
596
+ ):
574
597
"""
575
598
Append a message as a stream of message chunks.
576
599
@@ -583,6 +606,10 @@ async def append_message_stream(self, message: Iterable[Any] | AsyncIterable[Any
583
606
OpenAI, Anthropic, Ollama, and others. Content strings are interpreted as
584
607
markdown and rendered to HTML on the client. Content may also include
585
608
specially formatted **input suggestion** links (see note below).
609
+ icon
610
+ An optional icon to display next to the message, currently only used for
611
+ assistant messages. The icon can be any HTML element (e.g., an
612
+ :func:`~shiny.ui.img` tag) or a string of HTML.
586
613
587
614
Note
588
615
----
@@ -625,7 +652,7 @@ async def append_message_stream(self, message: Iterable[Any] | AsyncIterable[Any
625
652
# Run the stream in the background to get non-blocking behavior
626
653
@reactive .extended_task
627
654
async def _stream_task ():
628
- return await self ._append_message_stream (message )
655
+ return await self ._append_message_stream (message , icon = icon )
629
656
630
657
_stream_task ()
631
658
@@ -669,11 +696,15 @@ def get_latest_stream_result(self) -> str | None:
669
696
else :
670
697
return stream .result ()
671
698
672
- async def _append_message_stream (self , message : AsyncIterable [Any ]):
699
+ async def _append_message_stream (
700
+ self ,
701
+ message : AsyncIterable [Any ],
702
+ icon : HTML | Tag | None = None ,
703
+ ):
673
704
id = _utils .private_random_id ()
674
705
675
706
empty = ChatMessage (content = "" , role = "assistant" )
676
- await self ._append_message (empty , chunk = "start" , stream_id = id )
707
+ await self ._append_message (empty , chunk = "start" , stream_id = id , icon = icon )
677
708
678
709
try :
679
710
async for msg in message :
@@ -702,6 +733,7 @@ async def _send_append_message(
702
733
self ,
703
734
message : TransformedMessage ,
704
735
chunk : ChunkOption = False ,
736
+ icon : HTML | Tag | TagList | None = None ,
705
737
):
706
738
if message ["role" ] == "system" :
707
739
# System messages are not displayed in the UI
@@ -721,13 +753,17 @@ async def _send_append_message(
721
753
content = message ["content_client" ]
722
754
content_type = "html" if isinstance (content , HTML ) else "markdown"
723
755
756
+ # TODO: pass along dependencies for both content and icon (if any)
724
757
msg = ClientMessage (
725
758
content = str (content ),
726
759
role = message ["role" ],
727
760
content_type = content_type ,
728
761
chunk_type = chunk_type ,
729
762
)
730
763
764
+ if icon is not None :
765
+ msg ["icon" ] = str (icon )
766
+
731
767
# print(msg)
732
768
733
769
await self ._send_custom_message (msg_type , msg )
@@ -1118,7 +1154,6 @@ async def _send_custom_message(self, handler: str, obj: ClientMessage | None):
1118
1154
1119
1155
@add_example (ex_dir = "../templates/chat/starters/hello" )
1120
1156
class ChatExpress (Chat ):
1121
-
1122
1157
def ui (
1123
1158
self ,
1124
1159
* ,
@@ -1127,6 +1162,7 @@ def ui(
1127
1162
width : CssUnit = "min(680px, 100%)" ,
1128
1163
height : CssUnit = "auto" ,
1129
1164
fill : bool = True ,
1165
+ icon_assistant : HTML | Tag | TagList | None = None ,
1130
1166
** kwargs : TagAttrValue ,
1131
1167
) -> Tag :
1132
1168
"""
@@ -1148,6 +1184,10 @@ def ui(
1148
1184
fill
1149
1185
Whether the chat should vertically take available space inside a fillable
1150
1186
container.
1187
+ icon_assistant
1188
+ The icon to use for the assistant chat messages. Can be a HTML or a tag in
1189
+ the form of :class:`~htmltools.HTML` or :class:`~htmltools.Tag`. If `None`,
1190
+ a default robot icon is used.
1151
1191
kwargs
1152
1192
Additional attributes for the chat container element.
1153
1193
"""
@@ -1158,6 +1198,7 @@ def ui(
1158
1198
width = width ,
1159
1199
height = height ,
1160
1200
fill = fill ,
1201
+ icon_assistant = icon_assistant ,
1161
1202
** kwargs ,
1162
1203
)
1163
1204
@@ -1171,6 +1212,7 @@ def chat_ui(
1171
1212
width : CssUnit = "min(680px, 100%)" ,
1172
1213
height : CssUnit = "auto" ,
1173
1214
fill : bool = True ,
1215
+ icon_assistant : HTML | Tag | TagList | None = None ,
1174
1216
** kwargs : TagAttrValue ,
1175
1217
) -> Tag :
1176
1218
"""
@@ -1199,6 +1241,10 @@ def chat_ui(
1199
1241
The height of the chat container.
1200
1242
fill
1201
1243
Whether the chat should vertically take available space inside a fillable container.
1244
+ icon_assistant
1245
+ The icon to use for the assistant chat messages. Can be a HTML or a tag in
1246
+ the form of :class:`~htmltools.HTML` or :class:`~htmltools.Tag`. If `None`,
1247
+ a default robot icon is used.
1202
1248
kwargs
1203
1249
Additional attributes for the chat container element.
1204
1250
"""
@@ -1226,6 +1272,10 @@ def chat_ui(
1226
1272
1227
1273
message_tags .append (Tag (tag_name , content = msg ["content" ]))
1228
1274
1275
+ html_deps = None
1276
+ if isinstance (icon_assistant , (Tag , TagList )):
1277
+ html_deps = icon_assistant .get_dependencies ()
1278
+
1229
1279
res = Tag (
1230
1280
"shiny-chat-container" ,
1231
1281
Tag ("shiny-chat-messages" , * message_tags ),
@@ -1235,6 +1285,7 @@ def chat_ui(
1235
1285
placeholder = placeholder ,
1236
1286
),
1237
1287
chat_deps (),
1288
+ html_deps ,
1238
1289
{
1239
1290
"style" : css (
1240
1291
width = as_css_unit (width ),
@@ -1244,6 +1295,7 @@ def chat_ui(
1244
1295
id = id ,
1245
1296
placeholder = placeholder ,
1246
1297
fill = fill ,
1298
+ icon_assistant = str (icon_assistant ) if icon_assistant is not None else None ,
1247
1299
** kwargs ,
1248
1300
)
1249
1301
0 commit comments