From 392263e48f9c266c85960fb7a65b2852677328de Mon Sep 17 00:00:00 2001 From: Igor Novikov Date: Wed, 21 Feb 2024 03:41:43 +0400 Subject: [PATCH] [#83, #84] Moved each InferenceStrategy out into its separate module --- .../persona/cognitive_modules/plan.py | 7 + .../persona/prompt_template/run_gpt_prompt.py | 417 ------------------ .../prompts/run_gpt_prompt_act_obj_desc.py | 45 ++ .../run_gpt_prompt_act_obj_event_triple.py | 111 +++++ .../prompts/run_gpt_prompt_action_sector.py | 82 ++++ .../prompts/run_gpt_prompt_daily_plan.py | 97 ++++ .../prompts/run_gpt_prompt_task_decomp.py | 136 ++++++ .../prompts/run_gpt_prompt_wake_up_hour.py | 41 ++ 8 files changed, 519 insertions(+), 417 deletions(-) create mode 100644 reverie/backend_server/persona/prompts/run_gpt_prompt_act_obj_desc.py create mode 100644 reverie/backend_server/persona/prompts/run_gpt_prompt_act_obj_event_triple.py create mode 100644 reverie/backend_server/persona/prompts/run_gpt_prompt_action_sector.py create mode 100644 reverie/backend_server/persona/prompts/run_gpt_prompt_daily_plan.py create mode 100644 reverie/backend_server/persona/prompts/run_gpt_prompt_task_decomp.py create mode 100644 reverie/backend_server/persona/prompts/run_gpt_prompt_wake_up_hour.py diff --git a/reverie/backend_server/persona/cognitive_modules/plan.py b/reverie/backend_server/persona/cognitive_modules/plan.py index a6cda2205e..5800c289d2 100644 --- a/reverie/backend_server/persona/cognitive_modules/plan.py +++ b/reverie/backend_server/persona/cognitive_modules/plan.py @@ -19,6 +19,13 @@ from persona.cognitive_modules.converse import * from persona.prompt_template.embedding import get_embedding from persona.common import HourlyScheduleItem, string_to_time +from persona.prompts.run_gpt_prompt_wake_up_hour import run_gpt_prompt_wake_up_hour +from persona.prompts.run_gpt_prompt_daily_plan import run_gpt_prompt_daily_plan +from persona.prompts.run_gpt_prompt_task_decomp import run_gpt_prompt_task_decomp +from persona.prompts.run_gpt_prompt_action_sector import run_gpt_prompt_action_sector +from persona.prompts.run_gpt_prompt_act_obj_desc import run_gpt_prompt_act_obj_desc +from persona.prompts.run_gpt_prompt_act_obj_event_triple import run_gpt_prompt_act_obj_event_triple + ############################################################################## # CHAPTER 2: Generate ############################################################################## diff --git a/reverie/backend_server/persona/prompt_template/run_gpt_prompt.py b/reverie/backend_server/persona/prompt_template/run_gpt_prompt.py index 0923d3c52e..828440546b 100644 --- a/reverie/backend_server/persona/prompt_template/run_gpt_prompt.py +++ b/reverie/backend_server/persona/prompt_template/run_gpt_prompt.py @@ -9,21 +9,12 @@ import datetime import sys import ast -import pprint -from typing import Any, Dict, Optional -from random import Random sys.path.append('../../') from global_methods import * from persona.prompt_template.gpt_structure import * from persona.prompt_template.print_prompt import * -from persona.common import HourlyScheduleItem, is_valid_time, string_to_time - -from persona.prompt_template.InferenceStrategySK import kernel, JSONType, ReturnType, functor, OutputType, InferenceStrategySK - -kernel.set_default_chat_service("strong") -skill = kernel.import_semantic_skill_from_directory("persona/prompt_template", "v4_sk") def get_random_alphanumeric(i=6, j=6): """ @@ -44,349 +35,6 @@ def get_random_alphanumeric(i=6, j=6): # CHAPTER 1: Run GPT Prompt ############################################################################## -@functor -class run_gpt_prompt_wake_up_hour(InferenceStrategySK): - # semantic_function = skill["wake_up_hour_v1"] - output_type = OutputType.JSON - config = { - "max_tokens": 15, - "temperature": 1, - "top_p": 0.8, - } - prompt = """ - {{$iss}} - - What will be the {{$lifestyle}} {{$firstname}}'s wake up time today? - - Provide the answer in a JSON format with only the actual time, without any reasoning or deliberation. Format the time in a 24-hour format like "H:mm" (H for hours, mm for minutes) and include it as the value for the "time" key in the JSON object. Do not include am/pm or any other information aside from the actual time. The answer should be in the following format: {"time": "H:mm"}. - - Example: - If the wake up time is 6:00 AM, the answer should be: {"time": "6:00"} - """ - def prepare_context(self, persona): - return { - "iss": persona.scratch.get_str_iss(), - "lifestyle": persona.scratch.get_str_lifestyle(), - "firstname": persona.scratch.get_str_firstname() - } - - def validate_json(self, json: JSONType): - if "time" not in json: - return "Missing time value" - if not is_valid_time(json['time'], require_am_pm=True) and not is_valid_time(json['time'], require_am_pm=False): - return "Invalid time format" - - def extract_json(self, json: JSONType): - return re.search(r"^\s*([012]?\d:\d\d)\b", json['time']).group(1) - - def fallback(self, persona): - return "08:00" - -""" -Basically the long term planning that spans a day. Returns a list of actions -that the persona will take today. Usually comes in the following form: -'wake up and complete the morning routine at 6:00 am', -'eat breakfast at 7:00 am',.. -Note that the actions come without a period. - -INPUT: - persona: The Persona class instance -OUTPUT: - a list of daily actions in broad strokes. -""" -@functor -class run_gpt_prompt_daily_plan(InferenceStrategySK): - # semantic_function = skill["daily_planning_v6"] - output_type = OutputType.JSON - config = { - "max_tokens": 1000, - "temperature": 1, - "top_p": 0.8, - } - prompt = """ - Let's consider {{$firstname}}: - - {{$commonset}} - - We need to draft a daily plan for {{$firstname}} in broad-strokes (with the time of the day. e.g., have a lunch at 12:00 pm, watch TV from 7 to 8 pm). The plan must be formatted as a single JSON array of objects, each object containing the following fields: - - * start: start time with am/pm - * end: end time with am/pm - * activity: the activity {{$firstname}} is performing, in plain text - - The entries must be in the correct order and must not intersect. The plan starts with waking up at {{$wake_up_hour}} and completing the morning routine, and it ends with going to sleep. What would be other items in the {{$firstname}}'s daily plan? - """ - - def prepare_context(self, persona, wake_up_hour): - return { - "commonset": persona.scratch.get_str_iss(), - "date": persona.scratch.get_str_curr_date_str(), - "firstname": persona.scratch.get_str_firstname(), - "wake_up_hour": f"{str(wake_up_hour)}:00 am" - } - - def validate_json(self, json: JSONType): - if not isinstance(json, list): - return "Invalid JSON format (expected a JSON array)" - if not all(isinstance(item, dict) and 'start' in item and 'end' in item and 'activity' in item for item in json): - return "Invalid JSON format (expected an array of objects with 'start', 'end' and 'activity' fields)" - wake_up_time = string_to_time(json[0]["start"]) - prev_time = None - prev_task = None - for item in json: - for field in ["start", "end"]: - if not is_valid_time(item[field]): - return f'Invalid {field} time format: "{item[field]}". Example time format: "6:00 am".' - time = string_to_time(item["start"]) - # For night owls, activities may continue past midnight and resume before the "wake-up" time. - # This condition allows for time entries after midnight but before the first entry's time, - # accommodating a schedule that doesn't strictly follow chronological order across days. - is_past_midnight = time < wake_up_time and prev_time > wake_up_time - if prev_time and time < prev_time and not is_past_midnight: - raise ValueError(f'Tasks are not in chronological order. "{prev_task}" intersects with "{item["activity"]}"') - prev_time = string_to_time(item["end"]) - prev_task = item["activity"] - - def extract_json(self, json: JSONType): - rng = Random(str(json)) - activities = ["Relax", "Rest", "Chill", "Procrastinate"] - result = [] - for i, item in enumerate(json): - if i != 0: - start = item['start'] - prev_end = json[i-1]['end'] - if string_to_time(start) != string_to_time(prev_end): - random_activity = rng.choice(activities) - result.append(f"{prev_end} - {random_activity}") - result.append(f"{item['start']} - {item['activity']}") - return result - # return [line for line in output.split('\n') if line.strip() and line[0].isdigit()] - - def fallback(self, persona, wake_up_hour): - return [ - '6:00 am - wake up and complete the morning routine', - '7:00 am - eat breakfast', - '8:00 am - read a book', - '12:00 pm - have lunch', - '1:00 pm - take a nap', - '4:00 pm - relax', - '7:00 pm - watch TV', - '8:00 pm - relax', - '11:00 pm - go to bed', - ] - -# A few shot decomposition of a task given the task description -# -# Persona state: identity stable set, curr_date_str, first_name -# -# INPUT: -# persona: The Persona class instance -# task: the description of the task at hand in str form -# (e.g., "waking up and starting her morning routine") -# duration: an integer that indicates the number of minutes this task is -# meant to last (e.g., 60) -# OUTPUT: -# a list of list where the inner list contains the decomposed task -# description and the number of minutes the task is supposed to last. -# EXAMPLE OUTPUT: -# [['going to the bathroom', 5], ['getting dressed', 5], -# ['eating breakfast', 15], ['checking her email', 5], -# ['getting her supplies ready for the day', 15], -# ['starting to work on her painting', 15]] -@functor -class run_gpt_prompt_task_decomp(InferenceStrategySK): - output_type = OutputType.JSON - config = { - "max_tokens": 1000, - "temperature": 0.5, - "top_p": 1, - } - prompt = """ - Let's perform task decomposition, breaking down a larger activity into smaller, manageable subtasks. Each subtask will be detailed in a JSON array, providing a structured and clear view of the task's components. The JSON object for each subtask will include the following fields: - - - i (or "index"): A sequential number representing the order of the subtask. - - action: A brief description of the subtask being performed. - - duration: The time allocated for this subtask, in minutes. - - timeLeft: The remaining time until the activity's completion, in minutes. - - Here's an example of how it can be done: - - Name: Kelly Bronson - Age: 35 - Backstory: Kelly always wanted to be a teacher, and now she teaches kindergarten. During the week, she dedicates herself to her students, but on the weekends, she likes to try out new restaurants and hang out with friends. She is very warm and friendly, and loves caring for others. - Personality: sweet, gentle, meticulous - Location: Kelly is in an older condo that has the following areas: {kitchen, bedroom, dining, porch, office, bathroom, living room, hallway}. - Currently: Kelly is a teacher during the school year. She teaches at the school but works on lesson plans at home. She is currently living alone in a single bedroom condo. - Daily plan requirement: Kelly is planning to teach during the morning and work from home in the afternoon. - - Today is Saturday May 10. From 08:00am ~ 09:00am, Kelly is planning on having breakfast, from 09:00am ~ 12:00pm, Kelly is planning on working on the next day's kindergarten lesson plan, and from 12:00 ~ 13pm, Kelly is planning on taking a break. - - Given the total duration of 180 minutes for this task, here's how Kelly's subtasks can be represented in a JSON array: - - [ - {"i": 1, "action": "Reviewing curriculum standards", "duration": 15, "timeLeft": 165}, - {"i": 2, "action": "Brainstorming lesson ideas", "duration": 30, "timeLeft": 135}, - {"i": 3, "action": "Creating the lesson plan", "duration": 30, "timeLeft": 105}, - {"i": 4, "action": "Creating materials for the lesson", "duration": 30, "timeLeft": 75}, - {"i": 5, "action": "Taking a short break", "duration": 15, "timeLeft": 60}, - {"i": 6, "action": "Reviewing the lesson plan", "duration": 30, "timeLeft": 30}, - {"i": 7, "action": "Making final adjustments to the lesson plan", "duration": 15, "timeLeft": 15}, - {"i": 8, "action": "Printing the lesson plan", "duration": 10, "timeLeft": 5}, - {"i": 9, "action": "Packing the lesson plan in her bag", "duration": 5, "timeLeft": 0} - ] - - Now, let's consider {{$firstname}}, who is about to perform the task "{{$task}}". - - {{$commonset}} - {{$surrounding_schedule}} - - In 5 min increments, list the subtasks {{$firstname}} does when performing the task "{{$task}}" from {{$time_range}} (total duration in minutes {{$duration}}) as a JSON array in the format specified. - """ - - def prepare_context(self, persona, schedule_item: HourlyScheduleItem): - # The complex part is producing the surrounding schedule. - # Here's an example: - # - # Today is Saturday June 25. From 00:00 ~ 06:00am, Maeve is - # planning on sleeping, 06:00 ~ 07:00am, Maeve is - # planning on waking up and doing her morning routine, - # and from 07:00am ~08:00am, Maeve is planning on having - # breakfast. - - self.schedule_item = schedule_item - firstname = persona.scratch.get_str_firstname() - schedule = persona.scratch.f_daily_schedule_hourly_org - schedule_item_index = schedule.index(schedule_item) - start_index = max(schedule_item_index - 1, 0) - end_index = min(schedule_item_index + 2, len(schedule) - 1) - surrounding_schedule_items = schedule[start_index:end_index] - - primary_time_range = None - summ_str = f'Today is {persona.scratch.curr_time.strftime("%B %d, %Y")}. ' - summ_str += f'From ' - for cur_item in surrounding_schedule_items: - start_time_str = self.minutes_to_time_string(cur_item.start_time) - end_time_str = self.minutes_to_time_string(cur_item.start_time + cur_item.duration) - cur_time_range = f'{start_time_str} ~ {end_time_str}' - summ_str += f'{cur_time_range}, {firstname} is planning on "{cur_item.task}", ' - if cur_item is schedule_item: - primary_time_range = f'{start_time_str} ~ {end_time_str}' - summ_str = summ_str[:-2] + "." - - return { - "commonset": persona.scratch.get_str_iss(), - "surrounding_schedule": summ_str, - "firstname": firstname, - "task": schedule_item.task, - "time_range": primary_time_range, - "duration": schedule_item.duration - } - - def validate_json(self, json: JSONType): - if not isinstance(json, list): - return "Invalid JSON format (expected a JSON array)" - if not all(isinstance(item, dict) and 'action' in item and 'duration' in item for item in json): - return "Invalid JSON format (expected an array of objects with 'action' and 'duration' fields)" - if not all(isinstance(item['duration'], (int, float)) for item in json): - return "Invalid JSON format (the 'duration' field must be a number)" - - def extract_json(self, json: JSONType): - total_duration = sum(subtask['duration'] for subtask in json) - expected_duration = self.schedule_item.duration - if total_duration != expected_duration: - adjustment_ratio = expected_duration / total_duration - return [[subtask['action'], int(subtask['duration'] * adjustment_ratio)] for subtask in json] - else: - return [[subtask['action'], subtask['duration']] for subtask in json] - - def fallback(self, persona, schedule_item): - return [[schedule_item.task, schedule_item.duration]] - - def minutes_to_time_string(self, minutes): - time = (datetime.datetime.strptime("00:00:00", "%H:%M:%S") - + datetime.timedelta(minutes=minutes)) - return time.strftime("%H:%M %p").lower() - -@functor -class run_gpt_prompt_action_sector(InferenceStrategySK): - output_type = OutputType.JSON - config = { - "temperature": 0.3, - } - prompt = """ - We need to choose an appropriate Sector for the task at hand. - - * Stay in the current sector if the activity can be done there. Only go out if the activity needs to take place in another place. - * Must be one of the sectors from "All Sectors," verbatim. It must be a Sector, and not an Arena. - * If none of those fit very well, we must still choose the one that's the closest fit. - * Return the answer as a JSON object with a single key "area". The value is the chosen area name. - - Sam Kim lives in the "Sam Kim's house" Sector that has the following Arenas: ["Sam Kim's room", "bathroom", "kitchen"] - Sam Kim is currently in the "Sam Kim's house" Sector that has the following Arenas: ["Sam Kim's room", "bathroom", "kitchen"] - All Sectors: ["Sam Kim's house", "The Rose and Crown Pub", "Hobbs Cafe", "Oak Hill College", "Johnson Park", "Harvey Oak Supply Store", "The Willows Market and Pharmacy"]. - For performing the "taking a walk" Action, Sam Kim should go to the following Sector: - {"area": "Johnson Park"} - --- - Jane Anderson lives in the "Oak Hill College Student Dormitory" Sector that has the following Arenas: ["Jane Anderson's room"] - Jane Anderson is currently in the "Oak Hill College" Sector that has the following Arenas: ["classroom", "library"] - All Sectors: ["Oak Hill College Student Dormitory", "The Rose and Crown Pub", "Hobbs Cafe", "Oak Hill College", "Johnson Park", "Harvey Oak Supply Store", "The Willows Market and Pharmacy"]. - For performing the "eating dinner" Action, Jane Anderson should go to the following Sector: - {"area": "Hobbs Cafe"} - --- - {{$name}} lives in the {{$living_sector}} Sector that has the following Arenas: {{$living_sector_arenas}}. - {{$name}} is currently in the {{$current_sector}} Sector that has the following Arenas: {{$current_sector_arenas}}. - All Sectors: {{$all_sectors}}. - Pick the Sector for performing {{$name}}'s current activity. - * Stay in the current sector if the activity can be done there. Only go out if the activity needs to take place in another place. - * Must be one of the sectors from "All Sectors," verbatim. It must be a Sector, and not an Arena. - * If none of those fit very well, we must still choose the one that's the closest fit. - * Return the answer as a JSON object with a single key "area". The value is the chosen area name. - For performing the {{$action_description}} Action, {{$name}} should go to the following Sector: - """ - - def prepare_context(self, action_description, persona, maze): - self.persona = persona - world_area = maze.access_tile(persona.scratch.curr_tile)['world'] - self.path_to_living_sector = persona.scratch.living_area.split(":")[:2] - self.path_to_current_sector = [ - world_area, - maze.access_tile(persona.scratch.curr_tile)['sector'], - ] - self.living_sector_arenas = persona.s_mem.get_array_accessible_sector_arenas( - ":".join(self.path_to_living_sector) - ) - self.current_sector_arenas = persona.s_mem.get_array_accessible_sector_arenas( - ":".join(self.path_to_current_sector) - ) - known_sectors = persona.s_mem.get_str_accessible_sectors(world_area).split(", ") - self.all_sectors = [sector for sector in known_sectors if "'s house" not in sector or persona.scratch.last_name in sector] - - return { - "name": persona.scratch.get_str_name(), - "action_description": json.dumps(action_description), - "living_sector": json.dumps(self.path_to_living_sector[1]), - "living_sector_arenas": json.dumps(self.living_sector_arenas), - "current_sector": json.dumps(self.path_to_current_sector[1]), - "current_sector_arenas": json.dumps(self.current_sector_arenas), - "all_sectors": json.dumps(self.all_sectors), - } - - def validate_json(self, json: JSONType): - if "area" not in json: - return "Missing area name" - if json["area"] not in self.all_sectors: - if json["area"] in self.living_sector_arenas or json["area"] in self.current_sector_arenas: - return "Arena name was returned instead of the Sector name" - else: - return f"Specified Sector doesn't exist or isn't available to {self.persona.scratch.get_str_firstname()}" - - def extract_json(self, json: JSONType): - return json["area"] - - def fallback(self, action_description, persona, maze): - return maze.access_tile(persona.scratch.curr_tile)['sector'] - - def run_gpt_prompt_action_arena(action_description, persona, maze, act_world, act_sector, @@ -719,71 +367,6 @@ def get_fail_safe(persona): - -@functor -class run_gpt_prompt_act_obj_desc(InferenceStrategySK): - semantic_function = skill['generate_obj_event_v1'] - output_type = OutputType.JSON - - def prepare_context(self, act_game_object: str, act_desp: str, persona) -> Dict[str, str]: - return { - "object_name": act_game_object, - "action_description": act_desp, - "firstname": persona.scratch.get_str_firstname(), - } - - def validate_json(self, json: JSONType) -> Optional[str]: - # Check for the required fields in the JSON object - required_fields = ["object", "user", "state"] - for field in required_fields: - if field not in json: - return f"Missing field: {field}" - # Check if the "object" field matches the lowercased object_name property - if json["object"].lower() != self.context_variables['object_name'].lower(): - return "Object name mismatch" - # Check if the "object" field matches the lowercased object_name property - if json["user"] != self.context_variables['firstname']: - return "Object name mismatch" - - def extract_json(self, json: JSONType) -> str: - return json['state'] - - def fallback(self, act_game_object: str, act_desp: str, persona) -> str: - return f'being used by {persona.scratch.get_str_firstname()}' - -@functor -class run_gpt_prompt_act_obj_event_triple(InferenceStrategySK): - semantic_function = skill['action_object_event_triple'] - output_type = OutputType.JSON - - def prepare_context(self, persona, task, act_obj_desc, object_name): - return { - "object_name": object_name, - "action_description": task, - "object_state": act_obj_desc, - "firstname": persona.scratch.get_str_firstname(), - } - - def validate_json(self, json: JSONType) -> Optional[str]: - # Check for the required fields in the JSON object - required_fields = ["object", "predicate", "interaction"] - for field in required_fields: - if field not in json: - return f"Missing field: {field}" - # Check if the "object" field matches the lowercased object_name property - if json["object"].lower() != self.context_variables['object_name'].lower(): - return "Object name mismatch" - - def extract_json(self, json: JSONType) -> ReturnType: - return (json["object"], json["predicate"], json["interaction"]) - - def fallback(self, persona, task, act_obj_desc, object_name): - return (object_name, "is", "idle") - - - - - def run_gpt_prompt_new_decomp_schedule(persona, main_act_dur, truncated_act_dur, diff --git a/reverie/backend_server/persona/prompts/run_gpt_prompt_act_obj_desc.py b/reverie/backend_server/persona/prompts/run_gpt_prompt_act_obj_desc.py new file mode 100644 index 0000000000..bc33df11b7 --- /dev/null +++ b/reverie/backend_server/persona/prompts/run_gpt_prompt_act_obj_desc.py @@ -0,0 +1,45 @@ +from typing import Dict, Optional + +from persona.prompt_template.InferenceStrategySK import JSONType, OutputType, functor, InferenceStrategySK + +@functor +class run_gpt_prompt_act_obj_desc(InferenceStrategySK): + output_type = OutputType.JSON + config = { + "max_tokens": 50, + "temperature": 0, + "top_p": 1, + } + prompt = """ + We want to write an object description and to understand the state of an object that is being used by someone. For example, if Jack is fixing the generator, the description would state: + + {"object":"generator","user":"Jack","state":"being fixed"} + + Now, let's consider {{$object_name}}. {{$firstname}} is currently performing the task "{{$action_description}}", interacting with the {{$object_name}}. Describe the interaction in the same form as above. + """ + + def prepare_context(self, act_game_object: str, act_desp: str, persona) -> Dict[str, str]: + return { + "object_name": act_game_object, + "action_description": act_desp, + "firstname": persona.scratch.get_str_firstname(), + } + + def validate_json(self, json: JSONType) -> Optional[str]: + # Check for the required fields in the JSON object + required_fields = ["object", "user", "state"] + for field in required_fields: + if field not in json: + return f"Missing field: {field}" + # Check if the "object" field matches the lowercased object_name property + if json["object"].lower() != self.context_variables['object_name'].lower(): + return "Object name mismatch" + # Check if the "object" field matches the lowercased object_name property + if json["user"] != self.context_variables['firstname']: + return "Object name mismatch" + + def extract_json(self, json: JSONType) -> str: + return json['state'] + + def fallback(self, act_game_object: str, act_desp: str, persona) -> str: + return f'being used by {persona.scratch.get_str_firstname()}' diff --git a/reverie/backend_server/persona/prompts/run_gpt_prompt_act_obj_event_triple.py b/reverie/backend_server/persona/prompts/run_gpt_prompt_act_obj_event_triple.py new file mode 100644 index 0000000000..7d87ac04ed --- /dev/null +++ b/reverie/backend_server/persona/prompts/run_gpt_prompt_act_obj_event_triple.py @@ -0,0 +1,111 @@ +from typing import Optional + +from persona.prompt_template.InferenceStrategySK import JSONType, ReturnType, OutputType, functor, InferenceStrategySK + +@functor +class run_gpt_prompt_act_obj_event_triple(InferenceStrategySK): + output_type = OutputType.JSON + config = { + "max_tokens": 50, + "temperature": 0.8, + "top_p": 0.95, + "top_k": 40, # not supported by SK + "min_p": 0.05, # not supported by SK + + } + prompt = """ + Transform natural language descriptions into structured JSON, focusing on the object, predicate, and specific status. The 'status' should reflect the primary action being performed with the object, described in a passive form, and should not include additional details unrelated to the action itself. Here are examples: + + Name: Sam + Action description: Sam Johnson is eating breakfast. + Object: table + Object state: clear with a plate of food and a cup of coffee + Output: { + "object": "table", + "predicate": "is", + "interaction": "being eaten on" + } + --- + Name: Joon + Action description: Joon Park is brewing coffee. + Object: coffee maker + Object state: simmering + Output: { + "object": "coffee maker", + "predicate": "is", + "interaction": "brewing coffee" + } + --- + Name: Jane + Action description: Jane Cook is sleeping. + Object: bed + Object state: supported Jane during her sleep + Output: { + "object": "bed", + "predicate": "is", + "interaction": "being slept in" + } + --- + Name: Michael + Action description: Michael Bernstein is writing email on a computer. + Object: computer + Object state: in use + Output: { + "object": "computer", + "predicate": "is", + "interaction": "being used to write email" + } + --- + Name: Percy + Action description: Percy Liang is teaching students in a classroom. + Object: classroom + Object state: filled with students learning + Output: { + "object": "classroom", + "predicate": "is", + "interaction": "being used for teaching" + } + --- + Name: Merrie + Action description: Merrie Morris is running on a treadmill. + Object: treadmill + Object state: in use + Output: { + "object": "treadmill", + "predicate": "is", + "interaction": "being run on" + } + + Now, for a new case: + + Name: {{$firstname}} + Action description: {{$action_description}} + Object: {{$object_name}} + Object state: {{$object_state}} + + Based on this description, provide a single JSON object in the format shown above. The "object" field must contain object name. Do not make the "status" a generic action, such as "being used", but find a more specific word clarifying how the {{$object_name}} is being used. In addition, exclude any extraneous details not directly related to this action. No intro nor Markdown, respond just with the JSON object. + """ + + def prepare_context(self, persona, task, act_obj_desc, object_name): + return { + "object_name": object_name, + "action_description": task, + "object_state": act_obj_desc, + "firstname": persona.scratch.get_str_firstname(), + } + + def validate_json(self, json: JSONType) -> Optional[str]: + # Check for the required fields in the JSON object + required_fields = ["object", "predicate", "interaction"] + for field in required_fields: + if field not in json: + return f"Missing field: {field}" + # Check if the "object" field matches the lowercased object_name property + if json["object"].lower() != self.context_variables['object_name'].lower(): + return "Object name mismatch" + + def extract_json(self, json: JSONType) -> ReturnType: + return (json["object"], json["predicate"], json["interaction"]) + + def fallback(self, persona, task, act_obj_desc, object_name): + return (object_name, "is", "idle") diff --git a/reverie/backend_server/persona/prompts/run_gpt_prompt_action_sector.py b/reverie/backend_server/persona/prompts/run_gpt_prompt_action_sector.py new file mode 100644 index 0000000000..fb5e6857c6 --- /dev/null +++ b/reverie/backend_server/persona/prompts/run_gpt_prompt_action_sector.py @@ -0,0 +1,82 @@ +import json + +from persona.prompt_template.InferenceStrategySK import JSONType, OutputType, functor, InferenceStrategySK + +@functor +class run_gpt_prompt_action_sector(InferenceStrategySK): + output_type = OutputType.JSON + config = { + "temperature": 0.3, + } + prompt = """ + We need to choose an appropriate Sector for the task at hand. + + * Stay in the current sector if the activity can be done there. Only go out if the activity needs to take place in another place. + * Must be one of the sectors from "All Sectors," verbatim. It must be a Sector, and not an Arena. + * If none of those fit very well, we must still choose the one that's the closest fit. + * Return the answer as a JSON object with a single key "area". The value is the chosen area name. + + Sam Kim lives in the "Sam Kim's house" Sector that has the following Arenas: ["Sam Kim's room", "bathroom", "kitchen"] + Sam Kim is currently in the "Sam Kim's house" Sector that has the following Arenas: ["Sam Kim's room", "bathroom", "kitchen"] + All Sectors: ["Sam Kim's house", "The Rose and Crown Pub", "Hobbs Cafe", "Oak Hill College", "Johnson Park", "Harvey Oak Supply Store", "The Willows Market and Pharmacy"]. + For performing the "taking a walk" Action, Sam Kim should go to the following Sector: + {"area": "Johnson Park"} + --- + Jane Anderson lives in the "Oak Hill College Student Dormitory" Sector that has the following Arenas: ["Jane Anderson's room"] + Jane Anderson is currently in the "Oak Hill College" Sector that has the following Arenas: ["classroom", "library"] + All Sectors: ["Oak Hill College Student Dormitory", "The Rose and Crown Pub", "Hobbs Cafe", "Oak Hill College", "Johnson Park", "Harvey Oak Supply Store", "The Willows Market and Pharmacy"]. + For performing the "eating dinner" Action, Jane Anderson should go to the following Sector: + {"area": "Hobbs Cafe"} + --- + {{$name}} lives in the {{$living_sector}} Sector that has the following Arenas: {{$living_sector_arenas}}. + {{$name}} is currently in the {{$current_sector}} Sector that has the following Arenas: {{$current_sector_arenas}}. + All Sectors: {{$all_sectors}}. + Pick the Sector for performing {{$name}}'s current activity. + * Stay in the current sector if the activity can be done there. Only go out if the activity needs to take place in another place. + * Must be one of the sectors from "All Sectors," verbatim. It must be a Sector, and not an Arena. + * If none of those fit very well, we must still choose the one that's the closest fit. + * Return the answer as a JSON object with a single key "area". The value is the chosen area name. + For performing the {{$action_description}} Action, {{$name}} should go to the following Sector: + """ + + def prepare_context(self, action_description, persona, maze): + self.persona = persona + world_area = maze.access_tile(persona.scratch.curr_tile)['world'] + self.path_to_living_sector = persona.scratch.living_area.split(":")[:2] + self.path_to_current_sector = [ + world_area, + maze.access_tile(persona.scratch.curr_tile)['sector'], + ] + self.living_sector_arenas = persona.s_mem.get_array_accessible_sector_arenas( + ":".join(self.path_to_living_sector) + ) + self.current_sector_arenas = persona.s_mem.get_array_accessible_sector_arenas( + ":".join(self.path_to_current_sector) + ) + known_sectors = persona.s_mem.get_str_accessible_sectors(world_area).split(", ") + self.all_sectors = [sector for sector in known_sectors if "'s house" not in sector or persona.scratch.last_name in sector] + + return { + "name": persona.scratch.get_str_name(), + "action_description": json.dumps(action_description), + "living_sector": json.dumps(self.path_to_living_sector[1]), + "living_sector_arenas": json.dumps(self.living_sector_arenas), + "current_sector": json.dumps(self.path_to_current_sector[1]), + "current_sector_arenas": json.dumps(self.current_sector_arenas), + "all_sectors": json.dumps(self.all_sectors), + } + + def validate_json(self, json: JSONType): + if "area" not in json: + return "Missing area name" + if json["area"] not in self.all_sectors: + if json["area"] in self.living_sector_arenas or json["area"] in self.current_sector_arenas: + return "Arena name was returned instead of the Sector name" + else: + return f"Specified Sector doesn't exist or isn't available to {self.persona.scratch.get_str_firstname()}" + + def extract_json(self, json: JSONType): + return json["area"] + + def fallback(self, action_description, persona, maze): + return maze.access_tile(persona.scratch.curr_tile)['sector'] diff --git a/reverie/backend_server/persona/prompts/run_gpt_prompt_daily_plan.py b/reverie/backend_server/persona/prompts/run_gpt_prompt_daily_plan.py new file mode 100644 index 0000000000..f305ad8c2c --- /dev/null +++ b/reverie/backend_server/persona/prompts/run_gpt_prompt_daily_plan.py @@ -0,0 +1,97 @@ +from random import Random + +from persona.common import is_valid_time, string_to_time +from persona.prompt_template.InferenceStrategySK import JSONType, OutputType, functor, InferenceStrategySK + +""" +Basically the long term planning that spans a day. Returns a list of actions +that the persona will take today. Usually comes in the following form: +'wake up and complete the morning routine at 6:00 am', +'eat breakfast at 7:00 am',.. +Note that the actions come without a period. + +INPUT: + persona: The Persona class instance +OUTPUT: + a list of daily actions in broad strokes. +""" +@functor +class run_gpt_prompt_daily_plan(InferenceStrategySK): + # semantic_function = skill["daily_planning_v6"] + output_type = OutputType.JSON + config = { + "max_tokens": 1000, + "temperature": 1, + "top_p": 0.8, + } + prompt = """ + Let's consider {{$firstname}}: + + {{$commonset}} + + We need to draft a daily plan for {{$firstname}} in broad-strokes (with the time of the day. e.g., have a lunch at 12:00 pm, watch TV from 7 to 8 pm). The plan must be formatted as a single JSON array of objects, each object containing the following fields: + + * start: start time with am/pm + * end: end time with am/pm + * activity: the activity {{$firstname}} is performing, in plain text + + The entries must be in the correct order and must not intersect. The plan starts with waking up at {{$wake_up_hour}} and completing the morning routine, and it ends with going to sleep. What would be other items in the {{$firstname}}'s daily plan? + """ + + def prepare_context(self, persona, wake_up_hour): + return { + "commonset": persona.scratch.get_str_iss(), + "date": persona.scratch.get_str_curr_date_str(), + "firstname": persona.scratch.get_str_firstname(), + "wake_up_hour": f"{str(wake_up_hour)}:00 am" + } + + def validate_json(self, json: JSONType): + if not isinstance(json, list): + return "Invalid JSON format (expected a JSON array)" + if not all(isinstance(item, dict) and 'start' in item and 'end' in item and 'activity' in item for item in json): + return "Invalid JSON format (expected an array of objects with 'start', 'end' and 'activity' fields)" + wake_up_time = string_to_time(json[0]["start"]) + prev_time = None + prev_task = None + for item in json: + for field in ["start", "end"]: + if not is_valid_time(item[field]): + return f'Invalid {field} time format: "{item[field]}". Example time format: "6:00 am".' + time = string_to_time(item["start"]) + # For night owls, activities may continue past midnight and resume before the "wake-up" time. + # This condition allows for time entries after midnight but before the first entry's time, + # accommodating a schedule that doesn't strictly follow chronological order across days. + is_past_midnight = time < wake_up_time and prev_time > wake_up_time + if prev_time and time < prev_time and not is_past_midnight: + raise ValueError(f'Tasks are not in chronological order. "{prev_task}" intersects with "{item["activity"]}"') + prev_time = string_to_time(item["end"]) + prev_task = item["activity"] + + def extract_json(self, json: JSONType): + rng = Random(str(json)) + activities = ["Relax", "Rest", "Chill", "Procrastinate"] + result = [] + for i, item in enumerate(json): + if i != 0: + start = item['start'] + prev_end = json[i-1]['end'] + if string_to_time(start) != string_to_time(prev_end): + random_activity = rng.choice(activities) + result.append(f"{prev_end} - {random_activity}") + result.append(f"{item['start']} - {item['activity']}") + return result + # return [line for line in output.split('\n') if line.strip() and line[0].isdigit()] + + def fallback(self, persona, wake_up_hour): + return [ + '6:00 am - wake up and complete the morning routine', + '7:00 am - eat breakfast', + '8:00 am - read a book', + '12:00 pm - have lunch', + '1:00 pm - take a nap', + '4:00 pm - relax', + '7:00 pm - watch TV', + '8:00 pm - relax', + '11:00 pm - go to bed', + ] diff --git a/reverie/backend_server/persona/prompts/run_gpt_prompt_task_decomp.py b/reverie/backend_server/persona/prompts/run_gpt_prompt_task_decomp.py new file mode 100644 index 0000000000..bba702b7cc --- /dev/null +++ b/reverie/backend_server/persona/prompts/run_gpt_prompt_task_decomp.py @@ -0,0 +1,136 @@ +import datetime + +from persona.common import HourlyScheduleItem +from persona.prompt_template.InferenceStrategySK import JSONType, OutputType, functor, InferenceStrategySK + +# A few shot decomposition of a task given the task description +# +# Persona state: identity stable set, curr_date_str, first_name +# +# INPUT: +# persona: The Persona class instance +# task: the description of the task at hand in str form +# (e.g., "waking up and starting her morning routine") +# duration: an integer that indicates the number of minutes this task is +# meant to last (e.g., 60) +# OUTPUT: +# a list of list where the inner list contains the decomposed task +# description and the number of minutes the task is supposed to last. +# EXAMPLE OUTPUT: +# [['going to the bathroom', 5], ['getting dressed', 5], +# ['eating breakfast', 15], ['checking her email', 5], +# ['getting her supplies ready for the day', 15], +# ['starting to work on her painting', 15]] +@functor +class run_gpt_prompt_task_decomp(InferenceStrategySK): + output_type = OutputType.JSON + config = { + "max_tokens": 1000, + "temperature": 0.5, + "top_p": 1, + } + prompt = """ + Let's perform task decomposition, breaking down a larger activity into smaller, manageable subtasks. Each subtask will be detailed in a JSON array, providing a structured and clear view of the task's components. The JSON object for each subtask will include the following fields: + + - i (or "index"): A sequential number representing the order of the subtask. + - action: A brief description of the subtask being performed. + - duration: The time allocated for this subtask, in minutes. + - timeLeft: The remaining time until the activity's completion, in minutes. + + Here's an example of how it can be done: + + Name: Kelly Bronson + Age: 35 + Backstory: Kelly always wanted to be a teacher, and now she teaches kindergarten. During the week, she dedicates herself to her students, but on the weekends, she likes to try out new restaurants and hang out with friends. She is very warm and friendly, and loves caring for others. + Personality: sweet, gentle, meticulous + Location: Kelly is in an older condo that has the following areas: {kitchen, bedroom, dining, porch, office, bathroom, living room, hallway}. + Currently: Kelly is a teacher during the school year. She teaches at the school but works on lesson plans at home. She is currently living alone in a single bedroom condo. + Daily plan requirement: Kelly is planning to teach during the morning and work from home in the afternoon. + + Today is Saturday May 10. From 08:00am ~ 09:00am, Kelly is planning on having breakfast, from 09:00am ~ 12:00pm, Kelly is planning on working on the next day's kindergarten lesson plan, and from 12:00 ~ 13pm, Kelly is planning on taking a break. + + Given the total duration of 180 minutes for this task, here's how Kelly's subtasks can be represented in a JSON array: + + [ + {"i": 1, "action": "Reviewing curriculum standards", "duration": 15, "timeLeft": 165}, + {"i": 2, "action": "Brainstorming lesson ideas", "duration": 30, "timeLeft": 135}, + {"i": 3, "action": "Creating the lesson plan", "duration": 30, "timeLeft": 105}, + {"i": 4, "action": "Creating materials for the lesson", "duration": 30, "timeLeft": 75}, + {"i": 5, "action": "Taking a short break", "duration": 15, "timeLeft": 60}, + {"i": 6, "action": "Reviewing the lesson plan", "duration": 30, "timeLeft": 30}, + {"i": 7, "action": "Making final adjustments to the lesson plan", "duration": 15, "timeLeft": 15}, + {"i": 8, "action": "Printing the lesson plan", "duration": 10, "timeLeft": 5}, + {"i": 9, "action": "Packing the lesson plan in her bag", "duration": 5, "timeLeft": 0} + ] + + Now, let's consider {{$firstname}}, who is about to perform the task "{{$task}}". + + {{$commonset}} + {{$surrounding_schedule}} + + In 5 min increments, list the subtasks {{$firstname}} does when performing the task "{{$task}}" from {{$time_range}} (total duration in minutes {{$duration}}) as a JSON array in the format specified. + """ + + def prepare_context(self, persona, schedule_item: HourlyScheduleItem): + # The complex part is producing the surrounding schedule. + # Here's an example: + # + # Today is Saturday June 25. From 00:00 ~ 06:00am, Maeve is + # planning on sleeping, 06:00 ~ 07:00am, Maeve is + # planning on waking up and doing her morning routine, + # and from 07:00am ~08:00am, Maeve is planning on having + # breakfast. + + self.schedule_item = schedule_item + firstname = persona.scratch.get_str_firstname() + schedule = persona.scratch.f_daily_schedule_hourly_org + schedule_item_index = schedule.index(schedule_item) + start_index = max(schedule_item_index - 1, 0) + end_index = min(schedule_item_index + 2, len(schedule) - 1) + surrounding_schedule_items = schedule[start_index:end_index] + + primary_time_range = None + summ_str = f'Today is {persona.scratch.curr_time.strftime("%B %d, %Y")}. ' + summ_str += f'From ' + for cur_item in surrounding_schedule_items: + start_time_str = self.minutes_to_time_string(cur_item.start_time) + end_time_str = self.minutes_to_time_string(cur_item.start_time + cur_item.duration) + cur_time_range = f'{start_time_str} ~ {end_time_str}' + summ_str += f'{cur_time_range}, {firstname} is planning on "{cur_item.task}", ' + if cur_item is schedule_item: + primary_time_range = f'{start_time_str} ~ {end_time_str}' + summ_str = summ_str[:-2] + "." + + return { + "commonset": persona.scratch.get_str_iss(), + "surrounding_schedule": summ_str, + "firstname": firstname, + "task": schedule_item.task, + "time_range": primary_time_range, + "duration": schedule_item.duration + } + + def validate_json(self, json: JSONType): + if not isinstance(json, list): + return "Invalid JSON format (expected a JSON array)" + if not all(isinstance(item, dict) and 'action' in item and 'duration' in item for item in json): + return "Invalid JSON format (expected an array of objects with 'action' and 'duration' fields)" + if not all(isinstance(item['duration'], (int, float)) for item in json): + return "Invalid JSON format (the 'duration' field must be a number)" + + def extract_json(self, json: JSONType): + total_duration = sum(subtask['duration'] for subtask in json) + expected_duration = self.schedule_item.duration + if total_duration != expected_duration: + adjustment_ratio = expected_duration / total_duration + return [[subtask['action'], int(subtask['duration'] * adjustment_ratio)] for subtask in json] + else: + return [[subtask['action'], subtask['duration']] for subtask in json] + + def fallback(self, persona, schedule_item): + return [[schedule_item.task, schedule_item.duration]] + + def minutes_to_time_string(self, minutes): + time = (datetime.datetime.strptime("00:00:00", "%H:%M:%S") + + datetime.timedelta(minutes=minutes)) + return time.strftime("%H:%M %p").lower() diff --git a/reverie/backend_server/persona/prompts/run_gpt_prompt_wake_up_hour.py b/reverie/backend_server/persona/prompts/run_gpt_prompt_wake_up_hour.py new file mode 100644 index 0000000000..8a3b6e3834 --- /dev/null +++ b/reverie/backend_server/persona/prompts/run_gpt_prompt_wake_up_hour.py @@ -0,0 +1,41 @@ +import re + +from persona.common import is_valid_time +from persona.prompt_template.InferenceStrategySK import JSONType, OutputType, functor, InferenceStrategySK + +@functor +class run_gpt_prompt_wake_up_hour(InferenceStrategySK): + output_type = OutputType.JSON + config = { + "max_tokens": 15, + "temperature": 1, + "top_p": 0.8, + } + prompt = """ + {{$iss}} + + What will be the {{$lifestyle}} {{$firstname}}'s wake up time today? + + Provide the answer in a JSON format with only the actual time, without any reasoning or deliberation. Format the time in a 24-hour format like "H:mm" (H for hours, mm for minutes) and include it as the value for the "time" key in the JSON object. Do not include am/pm or any other information aside from the actual time. The answer should be in the following format: {"time": "H:mm"}. + + Example: + If the wake up time is 6:00 AM, the answer should be: {"time": "6:00"} + """ + def prepare_context(self, persona): + return { + "iss": persona.scratch.get_str_iss(), + "lifestyle": persona.scratch.get_str_lifestyle(), + "firstname": persona.scratch.get_str_firstname() + } + + def validate_json(self, json: JSONType): + if "time" not in json: + return "Missing time value" + if not is_valid_time(json['time'], require_am_pm=True) and not is_valid_time(json['time'], require_am_pm=False): + return "Invalid time format" + + def extract_json(self, json: JSONType): + return re.search(r"^\s*([012]?\d:\d\d)\b", json['time']).group(1) + + def fallback(self, persona): + return "08:00"