Skip to content

Commit 859432e

Browse files
authored
Merge pull request #106 from neph1/update-v0.39
Update v0.39
2 parents 753cbf2 + 05ad960 commit 859432e

16 files changed

+488
-113
lines changed

llm_config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ STORY_BACKGROUND_PROMPT: "[USER_START] For an RPG described as {story_type} set
3434
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":[]}}.'
3535
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."
3636
WORLD_ITEMS: '<context>{context}</context>\n[USER_START] Using the information supplied inside the <context> tags, come up with 7 common items that can be found in the world. Item example: {item_template}, type is one of: {item_types}; Reply with a list of items in JSON format and do not write anything else: {{"items": []}}.'
37+
WORLD_ITEM_SINGLE: '<context>{context}</context>\n[USER_START] Using the information supplied inside the <context> tags, come up with a common item that can be found in the world. {previously_generated}Item example: {item_template}, type is one of: {item_types}; Reply with a single item in JSON format and do not write anything else: {{"item": {{}}}}.'
3738
WORLD_CREATURES: '<context>{context}</context>\n[USER_START] Using the information supplied inside the <context> tags, come up with 5 creatures of various level and sentiment that can be found in the world. Consider the role of the creature in the story and whether it will be friendly or hostile to the player. Creature example: {creature_template}. Reply with a list of creatures in JSON format and do not write anything else: {{"creatures": []}}.'
39+
WORLD_CREATURE_SINGLE: '<context>{context}</context>\n[USER_START] Using the information supplied inside the <context> tags, come up with a creature that can be found in the world. {previously_generated}Consider the role of the creature in the story and whether it will be friendly or hostile to the player. Creature example: {creature_template}. Reply with a single creature in JSON format and do not write anything else: {{"creature": {{}}}}.'
3840
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}, describe a goal that goes 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":""}]}}'
3941
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)?"
4042
PLAYER_ENTER_PROMPT: '<context>{context}</context> Zone info: {zone_info}; Npc example: {npc_template}.\n[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":[]}}.'

stories/chat_room/story.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
import pathlib
22
import sys
3-
from typing import Optional, Generator
43

5-
import tale
64
from tale import parse_utils
7-
from tale.base import Location
85
from tale.driver import Driver
96
from tale.json_story import JsonStory
10-
from tale.llm.llm_ext import DynamicStory
117
from tale.main import run_from_cmdline
12-
from tale.player import Player, PlayerConnection
13-
from tale.charbuilder import PlayerNaming
14-
from tale.story import *
15-
from tale.skills.weapon_type import WeaponType
16-
from tale.zone import Zone
178

189
class Story(JsonStory):
1910

stories/prancingllama/story.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from tale.skills.skills import SkillType
1515
from tale.story import *
1616
from tale.skills.weapon_type import WeaponType
17-
from tale.story import StoryContext
17+
from tale.story_context import StoryContext
1818
from tale.zone import Zone
1919

2020
class Story(DynamicStory):

tale/json_util.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import json
2+
import re
3+
from typing import Any, Optional
4+
5+
def strip_markdown_fences(s: str) -> str:
6+
"""Remove ```json fences or plain ``` fences."""
7+
return re.sub(r'^```(?:json)?|```$', '', s.strip(), flags=re.MULTILINE).strip()
8+
9+
def extract_json_block(s: str) -> str:
10+
"""Extract the first {...} or [...] block if there is extra text around it."""
11+
match = re.search(r"(\{.*\}|\[.*\])", s, flags=re.DOTALL)
12+
return match.group(1) if match else s
13+
14+
def unwrap_double_encoded(s: str) -> str:
15+
"""Unwrap when JSON is a quoted string containing JSON."""
16+
try:
17+
parsed = json.loads(s)
18+
if isinstance(parsed, str):
19+
return parsed
20+
except Exception:
21+
pass
22+
return s
23+
24+
def fix_trailing_commas(s: str) -> str:
25+
"""Remove trailing commas before ] or }."""
26+
return re.sub(r',\s*([}\]])', r'\1', s)
27+
28+
def normalize_literals(s: str) -> str:
29+
"""Normalize Python-style literals to JSON literals."""
30+
return (s.replace("None", "null")
31+
.replace("True", "true")
32+
.replace("False", "false"))
33+
34+
def sanitize_json(s: str) -> str:
35+
"""Apply a pipeline of cleanup steps before parsing."""
36+
if not s:
37+
return ""
38+
s = strip_markdown_fences(s)
39+
s = extract_json_block(s)
40+
s = unwrap_double_encoded(s)
41+
s = fix_trailing_commas(s)
42+
s = normalize_literals(s)
43+
return s.strip()
44+
45+
def safe_load(s: str) -> Optional[Any]:
46+
"""
47+
Try to parse JSON string from LLM output with progressive cleanup.
48+
Returns Python object or raises last error if unrecoverable.
49+
"""
50+
cleaned = sanitize_json(s)
51+
try:
52+
return json.loads(cleaned)
53+
except json.JSONDecodeError:
54+
# Try double-unwrapping in case of double-encoded JSON
55+
try:
56+
return json.loads(json.loads(cleaned))
57+
except Exception:
58+
raise

tale/llm/character.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
import json
66
import random
77

8-
from tale import _MudContext, parse_utils
8+
from tale import json_util, parse_utils
99
from tale.base import Location
10-
from tale.errors import LlmResponseException
1110
from tale.llm import llm_config
1211
from tale.llm.contexts.ActionContext import ActionContext
1312
from tale.llm.contexts.CharacterContext import CharacterContext
@@ -58,7 +57,7 @@ def generate_dialogue(self,
5857
request_body['grammar'] = self.json_grammar
5958
response = self.io_util.synchronous_request(request_body, prompt=prompt, context=context.to_prompt_string())
6059
try:
61-
json_result = json.loads(parse_utils.sanitize_json(response))
60+
json_result = json_util.safe_load(response)
6261
text = json_result["response"]
6362
if isinstance(text, list):
6463
text = text[0]
@@ -84,7 +83,7 @@ def generate_character(self, character_context: CharacterContext) -> CharacterV2
8483
request_body[self.json_grammar_key] = self.json_grammar
8584
result = self.io_util.synchronous_request(request_body, prompt=prompt, context=character_context.to_prompt_string())
8685
try:
87-
json_result = json.loads(parse_utils.sanitize_json(result))
86+
json_result = json_util.safe_load(result)
8887
except JSONDecodeError as exc:
8988
print(exc)
9089
return None
@@ -165,7 +164,7 @@ def free_form_action(self, action_context: ActionContext) -> list:
165164
text = self.io_util.synchronous_request(request_body, prompt=prompt, context=action_context.to_prompt_string())
166165
if not text:
167166
return None
168-
response = json.loads(parse_utils.sanitize_json(text))
167+
response = json_util.safe_load(text)
169168
if isinstance(response, dict):
170169
return [ActionResponse(response)]
171170
actions = []
@@ -191,5 +190,5 @@ def request_follow(self, follow_context: FollowContext) -> FollowResponse:
191190
text = self.io_util.synchronous_request(request_body, prompt=prompt)
192191
if not text:
193192
return None
194-
return FollowResponse(json.loads(parse_utils.sanitize_json(text)))
193+
return FollowResponse(json_util.safe_load(text))
195194

tale/llm/llm_utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,20 +206,20 @@ def generate_start_zone(self, location_desc: str, story_type: str, story_context
206206
world_generation_context = WorldGenerationContext(story_context=story_context, story_type=story_type, world_info=world_info, world_mood=world_info['world_mood'])
207207
return self._world_building.generate_start_zone(location_desc, context=world_generation_context)
208208

209-
def generate_world_items(self, story_context: str = '', story_type: str = '', world_info: str = '', world_mood: int = None, item_types: list = []) -> WorldItemsResponse:
209+
def generate_world_items(self, story_context: str = '', story_type: str = '', world_info: str = '', world_mood: int = None, item_types: list = [], count: int = 7) -> WorldItemsResponse:
210210
world_generation_context = WorldGenerationContext(story_context=story_context or self.__story_context,
211211
story_type=story_type or self.__story_type,
212212
world_info=world_info or self.__world_info,
213213
world_mood=world_mood or self.__story.config.world_mood)
214-
return self._world_building.generate_world_items(world_generation_context, item_types=item_types)
214+
return self._world_building.generate_world_items(world_generation_context, item_types=item_types, count=count)
215215

216216

217-
def generate_world_creatures(self, story_context: str = '', story_type: str = '', world_info: str = '', world_mood: int = None) -> WorldCreaturesResponse:
217+
def generate_world_creatures(self, story_context: str = '', story_type: str = '', world_info: str = '', world_mood: int = None, count: int = 5) -> WorldCreaturesResponse:
218218
world_generation_context = WorldGenerationContext(story_context=story_context or self.__story_context,
219219
story_type=story_type or self.__story_type,
220220
world_info=world_info or self.__world_info,
221221
world_mood=world_mood or self.__story.config.world_mood)
222-
return self._world_building.generate_world_creatures(world_generation_context)
222+
return self._world_building.generate_world_creatures(world_generation_context, count=count)
223223

224224
def generate_random_spawn(self, location: Location, zone_info: dict) -> bool:
225225
return self._world_building.generate_random_spawn(location=location,

tale/llm/quest_building.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11

22
from copy import deepcopy
3-
import json
4-
from tale import parse_utils
3+
from tale import json_util, parse_utils
54
from tale.base import Location
65
from tale.llm import llm_config
76
from tale.llm.contexts.WorldGenerationContext import WorldGenerationContext
@@ -40,5 +39,5 @@ def generate_note_quest(self, context: WorldGenerationContext, zone_info: str) -
4039
if self.json_grammar_key:
4140
request_body[self.json_grammar_key] = self.json_grammar
4241
text = self.io_util.synchronous_request(request_body, prompt=prompt, context=context)
43-
quest_data = json.loads(parse_utils.sanitize_json(text))
42+
quest_data = json_util.safe_load(text)
4443
return Quest(name=quest_data['name'], type=QuestType[quest_data['type'].upper()], reason=quest_data['reason'], target=quest_data['target'])

0 commit comments

Comments
 (0)