Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom callback exception callable #7558

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Releases

## Version 1.6.0

### Enhancements

- Allow `ChatFeed.callback_exception` to accept a callable and 'traceback' (alias for verbose) ([#7558](https://github.com/holoviz/panel/pull/7558))

## Version 1.5.5

This release fixes a regression causing .node_modules to be bundled into our released wheel and introduces a number of bug fixes and enhancements. Many thanks to @mayonnaisecolouredbenz7, @pmeier, @Italirz, @Coderambling and our maintainer team @MarcSkovMadsen, @hoxbro, @ahuang11, @thuydotm, @maximlt and @philippjfr.
Expand Down
6 changes: 6 additions & 0 deletions doc/about/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

See [the HoloViz blog](https://blog.holoviz.org/#category=panel) for a visual summary of the major features added in each release.

## Version 1.6.0

### Enhancements

- Allow `ChatFeed.callback_exception` to accept a callable and 'traceback' (alias for verbose) ([#7558](https://github.com/holoviz/panel/pull/7558))

## Version 1.5.5

This release fixes a regression causing .node_modules to be bundled into our released wheel and introduces a number of bug fixes and enhancements. Many thanks to @mayonnaisecolouredbenz7, @pmeier, @Italirz, @Coderambling and our maintainer team @MarcSkovMadsen, @hoxbro, @ahuang11, @thuydotm, @maximlt and @philippjfr.
Expand Down
47 changes: 47 additions & 0 deletions examples/reference/chat/ChatFeed.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"* **`header`** (Any): The header of the chat feed; commonly used for the title. Can be a string, pane, or widget.\n",
"* **`callback_user`** (str): The default user name to use for the message provided by the callback.\n",
"* **`callback_avatar`** (str, BytesIO, bytes, ImageBase): The default avatar to use for the entry provided by the callback. Takes precedence over `ChatMessage.default_avatars` if set; else, if None, defaults to the avatar set in `ChatMessage.default_avatars` if matching key exists. Otherwise defaults to the first character of the `callback_user`.\n",
"* **`callback_exception`** (str, Callable): How to handle exceptions raised by the callback. If \"raise\", the exception will be raised. If \"summary\", a summary will be sent to the chat feed. If \"verbose\" or \"traceback\", the full traceback will be sent to the chat feed. If \"ignore\", the exception will be ignored. If a callable is provided, the signature must contain the `exception` and `instance` arguments and it will be called with the exception.\n",
"* **`help_text`** (str): If provided, initializes a chat message in the chat log using the provided help text as the message object and `help` as the user. This is useful for providing instructions, and will not be included in the `serialize` method by default.\n",
"* **`placeholder_text`** (str): The text to display next to the placeholder icon.\n",
"* **`placeholder_params`** (dict) Defaults to `{\"user\": \" \", \"reaction_icons\": {}, \"show_copy_icon\": False, \"show_timestamp\": False}` Params to pass to the placeholder `ChatMessage`, like `reaction_icons`, `timestamp_format`, `show_avatar`, `show_user`, `show_timestamp`.\n",
Expand Down Expand Up @@ -433,6 +434,52 @@
"chat_feed.send(\"This will fail...\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Alternatively, you can provide a callable that accepts the exception and the instance as arguments to handle different exception scenarios."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import random\n",
"\n",
"\n",
"def callback(content):\n",
" if random.random() < 0.5:\n",
" raise RuntimeError(\"This is an unhandled error\")\n",
" raise ValueError(\"This is a handled error\")\n",
"\n",
"\n",
"def callback_exception_handler(exception, instance):\n",
" if isinstance(exception, ValueError):\n",
" instance.stream(\"I can handle this\", user=\"System\")\n",
" return\n",
" instance.stream(\"Fatal error occurred\", user=\"System\")\n",
"\n",
" # you can raise a new exception here if desired\n",
" # raise RuntimeError(\"Fatal error occurred\") from exception\n",
"\n",
"\n",
"chat_feed = pn.chat.ChatFeed(\n",
" callback=callback, callback_exception=callback_exception_handler\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"chat_feed.send(\"This will sometimes fail...\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
23 changes: 23 additions & 0 deletions panel/chat/_param.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from param import Parameter


class CallbackException(Parameter):
"""
A Parameter type to validate callback_exception options. Supports
"raise", "summary", "verbose", "traceback", "ignore", or a callable.
"""

def __init__(self, default="summary", **params):
super().__init__(default=default, **params)
self._validate(default)

def _validate(self, val):
self._validate_value(val, self.allow_None)

def _validate_value(self, val, allow_None, valid=("raise", "summary", "verbose", "traceback", "ignore")):
if ((val is None and allow_None) or val in valid or callable(val)):
return
raise ValueError(
f"Callback exception mode {val} not recognized. "
f"Valid options are {valid} or a callable."
)
16 changes: 11 additions & 5 deletions panel/chat/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from ..viewable import Children
from ..widgets import Widget
from ..widgets.button import Button
from ._param import CallbackException
from .icon import ChatReactionIcons
from .message import ChatMessage
from .step import ChatStep
Expand Down Expand Up @@ -100,13 +101,16 @@ class ChatFeed(ListPanel):
the previous message value `contents`, the previous `user` name,
and the component `instance`.""")

callback_exception = param.Selector(
default="summary", objects=["raise", "summary", "verbose", "ignore"], doc="""
callback_exception = CallbackException(
default="summary", doc="""
How to handle exceptions raised by the callback.
If "raise", the exception will be raised.
If "summary", a summary will be sent to the chat feed.
If "verbose", the full traceback will be sent to the chat feed.
If "verbose" or "traceback", the full traceback will be sent to the chat feed.
If "ignore", the exception will be ignored.
If a callable is provided, the signature must contain the
`exception` and `instance` arguments and it
will be called with the exception.
""")

callback_user = param.String(default="Assistant", doc="""
Expand Down Expand Up @@ -574,13 +578,15 @@ async def _prepare_response(self, *_) -> None:
self._callback_state = CallbackState.STOPPED
except Exception as e:
send_kwargs: dict[str, Any] = dict(user="Exception", respond=False)
if self.callback_exception == "summary":
if callable(self.callback_exception):
self.callback_exception(e, self)
elif self.callback_exception == "summary":
self.send(
f"Encountered `{e!r}`. "
f"Set `callback_exception='verbose'` to see the full traceback.",
**send_kwargs
)
elif self.callback_exception == "verbose":
elif self.callback_exception in ("verbose", "traceback"):
self.send(f"```python\n{traceback.format_exc()}\n```", **send_kwargs)
elif self.callback_exception == "ignore":
return
Expand Down
24 changes: 21 additions & 3 deletions panel/tests/chat/test_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"max_width": 201,
}

ChatFeed.callback_exception = "raise"
ChatFeed.callback_exception = "raise" # type: ignore


@pytest.fixture
Expand Down Expand Up @@ -1083,12 +1083,13 @@ def callback(msg, user, instance):
assert "division by zero" in chat_feed.objects[-1].object
assert chat_feed.objects[-1].user == "Exception"

def test_callback_exception_traceback(self, chat_feed):
@pytest.mark.parametrize("callback_exception", ["traceback", "verbose"])
def test_callback_exception_traceback(self, chat_feed, callback_exception):
def callback(msg, user, instance):
return 1 / 0

chat_feed.callback = callback
chat_feed.callback_exception = "verbose"
chat_feed.callback_exception = callback_exception
chat_feed.send("Message", respond=True)
assert chat_feed.objects[-1].object.startswith(
"```python\nTraceback (most recent call last):"
Expand All @@ -1114,6 +1115,23 @@ def callback(msg, user, instance):
chat_feed.send("Message", respond=True)
wait_until(lambda: len(chat_feed.objects) == 1)

def test_callback_exception_callable(self, chat_feed):
def callback(msg, user, instance):
raise ValueError("Expected error")

def exception_callback(exception, instance):
instance.stream(f"The exception: {exception}")

chat_feed.callback = callback
chat_feed.callback_exception = exception_callback
chat_feed.send("Message", respond=True)
wait_until(lambda: len(chat_feed.objects) == 2)
assert chat_feed.objects[-1].object == "The exception: Expected error"

def test_callback_exception_invalid_option(self, chat_feed):
with pytest.raises(ValueError, match="Valid options are"):
chat_feed.callback_exception = "abc"

def test_callback_stop_generator(self, chat_feed):
def callback(msg, user, instance):
yield "A"
Expand Down
2 changes: 1 addition & 1 deletion panel/tests/chat/test_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from panel.widgets.input import FileInput, TextAreaInput, TextInput
from panel.widgets.select import RadioButtonGroup

ChatInterface.callback_exception = "raise"
ChatInterface.callback_exception = "raise" # type: ignore


class TestChatInterface:
Expand Down
Loading