From 02a6afcdeb0ca3990de2f9d33767ef94f3f1c61c Mon Sep 17 00:00:00 2001 From: Matt Barker <105945282+m-barker@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:42:26 +0100 Subject: [PATCH] merge: gpsr_command_parser (#149) * fix: incorrect remapping keys for handover * fix: remove speech server in launch file * feat: initial copy of @insertish 's script * data: add mock data jsons from competition template * feat: class to load gpsr known data into lists of strings as required for parsing * feat: add state machine for showcasing command parsing --- skills/src/lasr_skills/__init__.py | 3 +- tasks/gpsr/CMakeLists.txt | 1 + tasks/gpsr/data/.gitkeep | 0 tasks/gpsr/data/mock_data/locations.json | 102 +++++ tasks/gpsr/data/mock_data/names.json | 14 + tasks/gpsr/data/mock_data/objects.json | 76 ++++ tasks/gpsr/data/mock_data/rooms.json | 9 + tasks/gpsr/nodes/command_parser | 172 +++++++++ tasks/gpsr/src/gpsr/load_known_data.py | 163 ++++++++ tasks/gpsr/src/gpsr/regex_command_parser.py | 392 ++++++++++++++++++++ 10 files changed, 931 insertions(+), 1 deletion(-) create mode 100644 tasks/gpsr/data/.gitkeep create mode 100644 tasks/gpsr/data/mock_data/locations.json create mode 100644 tasks/gpsr/data/mock_data/names.json create mode 100644 tasks/gpsr/data/mock_data/objects.json create mode 100644 tasks/gpsr/data/mock_data/rooms.json create mode 100644 tasks/gpsr/nodes/command_parser create mode 100644 tasks/gpsr/src/gpsr/load_known_data.py create mode 100644 tasks/gpsr/src/gpsr/regex_command_parser.py diff --git a/skills/src/lasr_skills/__init__.py b/skills/src/lasr_skills/__init__.py index 4593f52fd..7875e7eca 100755 --- a/skills/src/lasr_skills/__init__.py +++ b/skills/src/lasr_skills/__init__.py @@ -12,4 +12,5 @@ from .listen_for import ListenFor from .play_motion import PlayMotion from .receive_object import ReceiveObject -from .handover_object import HandoverObject \ No newline at end of file +from .handover_object import HandoverObject +from .ask_and_listen import AskAndListen diff --git a/tasks/gpsr/CMakeLists.txt b/tasks/gpsr/CMakeLists.txt index b8c8a175f..af1f6c908 100644 --- a/tasks/gpsr/CMakeLists.txt +++ b/tasks/gpsr/CMakeLists.txt @@ -156,6 +156,7 @@ include_directories( catkin_install_python(PROGRAMS scripts/parse_gpsr_xmls.py nodes/commands/question_answer + nodes/command_parser DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} ) diff --git a/tasks/gpsr/data/.gitkeep b/tasks/gpsr/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tasks/gpsr/data/mock_data/locations.json b/tasks/gpsr/data/mock_data/locations.json new file mode 100644 index 000000000..4ec74e87c --- /dev/null +++ b/tasks/gpsr/data/mock_data/locations.json @@ -0,0 +1,102 @@ +{ + "bed": { + "placeable": true, + "object_category": null + }, + "bedside table": { + "placeable": true, + "object_category": null + }, + "shelf": { + "placeable": true, + "object_category": "cleaning supplies" + }, + "trashbin": { + "placeable": false, + "object_category": null + }, + "dishwasher": { + "placeable": true, + "object_category": null + }, + "potted plant": { + "placeable": false, + "object_category": null + }, + "kitchen table": { + "placeable": true, + "object_category": "dishes" + }, + "chairs": { + "placeable": false, + "object_category": null + }, + "pantry": { + "placeable": true, + "object_category": "food" + }, + "refigerator": { + "placeable": true, + "object_category": null + }, + "sink": { + "placeable": true, + "object_category": null + }, + "cabinet": { + "placeable": true, + "object_category": "drinks" + }, + "coatrack": { + "placeable": false, + "object_category": null + }, + "desk": { + "placeable": true, + "object_category": "fruits" + }, + "armchair": { + "placeable": false, + "object_category": null + }, + "desk lamp": { + "placeable": false, + "object_category": null + }, + "waste basket": { + "placeable": false, + "object_category": null + }, + "tv stand": { + "placeable": true, + "object_category": null + }, + "storage rack": { + "placeable": true, + "object_category": null + }, + "lamp": { + "placeable": false, + "object_category": null + }, + "side tables": { + "placeable": true, + "object_category": "snacks" + }, + "sofa": { + "placeable": true, + "object_category": null + }, + "bookshelf": { + "placeable": true, + "object_category": "toys" + }, + "entrance": { + "placeable": false, + "object_category": null + }, + "exit": { + "placeable": false, + "object_category": null + } +} \ No newline at end of file diff --git a/tasks/gpsr/data/mock_data/names.json b/tasks/gpsr/data/mock_data/names.json new file mode 100644 index 000000000..b388fee75 --- /dev/null +++ b/tasks/gpsr/data/mock_data/names.json @@ -0,0 +1,14 @@ +{ + "names": [ + "Adel", + "Angel", + "Axel", + "Charlie", + "Janes", + "Jules", + "Morgan", + "Paris", + "Robin", + "Simone" + ] +} \ No newline at end of file diff --git a/tasks/gpsr/data/mock_data/objects.json b/tasks/gpsr/data/mock_data/objects.json new file mode 100644 index 000000000..fec3bc340 --- /dev/null +++ b/tasks/gpsr/data/mock_data/objects.json @@ -0,0 +1,76 @@ +{ + "drinks": { + "items": [ + "orange juice", + "red wine", + "milk", + "iced tea", + "cola", + "tropical juice", + "juice pack" + ], + "singular": "drink" + }, + "fruits": { + "items": [ + "apple", + "pear", + "lemon", + "peach", + "banana", + "strawberry", + "orange", + "plum" + ], + "singular": "fruit" + }, + "snacks": { + "items": [ + "cheezit", + "cornflakes", + "pringles" + ], + "singular": "snack" + }, + "foods": { + "items": [ + "tuna", + "sugar", + "strawberry jello", + "tomato soup", + "mustard", + "chocolate jello", + "spam", + "coffee grounds" + ], + "singular": "food" + }, + "dishes": { + "items": [ + "plate", + "fork", + "spoon", + "cup", + "knife", + "bowl" + ], + "singular": "dish" + }, + "toys": { + "items": [ + "rubiks cube", + "soccer ball", + "dice", + "tennis ball", + "baseball" + ], + "singular": "toy" + }, + "cleaning supplies": { + "items": [ + "cleanser", + "sponge" + ], + "singular": "cleaning supply" + } +} \ No newline at end of file diff --git a/tasks/gpsr/data/mock_data/rooms.json b/tasks/gpsr/data/mock_data/rooms.json new file mode 100644 index 000000000..c880dac20 --- /dev/null +++ b/tasks/gpsr/data/mock_data/rooms.json @@ -0,0 +1,9 @@ +{ + "rooms": [ + "bedroom", + "kitchen", + "office", + "living room", + "bedroom" + ] +} \ No newline at end of file diff --git a/tasks/gpsr/nodes/command_parser b/tasks/gpsr/nodes/command_parser new file mode 100644 index 000000000..cd8c59341 --- /dev/null +++ b/tasks/gpsr/nodes/command_parser @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +import argparse +import smach +import rospy +from typing import Dict + +from gpsr.load_known_data import GPSRDataLoader +from gpsr.regex_command_parser import Configuration, gpsr_compile_and_parse +from lasr_skills import AskAndListen, Say + +ENGISH_MAPPING = { + "goToLoc": "Go to location", + "findPrsInRoom": "Find people in room", + "meetPrsAtBeac": "Meet person at beacon", + "countPrsInRoom": "Count people in room", + "tellPrsInfoInLoc": "Tell person information in location", + "talkInfoToGestPrsInRoom": "Talk information to guest in room", + "answerToGestPrsInRoom": "Answer to guest in room", + "followNameFromBeacToRoom": "Follow name from beacon to room", + "guideNameFromBeacToBeac": "Guide name from beacon to beacon", + "guidePrsFromBeacToBeac": "Guide person from beacon to beacon", + "guideClothPrsFromBeacToBeac": "Guide clothed person from beacon to beacon", + "greetClothDscInRm": "Greet clothed description in room", + "greetNameInRm": "Greet name in room", + "meetNameAtLocThenFindInRm": "Meet name at location then find in room", + "countClothPrsInRoom": "Count clothed people in room", + "tellPrsInfoAtLocToPrsAtLoc": "Tell person information at location to person at location", + "followPrsAtLoc": "Follow person at location", + "takeObjFromPlcmt": "Take object from placement", + "findObjInRoom": "Find object in room", + "countObjOnPlcmt": "Count object on placement", + "tellObjPropOnPlcmt": "Tell object properties on placement", + "bringMeObjFromPlcmt": "Bring me object from placement", + "tellCatPropOnPlcmt": "Tell category properties on placement", +} + + +def parse_args() -> Dict: + parser = argparse.ArgumentParser(description="GPSR Command Parser") + parser.add_argument( + "--data-dir", + type=str, + default="../data/mock_data/", + help="Path to the directory that contains the data json files.", + ) + known, unknown = parser.parse_known_args() + return vars(known) + + +class ParseCommand(smach.State): + def __init__(self, data_config: Configuration): + smach.State.__init__( + self, + outcomes=["succeeded", "failed"], + input_keys=["transcribed_speech"], + output_keys=["command"], + ) + self.data_config = data_config + + def execute(self, userdata): + try: + userdata.command = gpsr_compile_and_parse( + self.data_config, userdata.transcribed_speech.lower() + ) + except Exception as e: + rospy.logerr(e) + return "failed" + return "succeeded" + + +class OutputParsedCommand(smach.State): + def __init__(self): + smach.State.__init__( + self, + outcomes=["succeeded", "failed"], + input_keys=["command"], + output_keys=["command_string"], + ) + + def execute(self, userdata): + try: + command = userdata.command + english_command = ENGISH_MAPPING[command["command"]] + command_parameters = command[command["command"]] + rospy.loginfo(f"Command: {english_command}") + rospy.loginfo(f"Parameters: {command_parameters}") + tts_phrase = f"I parsed the command as you want me to: {english_command}, with the following parameters:" + for key, value in command_parameters.items(): + if isinstance(value, list): + value = " and ".join(value) + tts_phrase += f" {key}: {value}," + except Exception as e: + rospy.logerr(e) + return "failed" + rospy.loginfo(tts_phrase) + userdata.command_string = tts_phrase + return "succeeded" + + +class CommandParserStateMachine(smach.StateMachine): + def __init__(self, config: Configuration): + smach.StateMachine.__init__( + self, + outcomes=["succeeded", "failed"], + input_keys=["tts_phrase", "command_string"], + output_keys=["command"], + ) + self.config = config + with self: + smach.StateMachine.add( + "GET_COMMAND", + AskAndListen(), + transitions={"succeeded": "PARSE_COMMAND", "failed": "GET_COMMAND"}, + remapping={ + "tts_phrase": "tts_phrase", + "transcribed_speech": "transcribed_speech", + }, + ) + smach.StateMachine.add( + "PARSE_COMMAND", + ParseCommand(data_config=self.config), + transitions={ + "succeeded": "OUTPUT_PARSED_COMMAND", + "failed": "GET_COMMAND", + }, + remapping={ + "transcribed_speech": "transcribed_speech", + "command": "command", + }, + ) + smach.StateMachine.add( + "OUTPUT_PARSED_COMMAND", + OutputParsedCommand(), + transitions={ + "succeeded": "SAY_PARSED_COMMAND", + "failed": "GET_COMMAND", + }, + remapping={"command": "command", "tts_phrase": "tts_phrase"}, + ) + smach.StateMachine.add( + "SAY_PARSED_COMMAND", + Say(), + transitions={ + "succeeded": "GET_COMMAND", + "aborted": "GET_COMMAND", + "preempted": "GET_COMMAND", + }, + remapping={"text": "command_string"}, + ) + + +if __name__ == "__main__": + rospy.init_node("gpsr_command_parser") + args = parse_args() + data_loader = GPSRDataLoader(data_dir=args["data_dir"]) + gpsr_known_data: Dict = data_loader.load_data() + config = Configuration( + { + "person_names": gpsr_known_data["names"], + "location_names": gpsr_known_data["non_placeable_locations"], + "placement_location_names": gpsr_known_data["placeable_locations"], + "room_names": gpsr_known_data["rooms"], + "object_names": gpsr_known_data["objects"], + "object_categories_plural": gpsr_known_data["categories_plural"], + "object_categories_singular": gpsr_known_data["categories_singular"], + } + ) + rospy.loginfo("GPSR Command Parser: Initialized") + sm = CommandParserStateMachine(config) + sm.userdata.tts_phrase = "I am ready to receive a command; ask away!" + result = sm.execute() + rospy.spin() diff --git a/tasks/gpsr/src/gpsr/load_known_data.py b/tasks/gpsr/src/gpsr/load_known_data.py new file mode 100644 index 000000000..b2ac81184 --- /dev/null +++ b/tasks/gpsr/src/gpsr/load_known_data.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python + +""" +GPSR regex parser (and command generator) requires a list of the following names: + + - objects + - object categories plural + - object categories singular + - placeable locations + - non-placeable locations + - people + - rooms +""" + +import json +import os +from typing import List, Tuple, Dict + + +class GPSRDataLoader: + """Loads the a priori known data for the GPSR task from the + corresponding json files.""" + + def __init__(self, data_dir: str = "../data/mock_data/") -> None: + """Stores the data directory that contains the json files to load. + + Args: + data_dir (str, optional): Path to the directory that contains the + data json files. Defaults to "../data/mock_data/". + """ + self._data_dir: str = data_dir + + # assumed constant file names + self.LOCATIONS_FILE: str = "locations.json" + self.OBJECTS_FILE: str = "objects.json" + self.NAMES_FILE: str = "names.json" + self.ROOMS_FILE: str = "rooms.json" + + if not self._validate_dir(): + raise ValueError( + "The data directory does not contain all of the necessary json files." + ) + + def _validate_dir(self) -> bool: + """Checks if all of the necessary json files are present in the + data directory.""" + + if not all( + [ + self.LOCATIONS_FILE in os.listdir(self._data_dir), + self.OBJECTS_FILE in os.listdir(self._data_dir), + self.NAMES_FILE in os.listdir(self._data_dir), + self.ROOMS_FILE in os.listdir(self._data_dir), + ] + ): + return False + return True + + def _load_names(self) -> List[str]: + """Loads the names stored in the names json into a list of strings. + + Returns: + List[str]: list of names found in the names.json file + """ + + with open(os.path.join(self._data_dir, self.NAMES_FILE), "r") as f: + names = json.load(f) + return names[ + "names" + ] # names is a dictionary with a single key "names" that contains a list of names + + def _load_rooms(self) -> List[str]: + """Loads the rooms stored in the rooms json into a list of strings. + + Returns: + List[str]: list of rooms found in the rooms.json file + """ + + with open(os.path.join(self._data_dir, self.ROOMS_FILE), "r") as f: + rooms = json.load(f) + return rooms[ + "rooms" + ] # rooms is a dictionary with a single key "rooms" that contains a list of rooms + + def _load_locations(self) -> Tuple[List[str], List[str]]: + """Loads the locations into two lists of strings of location names: + one list for placeable locations and the other for non-placeable. + + Returns: + Tuple[List[str], List[str]]: placeable, non-placeable locations. + """ + + placeable_locations: List[str] = [] + non_placeable_locations: List[str] = [] + + with open(os.path.join(self._data_dir, self.LOCATIONS_FILE), "r") as f: + locations = json.load(f) + + for location, attributes in locations.items(): + if attributes["placeable"]: + placeable_locations.append(location) + non_placeable_locations.append(location) + + return placeable_locations, non_placeable_locations + + def _load_objects(self) -> Tuple[List[str], List[str], List[str]]: + """Loads the objects into three lists of strings: + one list for object names, one for the plural names of object categories, + and one for the singular names of object categories. + + Returns: + Tuple[List[str], List[str], List[str]]: objects, categories plural, categories singular + """ + + objects: List[str] = [] + categories_plural: List[str] = [] + categories_singular: List[str] = [] + + with open(os.path.join(self._data_dir, self.OBJECTS_FILE), "r") as f: + object_data = json.load(f) + + for category, category_data in object_data.items(): + categories_plural.append(category) + categories_singular.append(category_data["singular"]) + objects.extend(category_data["items"]) + + return objects, categories_plural, categories_singular + + def load_data(self) -> Dict[str, List[str]]: + """Loads all of the known data for the GPSR task. + + Returns: + Dict[str, List[str]]: dictionary containing the following + keys and their corresponding lists of strings: + - "names" + - "rooms" + - "placeable_locations" + - "non_placeable_locations" + - "objects" + - "categories_plural" + - "categories_singular" + """ + + names = self._load_names() + rooms = self._load_rooms() + placeable_locations, non_placeable_locations = self._load_locations() + objects, categories_plural, categories_singular = self._load_objects() + + return { + "names": names, + "rooms": rooms, + "placeable_locations": placeable_locations, + "non_placeable_locations": non_placeable_locations, + "objects": objects, + "categories_plural": categories_plural, + "categories_singular": categories_singular, + } + + +if __name__ == "__main__": + loader = GPSRDataLoader() + data = loader.load_data() + print(data) diff --git a/tasks/gpsr/src/gpsr/regex_command_parser.py b/tasks/gpsr/src/gpsr/regex_command_parser.py new file mode 100644 index 000000000..27a6913bc --- /dev/null +++ b/tasks/gpsr/src/gpsr/regex_command_parser.py @@ -0,0 +1,392 @@ +import itertools +import re + +from typing import List, Union, TypedDict, Dict + +counter = 0 + + +def uniq(i: str) -> str: + global counter + counter += 1 + return f"uniq{counter}_{i}" + + +def list_to_regex(list: List[str], key: Union[str, None] = None): + if key is None: + return f"(?:{'|'.join(list)})" + + return f"(?P<{uniq(key)}>{'|'.join(list)})" + + +# data from gpsr_commands +verb_dict = { + "take": ["take", "get", "grasp", "fetch"], + "place": ["put", "place"], + "deliver": ["bring", "give", "deliver"], + "bring": ["bring", "give"], + "go": ["go", "navigate"], + "find": ["find", "locate", "look for"], + "talk": ["tell", "say"], + "answer": ["answer"], + "meet": ["meet"], + "tell": ["tell"], + "greet": ["greet", "salute", "say hello to", "introduce yourself to"], + "remember": ["meet", "contact", "get to know", "get acquainted with"], + "count": ["tell me how many"], + "describe": ["tell me how", "describe"], + "offer": ["offer"], + "follow": ["follow"], + "guide": ["guide", "escort", "take", "lead"], + "accompany": ["accompany"], +} + + +def verb(v): + # return list_to_regex(verb_dict[v], f"verb_{v}") + return list_to_regex(verb_dict[v], None if len(verb_dict[v]) == 1 else "verb") + + +prep_dict = { + "deliverPrep": ["to"], + "placePrep": ["on"], + "inLocPrep": ["in"], + "fromLocPrep": ["from"], + "toLocPrep": ["to"], + "atLocPrep": ["at"], + "talkPrep": ["to"], + "locPrep": ["in", "at"], + "onLocPrep": ["on"], + "arePrep": ["are"], + "ofPrsPrep": ["of"], +} + + +def prep(v: str): + return list_to_regex(prep_dict[v]) + # return list_to_regex(prep_dict[v], f"prep_{v}") + + +class Configuration(TypedDict): + person_names: List[str] + location_names: List[str] + placement_location_names: List[str] + room_names: List[str] + object_names: List[str] + object_categories_plural: List[str] + object_categories_singular: List[str] + + def key_to_list(self, key: str): + if key == "name": + return self["person_names"] + elif key == "loc": + return self["location_names"] + elif key == "loc2": + return self["location_names"] + # return ['loc2'] + elif key == "plcmtLoc": + return self["placement_location_names"] + elif key == "plcmtLoc2": + return self["placement_location_names"] + # return ['plcmtLoc2'] + elif key == "room": + return self["room_names"] + elif key == "room2": + return self["room_names"] + # return ['room2'] + elif key == "obj": + return self["object_names"] + elif key == "singCat": + return self["object_categories_singular"] + elif key == "plurCat": + return self["object_categories_plural"] + else: + raise Exception("unreachable") + + def pick(self, key: str, lists: List[str]): + union = [] + for list in lists: + if list == "inRoom": + union.append( + f"(?:{prep('inLocPrep')} the (?P<{uniq('location')}>{'|'.join(Configuration.key_to_list(self, 'room'))}))" + ) + elif list == "atLoc": + union.append( + f"(?:{prep('atLocPrep')} the (?P<{uniq('location')}>{'|'.join(Configuration.key_to_list(self, 'loc'))}))" + ) + else: + union = union + Configuration.key_to_list(self, list) + + return f"(?P<{uniq(key)}>{'|'.join(union)})" + + +def gpsr_components(): + connector_list = ["and"] + gesture_person_list = [ + "waving person", + "person raising their left arm", + "person raising their right arm", + "person pointing to the left", + "person pointing to the right", + ] + pose_person_list = ["sitting person", "standing person", "lying person"] + # Ugly... + gesture_person_plural_list = [ + "waving persons", + "persons raising their left arm", + "persons raising their right arm", + "persons pointing to the left", + "persons pointing to the right", + ] + pose_person_plural_list = ["sitting persons", "standing persons", "lying persons"] + + person_info_list = ["name", "pose", "gesture"] + object_comp_list = [ + "biggest", + "largest", + "smallest", + "heaviest", + "lightest", + "thinnest", + ] + + talk_list = [ + "something about yourself", + "the time", + "what day is today", + "what day is tomorrow", + "your teams name", + "your teams country", + "your teams affiliation", + "the day of the week", + "the day of the month", + ] + question_list = ["question", "quiz"] + + color_list = ["blue", "yellow", "black", "white", "red", "orange", "gray"] + clothe_list = ["t shirt", "shirt", "blouse", "sweater", "coat", "jacket"] + clothes_list = ["t shirts", "shirts", "blouses", "sweaters", "coats", "jackets"] + color_clothe_list = [] + for a, b in list(itertools.product(color_list, clothe_list)): + color_clothe_list = color_clothe_list + [a + " " + b] + color_clothes_list = [] + for a, b in list(itertools.product(color_list, clothes_list)): + color_clothes_list = color_clothes_list + [a + " " + b] + + # convert lists to regex components + connector_list = list_to_regex(connector_list) + gesture_person_list = list_to_regex(gesture_person_list, "gesture") + pose_person_list = list_to_regex(pose_person_list, "pose") + gesture_person_plural_list = list_to_regex(gesture_person_plural_list, "gesture") + pose_person_plural_list = list_to_regex(pose_person_plural_list, "pose") + person_info_list = list_to_regex(person_info_list, "personinfo") + object_comp_list = list_to_regex(object_comp_list, "objectcomp") + talk_list = list_to_regex(talk_list, "talk") + question_list = list_to_regex(question_list, "question") + color_clothe_list = list_to_regex(color_clothe_list, "clothes") + color_clothes_list = list_to_regex(color_clothes_list, "clothes") + + return ( + verb, + prep, + "(?:an|a)", + connector_list, + gesture_person_list, + pose_person_list, + gesture_person_plural_list, + pose_person_plural_list, + person_info_list, + object_comp_list, + talk_list, + question_list, + color_clothe_list, + color_clothes_list, + ) + + +def gpsr_regex(configuration: Configuration): + ( + verb, + prep, + art, + connector_list, + gesture_person_list, + pose_person_list, + gesture_person_plural_list, + pose_person_plural_list, + person_info_list, + object_comp_list, + talk_list, + question_list, + color_clothe_list, + color_clothes_list, + ) = gpsr_components() + + commands = [] + + def command(key: str, matcher: str): + matcher = re.sub("\(\?P\<", f"(?P{matcher})") + + def SUB_COMMAND_TODO_UNIMPLEMENTED(): + return f'(?P<{uniq("subcommand")}>.*)' + + command( + "goToLoc", + f"{verb('go')} {prep('toLocPrep')} the {Configuration.pick(configuration, 'location', ['loc', 'room'])} then {SUB_COMMAND_TODO_UNIMPLEMENTED()}", + ) + command( + "takeObjFromPlcmt", + f"{verb('take')} {art} {Configuration.pick(configuration, 'object', ['obj', 'singCat'])} {prep('fromLocPrep')} the {Configuration.pick(configuration, 'location', ['plcmtLoc'])} and {SUB_COMMAND_TODO_UNIMPLEMENTED()}", + ) + command( + "findPrsInRoom", + f"{verb('find')} a (?:{gesture_person_list}|{pose_person_list}) {prep('inLocPrep')} the {Configuration.pick(configuration, 'location', ['room'])} and {SUB_COMMAND_TODO_UNIMPLEMENTED()}", + ) + command( + "findObjInRoom", + f"{verb('find')} {art} {Configuration.pick(configuration, 'object', ['obj', 'singCat'])} {prep('inLocPrep')} the {Configuration.pick(configuration, 'location', ['room'])} and {SUB_COMMAND_TODO_UNIMPLEMENTED()}", + ) + command( + "meetPrsAtBeac", + f"{verb('meet')} {Configuration.pick(configuration, 'name', ['name'])} {prep('inLocPrep')} the {Configuration.pick(configuration, 'location', ['room'])} and {SUB_COMMAND_TODO_UNIMPLEMENTED()}", + ) + command( + "countObjOnPlcmt", + f"{verb('count')} {Configuration.pick(configuration, 'object', ['plurCat'])} there are {prep('onLocPrep')} the {Configuration.pick(configuration, 'location', ['plcmtLoc'])}", + ) + command( + "countPrsInRoom", + f"{verb('count')} (?:{gesture_person_plural_list}|{pose_person_plural_list}) are {prep('inLocPrep')} the {Configuration.pick(configuration, 'location', ['room'])}", + ) + command( + "tellPrsInfoInLoc", + f"{verb('tell')} me the {person_info_list} of the person {Configuration.pick(configuration, 'location', ['inRoom', 'atLoc'])}", + ) + command( + "tellObjPropOnPlcmt", + f"{verb('tell')} me what is the {object_comp_list} object {prep('onLocPrep')} the {Configuration.pick(configuration, 'location', ['plcmtLoc'])}", + ) + command( + "talkInfoToGestPrsInRoom", + f"{verb('talk')} {talk_list} {prep('talkPrep')} the {gesture_person_list} {prep('inLocPrep')} the {Configuration.pick(configuration, 'location', ['room'])}", + ) + command( + "answerToGestPrsInRoom", + f"{verb('answer')} the {question_list} {prep('ofPrsPrep')} the {gesture_person_list} {prep('inLocPrep')} the {Configuration.pick(configuration, 'location', ['room'])}", + ) + command( + "followNameFromBeacToRoom", + f"{verb('follow')} {Configuration.pick(configuration, 'name', ['name'])} {prep('fromLocPrep')} the {Configuration.pick(configuration, 'start', ['loc'])} {prep('toLocPrep')} the {Configuration.pick(configuration, 'end', ['room'])}", + ) + command( + "guideNameFromBeacToBeac", + f"{verb('guide')} {Configuration.pick(configuration, 'name', ['name'])} {prep('fromLocPrep')} the {Configuration.pick(configuration, 'start', ['loc'])} {prep('toLocPrep')} the {Configuration.pick(configuration, 'end', ['loc', 'room'])}", + ) + command( + "guidePrsFromBeacToBeac", + f"{verb('guide')} the (?:{gesture_person_list}|{pose_person_list}) {prep('fromLocPrep')} the {Configuration.pick(configuration, 'start', ['loc'])} {prep('toLocPrep')} the {Configuration.pick(configuration, 'end', ['loc', 'room'])}", + ) + command( + "guideClothPrsFromBeacToBeac", + f"{verb('guide')} the person wearing a {color_clothe_list} {prep('fromLocPrep')} the {Configuration.pick(configuration, 'start', ['loc'])} {prep('toLocPrep')} the {Configuration.pick(configuration, 'end', ['loc', 'room'])}", + ) + command( + "bringMeObjFromPlcmt", + f"{verb('bring')} me {art} {Configuration.pick(configuration, 'object', ['obj'])} {prep('fromLocPrep')} the {Configuration.pick(configuration, 'location', ['plcmtLoc'])}", + ) + command( + "tellCatPropOnPlcmt", + f"{verb('tell')} me what is the {object_comp_list} {Configuration.pick(configuration, 'object', ['singCat'])} {prep('onLocPrep')} the {Configuration.pick(configuration, 'location', ['plcmtLoc'])}", + ) + command( + "greetClothDscInRm", + f"{verb('greet')} the person wearing {art} {color_clothe_list} {prep('inLocPrep')} the {Configuration.pick(configuration, 'location', ['room'])} and {SUB_COMMAND_TODO_UNIMPLEMENTED()}", + ) + command( + "greetNameInRm", + f"{verb('greet')} {Configuration.pick(configuration, 'name', ['name'])} {prep('inLocPrep')} the {Configuration.pick(configuration, 'location', ['room'])} and {SUB_COMMAND_TODO_UNIMPLEMENTED()}", + ) + command( + "meetNameAtLocThenFindInRm", + f"{verb('meet')} {Configuration.pick(configuration, 'name', ['name'])} {prep('atLocPrep')} the {Configuration.pick(configuration, 'location', ['room'])} then {verb('find')} them {prep('inLocPrep')} the {Configuration.pick(configuration, 'destination', ['room'])}", + ) + command( + "countClothPrsInRoom", + f"{verb('count')} people {prep('inLocPrep')} the {Configuration.pick(configuration, 'location', ['room'])} are wearing {color_clothes_list}", + ) + command( + "tellPrsInfoAtLocToPrsAtLoc", + f"{verb('tell')} the {person_info_list} of the person {prep('atLocPrep')} the {Configuration.pick(configuration, 'location', ['room'])} to the person {prep('atLocPrep')} the {Configuration.pick(configuration, 'destination', ['room'])}", + ) + command( + "followPrsAtLoc", + f"{verb('follow')} the (?:{gesture_person_list}|{pose_person_list}) {Configuration.pick(configuration, 'location', ['inRoom', 'atLoc'])}", + ) + return "|".join(commands) + + +def gpsr_parse(matches: Dict[str, str]): + result = {} + for key in matches.keys(): + value = matches[key] + if value is None: + continue + + write_into = result + key = re.sub("uniq\d+_", "", key) + + while key.startswith("CMD"): + cmd, rest = key.split("_", 1) + cmd = cmd[3:] # remove CMD prefix + + if cmd not in write_into: + write_into[cmd] = {} + + write_into = write_into[cmd] + key = rest + + if "_" in key: + actual_key, value = key.split("_") + write_into[actual_key] = value + else: + write_into[key] = value + return result + + +def gpsr_compile_and_parse(config: Configuration, input: str) -> dict: + input = input.lower() + # remove punctuation + input = re.sub(r"[^\w\s]", "", input) + print(input) + if input[0] == " ": + input = input[1:] + + regex_str = gpsr_regex(config) + regex = re.compile(regex_str) + matches = regex.match(input) + matches = matches.groupdict() + return gpsr_parse(matches) + + +if __name__ == "__main__": + config: Configuration = { + "person_names": ["guest1", "guest2"], + "location_names": ["sofa", "piano"], + "placement_location_names": ["kitchen table"], + "room_names": ["living room", "kitchen"], + "object_names": ["cup", "television"], + "object_categories_plural": ["sticks"], + "object_categories_singular": ["stick"], + } + + regex_str = gpsr_regex(config) + + regex = re.compile(regex_str) + + def execute(input: str): + matches = regex.match(input).groupdict() + return gpsr_parse(matches) + + # subcommands aren't implemented but are caught: + print(execute("go to the sofa then do something here"))