Skip to content

Commit

Permalink
Merge pull request #8 from neph1/llm_char_gen
Browse files Browse the repository at this point in the history
llm generates characters
  • Loading branch information
neph1 authored Aug 13, 2023
2 parents 67d7919 + 1ecb92b commit a436f9e
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 15 deletions.
23 changes: 23 additions & 0 deletions stories/prancingllama/npcs/npcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ def do_pick_up_dishes(self, ctx: Context) -> None:
self.location.tell(f"{lang.capital(self.title)} wipes a table and picks up dishes.", evoke=False, max_length=True)


class RoamingPatron(LivingNpc):


def __init__(self, name: str, gender: str, *,
title: str="", descr: str="", short_descr: str="", age: int, personality: str):
super(RoamingPatron, self).__init__(name=name, gender=gender,
title=title, descr=descr, short_descr=short_descr, age=age, personality=personality, occupation='')
self.sitting = False

@call_periodically(45, 120)
def do_random_move(self, ctx: Context) -> None:
if not self.sitting:
if random.random() < 0.25:
self.sitting = True
self.tell_others("{Actor} sits down.", evoke=False, max_length=True)
else:
direction = self.select_random_move()
if direction:
self.move(direction.target, self, direction_names=direction.names)
elif random.random() < 0.5:
self.sitting = False
self.tell_others("{Actor} stands up.", evoke=False, max_length=True)


class Patron(LivingNpc):

Expand Down
31 changes: 31 additions & 0 deletions stories/prancingllama/zones/prancingllama.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from tale.player import Player
from tale.util import Context, call_periodically
from tale.verbdefs import AGGRESSIVE_VERBS
from tale.verbdefs import VERBS
from npcs.npcs import *


Expand Down Expand Up @@ -57,3 +58,33 @@ def spawn_rat(self, ctx: Context) -> None:

norhardt.init_inventory([old_map])

all_locations = [main_hall, bar, kitchen, hearth, entrance, outside, cellar]

def _generate_character():
# select 5 random verbs from VERBS
verbs = []
for i in range(5):
verbs.append(random.choice(list(VERBS.keys())))

character = mud_context.driver.llm_util.generate_character(story_context=mud_context.config.context, keywords=verbs) # Characterv2
if character:
patron = RoamingPatron(character.name,
gender=character.gender,
title=lang.capital(character.name),
descr=character.description,
short_descr=character.appearance,
age=character.age,
personality=character.personality)
patron.aliases = [character.name.split(' ')[0]]
location = all_locations[random.randint(0, len(all_locations) - 1)]
location.insert(patron, None)
return True
return False

# 10 attempts to generate 2 characters
generated = 0
for i in range(10):
if _generate_character():
generated += 1
if generated == 2:
break
5 changes: 3 additions & 2 deletions tale/llm_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ PRE_PROMPT: 'Below is an instruction that describes a task, paired with an input
BASE_PROMPT: "[Story context: {story_context}]; [History: {history}]; ### Instruction: 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. Text:\n\n [{input_text}] \n\nEnd of text.\n\n### Response:\n"
ACTION_PROMPT: "[Story context: {story_context}]; [History: {history}]; The following Action is part of a roleplaying game. ### Instruction: 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:\n\n [{input_text}] \n\nEnd of text.\n\n### Response:\n"
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}]. ### Instruction: Write a single response for {character2} in third person pov, using {character2} description.\n\n### Response:\n'
ITEM_PROMPT: 'Items:[{items}];Characters:[{character1},{character2}] \n\n ### Instruction: Decide if there was an item 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". Example: {{ "thoughts":"my thoughts", "result": {{ "item":"", "from":"", "to":""}}, {{"sentiment":"sentiment assessment"}} }} End of example. \n\n Write your response in valid JSON\n\n### Response:\n'
COMBAT_PROMPT: 'Rewrite the following combat between user {attacker} and {victim} into a vivid description in less than 300 words. Location: {location}, {location_description}. ### Instruction: Rewrite the following combat result in about 150 words. Combat Result: {attacker_msg} ### Response:\n\n'
ITEM_PROMPT: 'Items:[{items}];Characters:[{character1},{character2}] \n\n ### Instruction: 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":"my thoughts", "result": {{ "item":"", "from":"", "to":""}}, {{"sentiment":"sentiment assessment"}} }} End of example. \n\n Write your response in valid JSON\n\n### Response:\n'
COMBAT_PROMPT: 'Rewrite the following combat between user {attacker} and {victim} into a vivid description in less than 300 words. Location: {location}, {location_description}. ### Instruction: Rewrite the following combat result in about 150 words. Combat Result: {attacker_msg} ### Response:\n\n'
CREATE_CHARACTER_PROMPT: '### Instruction: For a low-fantasy roleplaying game, create a diverse character with rich personality that can be interacted with using the story context and keywords. Do not mention height. Money as int value. Story context: {story_context}; keywords: {keywords}. Fill in this JSON template and write nothing else: {{"name":"", "description": "", "appearance": "", "personality": "", "money":"", "level":"", "gender":"", "age":"", "race":""}} ### Response:\n'
30 changes: 27 additions & 3 deletions tale/llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import yaml
from json import JSONDecodeError
from tale.llm_io import IoUtil
import tale.parse_utils as parse_utils
from tale.load_character import CharacterV2
from tale.player_utils import TextBuffer
import tale.parse_utils as parse_utils

class LlmUtil():
""" Prepares prompts for various LLM requests"""
Expand All @@ -27,6 +28,7 @@ def __init__(self):
self.dialogue_prompt = config_file['DIALOGUE_PROMPT']
self.action_prompt = config_file['ACTION_PROMPT']
self.combat_prompt = config_file['COMBAT_PROMPT']
self.character_prompt = config_file['CREATE_CHARACTER_PROMPT']
self.item_prompt = config_file['ITEM_PROMPT']
self.word_limit = config_file['WORD_LIMIT']
self._story_background = ''
Expand Down Expand Up @@ -88,7 +90,7 @@ def dialogue_analysis(self, text: str, character_card: str, character_name: str,
request_body['prompt'] = prompt
text = parse_utils.trim_response(self.io_util.synchronous_request(self.url + self.endpoint, request_body))
try:
json_result = json.loads(text.replace('\n', ''))
json_result = json.loads(parse_utils.sanitize_json(text))
except JSONDecodeError as exc:
print(exc)
return None, None
Expand Down Expand Up @@ -133,7 +135,29 @@ def update_memory(self, rolling_prompt: str, response_text: str):
if len(rolling_prompt) > self.memory_size:
rolling_prompt = rolling_prompt[len(rolling_prompt) - self.memory_size + 1:]
return rolling_prompt


def generate_character(self, story_context: str = '', keywords: list = []):
""" Generate a character card based on the current story context"""
prompt = self.character_prompt.format(story_context=story_context,
keywords=', '.join(keywords))
request_body = self.default_body
request_body['stop_sequence'] = ['\n\n']
request_body['temperature'] = 1.0
request_body['banned_tokens'] = ['```']
request_body['prompt'] = prompt
result = self.io_util.synchronous_request(self.url + self.endpoint, request_body)
try:
json_result = json.loads(parse_utils.sanitize_json(result))
except JSONDecodeError as exc:
print(exc)
return None
try:
return CharacterV2().from_json(json_result)
except:
print(f'Exception while parsing character {json_result}')
return None


@property
def story_background(self) -> str:
return self._story_background
Expand Down
2 changes: 1 addition & 1 deletion tale/load_character.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def __init__(self, name: str='',
def from_json(self, json: dict):
self.name = json.get('name')
self.race = json.get('race', 'human')
self.gender = json.get('gender', 'f')
self.gender = json.get('gender', 'f')[0].lower()
description = json.get('description')
self.description = description
self.appearance = json.get('appearance', description.split(';')[0])
Expand Down
7 changes: 7 additions & 0 deletions tale/parse_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,10 @@ def trim_response(message: str):
if last > lastChar:
lastChar = last
return message[:lastChar+1]

def sanitize_json(result: str):
""" Removes special chars from json string. Some common, and some 'creative' ones. """
# .replace('}}', '}')
result = result.replace('\\"', '"').replace('"\\n"', '","').replace('\\n', '').replace('}\n{', '},{').replace('}{', '},{').replace('\\r', '').replace('\\t', '').replace('"{', '{').replace('}"', '}').replace('"\\', '"').replace('""', '"').replace('\\”', '"').replace('" "', '","').replace(':,',':')
print('sanitized json: ' + result)
return result
1 change: 0 additions & 1 deletion tale/verbdefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,6 @@
"duck": (PERS, None, "duck$ \nHOW out of the way", "duck$ \nHOW out of \nPOSS way"),
} # type: Dict[str, Tuple]


assert all(v[1] is None or type(v[1]) is tuple for v in VERBS.values()), "Second specifier in verb list must be None or tuple, not str"

AGGRESSIVE_VERBS = {
Expand Down
19 changes: 11 additions & 8 deletions tests/test_character_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,34 @@ def test_load_image(self):
assert(json_data.get('description'))

def test_load_from_json(self):
path = 'tests/files/riley.json'
path = 'tests/files/test_character.json'
char_data = self.character_loader.load_character(path)
assert(char_data)
assert(char_data.get('name'))
assert(char_data.get('name') == 'test character')
assert(char_data.get('description'))

def test_CharacterV2(self):
path = 'tests/files/riley.json'
path = 'tests/files/test_character.json'
char_data = self.character_loader.load_character(path)
description = char_data.get('description')
character = CharacterV2(name = char_data.get('name'),
race = char_data.get('race', 'human'),
gender = char_data.get('gender', 'm'),
money = char_data.get('money', 0.0),
description = description)
appearance = char_data.get('appearance', ''),
description = description,
aliases = char_data.get('aliases', []))
self._verify_character(character)

def test_CharacterV2_from_json(self):
path = 'tests/files/riley.json'
path = 'tests/files/test_character.json'
char_data = self.character_loader.load_character(path)
character = CharacterV2().from_json(char_data)
self._verify_character(character)

def _verify_character(self, character: CharacterV2):
assert(character.name)
assert(character.appearance)
assert(character.description)
assert(character.name == 'test character')
assert(character.appearance == 'test appearance')
assert(character.description == 'test description')
assert(character.aliases == ['alias1', 'alias2'])

0 comments on commit a436f9e

Please sign in to comment.