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))
Example #2
0
 def __init__(self):
     """
     Creates a new domain with an empty dialogue state and list of models.
     """
     self._settings = Settings()
     self._models = []  # list of models
     self._initial_state = DialogueState()  # initial dialog state
     self._parameters = BNetwork()
     self._imported_files = []
     self._xml_file = None  # path to the source XML file (and its imports)
Example #3
0
    def reduce_light(state, nodes_to_keep):
        """
        "lightweight" reduction of the dialogue state (without actual inference).

        :param state: the dialogue state
        :param nodes_to_keep: the nodes to keep
        :return: the reduced dialogue state @
        """
        new_state = DialogueState(state, state.get_evidence())
        for chance_node in new_state.get_chance_nodes():
            if chance_node.get_id() not in nodes_to_keep:
                init_distrib = state.query_prob(chance_node.get_id(),
                                                False).to_discrete()
                for output_node in chance_node.get_output_nodes(ChanceNode):
                    new_distrib = MarginalDistribution(
                        output_node.get_distrib(), init_distrib)
                    output_node.set_distrib(new_distrib)

                new_state.remove_node(chance_node.get_id())

        return new_state
Example #4
0
    def import_content(system, file, tag):
        """
        Imports a dialogue state or prior parameter distributions.

        :param system: the dialogue system
        :param file: the file that contains the state or parameter content
        :param tag: the expected top XML tag. into the system
        """
        from readers.xml_state_reader import XMLStateReader
        if tag == "parameters":
            parameters = XMLStateReader.extract_bayesian_network(file, tag)
            for old_param in system.get_state().get_parameter_ids():
                if not parameters.has_chance_node(old_param):
                    parameters.add_node(
                        system.get_state().get_chance_node(old_param))
            system.get_state().set_parameters(parameters)
        else:
            state = XMLStateReader.extract_bayesian_network(file, tag)
            system.add_content(DialogueState(state))
    def extract_dialogue(data_file):
        """
        Extracts the dialogue specified in the data file. The result is a list of
        dialogue state (one for each turn).

        :param data_file: the XML file containing the turns
        :return: the list of dialogue state
        """
        doc = XMLUtils.get_xml_document(data_file)
        main_node = XMLUtils.get_main_node(doc)

        f = open(data_file)
        root_path = f.name

        sample = []

        for node in main_node:
            node_name = node.keys()[0]
            if "Turn" in node_name:
                state = DialogueState(
                    XMLStateReader.get_bayesian_network(node))
                sample.append(state)

                if node_name == "systemTurn" and state.has_chance_node("a_m"):
                    assign = Assignment("a_m",
                                        state.query_prob("a_m").get_best())
                    state.add_evidence(assign)

            elif node_name == "wiazard":
                assign = Assignment.create_from_string(
                    node.get_first_child().get_node_value().trim())
                sample[-1].add_evidence(assign)

            elif node_name == "import":
                file_name = main_node.get_attributes().get_named_item(
                    "href").get_node_value()
                points = XMLDialogueReader.extract_dialogue(root_path + "/" +
                                                            file_name)
                sample.append(points)

        return sample
    def extract_partial_domain(main_node, domain, root_path, full_extract):
        """
        Extracts a partially specified domain from the XML node and add its content to
        the dialogue domain.

        :param main_node: main XML node
        :param domain: dialogue domain
        :param root_path: root path (necessary to handle references)
        :param full_extract: whether to extract the full domain or only the files
        :return: the augmented dialogue domain
        """
        tag = main_node.tag

        if tag == 'domain':
            # extracting rule-based probabilistic model
            for child in main_node:
                domain = XMLDomainReader.extract_partial_domain(
                    child, domain, root_path, full_extract)
        elif tag == 'import':
            # extracting imported references
            try:
                file_name = main_node.attrib['href']
                file_path = str(root_path) + os.sep + file_name
                fl = Path(file_path)
                domain.add_imported_files(fl)
                sub_document = XMLUtils.get_xml_document(file_path)
                domain = XMLDomainReader.extract_partial_domain(
                    XMLUtils.get_main_node(sub_document), domain, root_path,
                    full_extract)
            except:
                raise ValueError()

        if not full_extract:
            return domain

        if tag == 'settings':
            # extracting settings
            settings = XMLUtils.extract_mapping(main_node)
            domain.get_settings().fill_settings(settings)
        if tag == 'function':
            # extracting custom functions
            # try:
            domain_function_name = main_node.attrib['name'].strip()

            module_name, actual_function_name = main_node.text.rsplit('.', 1)
            mod = importlib.import_module(module_name)
            func = getattr(mod, actual_function_name)

            domain.get_settings().add_function(domain_function_name, func)
            # except:
            #     raise ValueError()
        if tag == 'initialstate':
            # extracting initial state
            state = XMLStateReader.get_bayesian_network(main_node)
            domain.set_initial_state(DialogueState(state))
        if tag == 'model':
            # extracting rule-based probabilistic model
            model = XMLDomainReader._create_model(main_node)
            domain.add_model(model)
        if tag == 'parameters':
            # extracting parameters
            parameters = XMLStateReader.get_bayesian_network(main_node)
            domain.set_parameters(parameters)
        if XMLUtils.has_content(main_node):
            if main_node == '#text':  # TODO: main_node -> main_node.tag ??
                raise ValueError()

        return domain
Example #7
0
    def reduce(state, nodes_to_keep):
        """
        Reduces a Bayesian network to a subset of variables. The method is divided in
        three steps:

        - The method first checks whether inference is necessary at all or whether
        the current network can be returned as it is.
        - If inference is necessary, the algorithm divides the network into cliques
        and performs inference on each clique separately.
        - Finally, if only one clique is present, the reduction selects the best
        algorithm and return the result of the reduction process.

        :param state: the dialogue state to reduce
        :param nodes_to_keep: the nodes to preserve in the reduction

        :return: the reduced dialogue state
        """
        evidence = state.get_evidence()
        if evidence.contains_vars(nodes_to_keep):
            # if all nodes to keep are included in the evidence, no inference is needed
            new_state = DialogueState()
            for node_to_keep in nodes_to_keep:
                new_node = ChanceNode(node_to_keep,
                                      evidence.get_value(node_to_keep))
                new_state.add_node(new_node)

            return new_state
        elif (state.get_node_ids()).issubset(nodes_to_keep):
            # if the current network can be returned as such, do it
            return state
        elif state.is_clique(nodes_to_keep) and not evidence.contains_one_var(
                nodes_to_keep):
            # if all nodes belong to a single clique and the evidence does not
            # pertain to them, return the subset of nodes
            return DialogueState(state.get_nodes(nodes_to_keep), evidence)
        elif state.contains_distrib(nodes_to_keep, AnchoredRule):
            # if some rule nodes are included
            return StatePruner.reduce_light(state, nodes_to_keep)

        # if the network can be divided into cliques, extract the cliques
        # and do a separate reduction for each
        cliques = state.get_cliques(nodes_to_keep)
        if len(cliques) > 1:
            full_state = DialogueState()
            for clique in cliques:
                clique.intersection_update(nodes_to_keep)
                clique_state = StatePruner.reduce(state, clique)
                full_state.add_network(clique_state)
                full_state.add_evidence(clique_state.get_evidence())

            return full_state

        result = SwitchingAlgorithm().reduce(state, nodes_to_keep, evidence)
        return DialogueState(result)
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