def setUp(self) -> None: fitbit_patcher = mock.patch('abm_grant_interaction.abm_interaction.AbmFitbitClient') self._mock_fitbit = fitbit_patcher.start() self._mock_fitbit.return_value.get_last_sync.side_effect = lambda: datetime.datetime.now() self.set_steps_per_day(300) run_once_patcher = mock.patch('abm_grant_interaction.abm_interaction.AbmInteraction._build_and_run_plan') self._mock_run_once = run_once_patcher.start() self._mock_run_once.side_effect = self.build_and_simulate_plan state_db.reset() self.abm_interaction = AbmInteraction() self.mins_before_allowed = param_db.get(param_db.Keys.MINS_BEFORE_ALLOW_CHECKIN) self.mins_after_allowed = param_db.get(param_db.Keys.MINS_AFTER_ALLOW_CHECKIN)
def test_is_missed_am(self): am_checkin_hour = 8 am_checkin_min = 40 state_db.set(state_db.Keys.AM_CHECKIN_TIME, datetime.time(am_checkin_hour, am_checkin_min)) for hour, minute in [ (0, 0), (am_checkin_hour - 1 % 24, am_checkin_min % 60), (am_checkin_hour % 24, am_checkin_min - 1 % 60), (am_checkin_hour % 24, am_checkin_min % 60), (am_checkin_hour % 24, am_checkin_min + 1 % 60), ]: with freeze_time(f"2011-11-1 {hour:02d}:{minute:02d}:59"): state_db.set(state_db.Keys.IS_DONE_AM_CHECKIN_TODAY, False) self.assertFalse(self.builder._is_missed_am_checkin) time_after_allowed = param_db.get( param_db.Keys.MINS_AFTER_ALLOW_CHECKIN) for hour, minute in [ (am_checkin_hour % 24, am_checkin_min + 1 % 60), (am_checkin_hour + 1 % 24, am_checkin_min % 60), (am_checkin_hour + 1 % 24, am_checkin_min + 1 % 60), ]: with freeze_time(f"2011-11-1 {hour:02d}:{minute:02d}:59" ) as frozen_datetime: frozen_datetime.tick(delta=datetime.timedelta( minutes=time_after_allowed)) state_db.set(state_db.Keys.IS_DONE_AM_CHECKIN_TODAY, False) self.assertTrue(self.builder._is_missed_am_checkin)
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 _max_ii_questions(self): return param_db.get(param_db.Keys.MAX_NUM_QUESTIONS)
def _mins_after_checkin_allowed(self): return param_db.get(param_db.Keys.MINS_AFTER_ALLOW_CHECKIN)
def _mins_before_checkin_allowed(self): return param_db.get(param_db.Keys.MINS_BEFORE_ALLOW_CHECKIN)
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
from interaction_engine import InteractionEngine from interaction_engine.planner import MessagerPlanner from interaction_engine.interfaces import TerminalClientAndServerInterface possible_plans = [ PmCheckin.Messages.no_sync, PmCheckin.success_graph, PmCheckin.fail_graph ] steps_today = 300 steps_goal = 600 last_sync = ( datetime.datetime.now() - datetime.timedelta(minutes=param_db.get(param_db.Keys.MINS_BEFORE_WARNING_ABOUT_FITBIT_NOT_SYNCING) + 1) ) state_db.set(state_db.Keys.LAST_FITBIT_SYNC, last_sync) state_db.set(state_db.Keys.STEPS_TODAY, steps_today) state_db.set(state_db.Keys.STEPS_GOAL, steps_goal) # Create a plan plan_ = MessagerPlanner(possible_plans) if (datetime.datetime.now() - last_sync).seconds // 60 > \ param_db.get(param_db.Keys.MINS_BEFORE_WARNING_ABOUT_FITBIT_NOT_SYNCING): plan_.insert(PmCheckin.Messages.no_sync) else: if steps_goal <= steps_today: plan_.insert(PmCheckin.success_graph) else:
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()
logging.info('Updated steps this weeky') state_db.set(state_db.Keys.MIN_SUGGESTED_STEPS_TODAY, round(day_goal)) state_db.set(state_db.Keys.SUGGESTED_STEPS_TODAY, round(1.3 * day_goal)) state_db.set(state_db.Keys.MAX_SUGGESTED_STEPS_TODAY, round(2 * day_goal)) state_db.set(state_db.Keys.STEPS_LAST_WEEK, steps_last_week) state_db.set(state_db.Keys.STEPS_THIS_WEEK, steps_this_week) 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()
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")
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') ], )