Skip to content

Commit 70acf34

Browse files
authored
feat(Chat): A .on_user_submit callback can now access user input through a function parameter (#1801)
1 parent 2ce08c0 commit 70acf34

File tree

2 files changed

+56
-31
lines changed

2 files changed

+56
-31
lines changed

shiny/ui/_chat.py

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,18 @@
6565
TransformAssistantResponseChunk,
6666
TransformAssistantResponseChunkAsync,
6767
]
68-
SubmitFunction = Callable[[], None]
69-
SubmitFunctionAsync = Callable[[], Awaitable[None]]
68+
UserSubmitFunction0 = Union[
69+
Callable[[], None],
70+
Callable[[], Awaitable[None]],
71+
]
72+
UserSubmitFunction1 = Union[
73+
Callable[[str], None],
74+
Callable[[str], Awaitable[None]],
75+
]
76+
UserSubmitFunction = Union[
77+
UserSubmitFunction0,
78+
UserSubmitFunction1,
79+
]
7080

7181
ChunkOption = Literal["start", "end", True, False]
7282

@@ -79,9 +89,10 @@ class Chat:
7989
Create a chat interface.
8090
8191
A UI component for building conversational interfaces. With it, end users can submit
82-
messages, which will cause a `.on_user_submit()` callback to run. In that callback,
83-
a response can be generated based on the chat's `.messages()`, and appended to the
84-
chat using `.append_message()` or `.append_message_stream()`.
92+
messages, which will cause a `.on_user_submit()` callback to run. That callback gets
93+
passed the user input message, which can be used to generate a response. The
94+
response can then be appended to the chat using `.append_message()` or
95+
`.append_message_stream()`.
8596
8697
Here's a rough outline for how to implement a `Chat`:
8798
@@ -94,11 +105,9 @@ class Chat:
94105
95106
# Define a callback to run when the user submits a message
96107
@chat.on_user_submit
97-
async def _():
98-
# Get messages currently in the chat
99-
messages = chat.messages()
108+
async def handle_user_input(user_input):
100109
# Create a response message stream
101-
response = await my_model.generate_response(messages, stream=True)
110+
response = await my_model.generate_response(user_input, stream=True)
102111
# Append the response into the chat
103112
await chat.append_message_stream(response)
104113
```
@@ -112,6 +121,11 @@ async def _():
112121
response to the chat. Streaming is preferrable when available since it allows for
113122
more responsive and scalable chat interfaces.
114123
124+
It is also highly recommended to use a package like
125+
[chatlas](https://posit-dev.github.io/chatlas/) to generate responses, especially
126+
when responses should be aware of the chat history, support tool calls, etc.
127+
See this [article](https://posit-dev.github.io/chatlas/web-apps.html) to learn more.
128+
115129
Parameters
116130
----------
117131
id
@@ -278,33 +292,31 @@ def ui(
278292
)
279293

280294
@overload
281-
def on_user_submit(
282-
self, fn: SubmitFunction | SubmitFunctionAsync
283-
) -> reactive.Effect_: ...
295+
def on_user_submit(self, fn: UserSubmitFunction) -> reactive.Effect_: ...
284296

285297
@overload
286298
def on_user_submit(
287299
self,
288-
) -> Callable[[SubmitFunction | SubmitFunctionAsync], reactive.Effect_]: ...
300+
) -> Callable[[UserSubmitFunction], reactive.Effect_]: ...
289301

290302
def on_user_submit(
291-
self, fn: SubmitFunction | SubmitFunctionAsync | None = None
292-
) -> (
293-
reactive.Effect_
294-
| Callable[[SubmitFunction | SubmitFunctionAsync], reactive.Effect_]
295-
):
303+
self, fn: UserSubmitFunction | None = None
304+
) -> reactive.Effect_ | Callable[[UserSubmitFunction], reactive.Effect_]:
296305
"""
297306
Define a function to invoke when user input is submitted.
298307
299-
Apply this method as a decorator to a function (`fn`) that should be invoked when the
300-
user submits a message. The function should take no arguments.
308+
Apply this method as a decorator to a function (`fn`) that should be invoked
309+
when the user submits a message. This function can take an optional argument,
310+
which will be the user input message.
301311
302-
In many cases, the implementation of `fn` should do at least the following:
312+
In many cases, the implementation of `fn` should also do the following:
303313
304-
1. Call `.messages()` to obtain the current chat history.
305-
2. Generate a response based on those messages.
306-
3. Append the response to the chat history using `.append_message()` (
307-
or `.append_message_stream()` if the response is streamed).
314+
1. Generate a response based on the user input.
315+
* If the response should be aware of chat history, use a package
316+
like [chatlas](https://posit-dev.github.io/chatlas/) to manage the chat
317+
state, or use the `.messages()` method to get the chat history.
318+
2. Append that response to the chat component using `.append_message()` ( or
319+
`.append_message_stream()` if the response is streamed).
308320
309321
Parameters
310322
----------
@@ -318,8 +330,8 @@ def on_user_submit(
318330
but it will only be re-invoked when the user submits a message.
319331
"""
320332

321-
def create_effect(fn: SubmitFunction | SubmitFunctionAsync):
322-
afunc = _utils.wrap_async(fn)
333+
def create_effect(fn: UserSubmitFunction):
334+
fn_params = inspect.signature(fn).parameters
323335

324336
@reactive.effect
325337
@reactive.event(self._user_input)
@@ -329,7 +341,21 @@ async def handle_user_input():
329341

330342
req(False)
331343
try:
332-
await afunc()
344+
if len(fn_params) > 1:
345+
raise ValueError(
346+
"A on_user_submit function should not take more than 1 argument"
347+
)
348+
elif len(fn_params) == 1:
349+
input = self.user_input(transform=True)
350+
# The line immediately below handles the possibility of input
351+
# being transformed to None. Technically, input should never be
352+
# None at this point (since the handler should be suspended).
353+
input = "" if input is None else input
354+
afunc = _utils.wrap_async(cast(UserSubmitFunction1, fn))
355+
await afunc(input)
356+
else:
357+
afunc = _utils.wrap_async(cast(UserSubmitFunction0, fn))
358+
await afunc()
333359
except Exception as e:
334360
await self._raise_exception(e)
335361

tests/playwright/shiny/components/chat/basic/app.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717

1818
# Define a callback to run when the user submits a message
1919
@chat.on_user_submit
20-
async def _():
21-
user_msg = chat.user_input()
22-
await chat.append_message(f"You said: {user_msg}")
20+
async def handle_user_input(user_input: str):
21+
await chat.append_message(f"You said: {user_input}")
2322

2423

2524
"Message state:"

0 commit comments

Comments
 (0)