def _bkt(self) -> Bkt: pL = state_db.get(state_db.Keys.BKT_pL) pT = state_db.get(state_db.Keys.BKT_pT) pS = state_db.get(state_db.Keys.BKT_pS) pG = state_db.get(state_db.Keys.BKT_pG) return Bkt(pL0=pL, pT=pT, pS=pS, pG=pG)
class Messages: big_5_question = Message( content=("How do you feel about the following statement? " + "'{'var': 'big_5_question', 'index': " + "'{'db': '%s', 'post-op': 'increment'}'}'" % state_db.Keys.PSYCH_QUESTION_INDEX), options=[ 'Strongly agree', 'Agree', 'Neutral', 'disagree', 'Strongly disagree', ], message_type=Message.Type.MULTIPLE_CHOICE, result_db_key=state_db.Keys.PSYCH_QUESTION_ANSWERS, is_append_result=True, text_populator=text_populator, ) when_question = Message( content="{when_question}", options='is when', message_type=Message.Type.TIME_ENTRY, args=[ '15', lambda: "{'db': '%s'}" % state_db.Keys.WALK_TIME, ], result_db_key=state_db.Keys.WALK_TIME, result_convert_from_str_fn=lambda x: datetime.datetime.strptime( x, '%I:%M %p').time(), tests=lambda x: (state_db.get(state_db.Keys.AM_CHECKIN_TIME) <= x <= state_db.get(state_db.Keys.PM_CHECKIN_TIME)), error_message= "Please pick a time after now and before our evening checkin", text_populator=text_populator, ) set_goal = Message( content=("I suggest that you do {'db': '%s'} steps today. " % state_db.Keys.SUGGESTED_STEPS_TODAY + "How many steps would you like to do today?"), options='steps', args=[ lambda: "{'db': '%s'}" % state_db.Keys. MIN_SUGGESTED_STEPS_TODAY, lambda: "{'db': '%s'}" % state_db.Keys. MAX_SUGGESTED_STEPS_TODAY, '1', lambda: "{'db': '%s'}" % state_db.Keys.SUGGESTED_STEPS_TODAY, ], message_type=Message.Type.SLIDER, result_convert_from_str_fn=int, result_db_key=state_db.Keys.STEPS_GOAL, tests=lambda x: x >= state_db.get(state_db.Keys. MIN_SUGGESTED_STEPS_TODAY), error_message=( "Please select a goal that is at least {'db': '%s'} steps" % state_db.Keys.MIN_SUGGESTED_STEPS_TODAY, ), text_populator=text_populator, )
def test_check_first_run(self): self.assertFalse(state_db.is_set(state_db.Keys.FIRST_MEETING)) self.abm_interaction.set_prompt_to_handle() self.abm_interaction.run_scheduler_once() self.assertTrue(state_db.is_set(state_db.Keys.FIRST_MEETING)) self.assertTrue(state_db.get(state_db.Keys.IS_DONE_AM_CHECKIN_TODAY)) self.assertFalse(state_db.get(state_db.Keys.IS_DONE_PM_CHECKIN_TODAY))
def _build_checkin_schedule(self): am_checkin_time = state_db.get(state_db.Keys.AM_CHECKIN_TIME) pm_checkin_time = state_db.get(state_db.Keys.PM_CHECKIN_TIME) self._checkin_scheduler.clear() self._checkin_scheduler.every().day.at( self._time_to_schedule_str(am_checkin_time)).do( self._run_scheduled_if_still_open) self._checkin_scheduler.every().day.at( self._time_to_schedule_str(pm_checkin_time)).do( self._run_scheduled_if_still_open)
def test_prompt_interactions(self, _): """ Note that the steps goals and numbers get in a weird positive feedback loop because of how I monkey patched the fitbit reader """ num_days = param_db.get(param_db.Keys.WEEKS_WITH_ROBOT)*7 initial_datetime = datetime.datetime( year=2019, month=1, day=1, hour=8, minute=6, second=3 ) with freeze_time(initial_datetime) as frozen_datetime: state_db.reset() self.abm_interaction = AbmInteraction() self.update_after_first_meeting() pm_checkin_datetime = self.get_pm_checkin_datetime(0, initial_datetime) frozen_datetime.move_to(pm_checkin_datetime) self.update_day_steps() self.abm_interaction.set_prompt_to_handle() self.abm_interaction.run_scheduler_once() self.update_after_am_checkin() self.assertTrue(state_db.is_set(state_db.Keys.FIRST_MEETING)) self.assertTrue(state_db.get(state_db.Keys.IS_DONE_AM_CHECKIN_TODAY)) self.assertTrue(state_db.get(state_db.Keys.IS_DONE_PM_CHECKIN_TODAY)) self.assertTrue(state_db.get(state_db.Keys.IS_MET_GOAL)) for day in range(1, num_days + 2): am_checkin_datetime = self.get_am_checkin_datetime(day, initial_datetime) frozen_datetime.move_to(am_checkin_datetime) self.abm_interaction._new_day_update() self.abm_interaction.set_prompt_to_handle() self.abm_interaction.run_scheduler_once() self.update_after_am_checkin() # Run non consequential off-checkin for _ in range(2): self.abm_interaction.set_prompt_to_handle() self.abm_interaction.run_scheduler_once() self.assertTrue(state_db.get(state_db.Keys.IS_DONE_AM_CHECKIN_TODAY)) self.assertFalse(state_db.get(state_db.Keys.IS_DONE_PM_CHECKIN_TODAY)) pm_checkin_datetime = self.get_pm_checkin_datetime(day, initial_datetime) frozen_datetime.move_to(pm_checkin_datetime) self.update_day_steps() self.abm_interaction.set_prompt_to_handle() self.abm_interaction.run_scheduler_once() self.assertTrue(state_db.get(state_db.Keys.IS_DONE_AM_CHECKIN_TODAY)) self.assertTrue(state_db.get(state_db.Keys.IS_DONE_PM_CHECKIN_TODAY)) self.assertTrue(state_db.get(state_db.Keys.IS_MET_GOAL)) # Run non consequential off-checkin for _ in range(2): self.abm_interaction._build_and_run_plan()
def _new_day_update(self): last_day_update = state_db.get(state_db.Keys.LAST_DAY_UPDATE_DATE) current_date = datetime.datetime.now().date() days_since_last_day_update = (current_date - last_day_update).days if days_since_last_day_update > 0: self._update_week_steps_and_goals() if not state_db.get(state_db.Keys.IS_DONE_PM_CHECKIN_TODAY): state_db.set(state_db.Keys.IS_MISSED_PM_YESTERDAY, True) else: state_db.set(state_db.Keys.IS_MISSED_PM_YESTERDAY, False) self._publish_automaticity() state_db.set(state_db.Keys.IS_DONE_AM_CHECKIN_TODAY, False) state_db.set(state_db.Keys.IS_DONE_PM_CHECKIN_TODAY, False) state_db.set(state_db.Keys.LAST_DAY_UPDATE_DATE, datetime.datetime.now().date())
def get_am_checkin_datetime(day, initial_datetime): am_checkin_time = state_db.get(state_db.Keys.AM_CHECKIN_TIME) am_checkin_date_time = ( ( initial_datetime + datetime.timedelta(days=day) ).replace( hour=am_checkin_time.hour, minute=am_checkin_time.minute, ) ) return am_checkin_date_time
def test_bkt_operations(self): automaticity = self.builder._automaticity # True because of adding epsilon to make not degenerate self.assertLessEqual(state_db.get(state_db.Keys.BKT_pL), automaticity) old_pL = automaticity for _ in range(100): self.builder._bkt_update_pL(True) new_pL = self.builder._automaticity self.assertLessEqual(old_pL, new_pL) old_pL = new_pL self.assertLess(automaticity, old_pL)
def _build_off_checkin(self, planner=None): if planner is None: planner = MessagerPlanner(possible_plans) if self._is_time_for_status_update(): if self._is_synced_recently(): if state_db.get(state_db.Keys.STEPS_TODAY) >= state_db.get(state_db.Keys.STEPS_GOAL): planner.insert(OffCheckin.Messages.give_status_met_goal) else: planner.insert(OffCheckin.Messages.give_status) else: planner.insert(OffCheckin.Messages.no_sync) planner.insert( Options.options, post_hook=lambda: state_db.set( state_db.Keys.IS_REDO_SCHEDULE, True ) ) return planner
def update_after_first_meeting(self): state_db.set(state_db.Keys.FIRST_MEETING, datetime.datetime.now()) state_db.set(state_db.Keys.IS_DONE_AM_CHECKIN_TODAY, True) state_db.set(state_db.Keys.IS_DONE_PM_CHECKIN_TODAY, False) state_db.set(state_db.Keys.IS_MISSED_PM_YESTERDAY, False) state_db.set(state_db.Keys.AM_CHECKIN_TIME, datetime.time(8, 30)) state_db.set(state_db.Keys.PM_CHECKIN_TIME, datetime.time(20, 30)) state_db.set(state_db.Keys.DAY_OFF, 'saturday') state_db.set(state_db.Keys.USER_NAME, 'John') state_db.set(state_db.Keys.STEPS_GOAL, state_db.get(state_db.Keys.SUGGESTED_STEPS_TODAY)) self.abm_interaction._build_checkin_schedule() state_db.set(state_db.Keys.IS_REDO_SCHEDULE, False)
def _build_and_run_plan(self): plan = self._plan_builder.build() interaction_engine = InteractionEngine(self._interface, plan, possible_plans) try: self._publish_is_record_msg(True) interaction_engine.run() except TimeoutError: pass finally: self._publish_is_record_msg(False) if state_db.get(state_db.Keys.IS_REDO_SCHEDULE): self._build_checkin_schedule() state_db.set(state_db.Keys.IS_REDO_SCHEDULE, False)
def update_day_steps(self, steps_diff_from_goal=0): self.set_steps_per_day( state_db.get(state_db.Keys.STEPS_GOAL) + steps_diff_from_goal ) self.abm_interaction._update_todays_steps()
def update_after_am_checkin(self): state_db.set(state_db.Keys.STEPS_GOAL, state_db.get(state_db.Keys.SUGGESTED_STEPS_TODAY))
if __name__ == '__main__' and False: IS_RESET_STATE_DB = False interaction = AbmInteraction(is_reset_state_db=IS_RESET_STATE_DB) mins_before_allowed = param_db.get(param_db.Keys.MINS_BEFORE_ALLOW_CHECKIN) mins_after_allowed = param_db.get(param_db.Keys.MINS_AFTER_ALLOW_CHECKIN) print("FIRST INTERACTION") with freeze_time("2019-11-1 8:05:00"): interaction._build_and_run_plan() print("OFF CHECKIN") interaction._update_todays_steps() checkin_time = state_db.get(state_db.Keys.AM_CHECKIN_TIME) with freeze_time( f"2019-11-1 {checkin_time.hour:02d}:{checkin_time.minute:02d}:00"): interaction._build_and_run_plan() print("PM CHECKIN - Fail") interaction._update_todays_steps() state_db.set(state_db.Keys.STEPS_TODAY, state_db.get(state_db.Keys.STEPS_GOAL) - 1) checkin_time = state_db.get(state_db.Keys.PM_CHECKIN_TIME) with freeze_time( f"2019-11-1 {checkin_time.hour:02d}:{checkin_time.minute:02d}:00"): interaction._build_and_run_plan() print("OFF CHECKIN") checkin_time = state_db.get(state_db.Keys.PM_CHECKIN_TIME)
def _steps_today(self): return state_db.get(state_db.Keys.STEPS_TODAY)
def _goal_today(self): return state_db.get(state_db.Keys.STEPS_GOAL)
def _is_missed_pm_yesterday(self): return state_db.get(state_db.Keys.IS_MISSED_PM_YESTERDAY)
def _is_missed_am_checkin(self): return ( not state_db.get(state_db.Keys.IS_DONE_AM_CHECKIN_TODAY) and datetime.datetime.now() > self._am_checkin_datetime + datetime.timedelta(minutes=self._mins_after_checkin_allowed) )
def _is_done_pm_checkin_today(self): return state_db.get(state_db.Keys.IS_DONE_PM_CHECKIN_TODAY)
class AmCheckin: class Messages: big_5_question = Message( content=("How do you feel about the following statement? " + "'{'var': 'big_5_question', 'index': " + "'{'db': '%s', 'post-op': 'increment'}'}'" % state_db.Keys.PSYCH_QUESTION_INDEX), options=[ 'Strongly agree', 'Agree', 'Neutral', 'disagree', 'Strongly disagree', ], message_type=Message.Type.MULTIPLE_CHOICE, result_db_key=state_db.Keys.PSYCH_QUESTION_ANSWERS, is_append_result=True, text_populator=text_populator, ) when_question = Message( content="{when_question}", options='is when', message_type=Message.Type.TIME_ENTRY, args=[ '15', lambda: "{'db': '%s'}" % state_db.Keys.WALK_TIME, ], result_db_key=state_db.Keys.WALK_TIME, result_convert_from_str_fn=lambda x: datetime.datetime.strptime( x, '%I:%M %p').time(), tests=lambda x: (state_db.get(state_db.Keys.AM_CHECKIN_TIME) <= x <= state_db.get(state_db.Keys.PM_CHECKIN_TIME)), error_message= "Please pick a time after now and before our evening checkin", text_populator=text_populator, ) set_goal = Message( content=("I suggest that you do {'db': '%s'} steps today. " % state_db.Keys.SUGGESTED_STEPS_TODAY + "How many steps would you like to do today?"), options='steps', args=[ lambda: "{'db': '%s'}" % state_db.Keys. MIN_SUGGESTED_STEPS_TODAY, lambda: "{'db': '%s'}" % state_db.Keys. MAX_SUGGESTED_STEPS_TODAY, '1', lambda: "{'db': '%s'}" % state_db.Keys.SUGGESTED_STEPS_TODAY, ], message_type=Message.Type.SLIDER, result_convert_from_str_fn=int, result_db_key=state_db.Keys.STEPS_GOAL, tests=lambda x: x >= state_db.get(state_db.Keys. MIN_SUGGESTED_STEPS_TODAY), error_message=( "Please select a goal that is at least {'db': '%s'} steps" % state_db.Keys.MIN_SUGGESTED_STEPS_TODAY, ), text_populator=text_populator, ) where_graph = most_recent_options_graph( "ask where", "{where_question}", options=lambda: state_db.get(state_db.Keys.WALK_PLACES), max_num_options=param_db.get(param_db.Keys.NUM_OPTIONS_TO_DISPLAY), save_db_key=state_db.Keys.WALK_PLACES, text_populator=text_populator, new_entry_text_choice='Somewhere else', new_entry_message="So where will you walk?", new_entry_options="Is where", tests=lambda x: len(x) > 1, new_entry_error_message="Please write more than one letter") how_remember_graph = most_recent_options_graph( "how_remember", "{how_question_forget}", options=lambda: state_db.get(state_db.Keys.HOW_REMEMBER), max_num_options=param_db.get(param_db.Keys.NUM_OPTIONS_TO_DISPLAY), save_db_key=state_db.Keys.HOW_REMEMBER, text_populator=text_populator, new_entry_text_choice='Something else', tests=lambda x: len(x) > 1, new_entry_error_message="Please write more than one letter") how_busy_graph = most_recent_options_graph( "how_busy", "{how_question_busy}", options=lambda: state_db.get(state_db.Keys.HOW_BUSY), max_num_options=param_db.get(param_db.Keys.NUM_OPTIONS_TO_DISPLAY), save_db_key=state_db.Keys.HOW_BUSY, text_populator=text_populator, new_entry_text_choice='Something else', tests=lambda x: len(x) > 1, new_entry_error_message="Please write more than one letter") how_motivated_graph = most_recent_options_graph( "how_motivated", "{how_question_not_motivated}", options=lambda: state_db.get(state_db.Keys.HOW_MOTIVATE), max_num_options=param_db.get(param_db.Keys.NUM_OPTIONS_TO_DISPLAY), save_db_key=state_db.Keys.HOW_MOTIVATE, text_populator=text_populator, new_entry_text_choice='Something else', tests=lambda x: len(x) > 1, new_entry_error_message="Please write more than one letter")
def _publish_automaticity(self): automaticity = state_db.get(state_db.Keys.BKT_pL) rospy.loginfo("Publishing automaticity: {}".format(automaticity)) self._automaticity_publisher.publish(Float32(automaticity))
def _is_synced_recently(self): last_fitbit_sync = state_db.get(state_db.Keys.LAST_FITBIT_SYNC) max_mins_since_sync = param_db.get(param_db.Keys.MINS_BEFORE_WARNING_ABOUT_FITBIT_NOT_SYNCING) is_synced_recently = datetime.datetime.now() < last_fitbit_sync + datetime.timedelta( minutes=max_mins_since_sync) return is_synced_recently
def __init__( self, credentials_file_path="fitbit_credentials.yaml", redirect_url="http://localhost", is_data_recording_topic='data_capture/is_record', automaticity_topic='abm/automaticity', is_go_to_sleep_topic='cordial/sleep', interface=None, is_reset_state_db=False, goal_setter=None, ): if is_reset_state_db: logging.info("Reseting database") state_db.reset() self._plan_builder = PlanBuilder() if interface is None: interface = TerminalClientAndServerInterface(state_db) self._interface = interface self._fitbit = AbmFitbitClient( credentials_file_path=credentials_file_path, redirect_url=redirect_url, ) start_days_before_first_meeting = datetime.timedelta(days=7) if state_db.is_set(state_db.Keys.FIRST_MEETING): start_date = state_db.get( state_db.Keys.FIRST_MEETING) - start_days_before_first_meeting else: start_date = datetime.datetime.now( ) - start_days_before_first_meeting if goal_setter is None: logging.info('Creating goal setter') goal_setter = GoalSetter( fitbit_active_steps_fn=self._fitbit.get_total_steps, start_date=start_date, num_weeks=param_db.get(param_db.Keys.WEEKS_WITH_ROBOT) + 1, # +1 for week with just fitbit final_week_goal=param_db.get(param_db.Keys.FINAL_STEPS_GOAL), min_weekly_steps_goal=param_db.get( param_db.Keys.MIN_WEEKLY_STEPS_GOAL), week_goal_min_improvement_ratio=1.1, week_goal_max_improvement_ratio=2.0, daily_goal_min_to_max_ratio=2.5, ) self._goal_setter = goal_setter self._is_recording_publisher = rospy.Publisher(is_data_recording_topic, Bool, queue_size=1) self._automaticity_publisher = rospy.Publisher(automaticity_topic, Float32, queue_size=1) self._is_go_to_sleep_publisher = rospy.Publisher(is_go_to_sleep_topic, Bool, queue_size=1) self._update_week_steps_and_goals() self._update_todays_steps() self._checkin_scheduler = schedule.Scheduler() self._update_scheduler = schedule.Scheduler() self._update_scheduler.every(15).seconds.do(self._new_day_update) self._is_prompt_to_run = False if state_db.is_set(state_db.Keys.FIRST_MEETING): self._build_checkin_schedule() state_db.set(state_db.Keys.IS_REDO_SCHEDULE, False) else: self._init_vars()
def _pm_checkin_time(self): return state_db.get(state_db.Keys.PM_CHECKIN_TIME)
class FirstMeeting: class Messages: introduce_self = Message( content= ("*QT/hi*Nice to meet you, {'db': '%s'}. " % state_db.Keys.USER_NAME + "*QT/show_QT*My name is QT. I'm a robot made by LuxAI in Luxembourg, a country in Europe. " + "I'm going to try to help you walk more regularly. " + "I won't go on walks with you*wink*. " + "Instead, I'll ask you questions about your plans to walk and " + "keep track of your daily steps by wirelessly talking to your Fitbit watch. " ), message_type=Message.Type.MULTIPLE_CHOICE, text_populator=text_populator, options=['Sure thing!', 'Sure', 'No thanks'], ) explain_checkin = Message( content= ("To help you walk regularly, *QT/bored* we'll talk at least twice a day: " + "once in the morning and once in the evening. " + "In the morning, we'll set a walking goal for the day and I'll ask you some questions. " + "These questions will be about your plans to walk or about you. " + "In the evening, we'll review the day. If you don't meet your walking goal, I may ask you why. " ), options='Sounds good', message_type=Message.Type.MULTIPLE_CHOICE, text_populator=text_populator, ) explain_off_checkin = Message( content=( "You can also talk to me any other time. " + # "I'll be happy to see you. *nod* I can tell you jokes or tell you good things. " + # "You can ask me to introduce myself, if you'd like me to meet someone new. " + # "I can also explain how we'll work together again. " + "Lastly, you can change what I call you or when we have our morning and evening checkins. " ), options='Okay', message_type=Message.Type.MULTIPLE_CHOICE, text_populator=text_populator, ) explain_fitbit = Message( content= ("Now, I'll tell you about how I'll work with your Fitbit watch. " + "This part is important.*nod* " + "To know how much you walk, I'll talk to your Fitbit watch. " + "Unfortunately, this can take some time. " + "Since I want to give you credit for all the steps you do, " + "please try to have your watch sync with the phone before our evening checkin. " + "Can you try to do that?"), options=['Definitely', 'Alright', "I'll try"], message_type=Message.Type.MULTIPLE_CHOICE, text_populator=text_populator, ) first_meeting = DirectedGraph( name='first meeting', start_node='ask name', nodes=[ Node( name='ask name', message=Options.Messages.set_name, options='Okay', transitions='introduce self', ), Node(name='introduce self', message=Messages.introduce_self, options=['Nice to meet you', 'Sounds great', 'Okay'], transitions='can explain?'), Node( name='can explain?', content="Can I explain how we'll work together?", message_type=Message.Type.MULTIPLE_CHOICE, options=['Sure thing', 'Okay', 'No thanks'], transitions=[ 'explain checkins', 'explain checkins', 'set am checkin' ], text_populator=text_populator, ), Node( name='explain checkins', message=Messages.explain_checkin, transitions='explain off-checkins', ), Node( name='explain off-checkins', message=Messages.explain_off_checkin, transitions='explain fitbit', ), Node( name='explain fitbit', message=Messages.explain_fitbit, transitions='set am checkin', ), Node( name='set am checkin', message=Options.Messages.set_am_checkin, transitions='confirm am checkin', ), Node( name='confirm am checkin', message=Options.Messages.confirm_am_checkin, options=[ '{affirmative_button_response}', '{oops_button_response}' ], transitions=['set pm checkin', 'set am checkin'], ), Node( name='set pm checkin', message=Options.Messages.set_pm_checkin, transitions='confirm pm checkin', ), Node( name='confirm pm checkin', message=Options.Messages.confirm_pm_checkin, options=[ '{affirmative_button_response}', '{oops_button_response}' ], transitions=['set steps goal', 'set pm checkin'], ), # THIS NODE IS SKIPPED Node( name='set day off', message=Options.Messages.set_day_off, transitions='set steps goal', ), Node( name='confirm day off', message=Options.Messages.confirm_day_off, options=[ '{affirmative_button_response}', '{oops_button_response}' ], transitions=['set steps goal', 'set day off'], ), # --------------------- Node( name='set steps goal', content=( lambda: "The last thing to do is to set walking goals for today. " + "You did {'db': '%s'} steps last week. " % state_db.Keys. STEPS_LAST_WEEK + "To work towards the goal of %s steps in %s weeks, " % (param_db.get(param_db.Keys.FINAL_STEPS_GOAL), param_db.get(param_db.Keys.WEEKS_WITH_ROBOT) ) + "I suggest that you do {'db': '%s'} steps today. " % state_db.Keys.SUGGESTED_STEPS_TODAY + "How many steps would you like to do today?"), options='steps', message_type=Message.Type.SLIDER, args=[ lambda: "{'db': '%s'}" % state_db.Keys. MIN_SUGGESTED_STEPS_TODAY, lambda: "{'db': '%s'}" % state_db.Keys. MAX_SUGGESTED_STEPS_TODAY, '1', lambda: "{'db': '%s'}" % state_db.Keys. SUGGESTED_STEPS_TODAY, ], result_convert_from_str_fn=int, result_db_key=state_db.Keys.STEPS_GOAL, tests=lambda x: x >= state_db.get(state_db.Keys. MIN_SUGGESTED_STEPS_TODAY), error_message= "Please select a goal that is at least {'db': '%s'} steps" % state_db.Keys.MIN_SUGGESTED_STEPS_TODAY, text_populator=text_populator, transitions='set when walk', ), Node( name='set when walk', message=Message( content="{when_question}", options='is when', message_type=Message.Type.TIME_ENTRY, args=[ '15', lambda: (datetime.datetime.now() + datetime.timedelta( hours=1)).strftime("%H:%M"), ], result_db_key=state_db.Keys.WALK_TIME, result_convert_from_str_fn=lambda x: datetime.datetime. strptime(x, '%I:%M %p').time(), tests=lambda x: (state_db.get(state_db.Keys.AM_CHECKIN_TIME) <= x <= state_db.get(state_db.Keys.PM_CHECKIN_TIME)), error_message= "Please pick a time after now and before our evening checkin", text_populator=text_populator, ), transitions=['first closing'], ), Node(name='first closing', content=("*QT/bye*Alright, we're all setup! " + "I'll see you for checkin this evening!"), message_type=Message.Type.MULTIPLE_CHOICE, options=['Bye', 'Talk to you later'], transitions='exit') ], )