def extract_domain(top_domain_file, full_extract=True): """ Extract a dialogue domain from the XML specification :param top_domain_file: the filename of the top XML file :param full_extract: whether to extract the full domain or only the files :return: the extracted dialogue domain """ # create a new, empty domain domain = Domain() # determine the root path and filename fl = open(top_domain_file, 'r') domain.set_source_file(Path(top_domain_file)) # extract the XML document document = XMLUtils.get_xml_document(fl) main_node = XMLUtils.get_main_node(document) root_path = Path(top_domain_file).parent for child in main_node: domain = XMLDomainReader.extract_partial_domain( child, domain, root_path, full_extract) return domain
def __init__(self, arg1=None): if arg1 is None: """ Creates a new dialogue system with an empty dialogue system """ self._settings = Settings() # the system setting self._cur_state = DialogueState() # the dialogue state self._domain = Domain() # the dialogue domain self._paused = True # whether the system is paused or active self._modules = [] # the set of modules attached to the system # Inserting standard modules system = self self._modules.append(GUIFrame(system)) self._modules.append(DialogueRecorder(self)) if self._settings.planner == 'forward': self.log.info("Forward planner will be used.") self._modules.append(ForwardPlanner(self)) elif self._settings.planner == 'mcts': self.log.info("MCTS planner will be used.") self._modules.append(MCTSPlanner(self)) else: raise ValueError("Not supported planner: %s" % self._settings.planner) self._init_lock() elif isinstance(arg1, Domain): domain = arg1 """ Creates a new dialogue system with the provided dialogue domain :param domain: the dialogue domain to employ """ self.__init__() self.change_domain(domain) elif isinstance(arg1, str): domain_file = arg1 """ Creates a new dialogue system with the provided dialogue domain :param domain_file: the dialogue domain to employ """ self.__init__() self.change_domain(XMLDomainReader.extract_domain(domain_file))
def refresh_domain(self): """ Refreshes the dialogue domain by rereading its source file (in case it has been changed by the user). """ if self._domain.is_empty(): return src_file = self._domain.get_source_file().get_path() try: self._domain = XMLDomainReader.extract_domain(src_file) self.change_settings(self._domain.get_settings()) self.display_comment("Dialogue domain successfully updated") except Exception as e: self.log.critical("Cannot refresh domain %s" % e) self.display_comment("Syntax error: %s" % e) self._domain = Domain() self._domain.set_source_file(src_file)
# Config file for the destination domain import os import sys from keys import keys from domains.domain import Domain sys.path.append(os.path.join(sys.path[0], '../..')) # for loading apis from apis.maps.map_interface import MapInterface # Initialize interfaces interfaces = [MapInterface('google', keys['google_maps'])] # Create a Domain object, which will be used from this file destination_domain = Domain( 'destination', interfaces, initialization_file='domains/destination/initialization.json', apis_file='domains/destination/apis.json', agent_templates='domains/destination/agent_templates.txt', user_templates='domains/destination/user_templates.txt', )
# Config file for the weather domain import os import sys from keys import keys from domains.domain import Domain sys.path.append(os.path.join(sys.path[0], '../..')) # for loading apis from apis.weather.weather_interface import WeatherInterface from apis.maps.map_interface import MapInterface # Initialize interfaces interfaces = [ WeatherInterface(keys['darksky'], keys['wit_date']), MapInterface('google', keys['google_maps']) ] # Create a Domain object, which will be used from this file weather_domain = Domain( 'weather', interfaces, initialization_file='domains/weather/initialization.json', apis_file='domains/weather/apis.json', agent_templates='domains/weather/agent_templates.txt')
REPO_ROOT = pathlib.Path(__file__).resolve().parents[4] sys.path.append(str(REPO_ROOT)) # For loading apis sys.path.append(str(REPO_ROOT / "gui" / "backend")) # For loading domains from domains.domain import Domain from apis.maps.map_interface import MapInterface # Initialize interfaces interfaces = [MapInterface('microworld', map_path='microworld_map.txt')] # Create a Domain object, which will be used from this file DOMAIN_DIR = REPO_ROOT / "gui/backend/domains/microworld" microworld_domai1 = Domain( 'microworld1', interfaces, initialization_file=DOMAIN_DIR / 'initialization1.json', apis_file=DOMAIN_DIR / 'apis.json', agent_templates=DOMAIN_DIR / 'agent_templates.txt', user_templates=DOMAIN_DIR / 'user_templates.txt', ) # Microworld v2 microworld_domain2 = Domain( 'microworld2', [MapInterface('microworld', map_path='microworld_map2.txt')], initialization_file=DOMAIN_DIR / 'initialization2.json', apis_file=DOMAIN_DIR / 'apis.json', agent_templates=DOMAIN_DIR / 'agent_templates.txt', user_templates=DOMAIN_DIR / 'user_templates.txt', )
class DialogueSystem: """ Dialogue system based on probabilistic rules. A dialogue system comprises: - the current dialogue state - the dialogue domain with a list of rule-structured models - the list of system modules - the system settings. After initialising the dialogue system, the system should be started with the method startSystem(). The system can be paused or resumed at any time. """ # logger log = logging.getLogger('PyOpenDial') def __init__(self, arg1=None): if arg1 is None: """ Creates a new dialogue system with an empty dialogue system """ self._settings = Settings() # the system setting self._cur_state = DialogueState() # the dialogue state self._domain = Domain() # the dialogue domain self._paused = True # whether the system is paused or active self._modules = [] # the set of modules attached to the system # Inserting standard modules system = self self._modules.append(GUIFrame(system)) self._modules.append(DialogueRecorder(self)) if self._settings.planner == 'forward': self.log.info("Forward planner will be used.") self._modules.append(ForwardPlanner(self)) elif self._settings.planner == 'mcts': self.log.info("MCTS planner will be used.") self._modules.append(MCTSPlanner(self)) else: raise ValueError("Not supported planner: %s" % self._settings.planner) self._init_lock() elif isinstance(arg1, Domain): domain = arg1 """ Creates a new dialogue system with the provided dialogue domain :param domain: the dialogue domain to employ """ self.__init__() self.change_domain(domain) elif isinstance(arg1, str): domain_file = arg1 """ Creates a new dialogue system with the provided dialogue domain :param domain_file: the dialogue domain to employ """ self.__init__() self.change_domain(XMLDomainReader.extract_domain(domain_file)) def _init_lock(self): # TODO: need refactoring (decorator?) self._locks = { 'detach_module': threading.RLock(), 'start_system_update': threading.RLock(), 'pause_update': threading.RLock(), 'update': threading.RLock() } @dispatch() def start_system(self): """ Starts the dialogue system and its modules. """ self._paused = False for module in self._modules: try: if not module.is_running(): module.start() else: module.pause(False) except Exception as e: self.log.warning("could not start module %s: %s" % (type(module), e)) self._modules.remove(module) with self._locks['start_system_update']: self._cur_state.set_as_new() self.update() @dispatch(Domain) def change_domain(self, domain): """ Changes the dialogue domain for the dialogue domain :param domain: the dialogue domain to employ """ self._domain = domain self.change_settings(domain.get_settings()) self._cur_state = copy(domain.get_initial_state()) self._cur_state.set_parameters(domain.get_parameters()) if not self._paused: self.start_system() @dispatch(Module) def attach_module(self, module_instance): """ Attaches the module to the dialogue system. :param module_instance: the module to add """ if module_instance in self._modules or self.get_module( module_instance) is not None: self.log.info("Module %s is already attached" % type(module_instance)) return if len(self._modules) == 0: self._modules.append(module_instance) else: self._modules.insert(len(self._modules) - 1, module_instance) if not self._paused: try: module_instance.start() except Exception as e: self.log.warning("could not start module %s" % type(module_instance)) self._modules.remove(module_instance) @dispatch(type) def attach_module(self, module_type): """ Attaches the module to the dialogue system. :param module_type: the module class to instantiate """ try: module_instance = module_type(self) self.attach_module(module_instance) self.display_comment("Module %s successfully attached" % module_type.__name__) except Exception as e: self.log.warning("cannot attach %s: %s" % (module_type.__name__, e)) self.display_comment("cannot attach %s: %s" % (module_type.__name__, e)) @dispatch(type) def detach_module(self, module_type): """ Detaches the module of the dialogue system. If the module is not included in the system, does nothing. Only one of model_type or module_instance will be given as an input parameter. :param module_type: the class of the module to detach. :param module_instance: the module to detach """ with self._locks['detach_module']: module_instance = self.get_module(module_type) if module_instance is not None: module_instance.pause(True) self._modules.remove(module_instance) @dispatch(bool) def pause(self, to_pause): """ Pauses or resumes the dialogue system. :param to_pause: whether the system should be paused or resumed. """ self._paused = to_pause for module in self._modules: module.pause(to_pause) if not to_pause and not self._cur_state.get_new_variables().is_empty(): with self._locks['pause_update']: self.update() @dispatch(str) def display_comment(self, comment): """ Adds a comment on the GUI and the dialogue recorder. :param comment: comment the comment to display """ if self.get_module(GUIFrame) is not None and self.get_module( GUIFrame).is_running(): self.get_module(GUIFrame).add_comment(comment) else: self.log.info(comment) if self.get_module(DialogueRecorder) is not None and self.get_module( DialogueRecorder).is_running(): self.get_module(DialogueRecorder).add_comment(comment) @dispatch(Settings) def change_settings(self, settings): """ Changes the settings of the system :param settings: the new settings """ self._settings.fill_settings(settings.get_specified_mapping()) for module_type in settings.modules: if self.get_module(module_type) is None: self.attach_module(module_type) @dispatch(bool) def enable_speech(self, to_enable): if to_enable: if self.get_module(AudioModule) is None: self._settings.select_audio_mixers() self.attach_module(AudioModule(self)) if self._settings.show_gui: self.get_module(GUIFrame).enable_speech(True) else: raise NotImplementedError() # TODO: VAD not implemented # self.get_module(type(AudioModule)).activate_vad(True) else: self.detach_module(AudioModule) if self.get_module(GUIFrame) is not None: self.get_module(GUIFrame).enable_speech(False) @dispatch(str) def import_dialogues(self, dialogue_file): turns = XMLDialogueReader.extract_dialogue(dialogue_file) importer = DialogueImporter(self, turns) importer.start() return importer # =============================== # STATE UPDATE # =============================== @dispatch(str) def add_user_input(self, user_input): """ Adds the user input (assuming a perfect confidence score) to the dialogue state and subsequently updates it. :param user_input: the user input as a string :return: the variables that were updated in the process not be updated """ # perfect confidence score a = Assignment(self._settings.user_input, user_input) return self.add_content(a) @dispatch(dict) def add_user_input(self, user_input): """ Adds the user input (as a N-best list, where each hypothesis is associated with a probability) to the dialogue state and subsequently updates it. :param user_input: the user input as an N-best list :return: the variables that were updated in the process not be updated """ # user_input: N-best list, where each hypothesis is associated with a probability var = self._settings.user_input if not self._settings.inverted_role else self._settings.system_output builder = CategoricalTableBuilder(var) for input in user_input.keys(): builder.add_row(input, user_input.get(input)) return self.add_content(builder.build()) @dispatch(SpeechData) def add_user_input(self, input_speech): assignment = Assignment(self._settings.user_speech, input_speech) assignment.add_pair(self._settings.floor, 'user') return self.add_content(assignment) @dispatch(str, (str, bool, Value, float)) def add_content(self, variable, value): """ Adds the content (expressed as a pair of variable=value) to the current dialogue state, and subsequently updates the dialogue state. :param variable: the variable label :param value: the variable value :return: the variables that were updated in the process not be updated. """ if not self._paused: self._cur_state.add_to_state(Assignment(variable, value)) return self.update() else: self.log.info("System is paused, ignoring %s = %s" % (variable, value)) return set() @dispatch((Assignment, IndependentDistribution, ProbDistribution, MultivariateDistribution, BNetwork, DialogueState)) def add_content(self, distrib): """ Merges the dialogue state included as argument into the current one, and updates the dialogue state. :param distrib: the content to add :return: the set of variables that have been updated """ if not self._paused: self._cur_state.add_to_state(distrib) return self.update() else: self.log.info("System is paused, ignoring content %s" % distrib) return set() @dispatch(IndependentDistribution, bool) def add_incremental_content(self, content, follow_previous): """ Adds the incremental content (expressed as a distribution over variables) to the current dialogue state, and subsequently updates it. If followPrevious is set to true, the content is concatenated with the current distribution for the variable. :param content: the content to add / concatenate :param follow_previous: whether the results should be concatenated to the previous values, or reset the content (e.g. when starting a new utterance) :return: the set of variables that have been updated update failed """ if not self._paused: self._cur_state.add_to_state_incremental(content.to_discrete(), follow_previous) return self.update() else: self.log.info("System is paused, ignoring content " % content) return set() @dispatch(dict, bool) def add_incremental_user_input(self, user_input, follow_previous): """ Adds the incremental user input (expressed as an N-best list) to the current dialogue state, and subsequently updates it. If followPrevious is set to true, the content is concatenated with the current distribution for the variable. This allows (for instance) to perform incremental updates of user utterances. :param user_input: the user input to add / concatenate :param follow_previous: whether the results should be concatenated to the previous values, or reset the content (e.g. when starting a new utterance) :return: the set of variables that have been updated update failed """ builder = CategoricalTableBuilder(self._settings.user_input) for input in user_input.key_set(): builder.add_row(input, user_input.get(input)) return self.add_incremental_content(builder.build(), follow_previous) @dispatch() def remove_content(self, variable_id): """ Removes the variable from the dialogue state :param variable_id: the variable identifier """ if not self._paused: self._cur_state.remove_from_state(variable_id) self.update() else: self.log.info("System is paused, ignoring removal of %s" % variable_id) @dispatch() def update(self): """ Performs an update loop on the current dialogue state, by triggering all the models and modules attached to the system until all possible updates have been performed. The dialogue state is pruned at the end of the operation. :return: the set of variables that have been updated during the process. """ with self._locks['update']: updated_vars = dict() while len(self._cur_state.get_new_variables()) > 0: to_process = self._cur_state.get_new_variables() self._cur_state.reduce() for model in self._domain.get_models(): if not model.planning_only and model.is_triggered( self._cur_state, to_process): change = model.trigger(self._cur_state) if change and model.is_blocking(): break for i in range(len(self._modules)): self._modules[i].trigger(self._cur_state, to_process) for v in to_process: if v not in updated_vars or updated_vars[v] is None: count = 1 else: count = updated_vars[v] + 1 updated_vars[v] = count if count > 100: # TODO: count > 10 ? self.display_comment( "Warning: Recursive update of variable %s" % v) return set(updated_vars.keys()) return set(updated_vars.keys()) @dispatch() def refresh_domain(self): """ Refreshes the dialogue domain by rereading its source file (in case it has been changed by the user). """ if self._domain.is_empty(): return src_file = self._domain.get_source_file().get_path() try: self._domain = XMLDomainReader.extract_domain(src_file) self.change_settings(self._domain.get_settings()) self.display_comment("Dialogue domain successfully updated") except Exception as e: self.log.critical("Cannot refresh domain %s" % e) self.display_comment("Syntax error: %s" % e) self._domain = Domain() self._domain.set_source_file(src_file) # =============================== # GETTERS # =============================== @dispatch() def get_state(self): """ Returns the current dialogue state for the dialogue system. :return: the dialogue state """ return self._cur_state @dispatch() def get_floor(self): """ Returns who holds the current conversational floor (user, system, or free) :return: a string stating who currently owns the floor """ if self._cur_state.has_chance_node(self._settings.floor): return str(self.get_content(self._settings.floor).get_best()) else: return "free" @dispatch((str, Collection)) def get_content(self, variable): """ Returns the probability distribution associated with the variables in the current dialogue state. :param variable: the variable to query, which will be 'str' or 'list' :return: the resulting probability distribution for these variables """ if isinstance(variable, list): variable = set(variable) return self._cur_state.query_prob(variable) @dispatch(type) def get_module(self, module_type): """ Returns the module attached to the dialogue system and belonging to a particular class, if one exists. If no module exists, returns null :param module_type: the module class :return: the attached module of that class, if one exists. """ for module in self._modules: module_name = get_class_name_from_type(module_type) if get_class_name(module) == module_name: return module return None @dispatch(Module) def get_module(self, module_instance): """ Returns the module attached to the dialogue system and belonging to a particular class, if one exists. If no module exists, returns null :param module_instance: the module instance :return: the attached module of that class, if one exists. """ for module in self._modules: module_name = get_class_name(module_instance) if get_class_name(module) == module_name: return module return None @dispatch(type, Module) def get_module(self, module_type, module_instance): """ Returns the module attached to the dialogue system and belonging to a particular class, if one exists. If no module exists, returns null :param module_type: the module class :param module_instance: the module instance :return: the attached module of that class, if one exists. """ for module in self._modules: module_name = get_class_name(module_instance) if get_class_name(module) == module_name: return module return None @dispatch() def is_paused(self): """ Returns true is the system is paused, and false otherwise :return: true if paused, false otherwise. """ return self._paused @dispatch() def get_settings(self): """ Returns the settings for the dialogue system. :return: the system settings. """ return self._settings @dispatch() def get_domain(self): """ Returns the domain for the dialogue system. :return: the dialogue domain. """ return self._domain @dispatch() def get_modules(self): """ Returns the collection of modules attached to the system. :return: the modules (list). """ return self._modules