Skip to content

Commit

Permalink
Merge pull request #64 from neph1/update-v0.23.2
Browse files Browse the repository at this point in the history
Update v0.23.2
  • Loading branch information
neph1 authored Jan 29, 2024
2 parents 4cb8da4 + c710bd6 commit 6ca6330
Show file tree
Hide file tree
Showing 21 changed files with 232 additions and 149 deletions.
2 changes: 1 addition & 1 deletion llm_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ PLAYER_ENTER_PROMPT: '<context>{context}</context> Zone info: {zone_info}; Npc e
QUEST_PROMPT: '<context>{context}</context> Zone info: {zone_info}; Character: {character_card};\n[USER_START] Using the information supplied inside the <context> 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>{context}</context> Zone info: {zone_info};\n[USER_START]Using the information supplied inside the <context> 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>{context}</context> Zone info: {zone_info};\n[USER_START]FUsing the information supplied inside the <context> 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>{context}</context>\n[USER_START]Act as as {character_name}.\nUsing the information supplied inside the <context> 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>{context}</context>\n[USER_START]Act as as {character_name}.\nUsing the information supplied inside the <context> 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:'
6 changes: 4 additions & 2 deletions tale/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,12 +619,13 @@ def __str__(self) -> str:
return "<Weapon '%s' #%d @ 0x%x>" % (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):
Expand All @@ -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):
"""
Expand Down
24 changes: 24 additions & 0 deletions tale/cmds/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
6 changes: 4 additions & 2 deletions tale/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions tale/items/book.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@



from tale.base import Item


Expand Down
94 changes: 58 additions & 36 deletions tale/llm/LivingNpc.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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
Expand All @@ -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 ")
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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: *<it><rev> ' + 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:
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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', [])
Expand Down
Loading

0 comments on commit 6ca6330

Please sign in to comment.