diff --git a/llm_config.yaml b/llm_config.yaml index cbea7afe..050bfb4f 100644 --- a/llm_config.yaml +++ b/llm_config.yaml @@ -35,6 +35,6 @@ PLAYER_ENTER_PROMPT: '{context} Zone info: {zone_info}; Npc e QUEST_PROMPT: '{context} Zone info: {zone_info}; Character: {character_card};\n[USER_START] Using the information supplied inside the tags, {character_name} needs someone to perform a task. Based on the following input, come up with a suitable reason for it, using {character_name}s personality and history. Task info: {base_quest}. Fill in this JSON template and do not write anything else: {{"reason":""}} \n\n ' NOTE_QUEST_PROMPT: '{context} Zone info: {zone_info};\n[USER_START]Using the information supplied inside the tags, generate a quest that starts from reading a note. The reader must find and talk to a person. Fill in the following JSON template and write nothing else.: {{"reason": "only what the note says. 50 words.", "type":"talk", "target":"who to talk to", "location":"", "name":"name of quest"}}' NOTE_LORE_PROMPT: '{context} Zone info: {zone_info};\n[USER_START]FUsing the information supplied inside the tags, decide what is written on a note that has been found. Use the provided story and world information to generate a piece of lore. Use about 50 words.' -ACTION_PROMPT: '{context}\n[USER_START]Act as as {character_name}.\nUsing the information supplied inside the tag, pick an action according to {character_name}s description and mood. If suitable, select something to perform the action on (target). The action should be in the supplied list and should be related to {character_name}s thoughts. Build on events in "History" without repeating them. Respond using JSON in the following format: """{action_template}"""' +ACTION_PROMPT: '{context}\n[USER_START]Act as as {character_name}.\nUsing the information supplied inside the tag, pick an action according to {character_name}s description and mood. If suitable, select something to perform the action on (target). The action should be in the supplied list and should be related to {character_name}s thoughts. Build on events in "History" without repeating them. Respond using JSON in the following format: """{action_template}""". Continue the sequence of events: {previous_events}' USER_START: '### Instruction:' USER_END: '### Response:' \ No newline at end of file diff --git a/tale/base.py b/tale/base.py index 8b030619..87ac7457 100644 --- a/tale/base.py +++ b/tale/base.py @@ -619,12 +619,13 @@ def __str__(self) -> str: return "" % (self.name, self.vnum, id(self)) def to_dict(self) -> Dict[str, Any]: - return {**super().to_dict(),**{ + dict_values = {**super().to_dict(),**{ "wc": self.wc, "base_damage": self.base_damage, "bonus_damage": self.bonus_damage, "weapon_type": self.type.name, }} + return {key: value for key, value in dict_values.items() if value != 0} class Armour(Item): @@ -643,10 +644,11 @@ def __init__(self, name: str, weight: int = 0, value: int = 0, ac: int = 0, wear self.wear_location = wear_location def to_dict(self) -> Dict[str, Any]: - return {**super().to_dict(),**{ + dict_values = {**super().to_dict(),**{ "ac": self.ac, "wear_location": self.wear_location.name, }} + return {key: value for key, value in dict_values.items() if value != 0} class Location(MudObject): """ diff --git a/tale/cmds/wizard.py b/tale/cmds/wizard.py index f71d2ae3..77794370 100644 --- a/tale/cmds/wizard.py +++ b/tale/cmds/wizard.py @@ -866,3 +866,27 @@ def do_set_goal(player: Player, parsed: base.ParseResult, ctx: util.Context) -> player.tell("%s goal set to %s" % (character, parsed.args[1])) except ValueError as x: raise ActionRefused(str(x)) + +@wizcmd("create_item") +def do_create_item(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None: + """Create an item in the current location.""" + if len(parsed.args) < 1: + raise ParseError("You need to define an item type. Name and description are optional") + item_dict = dict() + item_dict['type'] = parsed.args[0] + if len(parsed.args) > 1: + item_dict['name'] = parsed.args[1] + if len(parsed.args) > 2: + item_dict['short_descr'] = parsed.args[2] + catalogue_item = ctx.driver.story._catalogue.get_item(item_dict['type']) + item = None + if catalogue_item: + item = parse_utils.load_items([catalogue_item])[catalogue_item['name']] + item.short_description = item_dict.get('short_descr', '') + if not item: + item = list(parse_utils.load_items([item_dict]).values())[0] + if item: + player.location.insert(item, actor=None) + player.tell(item.name + ' added.', evoke=False) + else: + raise ParseError("Item could not be added") diff --git a/tale/driver.py b/tale/driver.py index 9bcd1b8f..8bb5cdbb 100644 --- a/tale/driver.py +++ b/tale/driver.py @@ -858,19 +858,21 @@ def load_character(self, player: player.Player, char_data: dict) -> LivingNpc: race = character.race, occupation = character.occupation) npc.autonomous = character.autonomous + npc.output_thoughts = character.output_thoughts wearing = character.wearing.split(',') for item in wearing: if item: wearable = base.Wearable(name=item.lower().strip()) npc.set_wearable(wearable) - + if character.wielding: + npc.wielding = base.Weapon(name=character.wielding.lower()) npc.following = player npc.stats.hp = character.hp if isinstance(self.story, DynamicStory): dynamic_story = typing.cast(DynamicStory, self.story) dynamic_story.world.add_npc(npc) player.location.insert(npc, None) - player.location.tell("%s arrives." % npc.title) + player.location.tell("%s arrives." % npc.title, extra_context=f'Location:{player.location.description}; {npc.character_card}') return npc @property diff --git a/tale/items/book.py b/tale/items/book.py index 5186d126..aed69b08 100644 --- a/tale/items/book.py +++ b/tale/items/book.py @@ -1,6 +1,3 @@ - - - from tale.base import Item diff --git a/tale/llm/LivingNpc.py b/tale/llm/LivingNpc.py index 7cfcfab2..0a49784c 100644 --- a/tale/llm/LivingNpc.py +++ b/tale/llm/LivingNpc.py @@ -1,8 +1,9 @@ from tale.llm.item_handling_result import ItemHandlingResult import tale.llm.llm_cache as llm_cache -from tale import lang, mud_context +from tale import lang, mud_context, story from tale.base import ContainingType, Living, ParseResult from tale.errors import LlmResponseException +from tale.llm.responses.ActionResponse import ActionResponse from tale.player import Player @@ -30,12 +31,12 @@ def __init__(self, name: str, gender: str, *, self.goal = None # type: str # a free form string describing the goal of the NPC self.quest = None # type: Quest # a quest object self.deferred_actions = set() # type: set[str] + self.example_voice = '' # type: str self.autonomous = False + self.output_thoughts = False def notify_action(self, parsed: ParseResult, actor: Living) -> None: # store even our own events. - event_hash = llm_cache.cache_event(unpad_text(parsed.unparsed)) - self._observed_events.append(event_hash) if actor is self or parsed.verb in self.verbs: return # avoid reacting to ourselves, or reacting to verbs we already have a handler for greet = False @@ -57,6 +58,8 @@ def notify_action(self, parsed: ParseResult, actor: Living) -> None: self.do_say(parsed.unparsed, actor) elif (targeted and parsed.verb == "idle-action") or parsed.verb == "location-event": + event_hash = llm_cache.cache_event(unpad_text(parsed.unparsed)) + self._observed_events.append(event_hash) self._do_react(parsed, actor) elif targeted and parsed.verb == "give": parsed_split = parsed.unparsed.split(" to ") @@ -73,15 +76,20 @@ def notify_action(self, parsed: ParseResult, actor: Living) -> None: self.quest.check_completion({"item":result.item, "npc":result.to}) self.do_say(parsed.unparsed, actor) elif parsed.verb == 'attack' and targeted: + event_hash = llm_cache.cache_event(unpad_text(parsed.unparsed)) + self._observed_events.append(event_hash) # TODO: should llm decide sentiment? self.sentiments[actor.title] = 'hostile' + else: + event_hash = llm_cache.cache_event(unpad_text(parsed.unparsed)) + self._observed_events.append(event_hash) if self.quest and self.quest.is_completed(): # do last to give npc chance to react self._clear_quest() def do_say(self, what_happened: str, actor: Living) -> None: - tell_hash = llm_cache.cache_tell('{actor.title} says {what_happened}'.format(actor=actor, what_happened=unpad_text(what_happened))) - self._conversations.append(tell_hash) + tell_hash = llm_cache.cache_event('{actor.title} says {what_happened}'.format(actor=actor, what_happened=unpad_text(what_happened))) + self._observed_events.append(tell_hash) short_len = False if isinstance(actor, Player) else True item = None sentiment = None @@ -90,14 +98,13 @@ def do_say(self, what_happened: str, actor: Living) -> None: response = self.autonomous_action() else: response, item, sentiment = mud_context.driver.llm_util.generate_dialogue( - conversation=llm_cache.get_tells(self._conversations), + conversation=llm_cache.get_events(self._observed_events), character_card = self.character_card, character_name = self.title, target = actor.title, target_description = actor.short_description, sentiment = self.sentiments.get(actor.title, ''), location_description=self.location.look(exclude_living=self), - event_history=llm_cache.get_events(self._observed_events), short_len=short_len) if response: if not self.avatar: @@ -108,8 +115,8 @@ def do_say(self, what_happened: str, actor: Living) -> None: if not response: raise LlmResponseException("Failed to parse dialogue") - tell_hash = llm_cache.cache_tell('{actor.title} says: {response}'.format(actor=self, response=unpad_text(response))) - self._conversations.append(tell_hash) + tell_hash = llm_cache.cache_event('{actor.title} says: {response}'.format(actor=self, response=unpad_text(response))) + self._observed_events.append(tell_hash) self._defer_result(response, verb='say') if item: self.handle_item_result(ItemHandlingResult(item=item, from_=self.title, to=actor.title), actor) @@ -206,63 +213,78 @@ def autonomous_action(self) -> str: action = mud_context.driver.llm_util.free_form_action(character_card=self.character_card, character_name=self.title, location=self.location, - event_history=llm_cache.get_events(self._observed_events)) + event_history=llm_cache.get_events(self._observed_events)) # type: ActionResponse if not action: return None defered_actions = [] - if action.get('goal', ''): - self.goal = action['goal'] - if action.get('text', ''): - text = action['text'] - tell_hash = llm_cache.cache_tell('{actor.title} says: "{response}"'.format(actor=self, response=unpad_text(text))) - self._conversations.append(tell_hash) - #if mud_context.config.custom_resources: - if action.get('target'): - target = self.location.search_living(action['target']) + if action.goal: + self.goal = action.goal + if self.output_thoughts and action.thoughts: + self.tell_others(f'\n{self.title} thinks: * ' + action.thoughts + ' *', evoke=False) + if action.text: + text = action.text + tell_hash = llm_cache.cache_event('{actor.title} says: "{response}"'.format(actor=self, response=unpad_text(text))) + self._observed_events.append(tell_hash) + if action.target: + target = self.location.search_living(action.target) if target: - #target.tell(text, evoke=False) + target.tell('\n' + text, evoke=False) target.notify_action(ParseResult(verb='say', unparsed=text, who_list=[target]), actor=self) + else: + self.tell_others('\n' + text, evoke=False) + else: + self.tell_others('\n' + text, evoke=False) defered_actions.append(f'"{text}"') - if not action.get('action', ''): + if not action.action: return '\n'.join(defered_actions) - if action['action'] == 'move': + if action.action == 'move': try: - exit = self.location.exits[action['target']] + exit = self.location.exits[action.target] except KeyError: exit = None if exit: self.move(target=exit.target, actor=self, direction_names=exit.names) - elif action['action'] == 'give': - result = ItemHandlingResult(item=action['item'], to=action['target'], from_=self.title) + elif action.action == 'give' and action.item and action.target: + result = ItemHandlingResult(item=action.item, to=action.target, from_=self.title) self.handle_item_result(result, actor=self) - elif action['action'] == 'take': - item = self.search_item(action['item'], include_location=True, include_inventory=False) # Type: Item + elif action.action == 'take' and action.item: + item = self.search_item(action.item, include_location=True, include_inventory=False) # Type: Item if item: item.move(target=self, actor=self) defered_actions.append(f"{self.title} takes {item.title}") - elif action['action'] == 'attack': - target = self.location.search_living(action['target']) + elif action.action == 'attack' and action.target: + target = self.location.search_living(action.target) if target: self.start_attack(target) defered_actions.append(f"{self.title} attacks {target.title}") + elif action.action == 'wear' and action.item: + item = self.search_item(action.item, include_location=True, include_inventory=False) + if item: + self.set_wearable(item) + defered_actions.append(f"{self.title} wears {item.title}") return '\n'.join(defered_actions) def _defer_result(self, action: str, verb: str="idle-action"): + """ Defer an action to be performed at the next tick, + or immediately if the server tick method is set to command""" if mud_context.config.custom_resources and self.avatar: action = pad_text_for_avatar(text=action, npc_name=self.title) else: action = f"{self.title} : {action}" self.deferred_actions.add(action) - mud_context.driver.defer(1.0, self.tell_action_deferred) + if mud_context.config.server_tick_method == story.TickMethod.COMMAND: + self.tell_action_deferred() + else: + mud_context.driver.defer(0.5, self.tell_action_deferred) def tell_action_deferred(self): - actions = '\n'.join(self.deferred_actions) + actions = '\n'.join(self.deferred_actions) + '\n' deferred_action = ParseResult(verb='idle-action', unparsed=actions, who_info=None) - self.tell_others(actions + '\n') self.location._notify_action_all(deferred_action, actor=self) + self.tell_others(actions) self.deferred_actions.clear() def get_observed_events(self, amount: int) -> list: @@ -277,7 +299,7 @@ def character_card(self) -> str: items = [] for i in self.inventory: items.append(f'"{str(i.name)}"') - return '{{"name":"{name}", "gender":"{gender}","age":{age},"occupation":"{occupation}","personality":"{personality}","appearance":"{description}","items":[{items}], "race":"{race}", "quest":"{quest}", "wearing":"{wearing}"}}'.format( + return '{{"name":"{name}", "gender":"{gender}","age":{age},"occupation":"{occupation}","personality":"{personality}","appearance":"{description}","items":[{items}], "race":"{race}", "quest":"{quest}", "example_voice":"{example_voice}", "wearing":"{wearing}", "wielding":"{wielding}"}}'.format( name=self.title, gender=lang.gender_string(self.gender), age=self.age, @@ -287,14 +309,15 @@ def character_card(self) -> str: race=self.stats.race, quest=self.quest, goal=self.goal, + example_voice=self.example_voice, wearing=','.join([f'"{str(i.name)}"' for i in self.get_worn_items()]), + wielding=self.wielding.to_dict() if self.wielding else None, items=','.join(items)) def dump_memory(self) -> dict: return dict( known_locations=self.known_locations, observed_events=self._observed_events, - conversations=self._conversations, sentiments=self.sentiments, action_history=self.action_history, planned_actions=self.planned_actions, @@ -303,8 +326,7 @@ def dump_memory(self) -> dict: def load_memory(self, memory: dict): self.known_locations = memory.get('known_locations', {}) - self._observed_events = memory.get('observed_events', []) - self._conversations = memory.get('conversations', []) + self._observed_events = memory.get('observed_events', []) + memory.get('conversations', []) self.sentiments = memory.get('sentiments', {}) self.action_history = memory.get('action_history', []) self.planned_actions = memory.get('planned_actions', []) diff --git a/tale/llm/character.py b/tale/llm/character.py index 06fa96c7..8ec47b31 100644 --- a/tale/llm/character.py +++ b/tale/llm/character.py @@ -12,6 +12,7 @@ from tale.llm.contexts.ActionContext import ActionContext from tale.llm.llm_io import IoUtil from tale.llm.contexts.DialogueContext import DialogueContext +from tale.llm.responses.ActionResponse import ActionResponse from tale.load_character import CharacterV2 @@ -35,7 +36,6 @@ def __init__(self, backend: str, io_util: IoUtil, default_body: dict): def generate_dialogue(self, context: DialogueContext, sentiment = '', - event_history = '', short_len : bool=False): prompt = self.pre_prompt @@ -46,7 +46,6 @@ def generate_dialogue(self, character2=context.speaker_name, character1=context.target_name, dialogue_template=self.dialogue_template, - history=event_history, sentiment=sentiment) request_body = deepcopy(self.default_body) request_body['grammar'] = self.json_grammar @@ -102,7 +101,7 @@ def perform_idle_action(self, character_name: str, location: Location, story_con character=character_card, items=items, characters=json.dumps(characters), - history=event_history, + history=event_history.replace('', '\n'), sentiments=json.dumps(sentiments)) request_body = deepcopy(self.default_body) if self.backend == 'kobold_cpp': @@ -135,17 +134,18 @@ def perform_reaction(self, action: str, character_name: str, acting_character_na character=character_card, acting_character_name=acting_character_name, story_context=story_context, - history=event_history, + history=event_history.replace('', '\n'), sentiment=sentiment) request_body = deepcopy(self.default_body) text = self.io_util.synchronous_request(request_body, prompt=prompt) return parse_utils.trim_response(text) + "\n" - def free_form_action(self, action_context: ActionContext): + def free_form_action(self, action_context: ActionContext) -> ActionResponse: prompt = self.pre_prompt prompt += self.free_form_action_prompt.format( context = '{context}', character_name=action_context.character_name, + previous_events=action_context.event_history.replace('', '\n'), action_template=self.action_template) request_body = deepcopy(self.default_body) request_body['grammar'] = self.json_grammar @@ -154,32 +154,8 @@ def free_form_action(self, action_context: ActionContext): if not text: return None response = json.loads(parse_utils.sanitize_json(text)) - return self._sanitize_free_form_response(response) + return ActionResponse(response) except Exception as exc: print('Failed to parse action ' + str(exc)) return None - - - def _sanitize_free_form_response(self, action: dict): - if action.get('text'): - if isinstance(action['text'], list): - action['text'] = action['text'][0] - if action.get('target'): - target_name = action['target'] - if isinstance(target_name, list): - action['target'] = target_name[0] - elif isinstance(target_name, dict): - action['target'] = target_name.get('name', '') - if action.get('item'): - item_name = action['item'] - if isinstance(item_name, list): - action['item'] = item_name[0] - elif isinstance(item_name, dict): - action['item'] = item_name.get('name', '') - if action.get('action'): - action_name = action['action'] - if isinstance(action_name, list): - action['action'] = action_name[0] - elif isinstance(action_name, dict): - action['action'] = action_name.get('action', '') - return action \ No newline at end of file + \ No newline at end of file diff --git a/tale/llm/contexts/ActionContext.py b/tale/llm/contexts/ActionContext.py index 5c64d7ef..4d53a3d7 100644 --- a/tale/llm/contexts/ActionContext.py +++ b/tale/llm/contexts/ActionContext.py @@ -12,7 +12,7 @@ def __init__(self, story_context: str, story_type: str, character_name: str, cha self.story_type = story_type self.character_name = character_name self.character_card = character_card - self.event_history = event_history + self.event_history = event_history.replace('', '\n') self.location = location diff --git a/tale/llm/llm_cache.py b/tale/llm/llm_cache.py index 10c08c4c..51d0d84f 100644 --- a/tale/llm/llm_cache.py +++ b/tale/llm/llm_cache.py @@ -4,7 +4,6 @@ event_cache = {} look_cache = {} -tell_cache = {} def generate_hash(item: str) -> int: """ Generates a hash for an item. """ @@ -21,7 +20,7 @@ def cache_event(event: str, event_hash: int = -1) -> int: def get_events(event_hashes: [int]) -> str: """ Gets events from the cache. """ - return ", ".join([event_cache.get(event_hash, '') for event_hash in event_hashes]) + return "".join([event_cache.get(event_hash, '') for event_hash in event_hashes]) def cache_look(look: str, look_hash: int = -1) -> int: """ Adds an event to the cache. @@ -36,30 +35,14 @@ def get_looks(look_hashes: [int]) -> str: """ Gets an event from the cache. """ return ", ".join([look_cache.get(look_hash, '') for look_hash in look_hashes]) - -def cache_tell(tell: str, tell_hash: int = -1) -> int: - """ Adds a tell to the cache. - Generates a hash if none supplied""" - if tell_hash == -1: - tell_hash = generate_hash(tell) - if tell_cache.get(tell_hash) == None: - tell_cache[tell_hash] = tell - return tell_hash - -def get_tells(tell_hashes: [int]) -> str: - """ Gets tells from the cache as a string """ - return "".join([tell_cache.get(tell_hash, '') for tell_hash in tell_hashes]) - - def load(cache_file: dict): global event_cache, look_cache, tell_cache """ Loads the caches from disk. """ event_cache = cache_file.get("events", {}) look_cache = cache_file.get("looks", {}) - tell_cache = cache_file.get("tells", {}) def json_dump() -> dict: """ Saves the caches to disk. """ - return {"events":event_cache, "looks":look_cache, "tells":tell_cache} + return {"events":event_cache, "looks":look_cache} diff --git a/tale/llm/llm_ext.py b/tale/llm/llm_ext.py index b5adcc54..a87b4df6 100644 --- a/tale/llm/llm_ext.py +++ b/tale/llm/llm_ext.py @@ -227,11 +227,21 @@ def add_creature(self, creature: dict) -> bool: self._creatures.append(creature) return True - def get_creatures(self) -> dict: + def get_creatures(self) -> []: return self._creatures - def get_items(self) -> dict: + def get_items(self) -> []: return self._items + def get_item(self, name: str) -> dict: + for item in self._items: + if item['name'] == name: + return item + + def get_creature(self, name: str) -> dict: + for creature in self._creatures: + if creature['name'] == name: + return creature + def to_json(self) -> dict: return dict(items=self._items, creatures=self._creatures) diff --git a/tale/llm/llm_utils.py b/tale/llm/llm_utils.py index a6c67e20..87f08146 100644 --- a/tale/llm/llm_utils.py +++ b/tale/llm/llm_utils.py @@ -13,6 +13,7 @@ from tale.llm.llm_io import IoUtil from tale.llm.contexts.DialogueContext import DialogueContext from tale.llm.quest_building import QuestBuilding +from tale.llm.responses.ActionResponse import ActionResponse from tale.llm.story_building import StoryBuilding from tale.llm.world_building import WorldBuilding from tale.player_utils import TextBuffer @@ -112,7 +113,6 @@ def generate_dialogue(self, conversation: str, target_description: str='', sentiment = '', location_description = '', - event_history='', short_len : bool=False): dialogue_context = DialogueContext(story_context=self.__story_context, location_description=location_description, @@ -123,7 +123,6 @@ def generate_dialogue(self, conversation: str, conversation=conversation) return self._character.generate_dialogue(context=dialogue_context, sentiment=sentiment, - event_history=event_history, short_len=short_len) def update_memory(self, rolling_prompt: str, response_text: str): @@ -231,7 +230,7 @@ def generate_image(self, character_name: str, character_appearance: dict = '', s copy_single_image('./', image_name + '.jpg') return result - def free_form_action(self, location: Location, character_name: str, character_card: str = '', event_history: str = ''): + def free_form_action(self, location: Location, character_name: str, character_card: str = '', event_history: str = '') -> ActionResponse: action_context = ActionContext(story_context=self.__story_context, story_type=self.__story_type, character_name=character_name, diff --git a/tale/llm/responses/ActionResponse.py b/tale/llm/responses/ActionResponse.py new file mode 100644 index 00000000..fb1b958b --- /dev/null +++ b/tale/llm/responses/ActionResponse.py @@ -0,0 +1,37 @@ + + +class ActionResponse(): + + def __init__(self, response: dict): + response = self._sanitize_free_form_response(response) + self.goal = response.get('goal', '') + self.text = response.get('text', '') + self.target = response.get('target', '') + self.item = response.get('item', '') + self.action = response.get('action', '') + self.sentiment = response.get('sentiment', '') + self.thoughts = response.get('thoughts', '') + + def _sanitize_free_form_response(self, action: dict): + if action.get('text'): + if isinstance(action['text'], list): + action['text'] = action['text'][0] + if action.get('target'): + target_name = action['target'] + if isinstance(target_name, list): + action['target'] = target_name[0] + elif isinstance(target_name, dict): + action['target'] = target_name.get('name', '') + if action.get('item'): + item_name = action['item'] + if isinstance(item_name, list): + action['item'] = item_name[0] + elif isinstance(item_name, dict): + action['item'] = item_name.get('name', '') + if action.get('action'): + action_name = action['action'] + if isinstance(action_name, list): + action['action'] = action_name[0] + elif isinstance(action_name, dict): + action['action'] = action_name.get('action', '') + return action \ No newline at end of file diff --git a/tale/load_character.py b/tale/load_character.py index a9e0ba50..482cf293 100644 --- a/tale/load_character.py +++ b/tale/load_character.py @@ -47,7 +47,9 @@ def __init__(self, name: str='', hp: int=10, aliases: list=[], avatar: str = '', - wearing: str= '') -> None: + wearing: str= '', + wielding: str='', + items: str = '') -> None: self.name = name self.race = race self.gender = gender @@ -61,6 +63,9 @@ def __init__(self, name: str='', self.hp = hp self.avatar = avatar self.wearing = wearing + self.wielding = wielding + self.items = items + self.autonomous = False if aliases: self.aliases = aliases @@ -69,7 +74,7 @@ def from_json(self, json: dict): self.title = json.get('title', json.get('name')) self.race = json.get('race', 'human').lower() self.gender = json.get('gender', 'f')[0].lower() - description = json.get('description') + description = json.get('description', '') self.description = description self.appearance = json.get('appearance', description.split(';')[0]) self.personality = json.get('personality', '') @@ -80,6 +85,9 @@ def from_json(self, json: dict): self.aliases = json.get('aliases', []) self.avatar = json.get('avatar', '') self.wearing = json.get('wearing', '') + self.wielding = json.get('wielding', '') + self.items = json.get('items', []) self.autonomous = json.get('autonomous', False) + self.output_thoughts = json.get('output_thoughts', False) return self diff --git a/tale/parse_utils.py b/tale/parse_utils.py index 82ebbd12..e375af29 100644 --- a/tale/parse_utils.py +++ b/tale/parse_utils.py @@ -171,8 +171,7 @@ def load_npcs(json_npcs: [], locations = {}) -> dict: new_npc.aliases.add(name.split(' ')[0].lower()) new_npc.stats.set_weapon_skill(WeaponType.UNARMED, random.randint(10, 30)) new_npc.stats.level = npc.get('level', 1) - if npc.get('autonomous', False): - new_npc.autonomous = True + new_npc.autonomous = npc.get('autonomous', False) if npc.get('stats', None): new_npc.stats = load_stats(npc['stats']) diff --git a/tale/tio/if_browser_io.py b/tale/tio/if_browser_io.py index 934950b8..bcdb1cfd 100644 --- a/tale/tio/if_browser_io.py +++ b/tale/tio/if_browser_io.py @@ -480,6 +480,7 @@ def wsgi_serve_static(self, path: str, environ: Dict[str, Any], start_response: def modify_web_page(self, player_connection: PlayerConnection, html_content: str) -> None: """Modify the html before it is sent to the browser.""" if not "wizard" in player_connection.player.privileges: + html_content = html_content.replace('', '') html_content = html_content.replace('', '') return html_content diff --git a/tale/web/story.html b/tale/web/story.html index 31d74f6e..9edbc2e2 100644 --- a/tale/web/story.html +++ b/tale/web/story.html @@ -44,6 +44,7 @@

Your browser doesn't have Javascript or it is disabled. You can't use this w + diff --git a/tests/test_character_loader.py b/tests/test_character_loader.py index 4c7d156f..d2401d2b 100644 --- a/tests/test_character_loader.py +++ b/tests/test_character_loader.py @@ -54,4 +54,5 @@ def _verify_character(self, character: CharacterV2): assert(character.appearance == 'test appearance') assert(character.description == 'test description') assert(character.aliases == ['alias1', 'alias2']) + assert(character.autonomous == False) diff --git a/tests/test_llm_cache.py b/tests/test_llm_cache.py index 378fdc80..060eaacd 100644 --- a/tests/test_llm_cache.py +++ b/tests/test_llm_cache.py @@ -1,14 +1,10 @@ import json +import os import tale.llm.llm_cache as llm_cache class TestLlmCache(): """ Test LlmCache class""" - def setup_method(self, test_method): - llm_cache.event_cache = {} - llm_cache.look_cache = {} - llm_cache.tell_cache = {} - def test_hash(self): """ Test hash function """ hash_value = llm_cache.generate_hash("test") @@ -29,7 +25,7 @@ def test_cache_event(self): llm_cache.cache_event("test2", event_hash2) assert llm_cache.get_events([event_hash2]) == "test2" - assert llm_cache.get_events([event_hash, event_hash2]) == "test, test2" + assert llm_cache.get_events([event_hash, event_hash2]) == "testtest2" def test_cache_look(self): """ Test cache_look function """ @@ -44,20 +40,6 @@ def test_cache_look(self): assert llm_cache.get_looks([look_hash, look_hash2]) == "test, test2" - - def test_cache_tell(self): - """ Test cache_tell function """ - tell_hash = llm_cache.cache_tell("test") - assert(tell_hash != -1) - assert llm_cache.get_tells([tell_hash]) == "test" - - tell_hash2 = hash("test2") - assert(tell_hash2 != -1) - llm_cache.cache_tell("test2", tell_hash2) - assert llm_cache.get_tells([tell_hash2]) == "test2" - - assert llm_cache.get_tells([tell_hash, tell_hash2]) == "testtest2" - def test_load(self): """ Test load function """ with open("tests/files/test_cache.json", "r") as fp: @@ -66,13 +48,11 @@ def test_load(self): assert("test event" in llm_cache.event_cache.values()) assert("test look" in llm_cache.look_cache.values()) - assert("test tell" in llm_cache.tell_cache.values()) def test_save(self): """ Test save function """ llm_cache.cache_event("test event") llm_cache.cache_look("test look") - llm_cache.cache_tell("test tell") json_dump = llm_cache.json_dump() with open("tests/files/test_cache.json", "w") as fp: json.dump(json_dump, fp, indent=4) diff --git a/tests/test_llm_ext.py b/tests/test_llm_ext.py index c50cb5e1..74db0537 100644 --- a/tests/test_llm_ext.py +++ b/tests/test_llm_ext.py @@ -7,7 +7,7 @@ import tale from tale.llm import llm_cache -from tale.base import Exit, Item, Living, Location, ParseResult +from tale.base import Exit, Item, Living, Location, ParseResult, Weapon from tale.coord import Coord from tale.llm.LivingNpc import LivingNpc from tale.llm.item_handling_result import ItemHandlingResult @@ -66,12 +66,16 @@ def test_handle_item_result_drop(self): def test_character_card(self): npc = LivingNpc(name='test', gender='m', age=42, personality='') - npc.init_inventory([self.drink]) + knife = Weapon("knife", "knife", descr="A sharp knife.") + npc.wielding = knife + npc.init_inventory([self.drink, knife]) card = npc.character_card assert('ale' in card) json_card = json.loads(card) assert(json_card['name'] == 'test') - assert(json_card['items'][0] == 'ale') + assert('ale' in json_card['items']) + assert('knife' in json_card['items']) + assert(eval(json_card['wielding']) == knife.to_dict()) def test_wearing(self): npc = LivingNpc(name='test', gender='m', age=42, personality='') @@ -84,7 +88,6 @@ def test_memory(self): npc = LivingNpc(name='test', gender='m', age=42, personality='') npc._observed_events = [llm_cache.cache_event('test_event'), llm_cache.cache_event('test_event 2')] - npc._conversations = [llm_cache.cache_tell('test_tell'), llm_cache.cache_tell('test_tell_2'),llm_cache.cache_tell('test_tell_3')] npc.sentiments = {'test': 'neutral'} memories_json = npc.dump_memory() memories = json.loads(json.dumps(memories_json)) @@ -95,11 +98,9 @@ def test_memory(self): assert(memories['known_locations'] == {}) assert(memories['observed_events'] == list(npc_clean._observed_events)) - assert(memories['conversations'] == npc_clean._conversations) assert(memories['sentiments'] == npc_clean.sentiments) - assert(llm_cache.get_events(npc_clean._observed_events) == 'test_event, test_event 2') - assert(llm_cache.get_tells(npc_clean._conversations) == 'test_telltest_tell_2test_tell_3') + assert(llm_cache.get_events(npc_clean._observed_events) == 'test_eventtest_event 2') def test_avatar_not_exists(self): npc = LivingNpc(name='test', gender='m', age=42, personality='') @@ -110,7 +111,7 @@ def test_get_observed_events(self): npc = LivingNpc(name='test', gender='m', age=42, personality='') npc._observed_events = [llm_cache.cache_event('test_event'), llm_cache.cache_event('test_event 2')] assert(npc.get_observed_events(1) == 'test_event 2') - assert(npc.get_observed_events(2) == 'test_event, test_event 2') + assert(npc.get_observed_events(2) == 'test_eventtest_event 2') # def test_avatar_exists(self): @@ -157,11 +158,13 @@ def test_do_say(self): json={'results':[{'text':'{"response": "Hello there, how can I assist you today?", "sentiment":"kind"}'}]}, status=200) npc.do_say(what_happened='something', actor=npc2) assert(npc.sentiments['actor'] == 'kind') - assert(len(npc._conversations) == 2) + assert(len(npc._observed_events) == 2) @responses.activate def test_idle_action(self): + mud_context.config.server_tick_method = 'TIMER' npc = LivingNpc(name='test', gender='f', age=35, personality='') + npc.autonomous = False responses.add(responses.POST, self.dummy_backend_config['URL'] + self.dummy_backend_config['ENDPOINT'], json={'results':[{'text':'"sits down on a chair"'}]}, status=200) self.llm_util._character.io_util.response = [] diff --git a/tests/test_llm_utils.py b/tests/test_llm_utils.py index 75952481..229c7c8b 100644 --- a/tests/test_llm_utils.py +++ b/tests/test_llm_utils.py @@ -14,6 +14,7 @@ from tale.json_story import JsonStory from tale.llm.llm_io import IoUtil from tale.llm.llm_utils import LlmUtil +from tale.llm.responses.ActionResponse import ActionResponse from tale.npc_defs import StationaryMob from tale.races import UnarmedAttack from tale.util import MoneyFormatterFantasy @@ -110,31 +111,31 @@ def test_free_form_action(self): self.llm_util._character.io_util.response = '{"action":"test_action", "text":"test response", "target":"test target", "item":"test item"}' location = Location(name='Test Location') self.llm_util.set_story(self.story) - result = self.llm_util.free_form_action(location=location, character_name='', character_card='', event_history='') + result = self.llm_util.free_form_action(location=location, character_name='', character_card='', event_history='') # type: ActionResponse assert(result) - assert(result["action"] == 'test_action') - assert(result["text"] == 'test response') - assert(result["target"] == 'test target') - assert(result["item"] == 'test item') + assert(result.action == 'test_action') + assert(result.text == 'test response') + assert(result.target == 'test target') + assert(result.item == 'test item') def test_free_form_action_lists(self): self.llm_util._character.io_util.response = '{"action":["test_action"], "text":["test response"], "target":["test target"], "item":["test item"]}' location = Location(name='Test Location') self.llm_util.set_story(self.story) - result = self.llm_util.free_form_action(location=location, character_name='', character_card='', event_history='') + result = self.llm_util.free_form_action(location=location, character_name='', character_card='', event_history='') # type: ActionResponse assert(result) - assert(result["action"] == 'test_action') - assert(result["text"] == 'test response') - assert(result["target"] == 'test target') - assert(result["item"] == 'test item') + assert(result.action == 'test_action') + assert(result.text == 'test response') + assert(result.target == 'test target') + assert(result.item == 'test item') def test_free_form_action_dict(self): self.llm_util._character.io_util.response = '{"action":{"action":"test_action"}, "target":{"name":"test target"}}' location = Location(name='Test Location') self.llm_util.set_story(self.story) - result = self.llm_util.free_form_action(location=location, character_name='', character_card='', event_history='') - assert(result["action"] == 'test_action') - assert(result["target"] == 'test target') + result = self.llm_util.free_form_action(location=location, character_name='', character_card='', event_history='') # type: ActionResponse + assert(result.action == 'test_action') + assert(result.target == 'test target') def test_init_image_gen(self): self.llm_util._init_image_gen("Automatic1111") diff --git a/tests/test_wizard_commands.py b/tests/test_wizard_commands.py index 16742d2a..367d9856 100644 --- a/tests/test_wizard_commands.py +++ b/tests/test_wizard_commands.py @@ -3,9 +3,10 @@ import pytest import tale -from tale.base import Item, Location, ParseResult +from tale.base import Item, Location, ParseResult, Weapon from tale.cmds import wizard, wizcmd from tale.errors import ParseError +from tale.items.basic import Food from tale.llm.LivingNpc import LivingNpc from tale.llm.llm_ext import DynamicStory from tale.llm.llm_utils import LlmUtil @@ -90,6 +91,42 @@ def test_set_goal(self): wizard.do_set_goal(self.test_player, parse_result, self.context) assert(npc.goal == 'test goal') + def test_create_item(self): + location = Location('test_room') + location.init_inventory([self.test_player]) + parse_result = ParseResult(verb='create_item', args=['Item', 'test_item', 'test description']) + wizard.do_create_item(self.test_player, parse_result, self.context) + assert(len(location.items) == 1) + item = list(location.items)[0] + assert(item.name == 'test_item') + assert(item.short_description == 'test description') + + def test_create_food(self): + location = Location('test_room') + location.init_inventory([self.test_player]) + parse_result = ParseResult(verb='create_item', args=['Food', 'test_food', 'tasty test food']) + wizard.do_create_item(self.test_player, parse_result, self.context) + assert(len(location.items) == 1) + item = list(location.items)[0] + assert(isinstance(item, Food)) + + def test_create_weapon(self): + location = Location('test_room') + location.init_inventory([self.test_player]) + parse_result = ParseResult(verb='create_item', args=['Weapon', 'test_weapon', 'pointy test weapon']) + wizard.do_create_item(self.test_player, parse_result, self.context) + assert(len(location.items) == 1) + item = list(location.items)[0] + assert(isinstance(item, Weapon)) + assert(item.name == 'test_weapon') + + def test_create_item_no_args(self): + parse_result = ParseResult(verb='create_item', args=[]) + with pytest.raises(ParseError, match="You need to define an item type. Name and description are optional"): + wizard.do_create_item(self.test_player, parse_result, self.context) + + + class TestEnrichCommand(): context = tale._MudContext()