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

feat: add FallbackResponse class to core/processing/standard.py #414

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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: 1 addition & 1 deletion chatsky/core/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class ServiceState(BaseModel, arbitrary_types_allowed=True):
"""


class FrameworkData(BaseModel, arbitrary_types_allowed=True):
class FrameworkData(BaseModel, arbitrary_types_allowed=True, extra="allow"):
"""
Framework uses this to store data related to any of its modules.
"""
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, FallbackResponse
from .slots import Extract, Unset, UnsetAll, FillTemplate
49 changes: 48 additions & 1 deletion chatsky/processing/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
"""

import abc
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


class ModifyResponse(BaseProcessing, abc.ABC):
Expand All @@ -24,6 +26,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 +43,46 @@ async def call(self, ctx: Context) -> MessageInitTypes:
return await processing_object.modified_response(current_response, ctx)

ctx.current_node.response = ModifiedResponse()


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

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

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

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

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

: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.
"""
print("fallback modified framework", ctx.framework_data)
try:
return await original_response(ctx)
except Exception as e:
exception = self.exceptions.get(type(e), self.exceptions.get("Else"))
print(e, type(e), str(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.

3 changes: 2 additions & 1 deletion scripts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
def docker_client(wrapped: Callable[[Optional[DockerClient]], int], _, __, ___) -> int:
if "linux" in sys.platform:
docker = DockerClient(
compose_files=["compose.yml"],
# 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 TestFallbackResponse:
"""
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.FallbackResponse(exceptions=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.FallbackResponse(exceptions=exceptions)
3 changes: 2 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,11 @@ async def modified_response(self, original_response, ctx):
except Exception as exc:
return str(exc)

However, this functionality is now available in the core library
as the FallbackResponse class.
</div>
"""


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