Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update v0.23.2 #64

Merged
merged 7 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading