Skip to content

Commit 9c36e9a

Browse files
authored
Merge pull request #41 from neph1/story-content-hotfix
v0.14.1 using json grammar when generating creatures
2 parents 98240dc + 48c4a9a commit 9c36e9a

13 files changed

+144
-35
lines changed

llm_config.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ STREAM_ENDPOINT: "/api/extra/generate/stream"
55
DATA_ENDPOINT: "/api/extra/generate/check"
66
WORD_LIMIT: 200
77
BACKEND: "kobold_cpp" # "openai"
8-
DEFAULT_BODY: '{"stop_sequence": "", "max_length":500, "max_context_length":4096, "temperature":1.0, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}'
9-
GENERATION_BODY: '{"stop_sequence": "\n\n", "max_length":500, "max_context_length":4096, "temperature":1.0, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "sampler_order":[6,0,1,3,4,2,5], "seed":-1, "grammar":"grammars/json.gbnf"}'
10-
ANALYSIS_BODY: '{"banned_tokens":"\n\n", "stop_sequence": "\n\n\n", "max_length":500, "max_context_length":4096, "temperature":0.15, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "mirostat":2, "mirostat_tau":6.0, "mirostat_eta":0.1, "sampler_order":[6,0,1,3,4,2,5], "seed":-1, "grammar":"grammars/json.gbnf"}'
8+
DEFAULT_BODY: '{"stop_sequence": "", "max_length":750, "max_context_length":4096, "temperature":1.0, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}'
9+
GENERATION_BODY: '{"stop_sequence": "\n\n", "max_length":750, "max_context_length":4096, "temperature":1.0, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}'
10+
ANALYSIS_BODY: '{"banned_tokens":"\n\n", "stop_sequence": "\n\n\n\n", "max_length":500, "max_context_length":4096, "temperature":0.15, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "mirostat":2, "mirostat_tau":6.0, "mirostat_eta":0.1, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}'
1111
MEMORY_SIZE: 512
12-
PRE_PROMPT: 'Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n\n'
12+
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.\n\n'
1313
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:{input_text}] \n\nEnd of text.\n\n ### Instruction: Rewrite [Text] in your own words. ### Response:\n"
1414
ACTION_PROMPT: "[Story context: {story_context}]; [History: {history}]; ### 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: {input_text}] \n\nEnd of text.\n\n### Response:\n"
15-
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'
15+
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'
1616
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'
1717
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. ### Instruction: Rewrite the following combat result in about 150 words, using the characters weapons . Combat Result: {attacker_msg} ### Response:\n\n'
1818
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.\n\n'
@@ -31,6 +31,6 @@ STORY_BACKGROUND_PROMPT: "### Instruction: For an RPG described as {story_type}
3131
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"}}; ### Instruction: 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":[]}}.\n\n ### Response:\n'
3232
STORY_PLOT_PROMPT: "### Instruction: 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 ### Response:\n"
3333
WORLD_ITEMS: '[Story context: {story_context}]; For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, come up with 7 common items that can be found in the world. Item example: {{"name":"", "type":"", "short_descr":"", "level":int, "value":int}}, type can be "Weapon", "Wearable", "Health", "Other" or "Money"; Fill in this JSON template and do not write anything else: {{"items": []}}.\n\n ### Response:\n'
34-
WORLD_CREATURES: '[Story context: {story_context}]; For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, come up with 5 creatures of various level and sentiment that can be found in the world. Creature example: {{"name":"", "body":"", "mass":int(kg), "hp":int, "level":int, "unarmed_attack":One of [FISTS, CLAWS, BITE, TAIL, HOOVES, HORN, TUSKS, BEAK, TALON], "short_descr":""}}. Fill in this JSON template and do not write anything else: {{"creatures": []}}.\n\n ### Response:\n'
34+
WORLD_CREATURES: '[Story context: {story_context}]; For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, come up with 5 creatures of various level and sentiment that can be found in the world. Creature example: {{"name":"", "body":"", "mass":int(kg), "hp":int, "type":"Npc or Mob", "level":int, "unarmed_attack":One of [FISTS, CLAWS, BITE, TAIL, HOOVES, HORN, TUSKS, BEAK, TALON], "short_descr":""}}. Fill in this JSON template and do not write anything else: {{"creatures": []}}.\n\n ### Response:\n'
3535
GOAL_PROMPT: '[Characters:{characters}][Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Known locations: {locations}][Acting character: {character}] [Actions available:{actions}] ### Instruction: 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":""}]}}\n\n ### Response:\n'
3636
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)?"

tale/json_story.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,22 @@ def __init__(self, path: str, config: StoryConfig):
1414
self.config = config
1515
self.path = path
1616
locs = {}
17+
zones = []
1718
for zone in self.config.zones:
1819
zones, exits = parse_utils.load_locations(parse_utils.load_json(self.path +'zones/'+zone + '.json'))
20+
if len(zones) < 1:
21+
print("No zones found in story config")
22+
return
1923
for name in zones.keys():
2024
zone = zones[name]
2125
for loc in zone.locations.values():
2226
locs[loc.name] = loc
2327
self._locations = locs
2428
self._zones = zones # type: dict(str, dict)
25-
self._world["creatures"] = parse_utils.load_npcs(parse_utils.load_json(self.path +'npcs/'+self.config.npcs + '.json'), self._zones)
26-
self._world["items"] = parse_utils.load_items(parse_utils.load_json(self.path + self.config.items + '.json'), self._zones)
29+
if self.config.npcs:
30+
self._world["creatures"] = parse_utils.load_npcs(parse_utils.load_json(self.path +'npcs/'+self.config.npcs + '.json'), self._zones)
31+
if self.config.items:
32+
self._world["items"] = parse_utils.load_items(parse_utils.load_json(self.path + self.config.items + '.json'), self._zones)
2733

2834
def init(self, driver) -> None:
2935
pass

tale/llm/character.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
class Character():
1717

1818
def __init__(self, backend: str, io_util: IoUtil, default_body: dict):
19-
self.pre_prompt = llm_config.params['PRE_JSON_PROMPT']
19+
self.pre_prompt = llm_config.params['PRE_PROMPT']
2020
self.dialogue_prompt = llm_config.params['DIALOGUE_PROMPT']
2121
self.character_prompt = llm_config.params['CREATE_CHARACTER_PROMPT']
2222
self.item_prompt = llm_config.params['ITEM_PROMPT']
@@ -80,6 +80,7 @@ def dialogue_analysis(self, text: str, character_card: str, character_name: str,
8080
if self.backend == 'kobold_cpp':
8181
request_body = self.analysis_body
8282
request_body['prompt'] = prompt
83+
request_body['grammar'] = self.json_grammar
8384
elif self.backend == 'openai':
8485
request_body = self.default_body
8586
request_body['messages'][1]['content'] = prompt

tale/llm/llm_ext.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from tale import mud_context
23
from tale.base import ContainingType, Item, Living, Location, ParseResult
34
from tale.errors import TaleError
@@ -237,3 +238,16 @@ def world_items(self) -> dict:
237238
@world_items.setter
238239
def world_items(self, value: dict):
239240
self._world["items"] = value
241+
242+
def save(self) -> None:
243+
""" Save the story to disk."""
244+
story = dict()
245+
story["zones"] = dict()
246+
story["world"] = self._world
247+
for zone in self._zones.values():
248+
story["zones"][zone.name] = zone.get_info()
249+
250+
with open(self.config.name, "w") as fp:
251+
json.dump(story , fp)
252+
253+

tale/llm/llm_utils.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ def __init__(self, io_util: IoUtil = None):
3737
self._world_building = WorldBuilding(default_body=self.default_body,
3838
io_util=self.io_util,
3939
backend=self.backend)
40-
self._character = Character(
41-
backend=self.backend,
40+
self._character = Character(backend=self.backend,
4241
io_util=self.io_util,
4342
default_body=self.default_body)
4443
self._story_building = StoryBuilding(default_body=self.default_body,

tale/llm/world_building.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,12 @@ def _add_npcs(self, location: Location, json_result: dict, world_creatures: dict
108108
return location
109109
if world_creatures:
110110
generated_npcs = parse_utils.replace_creature_with_world_creature(generated_npcs, world_creatures)
111-
generated_npcs = parse_utils.load_npcs(generated_npcs)
112-
for npc in generated_npcs.values():
113-
location.insert(npc, None)
111+
try:
112+
generated_npcs = parse_utils.load_npcs(generated_npcs)
113+
for npc in generated_npcs.values():
114+
location.insert(npc, None)
115+
except Exception as exc:
116+
print(exc)
114117
return location
115118

116119

@@ -234,6 +237,9 @@ def generate_world_items(self, story_context: str, story_type: str, world_info:
234237
world_info=world_info,
235238
world_mood=parse_utils.mood_string_from_int(world_mood))
236239
request_body = self.default_body
240+
if self.backend == 'kobold_cpp':
241+
request_body = self._kobold_generation_prompt(request_body)
242+
237243
result = self.io_util.synchronous_request(request_body, prompt=prompt)
238244
try:
239245
json_result = json.loads(parse_utils.sanitize_json(result))
@@ -248,6 +254,9 @@ def generate_world_creatures(self, story_context: str, story_type: str, world_in
248254
world_info=world_info,
249255
world_mood=parse_utils.mood_string_from_int(world_mood))
250256
request_body = self.default_body
257+
if self.backend == 'kobold_cpp':
258+
request_body = self._kobold_generation_prompt(request_body)
259+
251260
result = self.io_util.synchronous_request(request_body, prompt=prompt)
252261
try:
253262
json_result = json.loads(parse_utils.sanitize_json(result))

tale/npc_defs.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11

22

3+
import random
34
from tale.base import Living
5+
from tale.llm.llm_ext import LivingNpc
46
from tale.player import Player
57
from tale.util import Context, call_periodically
68

9+
class StationaryNpc(LivingNpc):
10+
11+
def __init__(self, name: str, gender: str, *,
12+
title: str="", descr: str="", short_descr: str="", age: int, personality: str, occupation: str="", race: str=""):
13+
super(StationaryNpc, self).__init__(name=name, gender=gender,
14+
title=title, descr=descr, short_descr=short_descr, age=age, personality=personality, occupation=occupation, race=race)
15+
16+
17+
@call_periodically(20, 45)
18+
def do_idle_action(self, ctx: Context) -> None:
19+
""" Perform an idle action if a player is in the same location."""
20+
player_names = ctx.driver.all_players.keys()
21+
player_in_location = any(name == living.name for name in player_names for living in self.location.livings)
22+
if player_in_location:
23+
self.idle_action()
724

8-
class StationaryMob(Living):
25+
class StationaryMob(LivingNpc):
926

1027
def __init__(self, name: str, gender: str, *,
1128
title: str="", descr: str="", short_descr: str="", race: str=""):
1229
super(StationaryMob, self).__init__(name=name, gender=gender,
13-
title=title, descr=descr, short_descr=short_descr, race=race)
30+
title=title, descr=descr, short_descr=short_descr, race=race, age=0, personality='', occupation='')
1431

1532
@call_periodically(15, 60)
1633
def do_idle_action(self, ctx: Context) -> None:

tale/parse_utils.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from tale.base import Location, Exit, Item
55
from tale.coord import Coord
66
from tale.items.basic import Money, Note
7-
from tale.npc_defs import StationaryMob
7+
from tale.npc_defs import StationaryMob, StationaryNpc
88
from tale.story import GameMode, MoneyType, TickMethod, StoryConfig
99
from tale.llm.llm_ext import LivingNpc
1010
from tale.weapon_type import WeaponType
@@ -95,14 +95,17 @@ def load_npcs(json_file: [], locations = {}) -> dict:
9595
"""
9696
npcs = {}
9797
for npc in json_file:
98-
npc_type = npc.get('type', 'LivingNpc')
99-
if npc_type == 'LivingNpc':
100-
if npc['name'].startswith('the') or npc['name'].startswith('The'):
98+
npc_type = npc.get('type', 'Mob')
99+
if npc_type == 'ignore':
100+
continue
101+
if npc['name'].startswith('the') or npc['name'].startswith('The'):
101102
name = npc['name'].replace('the','').replace('The','').strip()
102-
else:
103-
name = npc['name']
104-
new_npc = LivingNpc(name=name,
105-
gender=lang.validate_gender(npc.get('gender', 'm')),
103+
else:
104+
name = npc['name']
105+
if 'npc' in npc_type.lower():
106+
107+
new_npc = StationaryNpc(name=name,
108+
gender=lang.validate_gender_mf(npc.get('gender', 'm')[0]),
106109
race=npc.get('race', 'human').lower(),
107110
title=npc.get('title', name),
108111
descr=npc.get('descr', ''),
@@ -114,8 +117,9 @@ def load_npcs(json_file: [], locations = {}) -> dict:
114117
new_npc.stats.set_weapon_skill(WeaponType.UNARMED, random.randint(10, 30))
115118
new_npc.stats.level = npc.get('level', 1)
116119
elif npc_type == 'Mob':
120+
117121
new_npc = StationaryMob(name=npc['name'],
118-
gender=lang.validate_gender(npc.get('gender', 'm')),
122+
gender=lang.validate_gender_mf(npc.get('gender', 'm')[0]),
119123
race=npc.get('race', 'human').lower(),
120124
title=npc.get('title', npc['name']),
121125
descr=npc.get('descr', ''),
@@ -198,7 +202,7 @@ def sanitize_json(result: str):
198202
""" Removes special chars from json string. Some common, and some 'creative' ones. """
199203
# .replace('}}', '}')
200204
# .replace('""', '"')
201-
result = result.replace('```json', '').replace('\\"', '"').replace('"\\n"', '","').replace('\\n', '').replace('}\n{', '},{').replace('}{', '},{').replace('\\r', '').replace('\\t', '').replace('"{', '{').replace('}"', '}').replace('"\\', '"').replace('\\”', '"').replace('" "', '","').replace(':,',':').replace('},]', '}]').replace('},}', '}}')
205+
result = result.replace('```json', '') #.replace('\\"', '"').replace('"\\n"', '","').replace('\\n', '').replace('}\n{', '},{').replace('}{', '},{').replace('\\r', '').replace('\\t', '').replace('"{', '{').replace('}"', '}').replace('"\\', '"').replace('\\”', '"').replace('" "', '","').replace(':,',':').replace('},]', '}]').replace('},}', '}}')
202206
result = result.split('```')[0]
203207
print('sanitized json: ' + result)
204208
return result

tale/story_builder.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,13 @@ def apply_to_story(self, story: DynamicStory, llm_util: LlmUtil):
103103

104104
self.connection.output("Generating starting zone...")
105105
world_info = {'world_description': self.story_info.world_info, 'world_mood': self.story_info.world_mood, 'world_items': items, 'world_creatures': creatures}
106-
zone = llm_util.generate_start_zone(location_desc=self.story_info.start_location,
106+
for i in range(3):
107+
zone = llm_util.generate_start_zone(location_desc=self.story_info.start_location,
107108
story_type=self.story_info.type,
108109
story_context=story.config.context,
109110
world_info=world_info)
111+
if zone:
112+
break
110113

111114
story.add_zone(zone)
112115

0 commit comments

Comments
 (0)