def apply_to(self, dialog_state: DialogState) -> None: dialog_state.drill_instance_id = None dialog_state.current_prompt_state = None dialog_state.current_drill = None dialog_state.user_profile.validated = True dialog_state.user_profile.is_demo = self.code_validation_payload.is_demo dialog_state.user_profile.account_info = self.code_validation_payload.account_info
def fetch_dialog_state(self, phone_number: str) -> DialogState: if phone_number in self.repo: state = DialogState(**json.loads(self.repo[phone_number])) return state else: return DialogState( phone_number=phone_number, seq="0", user_profile=UserProfile(validated=False, language=self.lang), )
def _check_response( self, dialog_state: DialogState, base_args: Dict[str, Any] ) -> Optional[List[stopcovid.dialog.models.events.DialogEvent]]: prompt = dialog_state.get_prompt() if prompt is None: return events = [] if prompt.should_advance_with_answer( self.content_lower, dialog_state.user_profile.language): events.append( CompletedPrompt( prompt=prompt, drill_instance_id=dialog_state. drill_instance_id, # type: ignore response=self.content, **base_args, )) should_advance = True else: should_advance = dialog_state.current_prompt_state.failures >= prompt.max_failures events.append( FailedPrompt( prompt=prompt, response=self.content, drill_instance_id=dialog_state. drill_instance_id, # type: ignore abandoned=should_advance, **base_args, )) if should_advance: next_prompt = dialog_state.get_next_prompt() if next_prompt is not None: events.append( AdvancedToNextPrompt( prompt=next_prompt, drill_instance_id=dialog_state. drill_instance_id, # type: ignore **base_args, )) if dialog_state.is_next_prompt_last(): # assume the last prompt doesn't wait for an answer events.append( DrillCompleted( drill_instance_id=dialog_state. drill_instance_id, # type: ignore **base_args, )) return events
def test_user_revalidated(self): profile = UserProfile(validated=True, is_demo=True) dialog_state = DialogState( "123456789", "0", user_profile=profile, current_drill=DRILL, drill_instance_id=uuid.uuid4(), current_prompt_state=PromptState(slug=DRILL.prompts[0].slug, start_time=NOW), ) event = UserValidated( phone_number="123456789", user_profile=profile, code_validation_payload=CodeValidationPayload( valid=True, is_demo=False, account_info={"foo": "bar"}), ) event.apply_to(dialog_state) self.assertTrue(dialog_state.user_profile.validated) self.assertFalse(dialog_state.user_profile.is_demo) self.assertIsNone(dialog_state.current_drill) self.assertIsNone(dialog_state.current_prompt_state) self.assertIsNone(dialog_state.drill_instance_id) self.assertEqual({"foo": "bar"}, dialog_state.user_profile.account_info)
def apply_to(self, dialog_state: DialogState) -> None: if self.abandoned: dialog_state.current_prompt_state = None else: assert dialog_state.current_prompt_state dialog_state.current_prompt_state.last_response_time = self.created_time dialog_state.current_prompt_state.failures += 1
def test_failed_and_not_abandoned(self): profile = UserProfile(validated=True) event = FailedPrompt( phone_number="123456789", user_profile=profile, prompt=DRILL.prompts[2], drill_instance_id=uuid.uuid4(), response="b", abandoned=False, ) dialog_state = DialogState( "123456789", seq="0", user_profile=profile, current_drill=DRILL, drill_instance_id=event.drill_instance_id, current_prompt_state=PromptState(DRILL.prompts[2].slug, start_time=NOW), ) event.apply_to(dialog_state) self.assertEqual( PromptState( slug=DRILL.prompts[2].slug, start_time=NOW, last_response_time=event.created_time, failures=1, ), dialog_state.current_prompt_state, )
def test_user_validated(self): profile = UserProfile(validated=False) dialog_state = DialogState(phone_number="123456789", seq="0", user_profile=profile) event = UserValidated( phone_number="123456789", user_profile=profile, code_validation_payload=CodeValidationPayload( valid=True, is_demo=False, account_info={ "employer_id": 1, "unit_id": 1, "employer_name": "employer_name", "unit_name": "unit_name", }, ), ) event.apply_to(dialog_state) self.assertTrue(dialog_state.user_profile.validated) self.assertEqual( { "employer_id": 1, "unit_id": 1, "employer_name": "employer_name", "unit_name": "unit_name", }, dialog_state.user_profile.account_info, )
def test_user_validation_failed(self): profile = UserProfile(validated=False) dialog_state = DialogState("123456789", "0", user_profile=profile) event = UserValidationFailed(phone_number="123456789", user_profile=profile) event.apply_to(dialog_state) self.assertFalse(dialog_state.user_profile.validated)
def apply_to(self, dialog_state: DialogState) -> None: # TODO: revisit this (pretty unfortunate) updating logic. Maybe we should always just # overwrite the dialog engine user profile data from scadmin, rather than having two # sources of truth? account_info = dialog_state.user_profile.account_info if "account_info" in self.user_profile_data: account_info = (account_info.copy( update=self.user_profile_data["account_info"]) if account_info else AccountInfo( **self.user_profile_data["account_info"])) dialog_state.user_profile = dialog_state.user_profile.copy( update=self.user_profile_data) dialog_state.user_profile.account_info = account_info if self.purge_drill_state: dialog_state.current_drill = None dialog_state.drill_instance_id = None dialog_state.current_prompt_state = None
def apply_to(self, dialog_state: DialogState) -> None: dialog_state.current_prompt_state = None if self.prompt.response_user_profile_key: setattr( dialog_state.user_profile, self.prompt.response_user_profile_key, self.response, )
def _next_drill_requested( self, dialog_state: DialogState, base_args: Dict[str, Any]) -> Optional[List[DialogEvent]]: prompt = dialog_state.get_prompt() if prompt is None: if self.content_lower in ["more", "mas", "más"]: return [NextDrillRequested(**base_args)] return None
def _advance_to_next_drill( self, dialog_state: DialogState, base_args: Dict[str, Any] ) -> Optional[List[stopcovid.dialog.models.events.DialogEvent]]: prompt = dialog_state.get_prompt() if prompt is None: if self.content_lower == "more": return [NextDrillRequested(**base_args)] return []
def test_save_and_fetch(self): dialog_state = self.repo.fetch_dialog_state(self.phone_number) self.assertEqual(self.phone_number, dialog_state.phone_number) event1 = CompletedPrompt( phone_number=self.phone_number, user_profile=UserProfile(validated=True), prompt=Prompt( slug="one", messages=[ PromptMessage(text="one"), PromptMessage(text="two") ], ), response="hi", drill_instance_id=uuid.uuid4(), ) event2 = AdvancedToNextPrompt( phone_number=self.phone_number, user_profile=UserProfile(validated=True), prompt=Prompt( slug="two", messages=[ PromptMessage(text="three"), PromptMessage(text="four") ], ), drill_instance_id=event1.drill_instance_id, ) dialog_state = DialogState( phone_number=self.phone_number, seq="0", user_profile=UserProfile(validated=True, language="de"), drill_instance_id=event1.drill_instance_id, ) batch = DialogEventBatch(phone_number=self.phone_number, events=[event1, event2], seq="216") self.repo.persist_dialog_state(batch, dialog_state) dialog_state2 = self.repo.fetch_dialog_state(self.phone_number) self.assertEqual(dialog_state.phone_number, dialog_state2.phone_number) self.assertEqual(dialog_state.user_profile.validated, dialog_state2.user_profile.validated) self.assertEqual(dialog_state.user_profile.language, dialog_state2.user_profile.language) batch_retrieved = self.repo.fetch_dialog_event_batch( self.phone_number, batch.batch_id) event1_retrieved = batch_retrieved.events[0] self.assertEqual(event1.response, event1_retrieved.response) # type: ignore event2_retrieved = batch_retrieved.events[1] self.assertEqual(event2.prompt.slug, event2_retrieved.prompt.slug) # type: ignore
def test_next_drill_requested(self): profile = UserProfile(validated=True, opted_out=True) event = NextDrillRequested("123456789", user_profile=profile, drill_instance_id=uuid.uuid4()) dialog_state = DialogState("123456789", seq="0", user_profile=profile) self.assertTrue(profile.opted_out) event.apply_to(dialog_state) self.assertFalse(profile.opted_out)
def test_opted_out_no_drill(self): profile = UserProfile(validated=True) event = OptedOut("123456789", user_profile=profile, drill_instance_id=None) dialog_state = DialogState("123456789", seq="0", user_profile=profile) self.assertFalse(profile.opted_out) event.apply_to(dialog_state) self.assertTrue(profile.opted_out) self.assertIsNone(dialog_state.drill_instance_id) self.assertIsNone(dialog_state.current_prompt_state) self.assertIsNone(dialog_state.current_drill)
def test_user_validated(self): profile = UserProfile(validated=False) dialog_state = DialogState("123456789", "0", user_profile=profile) event = UserValidated( phone_number="123456789", user_profile=profile, code_validation_payload=CodeValidationPayload( valid=True, is_demo=False, account_info={"foo": "bar"}), ) event.apply_to(dialog_state) self.assertTrue(dialog_state.user_profile.validated) self.assertEqual({"foo": "bar"}, dialog_state.user_profile.account_info)
def setUp(self) -> None: logging.disable(logging.CRITICAL) self.phone_number = "123456789" self.dialog_state = DialogState(phone_number=self.phone_number, seq="0") self.drill = DRILL self.repo = MagicMock() self.repo.fetch_dialog_state = MagicMock( return_value=self.dialog_state) self.repo.persist_dialog_state = MagicMock() self.next_seq = 1 self.now = datetime.datetime.now(datetime.timezone.utc) self.localization_patcher = patch("stopcovid.drills.drills.localize", return_value="translated") self.localization_patcher.start()
def test_drill_completed(self): profile = UserProfile(validated=False) event = DrillCompleted("123456789", user_profile=profile, drill_instance_id=uuid.uuid4()) dialog_state = DialogState( "123456789", seq="0", user_profile=profile, current_drill=DRILL, drill_instance_id=event.drill_instance_id, current_prompt_state=PromptState(DRILL.prompts[-1].slug, start_time=NOW), ) event.apply_to(dialog_state) self.assertIsNone(dialog_state.drill_instance_id) self.assertIsNone(dialog_state.current_prompt_state) self.assertIsNone(dialog_state.current_drill)
def test_completed_and_not_stored(self): profile = UserProfile(validated=True) event = CompletedPrompt( "123456789", user_profile=profile, prompt=DRILL.prompts[0], drill_instance_id=uuid.uuid4(), response="go", ) dialog_state = DialogState( "123456789", "0", user_profile=profile, current_drill=DRILL, current_prompt_state=PromptState(DRILL.prompts[0].slug, NOW), ) event.apply_to(dialog_state) self.assertEqual(profile, dialog_state.user_profile) self.assertIsNone(dialog_state.current_prompt_state)
def test_drill_started(self): profile = UserProfile(validated=True) event = DrillStarted( phone_number="123456789", user_profile=profile, drill=DRILL, first_prompt=DRILL.prompts[0], ) dialog_state = DialogState(phone_number="123456789", seq="0", user_profile=profile) event.apply_to(dialog_state) self.assertEqual(DRILL, dialog_state.current_drill) self.assertEqual( PromptState(slug=DRILL.prompts[0].slug, start_time=event.created_time), dialog_state.current_prompt_state, ) self.assertEqual(event.drill_instance_id, dialog_state.drill_instance_id)
def test_user_updated(self): phone_number = "123456789" profile = UserProfile(validated=True) event = UserUpdated( phone_number=phone_number, user_profile=profile, user_profile_data={"name": "Cat Stevens"}, purge_drill_state=True, ) dialog_state = DialogState( phone_number=phone_number, seq="0", user_profile=profile, drill_instance_id=uuid.uuid4(), current_drill=DRILL, ) event.apply_to(dialog_state) self.assertIsNone(dialog_state.current_drill) self.assertIsNone(dialog_state.drill_instance_id)
def test_opted_out_during_drill(self): profile = UserProfile(validated=True) event = OptedOut("123456789", user_profile=profile, drill_instance_id=uuid.uuid4()) dialog_state = DialogState( "123456789", seq="0", user_profile=profile, current_drill=DRILL, drill_instance_id=event.drill_instance_id, current_prompt_state=PromptState(DRILL.prompts[-1].slug, start_time=NOW), ) self.assertFalse(profile.opted_out) event.apply_to(dialog_state) self.assertTrue(profile.opted_out) self.assertIsNone(dialog_state.drill_instance_id) self.assertIsNone(dialog_state.current_prompt_state) self.assertIsNone(dialog_state.current_drill)
def test_completed_and_stored(self): profile = UserProfile(validated=True) event = CompletedPrompt( phone_number="123456789", user_profile=profile, prompt=DRILL.prompts[1], drill_instance_id=uuid.uuid4(), response="7", ) dialog_state = DialogState( phone_number="123456789", seq="0", user_profile=profile, current_drill=DRILL, current_prompt_state=PromptState(slug=DRILL.prompts[0].slug, start_time=NOW), ) event.apply_to(dialog_state) self.assertEqual(UserProfile(validated=True, self_rating_1="7"), dialog_state.user_profile) self.assertIsNone(dialog_state.current_prompt_state)
def test_advanced_to_next_prompt(self): profile = UserProfile(validated=True) event = AdvancedToNextPrompt( phone_number="123456789", user_profile=profile, prompt=DRILL.prompts[1], drill_instance_id=uuid.uuid4(), ) dialog_state = DialogState( "123456789", seq="0", user_profile=profile, current_drill=DRILL, drill_instance_id=event.drill_instance_id, current_prompt_state=PromptState(DRILL.prompts[0].slug, start_time=NOW), ) event.apply_to(dialog_state) self.assertEqual( PromptState(DRILL.prompts[1].slug, start_time=event.created_time), dialog_state.current_prompt_state, )
def setUp(self) -> None: logging.disable(logging.CRITICAL) self.drill_instance_id = uuid.UUID("11111111-1111-1111-1111-111111111111") self.phone_number = "123456789" self.dialog_state = DialogState( phone_number=self.phone_number, seq="0", drill_instance_id=self.drill_instance_id, ) self.drill = Drill( name="test-drill", slug="test-drill", prompts=[ Prompt(slug="ignore-response-1", messages=[PromptMessage(text="{{msg1}}")]), Prompt( slug="store-response", messages=[PromptMessage(text="{{msg1}}")], response_user_profile_key="self_rating_1", ), Prompt( slug="graded-response-1", messages=[PromptMessage(text="{{msg1}}")], correct_response="{{response1}}", ), Prompt( slug="graded-response-2", messages=[PromptMessage(text="{{msg1}}")], correct_response="{{response1}}", ), Prompt(slug="ignore-response-2", messages=[PromptMessage(text="{{msg1}}")]), ], ) self.repo = MagicMock() self.repo.fetch_dialog_state = MagicMock(return_value=self.dialog_state) self.repo.persist_dialog_state = MagicMock() self.next_seq = 1 self.now = datetime.now(UTC)
def _check_response( self, dialog_state: DialogState, base_args: Dict[str, Any]) -> Optional[List[DialogEvent]]: prompt = dialog_state.get_prompt() if prompt is None: return None events: List[DialogEvent] = [] if prompt.should_advance_with_answer(self.content_lower): user_profile_updates = None if prompt.response_user_profile_key: user_profile_updates = { prompt.response_user_profile_key: self.content } events.append( CompletedPrompt( prompt=prompt, drill_instance_id=dialog_state.drill_instance_id, response=self.content, user_profile_updates=user_profile_updates, **base_args, )) should_advance = True else: assert dialog_state.current_prompt_state should_advance = dialog_state.current_prompt_state.failures >= ( prompt.max_failures or 1) events.append( FailedPrompt( prompt=prompt, response=self.content or None, drill_instance_id=dialog_state.drill_instance_id, abandoned=should_advance, **base_args, )) if should_advance: assert dialog_state.current_drill next_prompt = dialog_state.get_next_prompt() if next_prompt is not None: events.append( AdvancedToNextPrompt( prompt=next_prompt, drill_instance_id=dialog_state.drill_instance_id, **base_args, )) if dialog_state.is_next_prompt_last(): # assume the last prompt doesn't wait for an answer events.append( DrillCompleted( drill_instance_id=dialog_state.drill_instance_id, **base_args, )) elif len(dialog_state.current_drill.prompts) == 1: events.append( DrillCompleted( drill_instance_id=dialog_state.drill_instance_id, last_prompt_response=self.content or None, **base_args, )) return events
def apply_to(self, dialog_state: DialogState) -> None: dialog_state.current_prompt_state = PromptState( slug=self.prompt.slug, start_time=self.created_time)
def apply_to(self, dialog_state: DialogState) -> None: dialog_state.current_drill = None dialog_state.drill_instance_id = None dialog_state.current_prompt_state = None
def apply_to(self, dialog_state: DialogState) -> None: dialog_state.current_drill = None dialog_state.drill_instance_id = None dialog_state.current_prompt_state = None dialog_state.user_profile.opted_out = False
def apply_to(self, dialog_state: DialogState) -> None: dialog_state.current_drill = self.drill dialog_state.drill_instance_id = self.drill_instance_id dialog_state.current_prompt_state = PromptState( slug=self.first_prompt.slug, start_time=self.created_time)