Skip to content

Commit 8abe0e3

Browse files
committed
Better error handling
1 parent 532b4f2 commit 8abe0e3

File tree

6 files changed

+113
-39
lines changed

6 files changed

+113
-39
lines changed

js/chat/chat.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ class ChatContainer extends LightElement {
198198
this.#onAppendChunk
199199
);
200200
this.addEventListener("shiny-chat-clear-messages", this.#onClear);
201+
this.addEventListener(
202+
"shiny-chat-remove-placeholder",
203+
this.#onRemovePlaceholder
204+
);
201205
}
202206

203207
disconnectedCallback(): void {
@@ -209,6 +213,10 @@ class ChatContainer extends LightElement {
209213
this.#onAppendChunk
210214
);
211215
this.removeEventListener("shiny-chat-clear-messages", this.#onClear);
216+
this.removeEventListener(
217+
"shiny-chat-remove-placeholder",
218+
this.#onRemovePlaceholder
219+
);
212220
}
213221

214222
#onInputSent(event: CustomEvent<Message>): void {
@@ -287,6 +295,11 @@ class ChatContainer extends LightElement {
287295
this.messages.innerHTML = "";
288296
}
289297

298+
#onRemovePlaceholder(): void {
299+
this.#removePlaceholder();
300+
this.#enableInput();
301+
}
302+
290303
#enableInput(): void {
291304
this.input.disabled = false;
292305
}

shiny/reactive/_reactives.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@
3434
from .._docstring import add_example
3535
from .._utils import is_async_callable, run_coro_sync
3636
from .._validation import req
37-
from ..types import MISSING, MISSING_TYPE, ActionButtonValue, SilentException
37+
from ..types import (
38+
MISSING,
39+
MISSING_TYPE,
40+
ActionButtonValue,
41+
NotifyException,
42+
SilentException,
43+
)
3844
from ._core import Context, Dependents, ReactiveWarning, isolate
3945

4046
if TYPE_CHECKING:
@@ -583,6 +589,19 @@ async def _run(self) -> None:
583589
except SilentException:
584590
# It's OK for SilentException to cause an Effect to stop running
585591
pass
592+
except NotifyException as e:
593+
traceback.print_exc()
594+
595+
if self._session:
596+
from .._app import SANITIZE_ERROR_MSG
597+
from ..ui import notification_show
598+
599+
msg = "Error in Effect: " + str(e)
600+
if e.sanitize:
601+
msg = SANITIZE_ERROR_MSG
602+
notification_show(msg, type="error", duration=5000)
603+
if e.close:
604+
await self._session._unhandled_error(e)
586605
except Exception as e:
587606
traceback.print_exc()
588607

shiny/types.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,30 @@ class SilentOperationInProgressException(SilentException):
160160
pass
161161

162162

163+
class NotifyException(Exception):
164+
"""
165+
This exception can be raised in a (non-output) reactive effect
166+
to display a message to the user.
167+
168+
Parameters
169+
----------
170+
message
171+
The message to display to the user.
172+
sanitize
173+
If ``True``, the message is sanitized to prevent leaking sensitive information.
174+
close
175+
If ``True``, the session is closed after the message is displayed.
176+
"""
177+
178+
sanitize: bool
179+
close: bool
180+
181+
def __init__(self, message: str, sanitize: bool = True, close: bool = False):
182+
super().__init__(message)
183+
self.sanitize = sanitize
184+
self.close = close
185+
186+
163187
class ActionButtonValue(int):
164188
pass
165189

shiny/ui/_chat.py

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515

1616
from htmltools import Tag
1717

18-
from .. import _utils, reactive, ui
19-
from .._app import SANITIZE_ERROR_MSG
18+
from .. import _utils, reactive
2019
from .._namespaces import resolve_id
2120
from ..session import Session, require_active_session, session_context
21+
from ..types import NotifyException
2222
from ._chat_types import (
2323
ChatMessage,
2424
ChatMessageChunk,
@@ -79,7 +79,7 @@ def __call__(
7979
width: str = "min(680px, 100%)",
8080
fill: bool = True,
8181
) -> Tag:
82-
if not self._is_express():
82+
if not _express_is_active():
8383
raise RuntimeError(
8484
"The `__call__()` method of the `ui.Chat` class only works in a Shiny Express context."
8585
" Use `ui.chat_ui()` instead in Shiny Core to locate the chat UI."
@@ -92,47 +92,40 @@ def __call__(
9292
fill=fill,
9393
)
9494

95-
# TODO: maybe this should be a utility function in express?
96-
@staticmethod
97-
def _is_express() -> bool:
98-
from ..express._run import get_top_level_recall_context_manager
99-
100-
try:
101-
get_top_level_recall_context_manager()
102-
return True
103-
except RuntimeError:
104-
return False
105-
10695
def on_user_submit(
10796
self,
108-
func: SubmitFunction | SubmitFunctionAsync,
109-
errors: Literal["sanitize", "show", "none"] = "sanitize",
110-
) -> reactive.Effect_:
97+
fn: SubmitFunction | SubmitFunctionAsync | None = None,
98+
*,
99+
error: Literal["sanitize", "actual", "unhandled"] = "sanitize",
100+
) -> (
101+
reactive.Effect_
102+
| Callable[[SubmitFunction | SubmitFunctionAsync], reactive.Effect_]
103+
):
111104
"""
112105
Register a callback to run when the user submits a message.
113106
"""
114107

115-
afunc = _utils.wrap_async(func)
108+
def create_effect(fn: SubmitFunction | SubmitFunctionAsync):
109+
afunc = _utils.wrap_async(fn)
116110

117-
@reactive.effect
118-
@reactive.event(self.user_input)
119-
async def wrapper():
120-
try:
121-
await afunc()
122-
# TODO: does this handle req() correctly?
123-
except Exception as e:
124-
if errors == "sanitize":
125-
ui.notification_show(
126-
SANITIZE_ERROR_MSG, type="error", duration=5000
127-
)
128-
elif errors == "show":
129-
ui.notification_show(
130-
ui.markdown(str(e)), type="error", duration=5000
131-
)
132-
133-
raise e
134-
135-
return wrapper
111+
@reactive.effect
112+
@reactive.event(self.user_input)
113+
async def _():
114+
if error == "unhandled":
115+
await afunc()
116+
else:
117+
try:
118+
await afunc()
119+
except Exception as e:
120+
await self._remove_placeholder()
121+
raise NotifyException(str(e), sanitize=error == "sanitize")
122+
123+
return _
124+
125+
if fn is None:
126+
return create_effect
127+
else:
128+
return create_effect(fn)
136129

137130
def user_input(self) -> str:
138131
"""
@@ -235,6 +228,9 @@ async def clear_messages(self):
235228

236229
await self._send_custom_message("shiny-chat-clear-messages", None)
237230

231+
async def _remove_placeholder(self):
232+
await self._send_custom_message("shiny-chat-remove-placeholder", None)
233+
238234
async def _send_custom_message(
239235
self, handler: str, obj: ChatMessage | ChatMessageChunk | None
240236
):
@@ -294,3 +290,13 @@ def chat_ui(
294290
res = as_fillable_container(as_fill_item(res))
295291

296292
return res
293+
294+
295+
def _express_is_active() -> bool:
296+
from ..express._run import get_top_level_recall_context_manager
297+
298+
try:
299+
get_top_level_recall_context_manager()
300+
return True
301+
except RuntimeError:
302+
return False

0 commit comments

Comments
 (0)