Source code for pydialogue.environment.entities

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
This module lists the classes that represent the entities/objects in the simulation.
"""

import copy

from ..language import components as lc
from . import descriptions as tdescriptions

from ..language import phrases as tphrases
from ..language import sentences as tsentences


[docs] class BaseEntity: """ Creates an entity that is abstract (not part of the world). For example, in the following sentence: Andy, get a red apple, a red apple is BaseEntity because it can refer to more than one entity in the world. Please refer to the Entity class if you want to create an entity that is part of the world. Attributes ---------- properties : dict The entity can contain properties like size, color, and material. attributes: dict The entity can contain attributes like open, static, place, player, etc. This dictionary always has values None. This is what makes the attributes different from the properties. In the future, we plan to replace the attributes type with a set since the dictionary values are always None. objects: list The list of other entities the entity might contain. For example, this parameter can be used when the entity is a container, place, supporter, or hollow. undo_changes: list A list of functions that undo the changes made to the entity. description: Description The description of the entity. The description uses properties and attributes to describe the entity. Please check the descriptions.Description for more information. descriptions: dict It stores the previously generated descriptions after they are converted to Phrase-s. It is done, so that the phrases can be reused and time can be saved. random_gen : random.Random The random generator is used instead of random, so that if the dialogue is run again the same generation process will proceed. """ def __init__(self, properties=None, attributes=None, objects=None, undo_changes=None, random_gen=None): self.properties = ({} if properties is None else properties) self.attributes = ({} if attributes is None else attributes) self.objects = ([] if objects is None else objects) self.undo_changes = ([] if undo_changes is None else undo_changes) self.description = None self.descriptions = dict() self.random_gen = random_gen def __eq__(self, other): """ Checks whether two entities are equal. Comparing the two entities is only possible if the properties do not contain any entities. If they do, the __eq__ function might be stuck in an infinite recursion. Parameters ---------- other : Entity The other entity to be compared with self. Returns ------- is_equal: bool Indicates whether the two entities are the same. """ is_equal = False if isinstance(other, BaseEntity): is_equal = self.properties == other.properties and self.attributes == other.attributes return is_equal def __hash__(self): """ Computes the hash for the Entity. Returns ------- hash_code : int An integer value representing the hash code. """ hash_code = hash(lc.prepare_hash(set(list(self.properties.items())+list(self.attributes.items())))) return hash_code
[docs] def mycopy(self, memo): """ Makes a copy of the Entity object so that any changes to the original object do not affect the copied one. Parameters ---------- memo : dict A dictionary that prevents a recursive loop. It stores the entities by id and checks if the current entity is already copied. Returns ------- result: Entity The copied entity. """ self_id = id(self) result = memo.get(self_id) if result is not None: return result cls = self.__class__ result = cls.__new__(cls) memo[self_id] = result setattr(result, 'undo_changes', list()) result.properties = lc.mycopy_el(self.properties, memo) result.attributes = lc.mycopy_el(self.attributes, memo) result.objects = lc.mycopy_el(self.objects, memo) if self.description is not None: setattr(result, 'description', self.description.mycopy()) else: setattr(result, 'description', self.description) setattr(result, 'descriptions', copy.copy(self.descriptions)) return result
[docs] def describe(self, elements=None, add_describer=True): """ Describes an Entity based on the elements using an instance of the Phrase class. Parameters ---------- elements : list of strings A list of property and attribute keys or other strings that describe the Entity. These elements are later converted to a Phrase object. If the elements are not provided, first, a description is generated. add_describer : bool Whether to add describers for each of the Word-s in the Phrase. Please refer to :func:`phrases.description() <pydialogue.language.phrases.description>` for more information. Returns ------- phrase: Phrase The Phrase that describes the entity. """ if elements is None: if self.description is None: generated_desc = self.generate_description() self.set_description(generated_desc) included_elements = self.description.elements else: included_elements = elements prop_key_val = dict() for el in included_elements: if el in self.properties: prop_key_val[el] = self.properties[el] elif el in self.attributes: prop_key_val[el] = self.attributes[el] else: prop_key_val[el] = None prop_key_val = frozenset(lc.prepare_hash(list(prop_key_val.items()))) if prop_key_val not in self.descriptions: phrase = tphrases.description(self, included_elements, add_describer) self.descriptions[prop_key_val] = phrase def undo_desc(ent=self, prop_key_value=prop_key_val): del ent.descriptions[prop_key_value] self.undo_changes.append(undo_desc) else: phrase = self.descriptions[prop_key_val] return phrase
[docs] def generate_description(self): """ Generates a description using all the properties and attributes of the entity. For example, if the entity has a color property 'blue' and an attribute 'container', the description will be 'blue container' """ all_elements = list(self.properties.keys())+list(self.attributes.keys()) mandatory = [True] * len(all_elements) base_desc = tdescriptions.BaseDescription(self, mandatory, all_elements) base_desc.generate_description() return base_desc
[docs] def set_description(self, description): """ Sets the entity description and saves the previous one. """ self.description = description
[docs] def get_property(self, property_name): """ Get the property value given the property key. Return an empty string if the property is non-existent. """ return self.properties.get(property_name, '')
[docs] class Entity(BaseEntity): """ Creates an instance of an entity that belongs to the physical world. Attributes ---------- world: World The world that the entity belongs to. prop_seen: dict A mapping property_key : property_value, that indicates what properties are revealed in the context. prop_seen_neg: dict A mapping property key => list of property values that are not equal to the correct property value. For example, the entity's size might be 'small', and the list of property values might be ['medium', 'large']. The properties have to be revealed in the context. attr_seen : dict A mapping attribute : None that indicates what attributes are revealed in the context. In the future, this dictionary can be replaced with a set since all values of the dictionary are None. attr_seen_neg: dict A mapping attribute : None that indicates what attributes the entity does not have. In the future, the dictionary can be replaced with a set since all values of the dictionary are None. elem_exists: set A set of elements (properties and attributes) that the entity has and that are revealed in the context. elem_not_exists: set A set of elements (properties and attributes) that the entity does not have and that this information is revealed in the context. """ def __init__(self, world=None, properties=None, attributes=None, objects=None): undo_changes = world.undo_changes if world is not None else list() super().__init__(properties, attributes, objects, undo_changes) self.world = world self.prop_seen = dict() self.prop_seen_neg = dict() self.attr_seen = dict() self.attr_seen_neg = dict() self.elem_exists = set() self.elem_not_exists = set()
[docs] def change_world(self, new_world): """ Changes the world that the entity is part of. """ self.world = new_world self.undo_changes = new_world.undo_changes
[docs] def mycopy(self, memo): """ Makes a copy of the Entity object so that any changes to the original object do not affect the copied one. Parameters ---------- memo : dict A dictionary that prevents a recursive loop. It stores the entities by id and checks if the current entity is already copied. Returns ------- result: Entity The copied entity. """ self_id = self.properties.get("var_name") if self_id is None: self_id = id(self) result = memo.get(self_id) if result is not None: return result cls = self.__class__ result = cls.__new__(cls) memo[self_id] = result setattr(result, 'undo_changes', list()) result.properties = lc.mycopy_el(self.properties, memo) result.attributes = lc.mycopy_el(self.attributes, memo) result.objects = lc.mycopy_el(self.objects, memo) if self.description is not None: setattr(result, 'description', self.description.mycopy()) else: setattr(result, 'description', None) setattr(result, 'descriptions', copy.copy(self.descriptions)) setattr(result, 'world', self.world) setattr(result, 'prop_seen', copy.copy(self.prop_seen)) setattr(result, 'attr_seen', copy.copy(self.attr_seen)) setattr(result, 'prop_seen_neg', copy.copy(self.prop_seen_neg)) setattr(result, 'attr_seen_neg', copy.copy(self.attr_seen_neg)) setattr(result, 'elem_exists', copy.copy(self.elem_exists)) setattr(result, 'elem_not_exists', copy.copy(self.elem_not_exists)) return result
def __eq__(self, other): """ Checks whether two entities are equal. In the case where the Entity does not have a var_name, comparing the two entities is only possible if the properties do not contain entities. If they do, the __eq__ function might be stuck in an infinite recursion. Parameters ---------- other : Entity The other entity to be compared with self. Returns ------- is_equal: bool A boolean value indicates whether the two entities are the same. """ is_equal = False if isinstance(other, Entity): is_equal = self.properties['var_name'] == other.properties['var_name'] return is_equal def __gt__(self, other): """ Checks whether the entity is greater than another entity. The comparison is made based on the property 'size'. If the property 'size' is not available, None is returned. """ result = None if isinstance(other, Entity) and 'size' in self.properties and 'size' in other.properties: order_size = {('very', 'small'): -1, 'small': 0, 'medium': 1, 'big': 2, ('very', 'big'): 3} self_size = self.properties['size'] other_size = other.properties['size'] if isinstance(self_size, list): self_size = tuple(self_size) if isinstance(other_size, list): other_size = tuple(other_size) result = order_size[self_size] > order_size[other_size] return result def __hash__(self): """ Computes the hash for the Entity. Returns ------- hash_code : int An integer value representing the hash code. """ if "var_name" in self.properties: hash_code = hash(self.properties["var_name"]) else: hash_code = hash(lc.prepare_hash(set(list(self.properties.items())+list(self.attributes.items())))) return hash_code
[docs] def select_unique_descriptions(self, cand_descriptions=None): """ Selects the unique descriptions from the candidate descriptions. If the candidate descriptions are not provided, they are instantiated from the world.all_description_objects. Parameters ---------- cand_descriptions : list A list of :class:`~pydialogue.environment.descriptions.BaseDescription` instances that are used to describe the world's entities. Returns ------- seen : list The list of unique descriptions where all the description's elements are seen by the players. not_seen : list The list of unique descriptions where all the description's elements are not seen by the players. partially_seen : list The list of unique descriptions where some of the description's elements are seen by the players. """ unique_descriptions = [] if cand_descriptions is None: cand_descriptions = [] for descr in self.world.all_description_objects: new_instance = descr.mycopy() new_instance.item = self # do not copy the random_gen new_instance.random_gen = descr.random_gen cand_descriptions.append(new_instance) seen = [] not_seen = [] partially_seen = [] for desc in cand_descriptions: desc.generate_description() if len(desc.elements) > 0: unique_descriptions.append(desc) seen_elems = [] not_seen_elems = [] filtered_elems = [] for elem in desc.elements: if elem not in ['the', 'entity']: if elem in self.prop_seen or elem in self.attr_seen: seen_elems.append(elem) else: not_seen_elems.append(elem) filtered_elems.append(elem) if len(seen_elems) == len(filtered_elems): seen.append(desc) elif len(not_seen_elems) == len(filtered_elems): not_seen.append(desc) else: partially_seen.append(desc) return seen, not_seen, partially_seen
[docs] def generate_description(self, cand_descriptions=None, random_gen=None, relaxed=True): """ Generates a description of the entity. Parameters ---------- cand_descriptions : list, optional A list of candidate descriptions (instances of the class BaseDescription). random_gen : random.Random, optional The random_gen makes sure that the dialogue always has the same randomization in case the dialogue is restarted. relaxed : bool, optional If False, when generating the descriptions, priority is given to the descriptions generated for the first time (the agent has not seen any of the entity's properties or attributes). Otherwise, priority is given to the descriptions where the agent has seen some properties/attributes. Returns ------- description : Description The description of the entity. """ unique_seen, unique_not_seen, unique_partially_seen = self.select_unique_descriptions(cand_descriptions) description = None if len(unique_seen) > 0: description = self.random_gen.choice(unique_seen) elif len(unique_partially_seen) > 0 and relaxed: description = self.random_gen.choice(unique_partially_seen) elif len(unique_not_seen) > 0: description = self.random_gen.choice(unique_not_seen) return description
[docs] def top_location(self): """ Compute the top location of an item. For example, if the item is in a container and the container is in a room, then the top location of the item is the room. """ if 'location' not in self.properties: return None item_location = self.properties['location'][1] prev_location = item_location while True: if 'location' in item_location.properties: item_location = item_location.properties['location'][1] if prev_location != item_location: prev_location = item_location else: break else: break return item_location
[docs] def validate_reachability(self, player, location_entity, neg_res=None): """ Checks whether the entity (self) is reachable to the player at a specific location. Parameters ---------- player : Entity The player that executes the action. location_entity : Entity The location entity where self is found. neg_res : Sentence The negative response in case the entity is not reachable. For example, <player> can not get <self>. Returns ------- log : list Returns an empty list if the item is reachable to the player at the location. If the entity is not reachable, return the negative response concatenated together with the reason why. """ log = [] player_top_loc = player.top_location() top_location_entity = location_entity.top_location() if 'player' not in player.attributes: log.append(tsentences.cont([neg_res, tsentences.be(player, 'is', 'not', 'player')])) if (('type' in self.properties and self.properties['type'] != 'door') or 'type' not in self.properties) and top_location_entity != player_top_loc: log.append(tsentences.cont([neg_res, tsentences.be([player, "'s", 'location'], 'is', 'not', [top_location_entity.properties["location"][0], top_location_entity])])) if ('type' in self.properties and self.properties['type'] == 'door' and "door_to" in self.properties and self.properties["door_to"] != player_top_loc and self.properties["location"][1] != player_top_loc and location_entity == self.properties["location"][1]): res1 = tsentences.be([player, "'s", 'location'], 'is', 'not', [self.properties["door_to"].properties["location"][0], self.properties["door_to"]]) res2 = tsentences.be([player, "'s", 'location'], 'is', 'not', [self.properties["location"][0], self.properties["location"][1]]) log.append(tsentences.cont([neg_res, res1, res2])) if (("container" in location_entity.attributes and self.properties["location"][1] != location_entity.top_location())): curr_loc = location_entity log_locked = [] log_not_open = [] while True: if curr_loc == curr_loc.properties['location'][1]: break if "locked" in curr_loc.attributes: res1 = tsentences.be(curr_loc, 'is', None, 'locked') log_locked.append(tsentences.cont([neg_res, res1])) elif "openable" in curr_loc.attributes and "open" not in curr_loc.attributes: res1 = tsentences.be(curr_loc, 'is', 'not', 'open') log_not_open.append(tsentences.cont([neg_res, res1])) curr_loc = curr_loc.properties['location'][1] log += log_locked if len(log_locked) == 0: log += log_not_open return log