Skip to content

Commit

Permalink
Better error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
cpsievert committed Jun 6, 2024
1 parent 532b4f2 commit 8abe0e3
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 39 deletions.
13 changes: 13 additions & 0 deletions js/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ class ChatContainer extends LightElement {
this.#onAppendChunk
);
this.addEventListener("shiny-chat-clear-messages", this.#onClear);
this.addEventListener(
"shiny-chat-remove-placeholder",
this.#onRemovePlaceholder
);
}

disconnectedCallback(): void {
Expand All @@ -209,6 +213,10 @@ class ChatContainer extends LightElement {
this.#onAppendChunk
);
this.removeEventListener("shiny-chat-clear-messages", this.#onClear);
this.removeEventListener(
"shiny-chat-remove-placeholder",
this.#onRemovePlaceholder
);
}

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

#onRemovePlaceholder(): void {
this.#removePlaceholder();
this.#enableInput();
}

#enableInput(): void {
this.input.disabled = false;
}
Expand Down
21 changes: 20 additions & 1 deletion shiny/reactive/_reactives.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@
from .._docstring import add_example
from .._utils import is_async_callable, run_coro_sync
from .._validation import req
from ..types import MISSING, MISSING_TYPE, ActionButtonValue, SilentException
from ..types import (
MISSING,
MISSING_TYPE,
ActionButtonValue,
NotifyException,
SilentException,
)
from ._core import Context, Dependents, ReactiveWarning, isolate

if TYPE_CHECKING:
Expand Down Expand Up @@ -583,6 +589,19 @@ async def _run(self) -> None:
except SilentException:
# It's OK for SilentException to cause an Effect to stop running
pass
except NotifyException as e:
traceback.print_exc()

if self._session:
from .._app import SANITIZE_ERROR_MSG
from ..ui import notification_show

msg = "Error in Effect: " + str(e)
if e.sanitize:
msg = SANITIZE_ERROR_MSG
notification_show(msg, type="error", duration=5000)
if e.close:
await self._session._unhandled_error(e)
except Exception as e:
traceback.print_exc()

Expand Down
24 changes: 24 additions & 0 deletions shiny/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,30 @@ class SilentOperationInProgressException(SilentException):
pass


class NotifyException(Exception):
"""
This exception can be raised in a (non-output) reactive effect
to display a message to the user.
Parameters
----------
message
The message to display to the user.
sanitize
If ``True``, the message is sanitized to prevent leaking sensitive information.
close
If ``True``, the session is closed after the message is displayed.
"""

sanitize: bool
close: bool

def __init__(self, message: str, sanitize: bool = True, close: bool = False):
super().__init__(message)
self.sanitize = sanitize
self.close = close


class ActionButtonValue(int):
pass

Expand Down
80 changes: 43 additions & 37 deletions shiny/ui/_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@

from htmltools import Tag

from .. import _utils, reactive, ui
from .._app import SANITIZE_ERROR_MSG
from .. import _utils, reactive
from .._namespaces import resolve_id
from ..session import Session, require_active_session, session_context
from ..types import NotifyException
from ._chat_types import (
ChatMessage,
ChatMessageChunk,
Expand Down Expand Up @@ -79,7 +79,7 @@ def __call__(
width: str = "min(680px, 100%)",
fill: bool = True,
) -> Tag:
if not self._is_express():
if not _express_is_active():
raise RuntimeError(
"The `__call__()` method of the `ui.Chat` class only works in a Shiny Express context."
" Use `ui.chat_ui()` instead in Shiny Core to locate the chat UI."
Expand All @@ -92,47 +92,40 @@ def __call__(
fill=fill,
)

# TODO: maybe this should be a utility function in express?
@staticmethod
def _is_express() -> bool:
from ..express._run import get_top_level_recall_context_manager

try:
get_top_level_recall_context_manager()
return True
except RuntimeError:
return False

def on_user_submit(
self,
func: SubmitFunction | SubmitFunctionAsync,
errors: Literal["sanitize", "show", "none"] = "sanitize",
) -> reactive.Effect_:
fn: SubmitFunction | SubmitFunctionAsync | None = None,
*,
error: Literal["sanitize", "actual", "unhandled"] = "sanitize",
) -> (
reactive.Effect_
| Callable[[SubmitFunction | SubmitFunctionAsync], reactive.Effect_]
):
"""
Register a callback to run when the user submits a message.
"""

afunc = _utils.wrap_async(func)
def create_effect(fn: SubmitFunction | SubmitFunctionAsync):
afunc = _utils.wrap_async(fn)

@reactive.effect
@reactive.event(self.user_input)
async def wrapper():
try:
await afunc()
# TODO: does this handle req() correctly?
except Exception as e:
if errors == "sanitize":
ui.notification_show(
SANITIZE_ERROR_MSG, type="error", duration=5000
)
elif errors == "show":
ui.notification_show(
ui.markdown(str(e)), type="error", duration=5000
)

raise e

return wrapper
@reactive.effect
@reactive.event(self.user_input)
async def _():
if error == "unhandled":
await afunc()
else:
try:
await afunc()
except Exception as e:
await self._remove_placeholder()
raise NotifyException(str(e), sanitize=error == "sanitize")

return _

if fn is None:
return create_effect
else:
return create_effect(fn)

def user_input(self) -> str:
"""
Expand Down Expand Up @@ -235,6 +228,9 @@ async def clear_messages(self):

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

async def _remove_placeholder(self):
await self._send_custom_message("shiny-chat-remove-placeholder", None)

async def _send_custom_message(
self, handler: str, obj: ChatMessage | ChatMessageChunk | None
):
Expand Down Expand Up @@ -294,3 +290,13 @@ def chat_ui(
res = as_fillable_container(as_fill_item(res))

return res


def _express_is_active() -> bool:
from ..express._run import get_top_level_recall_context_manager

try:
get_top_level_recall_context_manager()
return True
except RuntimeError:
return False
Loading

0 comments on commit 8abe0e3

Please sign in to comment.