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()