Skip to content
Snippets Groups Projects
Select Git revision
  • 7a83ee225c5d158df178f09778b2616b984f08dc
  • master default protected
  • release/1.1.4
  • release/1.1.3
  • release/1.1.1
  • 1.4.2
  • 1.4.1
  • 1.4.0
  • 1.3.0
  • 1.2.1
  • 1.2.0
  • 1.1.5
  • 1.1.4
  • 1.1.3
  • 1.1.1
  • 1.1.0
  • 1.0.9
  • 1.0.8
  • 1.0.7
  • v1.0.5
  • 1.0.5
21 results

ModuleErrorException.java

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    UserModel.py 34.62 KiB
    ###############################################################################
    # PyDial: Multi-domain Statistical Spoken Dialogue System Software
    ###############################################################################
    #
    # Copyright 2015 - 2019
    # Cambridge University Engineering Department Dialogue Systems Group
    #
    # 
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    # http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #
    ###############################################################################
    
    '''
    UserModel.py - goal, agenda inventor for sim user 
    ====================================================
    
    Copyright CUED Dialogue Systems Group 2015 - 2017
    
    .. seealso:: CUED Imports/Dependencies: 
        
        import :mod:`utils.DiaAct` |.|
        import :mod:`utils.dact` |.|
        import :mod:`usersimulator.UMHdcSim` |.|
        import :mod:`ontology.Ontology` |.|
        import :mod:`utils.Settings` |.|
        import :mod:`utils.ContextLogger`
    
    ************************
    
    '''
    
    __author__ = "cued_dialogue_systems_group"
    import copy
    
    from utils import DiaAct, dact, Settings, ContextLogger
    from usersimulator import UMHdcSim
    from ontology import Ontology 
    logger = ContextLogger.getLogger('')
    
    
    class UMAgenda(object):
        '''Agenda of :class:`DiaAct` acts corresponding to a goal.
        
        :param domain: domain tag (ie CamRestaurants)
        :type domain: str
        '''
        def __init__(self, dstring):
            self.dstring = dstring
            self.agenda_items = []  # Stack of DiaAct
            self.rules = None
            # HDC probs on how simuser will conditionally behave: values are upper limits of ranges uniform sample falls in
            self.NOT_MENTION = 1.0 # default - this value doesnt actually matter
            self.LEAVE_INFORM = 0.4
            self.CONFIRM = -1.0  # doesn't make sense actually to change inform(X=Y) to a confirm() act.
            # ONE OTHER OPTION IS TO APPEND constraint into another inform act so it is inform(X=Y,A=B)
    
        def init(self, goal):
            """
            Initialises the agenda by creating DiaActs corresponding to the
            constraints in the goal G. Uses the default order for the
            dialogue acts on the agenda: an inform act is created for
            each constraint. Finally a bye act is added at the bottom of the agenda.
            
            :param goal: 
                   
            .. Note::
                No requests are added to the agenda.
            """
            self.agenda_items = []
            self.append_dact_to_front(DiaAct.DiaAct('inform(type="%s")' % goal.request_type))
    
            for const in goal.constraints:
                slot = const.slot
                value = const.val
                if slot == 'method':
                    continue
    
                do_not_add = False
                dia_act = DiaAct.DiaAct('inform()')
                slots = Ontology.global_ontology.getSlotsToExpress(self.dstring, slot, value)
    
                for s in slots:
                    val = goal.get_correct_const_value(s)
                    if val is not None:
                        dia_act.append(slot, val)
                    if len(slots) == 1 and self.contains(s, val):
                        do_not_add = True
                    elif len(slots) > 1:
                        # Slot value pair might already be in other act on agenda: remove that one
                        self.filter_acts_slot(s)
    
                if not do_not_add:
                    self.append_dact_to_front(dia_act)
    
            # CONDITIONALLY INIT THE AGENDA:
            if len(goal.copied_constraints): # dont need self.CONDITIONAL_BEHAVIOUR - list will be empty as appropriate
                self._conditionally_init_agenda(goal)
              
            # Finally append a bye() act to complete agenda:
            self.append_dact_to_front(DiaAct.DiaAct('bye()'))
            return
    
        def _conditionally_init_agenda(self, goal):
            """Use goal.copied_constraints -- to conditionally init Agenda.
            Probabilistically remove/alter the agenda for this constraint then:
    
            :param:
            :returns:
            """
    
            for dact in goal.copied_constraints:
                #print dact.slot, dact.op, dact.val   # TODO - delete debug prints
                uniform_sample = Settings.random.uniform()
                #print uniform_sample
                if uniform_sample < self.CONFIRM:
                    # TODO - remove/change this - 
                    # Decided this doesn't make sense - (so set self.CONFIRM=0) - info is in belief state, that is enough.
                    logger.info("changing inform() act to a confirm() act")
                    self.replace_acts_slot(dact.slot, replaceact="confirm") 
                elif uniform_sample < self.LEAVE_INFORM:
                    pass
                else:
                    logger.info("removing the inform() act and not replacing with anything.") 
                    self.filter_acts_slot(dact.slot)
            return
    
    
        def contains(self, slot, value, negate=False):
            '''Check if slot, value pair is in an agenda dialogue act
    
            :param slot:
            :param value:
            :param negate: None
            :type negate: bool
            :returns: (bool) slot, value is in an agenda dact?
            '''
            for dact in self.agenda_items:
                if dact.contains(slot, value, negate):
                    return True
            return False
    
        def get_agenda_with_act(self, act):
            '''agenda items with this act
            :param act: dialogue act 
            :type act: str
            :return: (list) agenda items
            '''
            items = []
            for ait in self.agenda_items:
                if ait.act == act:
                    items.append(ait)
            return items
    
        def get_agenda_with_act_slot(self, act, slot):
            '''
            :param act: dialogue act
            :type act: str
            :param slot: slot name
            :type slot: str
            :return: (list) of agenda items
            '''
            items = []
            for ait in self.agenda_items:
                if ait.act == act:
                    for item in ait.items:
                        if item.slot == slot:
                            items.append(ait)
                            break
            return items
    
        def replace_acts_slot(self, slot, replaceact="confirm"):
            """
            """
            for ait in self.agenda_items:
                if len(ait.items) == 1:
                    if ait.items[0].slot == slot:
                        print(ait.act)
                        print(ait.items)
                        ait.act = replaceact
                        print(ait)
                        input('going to change this to confirm')
    
    
        def filter_acts_slot(self, slot):
            '''
            Any acts related to the given slot are removed from the agenda.
            :param slot: slot name
            :type slot: str
            :return: None
            '''
            deleted = []
            for ait in self.agenda_items:
                if ait.act in ['inform', 'confirm', 'affirm'] and len(ait.items) > 0:
                    if len(ait.items) > 1:
                        pass
    #                     logger.error('Assumes all agenda items have only one semantic items: {}'.format(ait))
                    for only_item in ait.items:
                        if only_item.slot == slot:
                            deleted.append(ait)
    
            for ait in deleted:
                self.agenda_items.remove(ait)
    
        def filter_constraints(self, dap):
            '''Filters out acts on the agenda that convey the constraints mentioned in the given dialogue act. 
            Calls :meth:`filter_acts_slot` to do so. 
    
            :param dap:
            :returns: None
            '''
            if dap.act in ['inform', 'confirm'] or \
                (dap.act in ['affirm', 'negate'] and len(dap.items) > 0):
                for item in dap.items:
                    self.filter_acts_slot(item.slot)
    
        def size(self):
            '''Utility func to get size of agenda_items list
            
            :returns: (int) length
            '''
            return len(self.agenda_items)
    
        def clear(self):
            '''
            Erases all acts on the agenda (empties list)
            
            :return: None
            '''
            self.agenda_items = []
    
        def append_dact_to_front(self, dact):
            '''Adds the given dialogue act to the front of the agenda
    
            :param (instance): dact
            :returns: None
            '''
            self.agenda_items = [dact] + self.agenda_items
    
        def push(self, dact):
            # if dact.act == 'null':
            #     logger.warning('null() in agenda')
            # if dact.act == 'bye' and len(self.agenda_items) > 0:
            #     logger.warning('bye() in agenda')
            self.agenda_items.append(dact)
    
        def pop(self):
            return self.agenda_items.pop()
    
        def remove(self, dact):
            self.agenda_items.remove(dact)
    
    
    class UMGoal(object):
        '''Defines a goal within a domain
    
        :param patience: user patience 
        :type patience: int
        '''
        def __init__(self, patience, domainString):
            self.constraints = []
            self.copied_constraints = []  # goals copied over from other domains. Used to conditionally create agenda.
            self.requests = {}
            self.prev_slot_values = {}
            self.patience = patience
            self.request_type = Ontology.global_ontology.get_type(domainString)  
    
            self.system_has_informed_name_none = False
            self.no_relaxed_constraints_after_name_none = False
    
        def clear(self, patience, domainString):
            self.constraints = []
            self.requests = {}
            self.prev_slot_values = {}
            self.patience = patience
            self.request_type = Ontology.global_ontology.get_type(domainString)
    
            self.system_has_informed_name_none = False
            self.no_relaxed_constraints_after_name_none = False
    
        '''
        Methods for constraints.
        '''
        def set_copied_constraints(self, all_conditional_constraints):
            """Creates a list of dacts, where the constraints have come from earlier domains in the dialog.
    
            :param all_conditional_constraints: of all previous constraints (over all domains in dialog)
            :type all_conditional_constraints: dict
            :returns: None
            """
            for dact in self.constraints:
                slot,op,value = dact.slot,dact.op,dact.val
                if slot in list(all_conditional_constraints.keys()):
                    if len(all_conditional_constraints[slot]):
                        if value in all_conditional_constraints[slot]:
                            self.copied_constraints.append(dact)
            return
    
        def add_const(self, slot, value, negate=False):
            """
            """
            if not negate:
                op = '='
            else:
                op = '!='
            item = dact.DactItem(slot, op, value)
            self.constraints.append(item)
    
        def replace_const(self, slot, value, negate=False):
            self.remove_slot_const(slot, negate)
            self.add_const(slot, value, negate)
    
        def contains_slot_const(self, slot):
            for item in self.constraints:
                # an error introduced here by dact.py __eq__ method: 
                #if item.slot == slot:
                if str(item.slot) == slot:
                    return True
            return False
    
        def remove_slot_const(self, slot, negate=None):
            copy_consts = copy.deepcopy(self.constraints)
            
            if negate is not None:
                if not negate:
                    op = '='
                else:
                    op = '!='
            
                for item in copy_consts:
                    if item.slot == slot:
                        if item.op == op:
                            self.constraints.remove(item)
            else:
                for item in copy_consts:
                    if item.slot == slot:
                        self.constraints.remove(item)
    
        def get_correct_const_value(self, slot, negate=False):
            '''
            :return: (list of) value of the given slot in user goal constraint.
    
            '''
            values = []
            for item in self.constraints:
                if item.slot == slot:
                    if item.op == '!=' and negate or item.op == '=' and not negate:
                        values.append(item.val)
    
            if len(values) == 1:
                return values[0]
            elif len(values) == 0:
                return None
            logger.error('Multiple values are found for %s in constraint: %s' % (slot, str(values)))
            return values
    
        def get_correct_const_value_list(self, slot, negate=False):
            '''
            :return: (list of) value of the given slot in user goal constraint.
            '''
            values = []
            for item in self.constraints:
                if item.slot == slot:
                    if (item.op == '!=' and negate) or (item.op == '=' and not negate):
                        values.append(item.val)
            return values
    
        def add_prev_used(self, slot, value):
            '''
            Adds the given slot-value pair to the record of previously used slot-value pairs.
            '''
            if slot not in self.prev_slot_values:
                self.prev_slot_values[slot] = set()
            self.prev_slot_values[slot].add(value)
    
        def add_name_constraint(self, value, negate=False):
            if value in [None, 'none']:
                return
    
            wrong_venues = self.get_correct_const_value_list('name', negate=True)
            correct_venue = self.get_correct_const_value('name', negate=False)
    
            if not negate:
                # Adding name=value but there is name!=value.
                if value in wrong_venues:
                    logger.error('Failed to add name=%s: already got constraint name!=%s.' %
                                 (value, value))
                    return
                
                # Can have only one name= constraint.
                if correct_venue is not None:
                    #logger.debug('Failed to add name=%s: already got constraint name=%s.' %
                    #             (value, correct_venue))
                    self.replace_const('name', value) # ic340: added to override previously informed venues, to avoid
                                                        # simuser getting obsessed with a wrong venue
                    return
    
                # Adding name=value, then remove all name!=other.
                self.replace_const('name', value)
                return
    
            # if not negate and not self.is_suitable_venue(value):
            #     logger.debug('Failed to add name=%s: %s is not a suitable venue for goals.' % (value, value))
            #     return
    
            if negate:
                # Adding name!=value but there is name=value.
                if correct_venue == value:
                    logger.error('Failed to add name!=%s: already got constraint name=%s.' % (value, value))
                    return
    
                # Adding name!=value, but there is name=other. No need to add.
                if correct_venue is not None:
                    return
    
                self.add_const('name', value, negate=True)
                return
    
        # def is_correct(self, item):
        #     '''
        #     Check if the given items are correct in goal constraints.
        #     :param item: set[(slot, op, value), ...]
        #     :return:
        #     '''
        #     if type(item) is not set:
        #         item = set([item])
        #     for it in item:
        #         for const in self.constraints:
        #             if const.match(it):
    
        def is_satisfy_all_consts(self, item):
            '''
            Check if all the given items set[(slot, op, value),..] satisfies all goal constraints (conjunction of constraints).
            '''
            if type(item) is not set:
                item = set([item])
            for it in item:
                for const in self.constraints:
                    if not const.match(it):
                        return False
            return True
    
        def is_completed(self):
            # If the user has not specified any constraints, return True
            if not self.constraints:
                return True
            if (self.system_has_informed_name_none and not self.no_relaxed_constraints_after_name_none) or\
                    (self.is_venue_recommended() and self.are_all_requests_filled()):
                return True
            return False
    
        '''
        Methods for requests.
        '''
        def reset_requests(self):
            for info in self.requests:
                self.requests[info] = None
    
        def fill_requests(self, dact_items):
            for item in dact_items:
                if item.op != '!=':
                    self.requests[item.slot] = item.val
    
        def are_all_requests_filled(self):
            '''
            :return: True if all request slots have a non-empty value.
            '''
            return None not in list(self.requests.values())
    
        def is_venue_recommended(self):
            '''
            Returns True if the request slot 'name' is not empty.
            :return:
            '''
            if 'name' in self.requests and self.requests['name'] is not None:
                return True
            return False
    
        def get_unsatisfied_requests(self):
            results = []
            for info in self.requests:
                if self.requests[info] is None:
                    results.append(info)
            return results
    
        def __str__(self):
            result = 'constraints: ' + str(self.constraints) + '\n'
            result += 'requests:    ' + str(self.requests) + '\n'
            if self.patience is not None:
                result += 'patience:    ' + str(self.patience) + '\n'
            return result
    
    
    class GoalGenerator(object):
        '''
        Master class for defining a goal generator to generate domain specific goals for the simulated user.
        
        This class also defines the interface for new goal generators.
        
        To implement domain-specific behaviour, derive from this class and override init_goals.
        '''
        def __init__(self, dstring):
            '''
            The init method of the goal generator reading the config and setting the default parameter values.
            
            :param dstring: domain name tag
            :type dstring: str
            '''
            self.dstring = dstring
            configlist = []
            
            self.CONDITIONAL_BEHAVIOUR = False
            if Settings.config.has_option("conditional","conditionalsimuser"):
                self.CONDITIONAL_BEHAVIOUR = Settings.config.getboolean("conditional","conditionalsimuser")
            
            self.MAX_VENUES_PER_GOAL = 4
            if Settings.config.has_option('goalgenerator','maxvenuespergoal'):
                configlist.append('maxvenuespergoal')
                self.MAX_VENUES_PER_GOAL = Settings.config.getint('goalgenerator','maxvenuespergoal')
            self.MIN_VENUES_PER_GOAL = 1
    
            self.MAX_CONSTRAINTS = 3
            if Settings.config.has_option('goalgenerator','maxconstraints'):
                configlist.append('maxconstraints')
                self.MAX_CONSTRAINTS = Settings.config.getint('goalgenerator','maxconstraints')
    
            self.MAX_REQUESTS = 3
            if Settings.config.has_option('goalgenerator','maxrequests'):
                configlist.append('maxrequests')
                self.MAX_REQUESTS = Settings.config.getint('goalgenerator','maxrequests')
            self.MIN_REQUESTS = 1
            if Settings.config.has_option('goalgenerator','minrequests'):
                configlist.append('minrequests')
                self.MIN_REQUESTS = Settings.config.getint('goalgenerator','minrequests')
            assert(self.MIN_REQUESTS > 0)
            if Settings.config.has_option('goalgenerator','minvenuespergoal'):
                self.MIN_VENUES_PER_GOAL = int(Settings.config.get('goalgenerator','minvenuespergoal'))
    #        self.PERCENTAGE_OF_ZERO_SOLUTION_TASKS = 50
    #        if Settings.config.has_option('GOALGENERATOR','PERCZEROSOLUTION'):
    #            self.PERCENTAGE_OF_ZERO_SOLUTION_TASKS = int(Settings.config.get('GOALGENERATOR','PERCZEROSOLUTION'))
    #        self.NO_REQUESTS_WITH_VALUE_NONE = False
    #        if Settings.config.has_option('GOALGENERATOR','NOREQUESTSWITHVALUENONE'):
    #            self.NO_REQUESTS_WITH_VALUE_NONE
    
            if self.MIN_VENUES_PER_GOAL > self.MAX_VENUES_PER_GOAL:
                logger.error('Invalid config: MIN_VENUES_PER_GOAL > MAX_VENUES_PER_GOAL')
            
            self.conditional_constraints = []
            self.conditional_constraints_slots = []
            
           
        def init_goal(self, otherDomainsConstraints, um_patience):
            '''
            Initialises the goal g with random constraints and requests
    
            :param otherDomainsConstraints: of constraints from other domains in this dialog which have already had goals generated.
            :type otherDomainsConstraints: list
            :param um_patience: the patiance value for this goal
            :type um_patience: int
            :returns: (instance) of :class:`UMGoal`
            '''
            
            # clean/parse the domainConstraints list - contains other domains already generated goals:
            self._set_other_domains_constraints(otherDomainsConstraints)
    
            # Set initial goal status vars
            goal = UMGoal(um_patience, domainString=self.dstring)
            logger.debug(str(goal))
            num_attempts_to_resample = 2000
            while True:
                num_attempts_to_resample -= 1
                # Randomly sample a goal (ie constraints):
                self._init_consts_requests(goal, um_patience)
                # Check that there are venues that satisfy the constraints:
                venues = Ontology.global_ontology.entity_by_features(self.dstring, constraints=goal.constraints)
                #logger.info('num_venues: %d' % len(venues))
                if self.MIN_VENUES_PER_GOAL < len(venues) < self.MAX_VENUES_PER_GOAL:
                    break
                if num_attempts_to_resample == 0:
                    logger.warning('Maximum number of goal resampling attempts reached.')
                    
            if self.CONDITIONAL_BEHAVIOUR:
                # now check self.generator.conditional_constraints list against self.goal -assume any values that are the same
                # are because they are conditionally copied over from earlier domains goal. - set self.goal.copied_constraints
                goal.set_copied_constraints(all_conditional_constraints=self.conditional_constraints)
    
            # logger.warning('SetSuitableVenues is deprecated.')
            return goal
            
        def _set_other_domains_constraints(self, otherDomainsConstraints):
            """Simplest approach for now: just look for slots with same name 
            """
            # Get a list of slots that are valid for this task.
            valid_const_slots = Ontology.global_ontology.getValidSlotsForTask(self.dstring) 
            self.conditional_constraints = {slot: [] for slot in valid_const_slots}
    
            if not self.CONDITIONAL_BEHAVIOUR:
                self.conditional_constraint_slots = []
                return
    
            for const in otherDomainsConstraints:
                if const.slot in valid_const_slots and const.val != "dontcare": #TODO think dontcare should be dealt with diff 
                    # issue is that first domain may be dontcare - but 2nd should be generated conditioned on first.
                    if const.op == "!=":
                        continue
                    
                    #TODO delete: if const.val in Ontology.global_ontology.ontology['informable'][const.slot]: #make sure value is valid for slot
                    if Ontology.global_ontology.is_value_in_slot(self.dstring, value=const.val, slot=const.slot):
                        self.conditional_constraints[const.slot] += [const.val]  
            self.conditional_constraint_slots = [s for s,v in self.conditional_constraints.items() if len(v)]
            return
    
    
        def _init_consts_requests(self, goal, um_patience):
            '''
            Randomly initialises constraints and requests of the given goal.
            '''
            goal.clear(um_patience, domainString=self.dstring)
    
            # Randomly pick a task: bar, hotel, or restaurant (in case of TownInfo)
            goal.request_type = Ontology.global_ontology.getRandomValueForSlot(self.dstring, slot='type')
    
            # Get a list of slots that are valid for this task.
            valid_const_slots = Ontology.global_ontology.getValidSlotsForTask(self.dstring)
            
            # First randomly sample some slots from those that are valid: 
            sampling_probs = Ontology.global_ontology.get_sample_prob(self.dstring, 
                                                                      candidate=valid_const_slots,
                                                                      conditional_values=self.conditional_constraint_slots)
            random_slots = list(Settings.random.choice(valid_const_slots,
                                    size=min(self.MAX_CONSTRAINTS, len(valid_const_slots)),
                                    replace=False,
                                    p=sampling_probs))
    
    
            # Now randomly fill in some constraints for the sampled slots:
            for slot in random_slots:
                goal.add_const(slot, Ontology.global_ontology.getRandomValueForSlot(self.dstring, slot=slot, 
                                                                nodontcare=False,
                                                                conditional_values=self.conditional_constraints[slot]))
            
            # Add requests. Assume that the user always wants to know the name of the place
            goal.requests['name'] = None
            
            if self.MIN_REQUESTS == self.MAX_REQUESTS:
                n = self.MIN_REQUESTS -1  # since 'name' is already included
            else:
                n = Settings.random.randint(low=self.MIN_REQUESTS-1,high=self.MAX_REQUESTS)
            valid_req_slots = Ontology.global_ontology.getValidRequestSlotsForTask(self.dstring)
            if n > 0 and len(valid_req_slots) >= n:   # ie more requests than just 'name'
                choosen = Settings.random.choice(valid_req_slots, n,replace=False)
                for reqslot in choosen:
                    goal.requests[reqslot] = None
    
    
    class UM(object):
        '''Simulated user
        
        :param None:
        '''
        def __init__(self, domainString):
            self.max_patience = 5
            self.sample_patience = None
            if Settings.config.has_option('goalgenerator', 'patience'): # only here for backwards compatibility; should actually be in um
                self.max_patience = Settings.config.get('goalgenerator', 'patience')
            if Settings.config.has_option('usermodel', 'patience'):
                self.max_patience = Settings.config.get('usermodel', 'patience')
            if isinstance(self.max_patience,str) and len(self.max_patience.split(',')) > 1:
                self.sample_patience = [int(x.strip()) for x in self.max_patience.split(',')]
                if len(self.sample_patience) != 2 or self.sample_patience[0] > self.sample_patience[1]:
                    logger.error('Patience should be either a single int or a range between 2 ints.')
            else:
                self.max_patience = int(self.max_patience)
            
            self.patience_old_style = False
            if Settings.config.has_option('usermodel', 'oldstylepatience'):
                self.patience_old_style = Settings.config.getboolean('usermodel', 'oldstylepatience')
    
            self.old_style_parameter_sampling = True
            if Settings.config.has_option('usermodel', 'oldstylesampling'):
                self.old_style_parameter_sampling = Settings.config.getboolean('usermodel', 'oldstylesampling')
                
            self.sampleParameters = False
            if Settings.config.has_option('usermodel', 'sampledialogueprobs'):
                self.sampleParameters = Settings.config.getboolean('usermodel', 'sampledialogueprobs')
                
            self.generator = self._load_domain_goal_generator(domainString)
            self.goal = None
            self.prev_goal = None
            self.hdcSim = self._load_domain_simulator(domainString)
            self.lastUserAct = None
            self.lastSysAct = None
            
        def init(self, otherDomainsConstraints):
            '''
            Initialises the simulated user.
            1. Initialises the goal G using the goal generator.
            2. Populates the agenda A using the goal G.
            Resets all UM status to their defaults.
    
            :param otherDomainsConstraints: of domain goals/constraints (slot=val) from other domains in dialog for which goal has already been generated.
            :type otherDomainsConstraints: list
            :returns None:
            '''
            if self.sampleParameters:
                self._sampleParameters()
    
            if self.sample_patience:
                self.max_patience = Settings.random.randint(self.sample_patience[0], self.sample_patience[1])
            
            self.goal = self.generator.init_goal(otherDomainsConstraints, self.max_patience)
            logger.debug(str(self.goal))
    
            self.lastUserAct = None
            self.lastSysAct = None
            self.hdcSim.init(self.goal, self.max_patience)  #uses infor in self.goal to do conditional generation of agenda as well.
    
        def receive(self, sys_act):
            '''
            This method is called to transmit the machine dialogue act to the user.
            It updates the goal and the agenda.
            :param sys_act: System action.
            :return:
            '''
            # Update previous goal.
            self.prev_goal = copy.deepcopy(self.goal)
    
            # Update the user patience level.
            if self.lastUserAct is not None and self.lastUserAct.act == 'repeat' and\
                            self.lastSysAct is not None and self.lastSysAct.act == 'repeat' and\
                            sys_act.act == 'repeat':
                # Endless cycle of repeating repeats: reduce patience to zero.
                logger.info("Endless cycle of repeating repeats. Setting patience to zero.")
                self.goal.patience = 0
    
            elif sys_act.act == 'badact' or sys_act.act == 'null' or\
                    (self.lastSysAct is not None and self.lastUserAct is not None and self.lastUserAct.act != 'repeat' and self.lastSysAct == sys_act):
                # Same action as last turn. Patience decreased.
                self.goal.patience -= 1
            elif self.patience_old_style:
                # not same action as last time so patience is restored
                self.goal.patience = self.max_patience
    
            if self.goal.patience < 1:
                logger.debug(str(self.goal))
                logger.debug('All patience gone. Clearing agenda.')
                self.hdcSim.agenda.clear()
                # Pushing bye act onto agenda.
                self.hdcSim.agenda.push(DiaAct.DiaAct('bye()'))
                return
    
            # Update last system action.
            self.lastSysAct = sys_act
    
            # Process the sys_act
            self.hdcSim.receive(sys_act, self.goal)
    
            # logger.warning('should update goal information')
    
        def respond(self):
            '''
            This method is called after receive() to get the user dialogue act response.
            The method first increments the turn counter, then pops n items off the agenda to form
            the response dialogue act. The agenda and goal are updated accordingly.
    
            :param None:
            :returns: (instance) of :class:`DiaAct` 
            '''
            user_output = self.hdcSim.respond(self.goal)
    
            if user_output.act == 'request' and len(user_output.items) > 0:
                # If there is a goal constraint on near, convert act type to confirm
                # TODO-  do we need to add more domain dependent type rules here for other domains beyond CamRestaurants?
                # this whole section mainly seems to stem from having "area" and "near" in ontology... 
                if user_output.contains_slot('near') and self.goal.contains_slot_const('near'):
                    for const in self.goal.constraints:
                        if const.slot == 'near':
                            near_const = const.val
                            near_op = const.op
                            break
                    # original: bug. constraints is a list --- near_const = self.goal.constraints['near']
                    if near_const != 'dontcare':
                        if near_op == "=":   # should be true for 'dontcare' value
                            #TODO - delete-WRONG-user_output.dact['act'] = 'confirm'
                            #TODO-delete-user_output.dact['slots'][0].val = near_const
                            user_output.act = 'confirm'
                            user_output.items[0].val = near_const
                        
            self.lastUserAct = user_output
            #self.goal.update_au(user_output)
            return user_output
        
        def _sampleParameters(self):
            if not self.old_style_parameter_sampling:
                self.max_patience = Settings.random.randint(2,10)
                
        def _load_domain_goal_generator(self, domainString):
            '''
            Loads and instantiates the respective goal generator object as configured in config file. The new object is returned.
            
            Default is GoalGenerator.
            
            .. Note:
                To dynamically load a class, the __init__() must take two arguments: domainString (str), conditional_behaviour (bool)
            
            :param domainString: the domain the goal generator will be loaded for.
            :type domainString: str
            :returns: goal generator object
            '''
            
            generatorClass = None
            
            if Settings.config.has_option('usermodel_' + domainString, 'goalgenerator'):
                generatorClass = Settings.config.get('usermodel_' + domainString, 'goalgenerator')
            
            if generatorClass is None:
                return GoalGenerator(domainString)
            else:
                try:
                    # try to view the config string as a complete module path to the class to be instantiated
                    components = generatorClass.split('.')
                    packageString = '.'.join(components[:-1]) 
                    classString = components[-1]
                    mod = __import__(packageString, fromlist=[classString])
                    klass = getattr(mod, classString)
                    return klass(domainString)
                except ImportError:
                    logger.error('Unknown domain ontology class "{}" for domain "{}"'.format(generatorClass, domainString))
                    
        def _load_domain_simulator(self, domainString):
            '''
            Loads and instantiates the respective simulator object as configured in config file. The new object is returned.
            
            Default is UMHdcSim.
            
            .. Note:
                To dynamically load a class, the __init__() must take one argument: domainString (str)
            
            :param domainString: the domain the simulator will be loaded for.
            :type domainString: str
            :returns: simulator object
            '''
            
            simulatorClass = None
            
            if Settings.config.has_option('usermodel_' + domainString, 'usersimulator'):
                simulatorClass = Settings.config.get('usermodel_' + domainString, 'usersimulator')
            
            if simulatorClass is None:
                return UMHdcSim.UMHdcSim(domainString)
            else:
                try:
                    # try to view the config string as a complete module path to the class to be instantiated
                    components = simulatorClass.split('.')
                    packageString = '.'.join(components[:-1]) 
                    classString = components[-1]
                    mod = __import__(packageString, fromlist=[classString])
                    klass = getattr(mod, classString)
                    return klass(domainString)
                except ImportError:
                    logger.error('Unknown domain ontology class "{}" for domain "{}"'.format(simulatorClass, domainString))
                    
                    
    
    #END OF FILE