Skip to content

Commit

Permalink
Merge pull request #44 from neph1/update-v0.15.1
Browse files Browse the repository at this point in the history
Update v0.15.1
  • Loading branch information
neph1 authored Oct 10, 2023
2 parents 6efb341 + 8502474 commit 339e179
Show file tree
Hide file tree
Showing 18 changed files with 364 additions and 223 deletions.
8 changes: 4 additions & 4 deletions llm_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ MEMORY_SIZE: 512
PRE_PROMPT: 'Below is an instruction that describes a task, paired with an input that provides further context. As an game keeper for an RPG, write a response that appropriately completes the request.'
BASE_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the following Text in your own words using the supplied Context and History to create a background for your text. Use about {max_words} words. [USER_START] Rewrite [{input_text}] in your own words."
ACTION_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the Action, and nothing else, in your own words using the supplied Context and Location. History is what happened before. Use less than {max_words} words. [Text: {input_text}] \n\nEnd of text."
DIALOGUE_PROMPT: '[Story context: {story_context}]; [Location: {location}]; The following is a conversation between {character1} and {character2}; {character1}:{character1_description}; character2:{character2_description}; [Chat history: {previous_conversation}]\n\n [{character2}s sentiment towards {character1}: {sentiment}].[USER_START] Write a single response for {character2} in third person pov, using {character2} description.'
DIALOGUE_PROMPT: '[Story context: {story_context}]; [Location: {location}]; [History: {history}]; The following is a conversation between {character1} and {character2}; {character1}:{character1_description}; character2:{character2_description}; [Chat history: {previous_conversation}]\n\n [{character2}s sentiment towards {character1}: {sentiment}].[USER_START] Write a single response for {character2} in third person pov, using {character2} description.'
ITEM_PROMPT: 'Items:[{items}];Characters:[{character1},{character2}] \n\n [USER_START] Decide if an item was explicitly given, taken, dropped or put somewhere in the following text:[Text:{text}]. Insert your thoughts about whether an item was explicitly given, taken, put somewhere or dropped in "my thoughts", and the results in "item", "from" and "to", or make them empty if . Insert {character1}s sentiment towards {character2} in a single word in "sentiment assessment". Fill in the following JSON template: {{ "thoughts":"", "result": {{ "item":"", "from":"", "to":""}}, {{"sentiment":"sentiment assessment"}} }} End of example. \n\n Write your response in valid JSON'
COMBAT_PROMPT: 'Rewrite the following combat between user {attacker} using {attacker_weapon}, and {victim} using {victim_weapon} in {location}, {location_description} into a vivid description in less than 300 words. [USER_START] Rewrite the following combat result in about 150 words, using the characters weapons . Combat Result: {attacker_msg}'
PRE_JSON_PROMPT: 'Below is an instruction that describes a task, paired with an input that provides further context. Write a response in valid JSON format that appropriately completes the request.'
Expand All @@ -24,9 +24,9 @@ OPENAI_BODY: '{"model":"gpt-3.5-turbo", "messages":[{"role":"system", "content":
OPENAI_API_KEY: "OPENAI_API_KEY"
ITEMS_PROMPT: "Add {items} of various type."
SPAWN_PROMPT: "Add a level {level}, {alignment} npc or mob."
IDLE_ACTION_PROMPT: " [Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Acting character: {character}] [USER_START] Choose an item from: {items}, or a character from:{characters}, to interact with, or perform a solo action. Do not make up new characters. Don't contradict or repeat Last Action. [USER_START] Write what {character_name} does, in present tense third person point of view. Use less than 40 words. Don't write what {character_name} thinks, or what the player (You) or anyone else does."
IDLE_ACTION_PROMPT: "[Sentiments towards characters: {sentiments}]; [History: {history}]; [Last action: {last_action}]; [Location: {location}]; [Acting character: {character}]; [USER_START] Choose an item from: {items}, or a character from:{characters}, to interact with, or perform a solo action. Do not make up new characters. Don't contradict or repeat Last Action. [USER_START] Don't write what {character_name} thinks, or what the player (You) or anyone else does. Write what {character_name} does, in present tense third person point of view. Use less than 40 words. "
TRAVEL_PROMPT: "[USER_START] For {character}: pick a location from [{locations}] they would like to travel to or a direction from [{directions}], or stay in the current location. Do not make up new locations. Write what {character_name} chooses. [USER_START] Write only the location name, direction, or write nothing to stay in the same location. Write nothing else."
REACTION_PROMPT: "[USER_START] Act as {character}. {acting_character_name} has performed the following action that involves {character_name}: {action}. {character_name}'s sentiment towards {acting_character_name}: {sentiment}. [USER_START] Respond with a suitable action for {character_name}, in present tense third person point of view. Use less than 40 words."
REACTION_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Act as {character}. {acting_character_name} has performed the following action that involves {character_name}: {action}. {character_name}'s sentiment towards {acting_character_name}: {sentiment}. [USER_START] Respond with a suitable action for {character_name}, in present tense third person point of view. Use less than 40 words."
STORY_BACKGROUND_PROMPT: "[USER_START] For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, write a captivating background story that the player can interact with. Include a large scale plot conflict that the player will encounter. Use less than 400 words"
START_LOCATION_PROMPT: '[Story context: {story_context}]; Zone info: {zone_info}; Item json example: {{"name":"", "type":"", "short_descr":"10 words"}}, type can be "Weapon", "Wearable", "Other" or "Money"; Npc example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words"}} ; Exit json example: {{"direction":"", "name":"name of new location", "short_descr":"exit description"}}; [USER_START] For a {story_type}, come up with a name for the location with this description: {location_description}. {items_prompt} {spawn_prompt} Add a brief description, and one to three additional exits leading to new locations. Fill in this JSON template and do not write anything else: {{"name": "", "exits":[], "items":[], "npcs":[]}}.'
STORY_PLOT_PROMPT: "[USER_START] For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}. Based on the following background: {story_background} write an innovative and engaging plot that the player can become part of. Use less than 400 words\n\n "
Expand All @@ -35,5 +35,5 @@ WORLD_CREATURES: '[Story context: {story_context}]; For an RPG described as {sto
GOAL_PROMPT: '[Characters:{characters}][Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Known locations: {locations}][Acting character: {character}] [Actions available:{actions}] [USER_START] For {character_name}, come up with a goal that plays along with their character description that involves an item, a character or a location in the prompt. Then construct up to three tasks that will lead towards the achievement of said goal. Fill in the following JSON template: {{"goal":"", "tasks":[{"action":"", "what":""}, {"action":"", "what":""}, {"action":"", "what":""}]}}'
JSON_GRAMMAR: "root ::= object\nvalue ::= object | array | string | number | (\"true\" | \"false\" | \"null\") ws\n\nobject ::=\n \"{\" ws (\n string \":\" ws value\n (\",\" ws string \":\" ws value)*\n )? \"}\" ws\n\narray ::=\n \"[\" ws (\n value\n (\",\" ws value)*\n )? \"]\" ws\n\nstring ::=\n \"\\\"\" (\n [^\"\\\\] |\n \"\\\\\" ([\"\\\\/bfnrt] | \"u\" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) # escapes\n )* \"\\\"\" ws\n\nnumber ::= (\"-\"? ([0-9] | [1-9] [0-9]*)) (\".\" [0-9]+)? ([eE] [-+]? [0-9]+)? ws\n\n# Optional space: by convention, applied in this grammar after literal chars when allowed\nws ::= ([ \\t\\n] ws)?"
PLAYER_ENTER_PROMPT: '[Story context: {story_context}]; World info: {world_info}; Zone info: {zone_info}; ; Npc example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words", "memories":"25 words"}}. [USER_START] The player has just re-entered this location: {location_info}. Consider whether any items, npcs or mobs should be spawned. For mobs, only enter the name of race. Fill in this JSON template and do not write anything else: {{"items":[], "npcs":[] "mobs":[]}}.'
USER_START: '### Instruction:\n'
USER_START: '### Instruction:'
USER_END: '### Response:\n'
2 changes: 1 addition & 1 deletion stories/anything/npcs/npc_defs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@


from tale.base import Living
from tale.llm.llm_ext import LivingNpc
from tale.llm.LivingNpc import LivingNpc
from tale.player import Player
from tale.util import Context, call_periodically

Expand Down
2 changes: 1 addition & 1 deletion stories/prancingllama/npcs/npcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from tale.base import Item, Living, ParseResult
from tale.errors import ParseError, ActionRefused
from tale.lang import capital
from tale.llm.llm_ext import LivingNpc
from tale.llm.LivingNpc import LivingNpc
from tale.player import Player
from tale.util import call_periodically, Context
from tale import lang
Expand Down
3 changes: 2 additions & 1 deletion tale/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import appdirs

from tale import story_builder
from tale.llm.LivingNpc import LivingNpc

from . import __version__ as tale_version_str, _check_required_libraries
from . import mud_context, errors, util, cmds, player, pubsub, charbuilder, lang, verbdefs, vfs, base
Expand All @@ -33,7 +34,7 @@
from .races import playable_races
from .errors import StoryCompleted, StoryConfigError
from tale.load_character import CharacterLoader, CharacterV2
from tale.llm.llm_ext import LivingNpc, DynamicStory
from tale.llm.llm_ext import DynamicStory
from tale.llm.llm_utils import LlmUtil


Expand Down
6 changes: 6 additions & 0 deletions tale/lang.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,9 @@ def validate_gender_mf(value: str) -> str:
return value
raise ValueError("That is not a valid gender.")

def gender_string(value: str) -> str:
if value == 'f':
return 'female'
if value == 'm':
return 'male'
return 'non-specified'
170 changes: 170 additions & 0 deletions tale/llm/LivingNpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import tale.llm.llm_cache as llm_cache
from tale import lang, mud_context
from tale.base import ContainingType, Living, ParseResult
from tale.errors import TaleError
from tale.player import Player


from typing import Sequence


class LivingNpc(Living):
"""An NPC with extra fields to define personality and help LLM generate dialogue"""

def __init__(self, name: str, gender: str, *,
title: str="", descr: str="", short_descr: str="", age: int, personality: str, occupation: str="", race: str=""):
super(LivingNpc, self).__init__(name=name, gender=gender, title=title, descr=descr, short_descr=short_descr, race=race)
self.age = age
self.personality = personality
self.occupation = occupation
self.memory_size = 1024
self.known_locations = dict()
self._observed_events = set() # type: set[int] # These are hashed values of action the character has been notified of
self._conversations = set() # type: set[str] # These are hashed values of conversations the character has involved in
self.sentiments = {}
self.action_history = [] # type: list[str]
self.planned_actions = [] # type: list[str]
self.goal = None # type: str # a free form string describing the goal of the NPC

def notify_action(self, parsed: ParseResult, actor: Living) -> None:
# store even our own events.
event_hash = llm_cache.cache_event(parsed.unparsed)
self._observed_events.add(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
targeted = False
for alias in self.aliases:
if alias in parsed.unparsed:
targeted = True
if self.name in parsed.unparsed or self in parsed.who_info or self.title in parsed.unparsed:
targeted = True
if parsed.verb in ("hi", "hello") or parsed.verb == "greet":
greet = True
if greet and targeted:
self.tell_others("{Actor} says: \"Hi.\"", evoke=True)
#self.update_conversation(f"{self.title} says: \"Hi.\"")
elif parsed.verb == "say" and targeted:
self.do_say(parsed.unparsed, actor)
elif targeted and parsed.verb == "idle-action":
self._do_react(parsed, actor)

def do_say(self, what_happened: str, actor: Living) -> None:
tell_hash = llm_cache.cache_tell('{actor.title}:{what_happened}'.format(actor=actor, what_happened=what_happened))
self._conversations.add(tell_hash)
short_len = False if isinstance(actor, Player) else True

response, item_result, sentiment = mud_context.driver.llm_util.generate_dialogue(
conversation=llm_cache.get_tells(self._conversations),
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 summary:
# self.update_conversation(f"{self.title} says: \"{summary}\"")
# else:

tell_hash = llm_cache.cache_tell('{actor.title}:{response}'.format(actor=self.title, response=response))
self._conversations.add(tell_hash)

self.tell_others("{response}".format(response=response), evoke=False)
if item_result:
self.handle_item_result(item_result, actor)

if sentiment:
self.sentiments[actor.title] = sentiment

def _do_react(self, parsed: ParseResult, actor: Living) -> None:
action = mud_context.driver.llm_util.perform_reaction(action=parsed.unparsed,
character_card=self.character_card,
character_name=self.title,
location=self.location,
acting_character_name=actor.title,
event_history=llm_cache.get_events(self._observed_events),
sentiment=self.sentiments.get(actor.name, ''))
if action:
self.action_history.append(action)
result = ParseResult(verb='idle-action', unparsed=action, who_info=None)
self.tell_others(action)
self.location._notify_action_all(result, actor=self)

def handle_item_result(self, result: str, actor: Living):

if result["from"] == self.title:
item = self.search_item(result["item"])
if not item:
raise TaleError("item not found on actor %s " % item)
if result["to"]:

if result["to"] == actor.name or result["to"] == actor.title:
item.move(actor, self)
elif result["to"] in ["user", "you", "player"] and isinstance(actor, Player):
item.move(actor, self)
actor.tell("%s gives you %s." % (self.subjective, item.title), evoke=False)
self.tell_others("{Actor} gives %s to %s" % (item.title, actor.title), evoke=False)
else:
item.move(self.location, self)
self.tell_others("{Actor} drops %s on the floor" % (item.title), evoke=False)

def move(self, target: ContainingType, actor: Living=None,
*, silent: bool=False, is_player: bool=False, verb: str="move", direction_names: Sequence[str]=None) -> None:
self.known_locations[self.location.name] = f"description: {self.location.description}. " + ". ".join(self.location.look(exclude_living=self, short=True))
super().move(target, actor, silent=silent, is_player=is_player, verb=verb, direction_names=direction_names)

def idle_action(self):
""" Plan and perform idle actions.
Currently handles planning several actions in advance, and then performing them in reverse order.
"""
if not self.planned_actions:
if self.action_history:
history_length = len(self.action_history)
previous_actions = self.action_history[-5:] if history_length > 4 else self.action_history[-history_length:]
else:
previous_actions = []
actions = mud_context.driver.llm_util.perform_idle_action(character_card=self.character_card,
character_name=self.title,
location=self.location,
last_action=previous_actions,
event_history=llm_cache.get_events(self._observed_events),
sentiments=self.sentiments)
if actions:
self.planned_actions.extend(actions)
if len(self.planned_actions) > 0:
action = self.planned_actions.pop(0)
self.action_history.append(action)
result = ParseResult(verb='idle-action', unparsed=action, who_info=None)
self.tell_others(action)
self.location.notify_action(result, actor=self)
self.location._notify_action_all(result, actor=self)

def travel(self):
result = mud_context.driver.llm_util.perform_travel_action(character_card=self.character_card,
character_name=self.title,
location=self.location,
locations=', '.join(self.location.exits.keys()),
directions=[])
if result:
exit = self.location.exits.get(result)
if exit:
self.move(target=exit.target, actor=self)

@property
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}"}}'.format(
name=self.title,
gender=lang.gender_string(self.gender),
age=self.age,
personality=self.personality,
description=self.description,
occupation=self.occupation,
race=self.stats.race,
items=','.join(items))
Loading

0 comments on commit 339e179

Please sign in to comment.