Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions chatsky/core/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class FrameworkData(BaseModel, arbitrary_types_allowed=True):
"Enables complex stats collection across multiple turns."
slot_manager: SlotManager = Field(default_factory=SlotManager)
"Stores extracted slots."
response_exception: Optional[str] = Field(default=None, exclude=True)
"Stores response errors as exception"


class Context(BaseModel):
Expand Down
2 changes: 1 addition & 1 deletion chatsky/processing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .standard import ModifyResponse
from .standard import ModifyResponse, AddFallbackResponses
from .slots import Extract, Unset, UnsetAll, FillTemplate
51 changes: 50 additions & 1 deletion chatsky/processing/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
"""

import abc
import logging
from typing import Literal, Type, Union, Dict
from pydantic import field_validator

from chatsky.core import BaseProcessing, BaseResponse, Context, MessageInitTypes
from chatsky.core import BaseProcessing, BaseResponse, Context, MessageInitTypes, AnyResponse

logger = logging.getLogger(__name__)


class ModifyResponse(BaseProcessing, abc.ABC):
Expand All @@ -24,6 +29,8 @@ async def modified_response(self, original_response: BaseResponse, ctx: Context)

:param original_response: Response of the current node when :py:class:`.ModifyResponse` is called.
:param ctx: Current context.

:return: Message to replace original response with modified.
"""
raise NotImplementedError

Expand All @@ -39,3 +46,45 @@ async def call(self, ctx: Context) -> MessageInitTypes:
return await processing_object.modified_response(current_response, ctx)

ctx.current_node.response = ModifiedResponse()


class AddFallbackResponses(ModifyResponse, arbitrary_types_allowed=True):
"""
ModifyResponse with pre-response processing to handle exceptions dynamically.
"""

exception_response: Dict[Union[Type[Exception], Literal["Else"]], AnyResponse]
"""
Dictionary mapping exception types to fallback responses.
"""

@field_validator("exception_response")
@classmethod
def validate_not_empty(cls, exception_response: dict) -> dict:
"""
Validate that the `exception_response` dictionary is not empty.

:param exception_response: Dictionary mapping exception types to fallback responses.
:raises ValueError: If the `exception_response` dictionary is empty.
:return: Not empty dictionary of exception_response.
"""
if len(exception_response) == 0:
raise ValueError("Exceptions dict is empty")
return exception_response

async def modified_response(self, original_response: BaseResponse, ctx: Context) -> MessageInitTypes:
"""
Catch response errors and process them based on `exception_response`.

:param original_response: The original response of the current node.
:param ctx: The current context.

:return: Message to replace original response with modified due to fallback response.
"""
try:
return await original_response(ctx)
except Exception as e:
exception = self.exception_response.get(type(e), self.exception_response.get("Else"))
logger.exception(e)
ctx.framework_data.response_exception = str(e)
return await exception(ctx)
6 changes: 6 additions & 0 deletions docs/source/get_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ Additionally, you also have the option to download the source code directly from

Once you are in the directory, you can run the command ``poetry install --all-extras`` to set up all the requirements for the library.

Quick start with a project template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you don't want to bother with setting up project files, you can use the `Chatsky Project Template <https://github.com/deeppavlov/chatsky-template>`_
repository, which offers a ready-to-use simple bot that can be modified to your needs.

Key concepts
~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions scripts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def docker_client(wrapped: Callable[[Optional[DockerClient]], int], _, __, ___)
docker = DockerClient(
compose_files=["compose.yml"],
compose_profiles=["context_storage", "stats"],
compose_compatibility=True,
)
docker.compose.up(detach=True, wait=True, quiet=True)
error = None
Expand Down
46 changes: 46 additions & 0 deletions tests/core/test_processing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from chatsky import proc, Context, BaseResponse, MessageInitTypes, Message
from chatsky.core.script import Node

Expand All @@ -22,3 +24,47 @@ async def modified_response(self, original_response: BaseResponse, ctx: Context)
assert ctx.current_node.response.__class__.__name__ == "ModifiedResponse"

assert await ctx.current_node.response(ctx) == Message(misc={"msg": Message("hi")})


class TestAddFallbackResponses:
"""
A class to group and test the functionality of FallbackResponse.
"""

class ReturnException(BaseResponse):
async def call(self, ctx: Context):
return ctx.framework_data.response_exception

class RaiseException(BaseResponse, arbitrary_types_allowed=True):
exception: Exception

async def call(self, ctx: Context):
raise self.exception

@pytest.mark.parametrize(
"response_with_exception, expected_response",
[
(RaiseException(exception=OverflowError()), "Overflow!"),
(RaiseException(exception=KeyError()), "Other exception occured"),
(RaiseException(exception=ValueError("some text")), "some text"),
],
)
@pytest.mark.asyncio
async def test_fallback_response(self, response_with_exception, expected_response):
ctx = Context()
ctx.framework_data.current_node = Node()

exceptions = {OverflowError: "Overflow!", ValueError: self.ReturnException(), "Else": "Other exception occured"}

fallback_response = proc.AddFallbackResponses(exception_response=exceptions)
ctx.current_node.response = response_with_exception
await fallback_response(ctx)
assert await ctx.current_node.response(ctx) == Message(text=expected_response)

async def test_fallback_empty_exceptions(self):
ctx = Context()
ctx.framework_data.current_node = Node()

exceptions = {}
with pytest.raises(ValueError, match="Exceptions dict is empty"):
proc.AddFallbackResponses(exception_response=exceptions)
4 changes: 3 additions & 1 deletion tutorials/script/core/7_pre_response_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@ async def modified_response(self, original_response, ctx):
except Exception as exc:
return str(exc)

Note: the functionality of adding custom responses for exceptions
is available in the core library as the
%mddoclink(api,processing.standard,AddFallbackResponses) class.
</div>
"""


# %%
toy_script = {
"root": {
Expand Down
Loading