From f1857f63bb4cfd77c9ef31fef41c583fa722cfe9 Mon Sep 17 00:00:00 2001 From: askatasuna Date: Tue, 1 Oct 2024 15:36:01 +0300 Subject: [PATCH 01/24] Added flag to --- chatsky/slots/slots.py | 24 ++++- tests/slots/manual_test.py | 154 ++++++++++++++++++++++++++++ tests/slots/test_slot_extraction.py | 15 +++ 3 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 tests/slots/manual_test.py create mode 100644 tests/slots/test_slot_extraction.py diff --git a/chatsky/slots/slots.py b/chatsky/slots/slots.py index 276a28f56..a83915aff 100644 --- a/chatsky/slots/slots.py +++ b/chatsky/slots/slots.py @@ -72,9 +72,16 @@ def recursive_setattr(obj, slot_name: SlotName, value): parent_slot, _, slot = slot_name.rpartition(".") if parent_slot: - setattr(recursive_getattr(obj, parent_slot), slot, value) + parent_obj = recursive_getattr(obj, parent_slot) + if isinstance(value, ExtractedGroupSlot): + getattr(parent_obj, slot).update(value) + else: + setattr(parent_obj, slot, value) else: - setattr(obj, slot, value) + if isinstance(value, ExtractedGroupSlot): + getattr(obj, slot).update(value) + else: + setattr(obj, slot, value) class SlotNotExtracted(Exception): @@ -261,9 +268,11 @@ class GroupSlot(BaseSlot, extra="allow", frozen=True): """ __pydantic_extra__: Dict[str, Annotated[Union["GroupSlot", "ValueSlot"], Field(union_mode="left_to_right")]] + allow_partially_extracted: bool = False + """If True, allows returning a partial dictionary with only successfully extracted slots.""" - def __init__(self, **kwargs): # supress unexpected argument warnings - super().__init__(**kwargs) + def __init__(self, allow_partially_extracted=False, **kwargs): + super().__init__(allow_partially_extracted=allow_partially_extracted, **kwargs) @model_validator(mode="after") def __check_extra_field_names__(self): @@ -279,8 +288,13 @@ def __check_extra_field_names__(self): async def get_value(self, ctx: Context) -> ExtractedGroupSlot: child_values = await asyncio.gather(*(child.get_value(ctx) for child in self.__pydantic_extra__.values())) + extracted_values = {} + for child_value, child_name in zip(child_values, self.__pydantic_extra__.keys()): + if child_value.__slot_extracted__ or not self.allow_partially_extracted: + extracted_values[child_name] = child_value + return ExtractedGroupSlot( - **{child_name: child_value for child_value, child_name in zip(child_values, self.__pydantic_extra__.keys())} + **extracted_values ) def init_value(self) -> ExtractedGroupSlot: diff --git a/tests/slots/manual_test.py b/tests/slots/manual_test.py new file mode 100644 index 000000000..969e05d04 --- /dev/null +++ b/tests/slots/manual_test.py @@ -0,0 +1,154 @@ +from chatsky import ( + RESPONSE, + TRANSITIONS, + PRE_TRANSITION, + PRE_RESPONSE, + GLOBAL, + LOCAL, + Pipeline, + Transition as Tr, + conditions as cnd, + processing as proc, + responses as rsp, +) + +from chatsky.slots import RegexpSlot, GroupSlot +import logging +logging.basicConfig(level=logging.DEBUG) + +from chatsky.utils.testing import ( + check_happy_path, + is_interactive_mode, +) + +SLOTS = { + "person": GroupSlot( + username=RegexpSlot( + regexp=r"username is ([a-zA-Z]+)", + match_group_idx=1, + ), + email=RegexpSlot( + regexp=r"email is ([a-z@\.A-Z]+)", + match_group_idx=1, + ) + ), + "friend": GroupSlot( + first_name=RegexpSlot(regexp=r"^[A-Z][a-z]+?(?= )", default_value="default_name"), + last_name=RegexpSlot(regexp=r"(?<= )[A-Z][a-z]+", default_value="default_surname"), + allow_partially_extracted=True + ) +} + +script = { + GLOBAL: { + TRANSITIONS: [ + Tr(dst=("username_flow", "ask"), cnd=cnd.Regexp(r"^[sS]tart")) + ] + }, + "username_flow": { + LOCAL: { + PRE_TRANSITION: {"get_slot": proc.Extract("person.username")}, + TRANSITIONS: [ + Tr( + dst=("email_flow", "ask"), + cnd=cnd.SlotsExtracted("person.username"), + priority=1.2, + ), + Tr(dst=("username_flow", "repeat_question"), priority=0.8), + ], + }, + "ask": { + RESPONSE: "Write your username (my username is ...):", + }, + "repeat_question": { + RESPONSE: "Please, type your username again (my username is ...):", + }, + }, + "email_flow": { + LOCAL: { + PRE_TRANSITION: {"get_slot": proc.Extract("person.email")}, + TRANSITIONS: [ + Tr( + dst=("friend_flow", "ask"), + cnd=cnd.SlotsExtracted("person.username", "person.email"), + priority=1.2, + ), + Tr(dst=("email_flow", "repeat_question"), priority=0.8), + ], + }, + "ask": { + RESPONSE: "Write your email (my email is ...):", + }, + "repeat_question": { + RESPONSE: "Please, write your email again (my email is ...):", + }, + }, + "friend_flow": { + LOCAL: { + PRE_TRANSITION: {"get_slots": proc.Extract("friend", success_only=False)}, + TRANSITIONS: [ + Tr( + dst=("root", "utter"), + cnd=cnd.SlotsExtracted( + "friend.first_name", "friend.last_name", mode="any" + ), + priority=1.2, + ), + Tr(dst=("friend_flow", "repeat_question"), priority=0.8), + ], + }, + "ask": {RESPONSE: "Please, name me one of your friends: (John Doe)"}, + "repeat_question": { + RESPONSE: "Please, name me one of your friends again: (John Doe)" + }, + }, + "root": { + "start": { + TRANSITIONS: [Tr(dst=("username_flow", "ask"))], + }, + "fallback": { + RESPONSE: "Finishing query", + TRANSITIONS: [Tr(dst=("username_flow", "ask"))], + }, + "utter": { + RESPONSE: rsp.FilledTemplate( + "Your friend is {friend.first_name} {friend.last_name}" + ), + TRANSITIONS: [Tr(dst=("root", "utter_alternative"))], + }, + "utter_alternative": { + RESPONSE: "Your username is {person.username}. " + "Your email is {person.email}.", + PRE_RESPONSE: {"fill": proc.FillTemplate()}, + }, + }, +} + +HAPPY_PATH = [ + ("hi", "Write your username (my username is ...):"), + ("my username is groot", "Write your email (my email is ...):"), + ( + "my email is groot@gmail.com", + "Please, name me one of your friends: (John Doe)", + ), + ("Bob Page", "Your friend is Bob Page"), + ("ok", "Your username is groot. Your email is groot@gmail.com."), + ("ok", "Finishing query"), +] + +# %% +pipeline = Pipeline( + script=script, + start_label=("root", "start"), + fallback_label=("root", "fallback"), + slots=SLOTS, +) + +if __name__ == "__main__": + check_happy_path( + pipeline, HAPPY_PATH, printout=True + ) # This is a function for automatic tutorial running + # (testing) with HAPPY_PATH + + if is_interactive_mode(): + pipeline.run() diff --git a/tests/slots/test_slot_extraction.py b/tests/slots/test_slot_extraction.py new file mode 100644 index 000000000..4d520d1de --- /dev/null +++ b/tests/slots/test_slot_extraction.py @@ -0,0 +1,15 @@ +from typing import Union, Any +import logging + +import pytest + +from chatsky import Context +from chatsky.core import BaseResponse, Node +from chatsky.core.message import MessageInitTypes, Message +from chatsky.slots.slots import ValueSlot, SlotNotExtracted, GroupSlot, SlotManager +from chatsky import conditions as cnd, responses as rsp, processing as proc +from chatsky.processing.slots import logger as proc_logger +from chatsky.slots.slots import logger as slot_logger +from chatsky.responses.slots import logger as rsp_logger + + From c334ff5bbb43248a232f16ccc34f39b1517bb2db Mon Sep 17 00:00:00 2001 From: askatasuna Date: Tue, 1 Oct 2024 15:59:38 +0300 Subject: [PATCH 02/24] First test attempts --- chatsky/slots/slots.py | 6 +-- tests/slots/test_slot_extraction.py | 15 ------ ...est.py => test_slot_partial_extraction.py} | 49 ++++++++----------- 3 files changed, 23 insertions(+), 47 deletions(-) delete mode 100644 tests/slots/test_slot_extraction.py rename tests/slots/{manual_test.py => test_slot_partial_extraction.py} (77%) diff --git a/chatsky/slots/slots.py b/chatsky/slots/slots.py index a83915aff..ad3515fa4 100644 --- a/chatsky/slots/slots.py +++ b/chatsky/slots/slots.py @@ -292,10 +292,8 @@ async def get_value(self, ctx: Context) -> ExtractedGroupSlot: for child_value, child_name in zip(child_values, self.__pydantic_extra__.keys()): if child_value.__slot_extracted__ or not self.allow_partially_extracted: extracted_values[child_name] = child_value - - return ExtractedGroupSlot( - **extracted_values - ) + + return ExtractedGroupSlot(**extracted_values) def init_value(self) -> ExtractedGroupSlot: return ExtractedGroupSlot( diff --git a/tests/slots/test_slot_extraction.py b/tests/slots/test_slot_extraction.py deleted file mode 100644 index 4d520d1de..000000000 --- a/tests/slots/test_slot_extraction.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Union, Any -import logging - -import pytest - -from chatsky import Context -from chatsky.core import BaseResponse, Node -from chatsky.core.message import MessageInitTypes, Message -from chatsky.slots.slots import ValueSlot, SlotNotExtracted, GroupSlot, SlotManager -from chatsky import conditions as cnd, responses as rsp, processing as proc -from chatsky.processing.slots import logger as proc_logger -from chatsky.slots.slots import logger as slot_logger -from chatsky.responses.slots import logger as rsp_logger - - diff --git a/tests/slots/manual_test.py b/tests/slots/test_slot_partial_extraction.py similarity index 77% rename from tests/slots/manual_test.py rename to tests/slots/test_slot_partial_extraction.py index 969e05d04..e39265769 100644 --- a/tests/slots/manual_test.py +++ b/tests/slots/test_slot_partial_extraction.py @@ -13,13 +13,13 @@ ) from chatsky.slots import RegexpSlot, GroupSlot -import logging -logging.basicConfig(level=logging.DEBUG) + from chatsky.utils.testing import ( check_happy_path, is_interactive_mode, ) +import pytest SLOTS = { "person": GroupSlot( @@ -30,21 +30,17 @@ email=RegexpSlot( regexp=r"email is ([a-z@\.A-Z]+)", match_group_idx=1, - ) + ), ), "friend": GroupSlot( first_name=RegexpSlot(regexp=r"^[A-Z][a-z]+?(?= )", default_value="default_name"), last_name=RegexpSlot(regexp=r"(?<= )[A-Z][a-z]+", default_value="default_surname"), - allow_partially_extracted=True - ) + allow_partially_extracted=True, + ), } script = { - GLOBAL: { - TRANSITIONS: [ - Tr(dst=("username_flow", "ask"), cnd=cnd.Regexp(r"^[sS]tart")) - ] - }, + GLOBAL: {TRANSITIONS: [Tr(dst=("username_flow", "ask"), cnd=cnd.Regexp(r"^[sS]tart"))]}, "username_flow": { LOCAL: { PRE_TRANSITION: {"get_slot": proc.Extract("person.username")}, @@ -89,18 +85,14 @@ TRANSITIONS: [ Tr( dst=("root", "utter"), - cnd=cnd.SlotsExtracted( - "friend.first_name", "friend.last_name", mode="any" - ), + cnd=cnd.SlotsExtracted("friend.first_name", "friend.last_name", mode="any"), priority=1.2, ), Tr(dst=("friend_flow", "repeat_question"), priority=0.8), ], }, "ask": {RESPONSE: "Please, name me one of your friends: (John Doe)"}, - "repeat_question": { - RESPONSE: "Please, name me one of your friends again: (John Doe)" - }, + "repeat_question": {RESPONSE: "Please, name me one of your friends again: (John Doe)"}, }, "root": { "start": { @@ -111,14 +103,11 @@ TRANSITIONS: [Tr(dst=("username_flow", "ask"))], }, "utter": { - RESPONSE: rsp.FilledTemplate( - "Your friend is {friend.first_name} {friend.last_name}" - ), + RESPONSE: rsp.FilledTemplate("Your friend is {friend.first_name} {friend.last_name}"), TRANSITIONS: [Tr(dst=("root", "utter_alternative"))], }, "utter_alternative": { - RESPONSE: "Your username is {person.username}. " - "Your email is {person.email}.", + RESPONSE: "Your username is {person.username}. " "Your email is {person.email}.", PRE_RESPONSE: {"fill": proc.FillTemplate()}, }, }, @@ -134,6 +123,15 @@ ("Bob Page", "Your friend is Bob Page"), ("ok", "Your username is groot. Your email is groot@gmail.com."), ("ok", "Finishing query"), + ("again", "Write your username (my username is ...):"), + ("my username is groot", "Write your email (my email is ...):"), + ( + "my email is groot@gmail.com", + "Please, name me one of your friends: (John Doe)", + ), + ("Jim ", "Your friend is Jim Page"), + ("ok", "Your username is groot. Your email is groot@gmail.com."), + ("ok", "Finishing query"), ] # %% @@ -144,11 +142,6 @@ slots=SLOTS, ) -if __name__ == "__main__": - check_happy_path( - pipeline, HAPPY_PATH, printout=True - ) # This is a function for automatic tutorial running - # (testing) with HAPPY_PATH - if is_interactive_mode(): - pipeline.run() +def test_happy_path(): + check_happy_path(pipeline, HAPPY_PATH, printout=True) # This is a function for automatic tutorial running From 8306bbbe939bea4c663795a47929894ea8ac9c8b Mon Sep 17 00:00:00 2001 From: askatasuna Date: Tue, 1 Oct 2024 16:03:55 +0300 Subject: [PATCH 03/24] linting --- tests/slots/test_slot_partial_extraction.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/slots/test_slot_partial_extraction.py b/tests/slots/test_slot_partial_extraction.py index e39265769..f95efa3c7 100644 --- a/tests/slots/test_slot_partial_extraction.py +++ b/tests/slots/test_slot_partial_extraction.py @@ -17,9 +17,7 @@ from chatsky.utils.testing import ( check_happy_path, - is_interactive_mode, ) -import pytest SLOTS = { "person": GroupSlot( From 33f05d0886b60dfb1b4192dc0295c1e4c8ea0e2d Mon Sep 17 00:00:00 2001 From: askatasuna Date: Mon, 7 Oct 2024 12:43:30 +0300 Subject: [PATCH 04/24] Added groupslot tutorial for slots --- tutorials/slots/2_group_slots.py | 136 +++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tutorials/slots/2_group_slots.py diff --git a/tutorials/slots/2_group_slots.py b/tutorials/slots/2_group_slots.py new file mode 100644 index 000000000..f2715eb78 --- /dev/null +++ b/tutorials/slots/2_group_slots.py @@ -0,0 +1,136 @@ +# %% [markdown] +""" +# 2. Group slots usage + +This tutorial will show more advanced way of using slots by utilizing `GroupSlot` and different parameters it provides us with. +By using Group slots you can extract multiple slots at once if they are placed in one group. +""" + +# %pip install chatsky + +# %% +from chatsky import ( + RESPONSE, + TRANSITIONS, + PRE_TRANSITION, + PRE_RESPONSE, + GLOBAL, + LOCAL, + Pipeline, + Transition as Tr, + conditions as cnd, + processing as proc, + responses as rsp, +) + +from chatsky.slots import RegexpSlot, GroupSlot + +from chatsky.utils.testing import ( + check_happy_path, + is_interactive_mode, +) + +# %% [markdown] +""" +In this example we define two group slots for `person` and for a `friend`. Note, that in `friend` slot we set a flag `allow_partially_extracted` to `True` that allows us to _update_ slot values and not totally rewrite them in case we did not get full information first time. + +So in this example if we send "John Doe" as a friends name and after that send only name e.g. "Mike" the last extracted friends name would be "Mike Doe" and not "Mike default_surname". + +Another feature is `success_only` flag in `Extract` function that ensures that group slot will be extracted if ALL of the slots in it were extracted successfully. +""" + +# %% +SLOTS = { + "person": GroupSlot( + username=RegexpSlot( + regexp=r"([a-zA-Z]+)", + match_group_idx=1, + ), + email=RegexpSlot( + regexp=r"([a-z@\.A-Z]+)", + match_group_idx=1, + ), + ), + "friend": GroupSlot( + first_name=RegexpSlot(regexp=r"^[A-Z][a-z]+?(?= )", default_value="default_name"), + last_name=RegexpSlot(regexp=r"(?<= )[A-Z][a-z]+", default_value="default_surname"), + allow_partially_extracted=True, + ) +} + +script = { + GLOBAL: {TRANSITIONS: [Tr(dst=("user_flow", "ask"), cnd=cnd.Regexp(r"^[sS]tart"))]}, + "user_flow": { + LOCAL: { + PRE_TRANSITION: {"get_slots": proc.Extract("person", success_only=True)}, + TRANSITIONS: [ + Tr( + dst=("root", "utter_user"), + cnd=cnd.SlotsExtracted("person.username", "person.email", mode="any"), + priority=1.2, + ), + Tr(dst=("user_flow", "repeat_question"), priority=0.8), + ], + }, + "ask": {RESPONSE: "Please, send your username and email in one message."}, + "repeat_question": {RESPONSE: "Please, send your username and email again."}, + }, + "friend_flow": { + LOCAL: { + PRE_TRANSITION: {"get_slots": proc.Extract("friend", success_only=False)}, + TRANSITIONS: [ + Tr( + dst=("root", "utter_friends"), + cnd=cnd.SlotsExtracted("friend.first_name", "friend.last_name", mode="any"), + priority=1.2, + ), + Tr(dst=("friend_flow", "repeat_question"), priority=0.8), + ], + }, + "ask": {RESPONSE: "Please, send your friends name"}, + "repeat_question": {RESPONSE: "Please, send your friends name again."}, + }, + "root": { + "start": { + TRANSITIONS: [Tr(dst=("user_flow", "ask"))], + }, + "fallback": { + RESPONSE: "Finishing query", + TRANSITIONS: [Tr(dst=("user_flow", "ask"))], + }, + "utter_friend": { + RESPONSE: rsp.FilledTemplate("Your friend is {friend.first_name} {friend.last_name}"), + TRANSITIONS: [Tr(dst=("friend_flow", "ask"))], + }, + "utter_user": { + RESPONSE: "Your username is {person.username}. " "Your email is {person.email}.", + PRE_RESPONSE: {"fill": proc.FillTemplate()}, + TRANSITIONS: [Tr(dst=("root", "utter_friend"))] + }, + }, +} + +HAPPY_PATH = [ + ("hi", "Write your username (my username is ...):"), + ("my username is groot", "Write your email (my email is ...):"), + ( + "my email is groot@gmail.com", + "Please, name me one of your friends: (John Doe)", + ), + ("Bob Page", "Your friend is Bob Page"), + ("ok", "Your username is groot. Your email is groot@gmail.com."), + ("ok", "Finishing query"), + ( + "again", + "Please, name me one of your friends: (John Doe)", + ), + ("Jim ", "Your friend is Jim Page") +] + +# %% +pipeline = Pipeline( + script=script, + start_label=("root", "start"), + fallback_label=("root", "fallback"), + slots=SLOTS, +) From 09937ae76fa91892433530c8977f25c4da7ac2fb Mon Sep 17 00:00:00 2001 From: askatasuna Date: Wed, 9 Oct 2024 12:40:39 +0300 Subject: [PATCH 05/24] Switched to unit tests --- tests/llm/test_llm.py | 1 + tests/slots/test_slot_partial_extraction.py | 178 +++++++----------- ...group_slots.py => 2_partial_extraction.py} | 4 +- 3 files changed, 69 insertions(+), 114 deletions(-) create mode 100644 tests/llm/test_llm.py rename tutorials/slots/{2_group_slots.py => 2_partial_extraction.py} (97%) diff --git a/tests/llm/test_llm.py b/tests/llm/test_llm.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tests/llm/test_llm.py @@ -0,0 +1 @@ + diff --git a/tests/slots/test_slot_partial_extraction.py b/tests/slots/test_slot_partial_extraction.py index f95efa3c7..b81c481b6 100644 --- a/tests/slots/test_slot_partial_extraction.py +++ b/tests/slots/test_slot_partial_extraction.py @@ -13,133 +13,87 @@ ) from chatsky.slots import RegexpSlot, GroupSlot - +from chatsky.slots.slots import SlotManager, ExtractedValueSlot, ExtractedGroupSlot +from chatsky.core import Message, Context from chatsky.utils.testing import ( check_happy_path, ) -SLOTS = { - "person": GroupSlot( - username=RegexpSlot( - regexp=r"username is ([a-zA-Z]+)", +import pytest + +test_slot = GroupSlot( + person=GroupSlot( + username=RegexpSlot( + regexp=r"([a-z]+_[a-z]+)", match_group_idx=1, ), - email=RegexpSlot( - regexp=r"email is ([a-z@\.A-Z]+)", + email=RegexpSlot( + regexp=r"([a-z]+@[a-z]+\.[a-z]+)", match_group_idx=1, ), + allow_partially_extracted=True + ) + ) + + +extracted_slot_values_turn_1 = { + "person.username": ExtractedValueSlot.model_construct( + is_slot_extracted=True, extracted_value="test_name", default_value=None ), - "friend": GroupSlot( - first_name=RegexpSlot(regexp=r"^[A-Z][a-z]+?(?= )", default_value="default_name"), - last_name=RegexpSlot(regexp=r"(?<= )[A-Z][a-z]+", default_value="default_surname"), - allow_partially_extracted=True, + "person.email": ExtractedValueSlot.model_construct( + is_slot_extracted=True, extracted_value="test@email.com", default_value=None ), } -script = { - GLOBAL: {TRANSITIONS: [Tr(dst=("username_flow", "ask"), cnd=cnd.Regexp(r"^[sS]tart"))]}, - "username_flow": { - LOCAL: { - PRE_TRANSITION: {"get_slot": proc.Extract("person.username")}, - TRANSITIONS: [ - Tr( - dst=("email_flow", "ask"), - cnd=cnd.SlotsExtracted("person.username"), - priority=1.2, - ), - Tr(dst=("username_flow", "repeat_question"), priority=0.8), - ], - }, - "ask": { - RESPONSE: "Write your username (my username is ...):", - }, - "repeat_question": { - RESPONSE: "Please, type your username again (my username is ...):", - }, - }, - "email_flow": { - LOCAL: { - PRE_TRANSITION: {"get_slot": proc.Extract("person.email")}, - TRANSITIONS: [ - Tr( - dst=("friend_flow", "ask"), - cnd=cnd.SlotsExtracted("person.username", "person.email"), - priority=1.2, - ), - Tr(dst=("email_flow", "repeat_question"), priority=0.8), - ], - }, - "ask": { - RESPONSE: "Write your email (my email is ...):", - }, - "repeat_question": { - RESPONSE: "Please, write your email again (my email is ...):", - }, - }, - "friend_flow": { - LOCAL: { - PRE_TRANSITION: {"get_slots": proc.Extract("friend", success_only=False)}, - TRANSITIONS: [ - Tr( - dst=("root", "utter"), - cnd=cnd.SlotsExtracted("friend.first_name", "friend.last_name", mode="any"), - priority=1.2, - ), - Tr(dst=("friend_flow", "repeat_question"), priority=0.8), - ], - }, - "ask": {RESPONSE: "Please, name me one of your friends: (John Doe)"}, - "repeat_question": {RESPONSE: "Please, name me one of your friends again: (John Doe)"}, - }, - "root": { - "start": { - TRANSITIONS: [Tr(dst=("username_flow", "ask"))], - }, - "fallback": { - RESPONSE: "Finishing query", - TRANSITIONS: [Tr(dst=("username_flow", "ask"))], - }, - "utter": { - RESPONSE: rsp.FilledTemplate("Your friend is {friend.first_name} {friend.last_name}"), - TRANSITIONS: [Tr(dst=("root", "utter_alternative"))], - }, - "utter_alternative": { - RESPONSE: "Your username is {person.username}. " "Your email is {person.email}.", - PRE_RESPONSE: {"fill": proc.FillTemplate()}, - }, - }, -} - -HAPPY_PATH = [ - ("hi", "Write your username (my username is ...):"), - ("my username is groot", "Write your email (my email is ...):"), - ( - "my email is groot@gmail.com", - "Please, name me one of your friends: (John Doe)", +extracted_slot_values_turn_2 = { + "person.username": ExtractedValueSlot.model_construct( + is_slot_extracted=True, extracted_value="new_name", default_value=None ), - ("Bob Page", "Your friend is Bob Page"), - ("ok", "Your username is groot. Your email is groot@gmail.com."), - ("ok", "Finishing query"), - ("again", "Write your username (my username is ...):"), - ("my username is groot", "Write your email (my email is ...):"), - ( - "my email is groot@gmail.com", - "Please, name me one of your friends: (John Doe)", + "person.email": ExtractedValueSlot.model_construct( + is_slot_extracted=True, extracted_value="test@email.com", default_value=None ), - ("Jim ", "Your friend is Jim Page"), - ("ok", "Your username is groot. Your email is groot@gmail.com."), - ("ok", "Finishing query"), -] +} -# %% -pipeline = Pipeline( - script=script, - start_label=("root", "start"), - fallback_label=("root", "fallback"), - slots=SLOTS, -) +@pytest.fixture(scope="function") +def context_with_request_1(context): + new_ctx = context.model_copy(deep=True) + new_ctx.add_request(Message(text="I am test_name. My email is test@email.com")) + return new_ctx + +@pytest.fixture(scope="function") +def context_with_request_2(context): + context.add_request(Message(text="I am new_name.")) + return context +@pytest.fixture(scope="function") +def empty_slot_manager(): + manager = SlotManager() + manager.set_root_slot(test_slot) + return manager -def test_happy_path(): - check_happy_path(pipeline, HAPPY_PATH, printout=True) # This is a function for automatic tutorial running +@pytest.mark.parametrize( + "slot_name,expected_slot_storage_1,expected_slot_storage_2", + [ + ( + "person", + ExtractedGroupSlot( + person=ExtractedGroupSlot( + username=extracted_slot_values_turn_1["person.username"], + email=extracted_slot_values_turn_1["person.email"], + ) + ), + ExtractedGroupSlot( + person=ExtractedGroupSlot( + username=extracted_slot_values_turn_2["person.username"], + email=extracted_slot_values_turn_2["person.email"], + ) + ), + ), + ], +) +async def test_slot_extraction(slot_name, expected_slot_storage_1, expected_slot_storage_2, empty_slot_manager, context_with_request_1, context_with_request_2): + await empty_slot_manager.extract_slot(slot_name, context_with_request_1, success_only=False) + assert empty_slot_manager.slot_storage == expected_slot_storage_1 + await empty_slot_manager.extract_slot(slot_name, context_with_request_2, success_only=False) + assert empty_slot_manager.slot_storage == expected_slot_storage_2 \ No newline at end of file diff --git a/tutorials/slots/2_group_slots.py b/tutorials/slots/2_partial_extraction.py similarity index 97% rename from tutorials/slots/2_group_slots.py rename to tutorials/slots/2_partial_extraction.py index f2715eb78..92de9022f 100644 --- a/tutorials/slots/2_group_slots.py +++ b/tutorials/slots/2_partial_extraction.py @@ -1,6 +1,6 @@ # %% [markdown] """ -# 2. Group slots usage +# 2. Partial slot extraction This tutorial will show more advanced way of using slots by utilizing `GroupSlot` and different parameters it provides us with. By using Group slots you can extract multiple slots at once if they are placed in one group. @@ -66,7 +66,7 @@ TRANSITIONS: [ Tr( dst=("root", "utter_user"), - cnd=cnd.SlotsExtracted("person.username", "person.email", mode="any"), + cnd=cnd.SlotsExtracted("person", mode="any"), priority=1.2, ), Tr(dst=("user_flow", "repeat_question"), priority=0.8), From 218e8e951c1aecd6b10d802f1e6dcbfa317e0e16 Mon Sep 17 00:00:00 2001 From: askatasuna Date: Wed, 9 Oct 2024 12:43:33 +0300 Subject: [PATCH 06/24] Lint --- tests/slots/test_slot_partial_extraction.py | 45 +++++++++------------ 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/tests/slots/test_slot_partial_extraction.py b/tests/slots/test_slot_partial_extraction.py index b81c481b6..d8d60bb07 100644 --- a/tests/slots/test_slot_partial_extraction.py +++ b/tests/slots/test_slot_partial_extraction.py @@ -1,40 +1,22 @@ -from chatsky import ( - RESPONSE, - TRANSITIONS, - PRE_TRANSITION, - PRE_RESPONSE, - GLOBAL, - LOCAL, - Pipeline, - Transition as Tr, - conditions as cnd, - processing as proc, - responses as rsp, -) - from chatsky.slots import RegexpSlot, GroupSlot from chatsky.slots.slots import SlotManager, ExtractedValueSlot, ExtractedGroupSlot -from chatsky.core import Message, Context - -from chatsky.utils.testing import ( - check_happy_path, -) +from chatsky.core import Message import pytest test_slot = GroupSlot( - person=GroupSlot( - username=RegexpSlot( + person=GroupSlot( + username=RegexpSlot( regexp=r"([a-z]+_[a-z]+)", match_group_idx=1, ), - email=RegexpSlot( + email=RegexpSlot( regexp=r"([a-z]+@[a-z]+\.[a-z]+)", match_group_idx=1, ), - allow_partially_extracted=True - ) + allow_partially_extracted=True, ) +) extracted_slot_values_turn_1 = { @@ -55,23 +37,27 @@ ), } + @pytest.fixture(scope="function") def context_with_request_1(context): new_ctx = context.model_copy(deep=True) new_ctx.add_request(Message(text="I am test_name. My email is test@email.com")) return new_ctx + @pytest.fixture(scope="function") def context_with_request_2(context): context.add_request(Message(text="I am new_name.")) return context + @pytest.fixture(scope="function") def empty_slot_manager(): manager = SlotManager() manager.set_root_slot(test_slot) return manager + @pytest.mark.parametrize( "slot_name,expected_slot_storage_1,expected_slot_storage_2", [ @@ -92,8 +78,15 @@ def empty_slot_manager(): ), ], ) -async def test_slot_extraction(slot_name, expected_slot_storage_1, expected_slot_storage_2, empty_slot_manager, context_with_request_1, context_with_request_2): +async def test_slot_extraction( + slot_name, + expected_slot_storage_1, + expected_slot_storage_2, + empty_slot_manager, + context_with_request_1, + context_with_request_2, +): await empty_slot_manager.extract_slot(slot_name, context_with_request_1, success_only=False) assert empty_slot_manager.slot_storage == expected_slot_storage_1 await empty_slot_manager.extract_slot(slot_name, context_with_request_2, success_only=False) - assert empty_slot_manager.slot_storage == expected_slot_storage_2 \ No newline at end of file + assert empty_slot_manager.slot_storage == expected_slot_storage_2 From f217832a29b1a025f591fae73647b5ec3a46b294 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 14 Oct 2024 17:33:19 +0300 Subject: [PATCH 07/24] simplify recursive_setattr --- chatsky/slots/slots.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/chatsky/slots/slots.py b/chatsky/slots/slots.py index ad3515fa4..eeb2614e6 100644 --- a/chatsky/slots/slots.py +++ b/chatsky/slots/slots.py @@ -69,19 +69,17 @@ def two_arg_getattr(__o, name): def recursive_setattr(obj, slot_name: SlotName, value): - parent_slot, _, slot = slot_name.rpartition(".") + parent_slot, sep, slot = slot_name.rpartition(".") - if parent_slot: + if sep == ".": parent_obj = recursive_getattr(obj, parent_slot) - if isinstance(value, ExtractedGroupSlot): - getattr(parent_obj, slot).update(value) - else: - setattr(parent_obj, slot, value) else: - if isinstance(value, ExtractedGroupSlot): - getattr(obj, slot).update(value) - else: - setattr(obj, slot, value) + parent_obj = obj + + if isinstance(value, ExtractedGroupSlot): + getattr(parent_obj, slot).update(value) + else: + setattr(parent_obj, slot, value) class SlotNotExtracted(Exception): From da48f686b7b18b40f9c925680a23ea13df319ebe Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 14 Oct 2024 17:33:44 +0300 Subject: [PATCH 08/24] update docstrings --- chatsky/slots/slots.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chatsky/slots/slots.py b/chatsky/slots/slots.py index eeb2614e6..77f3c6f08 100644 --- a/chatsky/slots/slots.py +++ b/chatsky/slots/slots.py @@ -267,7 +267,7 @@ class GroupSlot(BaseSlot, extra="allow", frozen=True): __pydantic_extra__: Dict[str, Annotated[Union["GroupSlot", "ValueSlot"], Field(union_mode="left_to_right")]] allow_partially_extracted: bool = False - """If True, allows returning a partial dictionary with only successfully extracted slots.""" + """If True, extraction returns only successfully extracted child slots.""" def __init__(self, allow_partially_extracted=False, **kwargs): super().__init__(allow_partially_extracted=allow_partially_extracted, **kwargs) @@ -378,6 +378,8 @@ async def extract_slot(self, slot_name: SlotName, ctx: Context, success_only: bo """ Extract slot `slot_name` and store extracted value in `slot_storage`. + Extracted group slots update slot storage instead of overwriting it. + :raises KeyError: If the slot with the specified name does not exist. :param slot_name: Name of the slot to extract. From a09037c4bcdfe9be4a80836b01e3c57ac569aaa2 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 14 Oct 2024 17:34:47 +0300 Subject: [PATCH 09/24] remove unrelated llm tests --- tests/llm/test_llm.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tests/llm/test_llm.py diff --git a/tests/llm/test_llm.py b/tests/llm/test_llm.py deleted file mode 100644 index 8b1378917..000000000 --- a/tests/llm/test_llm.py +++ /dev/null @@ -1 +0,0 @@ - From e534f4c5097fd2de120abda0a82e7447d29a9d0f Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Tue, 15 Oct 2024 00:34:03 +0300 Subject: [PATCH 10/24] rewrite partial extraction tests - Simplify slot regexps - More extensive tests for slot storage changes --- tests/slots/test_slot_partial_extraction.py | 120 +++++++++----------- 1 file changed, 56 insertions(+), 64 deletions(-) diff --git a/tests/slots/test_slot_partial_extraction.py b/tests/slots/test_slot_partial_extraction.py index d8d60bb07..39ab79017 100644 --- a/tests/slots/test_slot_partial_extraction.py +++ b/tests/slots/test_slot_partial_extraction.py @@ -1,54 +1,44 @@ from chatsky.slots import RegexpSlot, GroupSlot -from chatsky.slots.slots import SlotManager, ExtractedValueSlot, ExtractedGroupSlot +from chatsky.slots.slots import SlotManager from chatsky.core import Message import pytest test_slot = GroupSlot( - person=GroupSlot( - username=RegexpSlot( - regexp=r"([a-z]+_[a-z]+)", - match_group_idx=1, + root_slot=GroupSlot( + one=RegexpSlot(regexp=r"1"), + two=RegexpSlot(regexp=r"2"), + nested_group=GroupSlot( + three=RegexpSlot(regexp=r"3"), + four=RegexpSlot(regexp=r"4"), + allow_partially_extracted=False, ), - email=RegexpSlot( - regexp=r"([a-z]+@[a-z]+\.[a-z]+)", - match_group_idx=1, + nested_partial_group=GroupSlot( + five=RegexpSlot(regexp=r"5"), + six=RegexpSlot(regexp=r"6"), + allow_partially_extracted=True, ), allow_partially_extracted=True, ) ) - -extracted_slot_values_turn_1 = { - "person.username": ExtractedValueSlot.model_construct( - is_slot_extracted=True, extracted_value="test_name", default_value=None - ), - "person.email": ExtractedValueSlot.model_construct( - is_slot_extracted=True, extracted_value="test@email.com", default_value=None - ), -} - -extracted_slot_values_turn_2 = { - "person.username": ExtractedValueSlot.model_construct( - is_slot_extracted=True, extracted_value="new_name", default_value=None - ), - "person.email": ExtractedValueSlot.model_construct( - is_slot_extracted=True, extracted_value="test@email.com", default_value=None - ), +extracted_slots = { + "root_slot.one": "1", + "root_slot.two": "2", + "root_slot.nested_group.three": "3", + "root_slot.nested_group.four": "4", + "root_slot.nested_partial_group.five": "5", + "root_slot.nested_partial_group.six": "6", } @pytest.fixture(scope="function") -def context_with_request_1(context): - new_ctx = context.model_copy(deep=True) - new_ctx.add_request(Message(text="I am test_name. My email is test@email.com")) - return new_ctx - +def context_with_request(context): + def inner(request): + context.add_request(Message(request)) + return context -@pytest.fixture(scope="function") -def context_with_request_2(context): - context.add_request(Message(text="I am new_name.")) - return context + return inner @pytest.fixture(scope="function") @@ -58,35 +48,37 @@ def empty_slot_manager(): return manager +def get_extracted_slots(manager: SlotManager): + values = [] + for slot, value in extracted_slots.items(): + extracted_value = manager.get_extracted_slot(slot) + if extracted_value.__slot_extracted__: + if extracted_value.value == value: + values.append(value) + else: + raise RuntimeError(f"Extracted value {extracted_value} does not match expected {value}.") + return values + + @pytest.mark.parametrize( - "slot_name,expected_slot_storage_1,expected_slot_storage_2", - [ - ( - "person", - ExtractedGroupSlot( - person=ExtractedGroupSlot( - username=extracted_slot_values_turn_1["person.username"], - email=extracted_slot_values_turn_1["person.email"], - ) - ), - ExtractedGroupSlot( - person=ExtractedGroupSlot( - username=extracted_slot_values_turn_2["person.username"], - email=extracted_slot_values_turn_2["person.email"], - ) - ), - ), - ], + "message,extracted", + [("1 2 3", ["1", "2"]), ("1 3 5", ["1", "5"]), ("3 4 5 6", ["3", "4", "5", "6"])], ) -async def test_slot_extraction( - slot_name, - expected_slot_storage_1, - expected_slot_storage_2, - empty_slot_manager, - context_with_request_1, - context_with_request_2, -): - await empty_slot_manager.extract_slot(slot_name, context_with_request_1, success_only=False) - assert empty_slot_manager.slot_storage == expected_slot_storage_1 - await empty_slot_manager.extract_slot(slot_name, context_with_request_2, success_only=False) - assert empty_slot_manager.slot_storage == expected_slot_storage_2 +async def test_partial_extraction(message, extracted, context_with_request, empty_slot_manager): + await empty_slot_manager.extract_slot("root_slot", context_with_request(message), success_only=False) + + assert extracted == get_extracted_slots(empty_slot_manager) + + +async def test_slot_storage_update(context_with_request, empty_slot_manager): + await empty_slot_manager.extract_slot("root_slot", context_with_request("1 3 5"), success_only=False) + + assert get_extracted_slots(empty_slot_manager) == ["1", "5"] + + await empty_slot_manager.extract_slot("root_slot", context_with_request("2 4 6"), success_only=False) + + assert get_extracted_slots(empty_slot_manager) == ["1", "2", "5", "6"] + + await empty_slot_manager.extract_slot("root_slot.nested_group", context_with_request("3 4"), success_only=False) + + assert get_extracted_slots(empty_slot_manager) == ["1", "2", "3", "4", "5", "6"] From 66f3db0e63d88349556f65f83aed98e812be6299 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Tue, 15 Oct 2024 17:10:18 +0300 Subject: [PATCH 11/24] rename allow_partially_extracted to allow_partial_extraction --- chatsky/slots/slots.py | 8 ++++---- tests/slots/test_slot_partial_extraction.py | 6 +++--- tutorials/slots/2_partial_extraction.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/chatsky/slots/slots.py b/chatsky/slots/slots.py index 77f3c6f08..3cadd9205 100644 --- a/chatsky/slots/slots.py +++ b/chatsky/slots/slots.py @@ -266,11 +266,11 @@ class GroupSlot(BaseSlot, extra="allow", frozen=True): """ __pydantic_extra__: Dict[str, Annotated[Union["GroupSlot", "ValueSlot"], Field(union_mode="left_to_right")]] - allow_partially_extracted: bool = False + allow_partial_extraction: bool = False """If True, extraction returns only successfully extracted child slots.""" - def __init__(self, allow_partially_extracted=False, **kwargs): - super().__init__(allow_partially_extracted=allow_partially_extracted, **kwargs) + def __init__(self, allow_partial_extraction=False, **kwargs): + super().__init__(allow_partial_extraction=allow_partial_extraction, **kwargs) @model_validator(mode="after") def __check_extra_field_names__(self): @@ -288,7 +288,7 @@ async def get_value(self, ctx: Context) -> ExtractedGroupSlot: child_values = await asyncio.gather(*(child.get_value(ctx) for child in self.__pydantic_extra__.values())) extracted_values = {} for child_value, child_name in zip(child_values, self.__pydantic_extra__.keys()): - if child_value.__slot_extracted__ or not self.allow_partially_extracted: + if child_value.__slot_extracted__ or not self.allow_partial_extraction: extracted_values[child_name] = child_value return ExtractedGroupSlot(**extracted_values) diff --git a/tests/slots/test_slot_partial_extraction.py b/tests/slots/test_slot_partial_extraction.py index 39ab79017..234287c17 100644 --- a/tests/slots/test_slot_partial_extraction.py +++ b/tests/slots/test_slot_partial_extraction.py @@ -11,14 +11,14 @@ nested_group=GroupSlot( three=RegexpSlot(regexp=r"3"), four=RegexpSlot(regexp=r"4"), - allow_partially_extracted=False, + allow_partial_extraction=False, ), nested_partial_group=GroupSlot( five=RegexpSlot(regexp=r"5"), six=RegexpSlot(regexp=r"6"), - allow_partially_extracted=True, + allow_partial_extraction=True, ), - allow_partially_extracted=True, + allow_partial_extraction=True, ) ) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 92de9022f..945b0b464 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -32,7 +32,7 @@ # %% [markdown] """ -In this example we define two group slots for `person` and for a `friend`. Note, that in `friend` slot we set a flag `allow_partially_extracted` to `True` that allows us to _update_ slot values and not totally rewrite them in case we did not get full information first time. +In this example we define two group slots for `person` and for a `friend`. Note, that in `friend` slot we set a flag `allow_partial_extraction` to `True` that allows us to _update_ slot values and not totally rewrite them in case we did not get full information first time. So in this example if we send "John Doe" as a friends name and after that send only name e.g. "Mike" the last extracted friends name would be "Mike Doe" and not "Mike default_surname". @@ -54,7 +54,7 @@ "friend": GroupSlot( first_name=RegexpSlot(regexp=r"^[A-Z][a-z]+?(?= )", default_value="default_name"), last_name=RegexpSlot(regexp=r"(?<= )[A-Z][a-z]+", default_value="default_surname"), - allow_partially_extracted=True, + allow_partial_extraction=True, ) } From c489024025e04dac8a5dd39b35006d0d23d7d2a5 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Tue, 15 Oct 2024 17:13:27 +0300 Subject: [PATCH 12/24] add check_happy_path block to tutorial --- tutorials/slots/2_partial_extraction.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 945b0b464..36dd360b0 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -127,6 +127,7 @@ ("Jim ", "Your friend is Jim Page") ] + # %% pipeline = Pipeline( script=script, @@ -134,3 +135,11 @@ fallback_label=("root", "fallback"), slots=SLOTS, ) + + +# %% +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH, printout=True) + + if is_interactive_mode(): + pipeline.run() From e6a9468ef6050039805aa1049d79bce46aa11a45 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Tue, 15 Oct 2024 17:13:51 +0300 Subject: [PATCH 13/24] reformat tutorial --- tutorials/slots/2_partial_extraction.py | 47 ++++++++++++++++++------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 36dd360b0..637b1428c 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -52,17 +52,27 @@ ), ), "friend": GroupSlot( - first_name=RegexpSlot(regexp=r"^[A-Z][a-z]+?(?= )", default_value="default_name"), - last_name=RegexpSlot(regexp=r"(?<= )[A-Z][a-z]+", default_value="default_surname"), + first_name=RegexpSlot( + regexp=r"^[A-Z][a-z]+?(?= )", default_value="default_name" + ), + last_name=RegexpSlot( + regexp=r"(?<= )[A-Z][a-z]+", default_value="default_surname" + ), allow_partial_extraction=True, - ) + ), } script = { - GLOBAL: {TRANSITIONS: [Tr(dst=("user_flow", "ask"), cnd=cnd.Regexp(r"^[sS]tart"))]}, + GLOBAL: { + TRANSITIONS: [ + Tr(dst=("user_flow", "ask"), cnd=cnd.Regexp(r"^[sS]tart")) + ] + }, "user_flow": { LOCAL: { - PRE_TRANSITION: {"get_slots": proc.Extract("person", success_only=True)}, + PRE_TRANSITION: { + "get_slots": proc.Extract("person", success_only=True) + }, TRANSITIONS: [ Tr( dst=("root", "utter_user"), @@ -72,16 +82,24 @@ Tr(dst=("user_flow", "repeat_question"), priority=0.8), ], }, - "ask": {RESPONSE: "Please, send your username and email in one message."}, - "repeat_question": {RESPONSE: "Please, send your username and email again."}, + "ask": { + RESPONSE: "Please, send your username and email in one message." + }, + "repeat_question": { + RESPONSE: "Please, send your username and email again." + }, }, "friend_flow": { LOCAL: { - PRE_TRANSITION: {"get_slots": proc.Extract("friend", success_only=False)}, + PRE_TRANSITION: { + "get_slots": proc.Extract("friend", success_only=False) + }, TRANSITIONS: [ Tr( dst=("root", "utter_friends"), - cnd=cnd.SlotsExtracted("friend.first_name", "friend.last_name", mode="any"), + cnd=cnd.SlotsExtracted( + "friend.first_name", "friend.last_name", mode="any" + ), priority=1.2, ), Tr(dst=("friend_flow", "repeat_question"), priority=0.8), @@ -99,13 +117,16 @@ TRANSITIONS: [Tr(dst=("user_flow", "ask"))], }, "utter_friend": { - RESPONSE: rsp.FilledTemplate("Your friend is {friend.first_name} {friend.last_name}"), + RESPONSE: rsp.FilledTemplate( + "Your friend is {friend.first_name} {friend.last_name}" + ), TRANSITIONS: [Tr(dst=("friend_flow", "ask"))], }, "utter_user": { - RESPONSE: "Your username is {person.username}. " "Your email is {person.email}.", + RESPONSE: "Your username is {person.username}. " + "Your email is {person.email}.", PRE_RESPONSE: {"fill": proc.FillTemplate()}, - TRANSITIONS: [Tr(dst=("root", "utter_friend"))] + TRANSITIONS: [Tr(dst=("root", "utter_friend"))], }, }, } @@ -124,7 +145,7 @@ "again", "Please, name me one of your friends: (John Doe)", ), - ("Jim ", "Your friend is Jim Page") + ("Jim ", "Your friend is Jim Page"), ] From 78f3b2b7aea74fa3b9f1f2b03c726763fd89247b Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Wed, 16 Oct 2024 00:37:05 +0300 Subject: [PATCH 14/24] rewrite tutorial text --- tutorials/slots/2_partial_extraction.py | 46 ++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 637b1428c..729d45c5c 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -2,8 +2,8 @@ """ # 2. Partial slot extraction -This tutorial will show more advanced way of using slots by utilizing `GroupSlot` and different parameters it provides us with. -By using Group slots you can extract multiple slots at once if they are placed in one group. +This tutorial shows advanced options for slot extraction allowing +to extract only some of the slots. """ # %pip install chatsky @@ -32,11 +32,47 @@ # %% [markdown] """ -In this example we define two group slots for `person` and for a `friend`. Note, that in `friend` slot we set a flag `allow_partial_extraction` to `True` that allows us to _update_ slot values and not totally rewrite them in case we did not get full information first time. +## Default behavior -So in this example if we send "John Doe" as a friends name and after that send only name e.g. "Mike" the last extracted friends name would be "Mike Doe" and not "Mike default_surname". +By default, slot extraction will write a value into slot storage regardless +of whether the extraction was success. +If extraction fails, the slot will be marked as not-extracted +and its value will be the `default_value` (`None` by default). -Another feature is `success_only` flag in `Extract` function that ensures that group slot will be extracted if ALL of the slots in it were extracted successfully. +If group slot is being extracted, the extraction is considered successful +only if all child slots are successfully extracted. + +## Success only extraction + +The `Extract` function accepts `success_only` flag which makes it so +that extracted value is not saved unless extraction is successful. + +This means that unsuccessfully trying to extract a slot after +it has already been extracted will not overwrite the previously extracted +value. + +Note that `success_only` is `True` by default. + +## Partial group slot extraction + +A group slot marked with `allow_partial_extraction` only saves values +of successfully extracted child slots. +Extracting such group slot is equivalent to extracting every child slot +with the `success_only` flag. + +Partially extracted group slot is always considered successfully extracted +for the purposes of `success_only` flag. + +## Code explanation + +In this example we define two group slots: `person` and `friend`. +Note that in the `friend` slot we set `allow_partial_extraction` to `True` +which allows us to _update_ slot values and not +rewrite them in case we don't get full information at once. + +So if we send "John Doe" as a full name and after that send only first name +(e.g. "Mike") the extracted friends name would be "Mike Doe" +and not "Mike default_surname". """ # %% From 37b0218073f8023013ccc188817b10e6b9d410d2 Mon Sep 17 00:00:00 2001 From: askatasuna Date: Tue, 22 Oct 2024 13:56:06 +0300 Subject: [PATCH 15/24] Updated happy path, fixed the script --- tutorials/slots/2_partial_extraction.py | 26 +++++++++---------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 729d45c5c..6346bc36e 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -83,7 +83,7 @@ match_group_idx=1, ), email=RegexpSlot( - regexp=r"([a-z@\.A-Z]+)", + regexp=r"([a-z]+@[a-zA-Z]+\.[a-z]+)", match_group_idx=1, ), ), @@ -132,7 +132,7 @@ }, TRANSITIONS: [ Tr( - dst=("root", "utter_friends"), + dst=("root", "utter_friend"), cnd=cnd.SlotsExtracted( "friend.first_name", "friend.last_name", mode="any" ), @@ -162,26 +162,18 @@ RESPONSE: "Your username is {person.username}. " "Your email is {person.email}.", PRE_RESPONSE: {"fill": proc.FillTemplate()}, - TRANSITIONS: [Tr(dst=("root", "utter_friend"))], + TRANSITIONS: [Tr(dst=("friend_flow", "ask"))], }, }, } HAPPY_PATH = [ - ("hi", "Write your username (my username is ...):"), - ("my username is groot", "Write your email (my email is ...):"), - ( - "my email is groot@gmail.com", - "Please, name me one of your friends: (John Doe)", - ), - ("Bob Page", "Your friend is Bob Page"), - ("ok", "Your username is groot. Your email is groot@gmail.com."), - ("ok", "Finishing query"), - ( - "again", - "Please, name me one of your friends: (John Doe)", - ), - ("Jim ", "Your friend is Jim Page"), + ("Start", "Please, send your username and email in one message."), + ("groot, groot@gmail.com", "Your username is groot. Your email is groot@gmail.com."), + ("ok", "Please, send your friends name"), + ("Jonh Doe", "Your friend is Jonh Doe"), + ("ok", "Please, send your friends name"), + ("Mike ", "Your friend is Mike Doe") ] From 7915188ba808fad33b2ee0fa2b573ea472182f31 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Wed, 23 Oct 2024 14:57:35 +0300 Subject: [PATCH 16/24] minor text changes --- tutorials/slots/2_partial_extraction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 6346bc36e..311e7e3ca 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -35,8 +35,8 @@ ## Default behavior By default, slot extraction will write a value into slot storage regardless -of whether the extraction was success. -If extraction fails, the slot will be marked as not-extracted +of whether the extraction was successful. +If extraction fails, the slot will be marked as "not extracted" and its value will be the `default_value` (`None` by default). If group slot is being extracted, the extraction is considered successful From a06ea3caf9431150e33df894645fdfad16a35c0f Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Wed, 23 Oct 2024 14:58:45 +0300 Subject: [PATCH 17/24] fix codestyle --- tutorials/slots/2_partial_extraction.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 311e7e3ca..7be5dd637 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -169,11 +169,14 @@ HAPPY_PATH = [ ("Start", "Please, send your username and email in one message."), - ("groot, groot@gmail.com", "Your username is groot. Your email is groot@gmail.com."), + ( + "groot, groot@gmail.com", + "Your username is groot. Your email is groot@gmail.com.", + ), ("ok", "Please, send your friends name"), ("Jonh Doe", "Your friend is Jonh Doe"), ("ok", "Please, send your friends name"), - ("Mike ", "Your friend is Mike Doe") + ("Mike ", "Your friend is Mike Doe"), ] From 204c4e20b729bd9469ed84ed06f1483457e66348 Mon Sep 17 00:00:00 2001 From: askatasuna Date: Wed, 30 Oct 2024 12:21:08 +0300 Subject: [PATCH 18/24] Working on tutorial --- tutorials/slots/2_partial_extraction.py | 41 ++++++++++++++----------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 7be5dd637..8c333e945 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -29,6 +29,9 @@ check_happy_path, is_interactive_mode, ) +import logging +logging.basicConfig(level=logging.DEBUG) + # %% [markdown] """ @@ -78,21 +81,24 @@ # %% SLOTS = { "person": GroupSlot( - username=RegexpSlot( - regexp=r"([a-zA-Z]+)", + coin_address=RegexpSlot( + regexp=r"(\b[a-zA-Z0-9]{34}\b)", + default_value="default_address", match_group_idx=1, ), email=RegexpSlot( - regexp=r"([a-z]+@[a-zA-Z]+\.[a-z]+)", + regexp=r"([\w\.-]+@[\w\.-]+\.\w{2,4})", + default_value="default_email", match_group_idx=1, ), + allow_partial_extraction=True ), "friend": GroupSlot( - first_name=RegexpSlot( - regexp=r"^[A-Z][a-z]+?(?= )", default_value="default_name" + coin_address=RegexpSlot( + regexp=r"(\b[a-zA-Z0-9]{34}\b)", default_value="default_address" ), - last_name=RegexpSlot( - regexp=r"(?<= )[A-Z][a-z]+", default_value="default_surname" + email=RegexpSlot( + regexp=r"([\w\.-]+@[\w\.-]+\.\w{2,4})", default_value="default_email" ), allow_partial_extraction=True, ), @@ -107,7 +113,7 @@ "user_flow": { LOCAL: { PRE_TRANSITION: { - "get_slots": proc.Extract("person", success_only=True) + "get_slots": proc.Extract("person", success_only=False) }, TRANSITIONS: [ Tr( @@ -119,10 +125,10 @@ ], }, "ask": { - RESPONSE: "Please, send your username and email in one message." + RESPONSE: "Please, send your email and bitcoin address in one message." }, "repeat_question": { - RESPONSE: "Please, send your username and email again." + RESPONSE: "Please, send your bitcoin address and email again." }, }, "friend_flow": { @@ -134,15 +140,15 @@ Tr( dst=("root", "utter_friend"), cnd=cnd.SlotsExtracted( - "friend.first_name", "friend.last_name", mode="any" + "friend.coin_address", "friend.email", mode="any" ), priority=1.2, ), Tr(dst=("friend_flow", "repeat_question"), priority=0.8), ], }, - "ask": {RESPONSE: "Please, send your friends name"}, - "repeat_question": {RESPONSE: "Please, send your friends name again."}, + "ask": {RESPONSE: "Please, send your friends bitcoin address and email"}, + "repeat_question": {RESPONSE: "Please, send your friends bitcoin address and email again."}, }, "root": { "start": { @@ -154,13 +160,12 @@ }, "utter_friend": { RESPONSE: rsp.FilledTemplate( - "Your friend is {friend.first_name} {friend.last_name}" + "Your friends address is {friend.coin_address} and email is {friend.email}" ), TRANSITIONS: [Tr(dst=("friend_flow", "ask"))], }, "utter_user": { - RESPONSE: "Your username is {person.username}. " - "Your email is {person.email}.", + RESPONSE: "Your bitcoin address is {person.coin_address}. Your email is {person.email}.", PRE_RESPONSE: {"fill": proc.FillTemplate()}, TRANSITIONS: [Tr(dst=("friend_flow", "ask"))], }, @@ -168,10 +173,10 @@ } HAPPY_PATH = [ - ("Start", "Please, send your username and email in one message."), + ("Start", "Please, send your email and bitcoin address in one message."), ( "groot, groot@gmail.com", - "Your username is groot. Your email is groot@gmail.com.", + "Your bitcoin address is default_address. Your email is groot@gmail.com.", ), ("ok", "Please, send your friends name"), ("Jonh Doe", "Your friend is Jonh Doe"), From 8869eef996c8aa3a01c774e57e13b63a90433df7 Mon Sep 17 00:00:00 2001 From: askatasuna Date: Thu, 31 Oct 2024 13:52:14 +0300 Subject: [PATCH 19/24] Added GroupSlotsExtracted class with required field --- chatsky/conditions/__init__.py | 2 +- chatsky/conditions/slots.py | 28 +++++++++++++++++++++++++ tutorials/slots/2_partial_extraction.py | 10 ++++----- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/chatsky/conditions/__init__.py b/chatsky/conditions/__init__.py index 0d02477dd..f03abd3e8 100644 --- a/chatsky/conditions/__init__.py +++ b/chatsky/conditions/__init__.py @@ -9,5 +9,5 @@ Not, HasCallbackQuery, ) -from chatsky.conditions.slots import SlotsExtracted +from chatsky.conditions.slots import SlotsExtracted, GroupSlotsExtracted from chatsky.conditions.service import ServiceFinished diff --git a/chatsky/conditions/slots.py b/chatsky/conditions/slots.py index eaddd3140..90e74967c 100644 --- a/chatsky/conditions/slots.py +++ b/chatsky/conditions/slots.py @@ -36,3 +36,31 @@ async def call(self, ctx: Context) -> bool: return all(manager.is_slot_extracted(slot) for slot in self.slots) elif self.mode == "any": return any(manager.is_slot_extracted(slot) for slot in self.slots) + + +class GroupSlotsExtracted(BaseCondition): + """ + Check if :py:attr:`.slots` are extracted. + + :param mode: Whether to check if all slots are extracted or any slot is extracted. + """ + + slots: List[SlotName] + """ + Names of the slots that need to be checked. + """ + required: List[SlotName] + """ + If required slots are extracted condition will return true despite non-required slots being extracted or not. + By default set to `["all"]` that sets all slots to required. + """ + + def __init__(self, *slots: SlotName, required: List[str] = ["all"]): + super().__init__(slots=slots, required=required) + + async def call(self, ctx: Context) -> bool: + manager = ctx.framework_data.slot_manager + if self.required == ["all"]: + return all(manager.is_slot_extracted(slot) for slot in self.slots) + else: + return all(manager.is_slot_extracted(slot) for slot in self.required) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 8c333e945..7094e58a4 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -30,7 +30,7 @@ is_interactive_mode, ) import logging -logging.basicConfig(level=logging.DEBUG) +# logging.basicConfig(level=logging.DEBUG) # %% [markdown] @@ -85,13 +85,13 @@ regexp=r"(\b[a-zA-Z0-9]{34}\b)", default_value="default_address", match_group_idx=1, + required=True ), email=RegexpSlot( regexp=r"([\w\.-]+@[\w\.-]+\.\w{2,4})", default_value="default_email", match_group_idx=1, - ), - allow_partial_extraction=True + ) ), "friend": GroupSlot( coin_address=RegexpSlot( @@ -118,10 +118,10 @@ TRANSITIONS: [ Tr( dst=("root", "utter_user"), - cnd=cnd.SlotsExtracted("person", mode="any"), + cnd=cnd.GroupSlotsExtracted("person", required=["person.coin_address"]), priority=1.2, ), - Tr(dst=("user_flow", "repeat_question"), priority=0.8), + # Tr(dst=("user_flow", "repeat_question"), priority=0.8), ], }, "ask": { From 376bebd03d2d25b792844bd029fb99ac8c56e185 Mon Sep 17 00:00:00 2001 From: askatasuna Date: Thu, 31 Oct 2024 14:33:09 +0300 Subject: [PATCH 20/24] lint --- tutorials/slots/2_partial_extraction.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 7094e58a4..75d80937d 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -29,9 +29,6 @@ check_happy_path, is_interactive_mode, ) -import logging -# logging.basicConfig(level=logging.DEBUG) - # %% [markdown] """ @@ -85,20 +82,21 @@ regexp=r"(\b[a-zA-Z0-9]{34}\b)", default_value="default_address", match_group_idx=1, - required=True + required=True, ), email=RegexpSlot( regexp=r"([\w\.-]+@[\w\.-]+\.\w{2,4})", default_value="default_email", match_group_idx=1, - ) + ), ), "friend": GroupSlot( coin_address=RegexpSlot( regexp=r"(\b[a-zA-Z0-9]{34}\b)", default_value="default_address" ), email=RegexpSlot( - regexp=r"([\w\.-]+@[\w\.-]+\.\w{2,4})", default_value="default_email" + regexp=r"([\w\.-]+@[\w\.-]+\.\w{2,4})", + default_value="default_email", ), allow_partial_extraction=True, ), @@ -118,7 +116,9 @@ TRANSITIONS: [ Tr( dst=("root", "utter_user"), - cnd=cnd.GroupSlotsExtracted("person", required=["person.coin_address"]), + cnd=cnd.GroupSlotsExtracted( + "person", required=["person.coin_address"] + ), priority=1.2, ), # Tr(dst=("user_flow", "repeat_question"), priority=0.8), @@ -147,8 +147,12 @@ Tr(dst=("friend_flow", "repeat_question"), priority=0.8), ], }, - "ask": {RESPONSE: "Please, send your friends bitcoin address and email"}, - "repeat_question": {RESPONSE: "Please, send your friends bitcoin address and email again."}, + "ask": { + RESPONSE: "Please, send your friends bitcoin address and email" + }, + "repeat_question": { + RESPONSE: "Please, send your friends bitcoin address and email again." + }, }, "root": { "start": { From 713203ca412c655b1a0af9f8552fc077bec07709 Mon Sep 17 00:00:00 2001 From: askatasuna Date: Sat, 2 Nov 2024 12:34:40 +0300 Subject: [PATCH 21/24] Removed `GroupSlotsExtracted`, updated tutorial --- chatsky/conditions/__init__.py | 2 +- chatsky/conditions/slots.py | 28 ----------- tutorials/slots/2_partial_extraction.py | 63 +++++++++++++++---------- 3 files changed, 40 insertions(+), 53 deletions(-) diff --git a/chatsky/conditions/__init__.py b/chatsky/conditions/__init__.py index f03abd3e8..0d02477dd 100644 --- a/chatsky/conditions/__init__.py +++ b/chatsky/conditions/__init__.py @@ -9,5 +9,5 @@ Not, HasCallbackQuery, ) -from chatsky.conditions.slots import SlotsExtracted, GroupSlotsExtracted +from chatsky.conditions.slots import SlotsExtracted from chatsky.conditions.service import ServiceFinished diff --git a/chatsky/conditions/slots.py b/chatsky/conditions/slots.py index 90e74967c..eaddd3140 100644 --- a/chatsky/conditions/slots.py +++ b/chatsky/conditions/slots.py @@ -36,31 +36,3 @@ async def call(self, ctx: Context) -> bool: return all(manager.is_slot_extracted(slot) for slot in self.slots) elif self.mode == "any": return any(manager.is_slot_extracted(slot) for slot in self.slots) - - -class GroupSlotsExtracted(BaseCondition): - """ - Check if :py:attr:`.slots` are extracted. - - :param mode: Whether to check if all slots are extracted or any slot is extracted. - """ - - slots: List[SlotName] - """ - Names of the slots that need to be checked. - """ - required: List[SlotName] - """ - If required slots are extracted condition will return true despite non-required slots being extracted or not. - By default set to `["all"]` that sets all slots to required. - """ - - def __init__(self, *slots: SlotName, required: List[str] = ["all"]): - super().__init__(slots=slots, required=required) - - async def call(self, ctx: Context) -> bool: - manager = ctx.framework_data.slot_manager - if self.required == ["all"]: - return all(manager.is_slot_extracted(slot) for slot in self.slots) - else: - return all(manager.is_slot_extracted(slot) for slot in self.required) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 75d80937d..0d146e7cf 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -66,13 +66,15 @@ ## Code explanation In this example we define two group slots: `person` and `friend`. -Note that in the `friend` slot we set `allow_partial_extraction` to `True` +Note that in the `person` slot we set `allow_partial_extraction` to `True` which allows us to _update_ slot values and not rewrite them in case we don't get full information at once. -So if we send "John Doe" as a full name and after that send only first name -(e.g. "Mike") the extracted friends name would be "Mike Doe" -and not "Mike default_surname". +So if we send "groot@gmail.com" as user email and after that send only Bitcoin address +the extracted user data would be " groot@gmail.com" +and not " default_email". +We can compare that behaviour with `fried` slot extraction where we have set `success_only=False` +that enables us to send unly partial info that can be overwritten with default values. """ # %% @@ -89,6 +91,7 @@ default_value="default_email", match_group_idx=1, ), + allow_partial_extraction=True, ), "friend": GroupSlot( coin_address=RegexpSlot( @@ -98,7 +101,6 @@ regexp=r"([\w\.-]+@[\w\.-]+\.\w{2,4})", default_value="default_email", ), - allow_partial_extraction=True, ), } @@ -110,23 +112,17 @@ }, "user_flow": { LOCAL: { - PRE_TRANSITION: { - "get_slots": proc.Extract("person", success_only=False) - }, + PRE_TRANSITION: {"get_slots": proc.Extract("person")}, TRANSITIONS: [ Tr( dst=("root", "utter_user"), - cnd=cnd.GroupSlotsExtracted( - "person", required=["person.coin_address"] - ), + cnd=cnd.SlotsExtracted("person.email"), priority=1.2, ), # Tr(dst=("user_flow", "repeat_question"), priority=0.8), ], }, - "ask": { - RESPONSE: "Please, send your email and bitcoin address in one message." - }, + "ask": {RESPONSE: "Please, send your email and bitcoin address."}, "repeat_question": { RESPONSE: "Please, send your bitcoin address and email again." }, @@ -144,11 +140,16 @@ ), priority=1.2, ), + Tr( + dst=("friend_flow", "ask"), + cnd=cnd.ExactMatch("update"), + priority=0.8, + ), Tr(dst=("friend_flow", "repeat_question"), priority=0.8), ], }, "ask": { - RESPONSE: "Please, send your friends bitcoin address and email" + RESPONSE: "Please, send your friends bitcoin address and email." }, "repeat_question": { RESPONSE: "Please, send your friends bitcoin address and email again." @@ -169,23 +170,37 @@ TRANSITIONS: [Tr(dst=("friend_flow", "ask"))], }, "utter_user": { - RESPONSE: "Your bitcoin address is {person.coin_address}. Your email is {person.email}.", + RESPONSE: "Your bitcoin address is {person.coin_address}. Your email is {person.email}. You can update your data or type /send to proceed.", PRE_RESPONSE: {"fill": proc.FillTemplate()}, - TRANSITIONS: [Tr(dst=("friend_flow", "ask"))], + TRANSITIONS: [ + Tr(dst=("friend_flow", "ask"), cnd=cnd.ExactMatch("/send")), + Tr(dst=("user_flow", "ask")), + ], }, }, } HAPPY_PATH = [ - ("Start", "Please, send your email and bitcoin address in one message."), + ("Start", "Please, send your email and bitcoin address."), + ( + "groot@gmail.com", + "Your bitcoin address is default_address. Your email is groot@gmail.com. You can update your data or type /send to proceed.", + ), + ("update", "Please, send your email and bitcoin address."), + ( + "1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF", + "Your bitcoin address is 1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF. Your email is groot@gmail.com. You can update your data or type /send to proceed.", + ), + ("/send", "Please, send your friends bitcoin address and email."), + ( + "john_doe@gmail.com", + "Your friends address is default_address and email is john_doe@gmail.com", + ), + ("update", "Please, send your friends bitcoin address and email."), ( - "groot, groot@gmail.com", - "Your bitcoin address is default_address. Your email is groot@gmail.com.", + "3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v", + "Your friends address is 3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v and email is default_email", ), - ("ok", "Please, send your friends name"), - ("Jonh Doe", "Your friend is Jonh Doe"), - ("ok", "Please, send your friends name"), - ("Mike ", "Your friend is Mike Doe"), ] From 08dab5d60ba107403406666ab3766f13af2bc29a Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Wed, 6 Nov 2024 15:02:56 +0300 Subject: [PATCH 22/24] update tutorial: fix wording --- tutorials/slots/2_partial_extraction.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 0d146e7cf..075923d2a 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -32,24 +32,22 @@ # %% [markdown] """ -## Default behavior +## Extracted values -By default, slot extraction will write a value into slot storage regardless -of whether the extraction was successful. -If extraction fails, the slot will be marked as "not extracted" -and its value will be the `default_value` (`None` by default). +Result of successful slot extraction is the extracted value, *but* +if the extraction fails, the slot will be marked as "not extracted" +and its value will be set to the slot's `default_value` (`None` by default). If group slot is being extracted, the extraction is considered successful -only if all child slots are successfully extracted. +if and only if all child slots are successfully extracted. ## Success only extraction The `Extract` function accepts `success_only` flag which makes it so that extracted value is not saved unless extraction is successful. -This means that unsuccessfully trying to extract a slot after -it has already been extracted will not overwrite the previously extracted -value. +This means that unsuccessfully trying to extract a slot will not overwrite +its previously extracted value. Note that `success_only` is `True` by default. @@ -61,7 +59,7 @@ with the `success_only` flag. Partially extracted group slot is always considered successfully extracted -for the purposes of `success_only` flag. +for the purposes of the `success_only` flag. ## Code explanation From 332fa34e966b24ff89a92d15eb6a675ba15a7046 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Wed, 6 Nov 2024 15:03:26 +0300 Subject: [PATCH 23/24] update tutorial: change script to showcase behavior with different settings --- tutorials/slots/2_partial_extraction.py | 231 +++++++++++++----------- 1 file changed, 124 insertions(+), 107 deletions(-) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index 075923d2a..aa0ff8062 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -12,10 +12,8 @@ from chatsky import ( RESPONSE, TRANSITIONS, - PRE_TRANSITION, PRE_RESPONSE, GLOBAL, - LOCAL, Pipeline, Transition as Tr, conditions as cnd, @@ -63,141 +61,161 @@ ## Code explanation -In this example we define two group slots: `person` and `friend`. -Note that in the `person` slot we set `allow_partial_extraction` to `True` -which allows us to _update_ slot values and not -rewrite them in case we don't get full information at once. +In this example we showcase the behavior of +different group slot extraction settings: -So if we send "groot@gmail.com" as user email and after that send only Bitcoin address -the extracted user data would be " groot@gmail.com" -and not " default_email". -We can compare that behaviour with `fried` slot extraction where we have set `success_only=False` -that enables us to send unly partial info that can be overwritten with default values. +Group `partial_extraction` is marked with `allow_partial_extraction`. +Any slot in this group is saved if and only if that slot is successfully +extracted. + +Group `success_only_extraction` is extracted with the `success_only` +flag set to True. +Any slot in this group is saved if and only if all of the slots in the group +are successfully extracted within a single `Extract` call. + +Group `success_only_false` is extracted with the `success_only` set to False. +Any slot in this group is saved (even if extraction was not successful). + +Group `sub_slot_success_only_extraction` is extracted by passing all of its +child slots to the `Extract` method with the `success_only` flag set to True. +The behavior is equivalent to that of `partial_extraction`. """ # %% +sub_slots = { + "date": RegexpSlot( + regexp=r"(0?[1-9]|(?:1|2)[0-9]|3[0-1])[\.\/]" + r"(0?[1-9]|1[0-2])[\.\/](\d{4}|\d{2})", + ), + "email": RegexpSlot( + regexp=r"[\w\.-]+@[\w\.-]+\.\w{2,4}", + ), +} + SLOTS = { - "person": GroupSlot( - coin_address=RegexpSlot( - regexp=r"(\b[a-zA-Z0-9]{34}\b)", - default_value="default_address", - match_group_idx=1, - required=True, - ), - email=RegexpSlot( - regexp=r"([\w\.-]+@[\w\.-]+\.\w{2,4})", - default_value="default_email", - match_group_idx=1, - ), + "partial_extraction": GroupSlot( + **sub_slots, allow_partial_extraction=True, ), - "friend": GroupSlot( - coin_address=RegexpSlot( - regexp=r"(\b[a-zA-Z0-9]{34}\b)", default_value="default_address" - ), - email=RegexpSlot( - regexp=r"([\w\.-]+@[\w\.-]+\.\w{2,4})", - default_value="default_email", - ), + "success_only_extraction": GroupSlot( + **sub_slots, + ), + "success_only_false": GroupSlot( + **sub_slots, + ), + "sub_slot_success_only_extraction": GroupSlot( + **sub_slots, ), } script = { GLOBAL: { TRANSITIONS: [ - Tr(dst=("user_flow", "ask"), cnd=cnd.Regexp(r"^[sS]tart")) + Tr(dst=("main", "start"), cnd=cnd.ExactMatch("/start")), + Tr(dst=("main", "reset"), cnd=cnd.ExactMatch("/reset")), + Tr(dst=("main", "print"), priority=0.5), ] }, - "user_flow": { - LOCAL: { - PRE_TRANSITION: {"get_slots": proc.Extract("person")}, - TRANSITIONS: [ - Tr( - dst=("root", "utter_user"), - cnd=cnd.SlotsExtracted("person.email"), - priority=1.2, - ), - # Tr(dst=("user_flow", "repeat_question"), priority=0.8), - ], - }, - "ask": {RESPONSE: "Please, send your email and bitcoin address."}, - "repeat_question": { - RESPONSE: "Please, send your bitcoin address and email again." + "main": { + "start": {RESPONSE: "Hi! Send me email and date."}, + "reset": { + PRE_RESPONSE: {"reset_slots": proc.UnsetAll()}, + RESPONSE: "All slots have been reset.", }, - }, - "friend_flow": { - LOCAL: { - PRE_TRANSITION: { - "get_slots": proc.Extract("friend", success_only=False) - }, - TRANSITIONS: [ - Tr( - dst=("root", "utter_friend"), - cnd=cnd.SlotsExtracted( - "friend.coin_address", "friend.email", mode="any" - ), - priority=1.2, + "print": { + PRE_RESPONSE: { + "partial_extraction": proc.Extract("partial_extraction"), + # partial extraction is always successful; + # success_only doesn't matter + "success_only_extraction": proc.Extract( + "success_only_extraction", success_only=True ), - Tr( - dst=("friend_flow", "ask"), - cnd=cnd.ExactMatch("update"), - priority=0.8, + # success_only is True by default + "success_only_false": proc.Extract( + "success_only_false", success_only=False ), - Tr(dst=("friend_flow", "repeat_question"), priority=0.8), - ], - }, - "ask": { - RESPONSE: "Please, send your friends bitcoin address and email." - }, - "repeat_question": { - RESPONSE: "Please, send your friends bitcoin address and email again." - }, - }, - "root": { - "start": { - TRANSITIONS: [Tr(dst=("user_flow", "ask"))], - }, - "fallback": { - RESPONSE: "Finishing query", - TRANSITIONS: [Tr(dst=("user_flow", "ask"))], - }, - "utter_friend": { + "sub_slot_success_only_extraction": proc.Extract( + "sub_slot_success_only_extraction.email", + "sub_slot_success_only_extraction.date", + success_only=True, + ), + }, RESPONSE: rsp.FilledTemplate( - "Your friends address is {friend.coin_address} and email is {friend.email}" + "Extracted slots:\n" + " Group with partial extraction:\n" + " {partial_extraction}\n" + " Group with success_only:\n" + " {success_only_extraction}\n" + " Group without success_only:\n" + " {success_only_false}\n" + " Extracting sub-slots with success_only:\n" + " {sub_slot_success_only_extraction}" ), - TRANSITIONS: [Tr(dst=("friend_flow", "ask"))], - }, - "utter_user": { - RESPONSE: "Your bitcoin address is {person.coin_address}. Your email is {person.email}. You can update your data or type /send to proceed.", - PRE_RESPONSE: {"fill": proc.FillTemplate()}, - TRANSITIONS: [ - Tr(dst=("friend_flow", "ask"), cnd=cnd.ExactMatch("/send")), - Tr(dst=("user_flow", "ask")), - ], }, }, } HAPPY_PATH = [ - ("Start", "Please, send your email and bitcoin address."), + ("/start", "Hi! Send me email and date."), + ( + "Only email: email@email.com", + "Extracted slots:\n" + " Group with partial extraction:\n" + " {'date': 'None', 'email': 'email@email.com'}\n" + " Group with success_only:\n" + " {'date': 'None', 'email': 'None'}\n" + " Group without success_only:\n" + " {'date': 'None', 'email': 'email@email.com'}\n" + " Extracting sub-slots with success_only:\n" + " {'date': 'None', 'email': 'email@email.com'}", + ), ( - "groot@gmail.com", - "Your bitcoin address is default_address. Your email is groot@gmail.com. You can update your data or type /send to proceed.", + "Only date: 01.01.2024", + "Extracted slots:\n" + " Group with partial extraction:\n" + " {'date': '01.01.2024', 'email': 'email@email.com'}\n" + " Group with success_only:\n" + " {'date': 'None', 'email': 'None'}\n" + " Group without success_only:\n" + " {'date': '01.01.2024', 'email': 'None'}\n" + " Extracting sub-slots with success_only:\n" + " {'date': '01.01.2024', 'email': 'email@email.com'}", ), - ("update", "Please, send your email and bitcoin address."), ( - "1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF", - "Your bitcoin address is 1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF. Your email is groot@gmail.com. You can update your data or type /send to proceed.", + "Both email and date: another_email@email.com; 02.01.2024", + "Extracted slots:\n" + " Group with partial extraction:\n" + " {'date': '02.01.2024', 'email': 'another_email@email.com'}\n" + " Group with success_only:\n" + " {'date': '02.01.2024', 'email': 'another_email@email.com'}\n" + " Group without success_only:\n" + " {'date': '02.01.2024', 'email': 'another_email@email.com'}\n" + " Extracting sub-slots with success_only:\n" + " {'date': '02.01.2024', 'email': 'another_email@email.com'}", ), - ("/send", "Please, send your friends bitcoin address and email."), ( - "john_doe@gmail.com", - "Your friends address is default_address and email is john_doe@gmail.com", + "Partial update (date only): 03.01.2024", + "Extracted slots:\n" + " Group with partial extraction:\n" + " {'date': '03.01.2024', 'email': 'another_email@email.com'}\n" + " Group with success_only:\n" + " {'date': '02.01.2024', 'email': 'another_email@email.com'}\n" + " Group without success_only:\n" + " {'date': '03.01.2024', 'email': 'None'}\n" + " Extracting sub-slots with success_only:\n" + " {'date': '03.01.2024', 'email': 'another_email@email.com'}", ), - ("update", "Please, send your friends bitcoin address and email."), ( - "3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v", - "Your friends address is 3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v and email is default_email", + "No slots here but `Extract` will still be called.", + "Extracted slots:\n" + " Group with partial extraction:\n" + " {'date': '03.01.2024', 'email': 'another_email@email.com'}\n" + " Group with success_only:\n" + " {'date': '02.01.2024', 'email': 'another_email@email.com'}\n" + " Group without success_only:\n" + " {'date': 'None', 'email': 'None'}\n" + " Extracting sub-slots with success_only:\n" + " {'date': '03.01.2024', 'email': 'another_email@email.com'}", ), ] @@ -205,8 +223,7 @@ # %% pipeline = Pipeline( script=script, - start_label=("root", "start"), - fallback_label=("root", "fallback"), + start_label=("main", "start"), slots=SLOTS, ) From b31936aca3fa8d7feac8e80d5d1e41f42854d7db Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Thu, 7 Nov 2024 15:27:40 +0300 Subject: [PATCH 24/24] update tutorial: fix wording --- tutorials/slots/2_partial_extraction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/slots/2_partial_extraction.py b/tutorials/slots/2_partial_extraction.py index aa0ff8062..87cc4bab4 100644 --- a/tutorials/slots/2_partial_extraction.py +++ b/tutorials/slots/2_partial_extraction.py @@ -74,7 +74,7 @@ are successfully extracted within a single `Extract` call. Group `success_only_false` is extracted with the `success_only` set to False. -Any slot in this group is saved (even if extraction was not successful). +Every slot in this group is saved (even if extraction was not successful). Group `sub_slot_success_only_extraction` is extracted by passing all of its child slots to the `Extract` method with the `success_only` flag set to True.