Skip to content

Commit 455a30c

Browse files
authored
Merge pull request #86 from neph1/update-v0.31.0
Update v0.31.0
2 parents d7bed5a + 8a4598f commit 455a30c

22 files changed

+188
-116
lines changed

llm_config.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ ITEM_TEMPLATE: '{{"name":"", "type":"", "short_descr":"", "level":int, "value":i
1010
CREATURE_TEMPLATE: '{{"name":"", "body":"", "mass":int(kg), "hp":int, "type":"Npc or Mob", "level":int, "aggressive":bool, "unarmed_attack":One of [FISTS, CLAWS, BITE, TAIL, HOOVES, HORN, TUSKS, BEAK, TALON], "short_descr":""}}'
1111
EXIT_TEMPLATE: '{{"direction":"", "name":"name of new location", "short_descr":"exit description"}}'
1212
NPC_TEMPLATE: '{{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "aggressive":bool, "description":"25 words", "appearance":"25 words"}}'
13-
LOCATION_TEMPLATE: '{{"name": "", "exits":[], "items":[], "npcs":[]}}'
13+
LOCATION_TEMPLATE: '{{"name": "", "description":"", "exits":[], "items":[], "npcs":[], "indoors":"true or false"}}'
1414
ZONE_TEMPLATE: '{{"name":name of the room, "description": "75 words", "races":[], "items":[], "mood":"5 to -5, where 5 is extremely friendly and -5 is extremely hostile.", "level":(int)}}'
1515
DUNGEON_LOCATION_TEMPLATE: '{"index": (int), "name": "", "description": 25 words}'
1616
CHARACTER_TEMPLATE: '{"name":"", "description": "50 words", "appearance": "25 words", "personality": "50 words", "money":(int), "level":"", "gender":"m/f/n", "age":(int), "race":""}'
@@ -44,5 +44,7 @@ NOTE_LORE_PROMPT: '<context>{context}</context> Zone info: {zone_info};\n[USER_S
4444
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 goal and thoughts. Build on events in "History" without repeating them. Respond using JSON in the following format with up to 3 actions: """{action_template}""". Continue the sequence of events: {previous_events}'
4545
REQUEST_FOLLOW_PROMPT: '<context>{context}</context>\n[USER_START]Act as as {character_name}.\nUsing the information supplied inside the <context> tag. {character_name} has received a request to follow {target}. Answer based on {character_name}s description and mood. Reason given by {target}: {target_reason}. Respond using JSON in the following format: {follow_template}'
4646
DAY_CYCLE_EVENT_PROMPT: '<context>{context}</context>\n[USER_START] Write up to two sentences describing the transition from {from_time} to {to_time} in {location_name}, using the information supplied inside the <context> tags.'
47+
NARRATIVE_EVENT_PROMPT: '<context>{context}</context>\n[USER_START] Write a narrative event that occurs in {location_name} using the information supplied inside the <context> tags. The event should be related to the location and the characters present. Use up to 50 words.'
48+
RANDOM_SPAWN_PROMPT: '<context>{context}</context>\n[USER_START] An npc or a mob has entered {location_name}. Select either and fill in one of the following templates using the information supplied inside the <context> tags. Respond using JSON in the following format: {npc_template}'
4749
USER_START: '### Instruction:\n'
4850
USER_END: '### Response:\n'

tale/base.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ def __init__(self, name: str, descr: str="") -> None:
668668
self.built = True # has this location been built yet? If not, LLM will describe it.
669669
self.generated = False # whether this location is LLM generated or not
670670
self.world_location = Coord() # the world location of this location
671+
self.indoors = False # is this location indoors?
671672

672673
def __contains__(self, obj: Union['Living', Item]) -> bool:
673674
return obj in self.livings or obj in self.items
@@ -1217,7 +1218,7 @@ def remember_previous_parse(self) -> None:
12171218
"""remember the previously parsed data, soul uses this to reference back to earlier items/livings"""
12181219
self.soul.remember_previous_parse(self._previous_parse)
12191220

1220-
def do_socialize(self, cmdline: str, external_verbs: Set[str]=set()) -> None:
1221+
def do_socialize(self, cmdline: str, external_verbs: Set[str]=set(), evoke=True) -> None:
12211222
"""
12221223
Perform a command line with a socialize/soul verb on the living's behalf.
12231224
It only performs soul emotes, no custom command functions!
@@ -1230,7 +1231,7 @@ def do_socialize(self, cmdline: str, external_verbs: Set[str]=set()) -> None:
12301231
# emulate the say command (which is not an emote, but it's convenient to be able to use it as such)
12311232
verb, _, rest = cmdline.partition(" ")
12321233
rest = rest.strip()
1233-
self.tell_others("{Actor} says: " + rest)
1234+
self.tell_others("{Actor} says: " + rest, evoke=evoke)
12341235
else:
12351236
raise
12361237

@@ -1246,7 +1247,7 @@ def do_command_verb(self, cmdline: str, ctx: util.Context) -> None:
12461247
"""
12471248
command_verbs = ctx.driver.current_verbs(self)
12481249
try:
1249-
self.do_socialize(cmdline, command_verbs)
1250+
self.do_socialize(cmdline, command_verbs, evoke=False)
12501251
except NonSoulVerb as nx:
12511252
parsed = nx.parsed
12521253
if parsed.verb in command_verbs:

tale/day_cycle/day_cycle.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11

22

3+
import enum
34
from typing import List, Protocol
45
from tale import mud_context
6+
from tale.driver import Driver
57
from tale.util import GameDateTime, call_periodically
68

79
class DayCycleEventObserver(Protocol):
@@ -11,13 +13,20 @@ def on_day(self): pass
1113
def on_night(self): pass
1214
def on_midnight(self): pass
1315

16+
class TimeOfDay(str, enum.Enum):
17+
DAWN = 'Dawn'
18+
DAY = 'Day'
19+
DUSK = 'Dusk'
20+
NIGHT = 'Night'
1421
class DayCycle:
15-
16-
def __init__(self, game_date_time: GameDateTime):
17-
self.game_date_time = game_date_time
18-
self.current_hour = game_date_time.clock.hour
22+
23+
def __init__(self, driver: Driver):
24+
25+
self.game_date_time = driver.game_clock
26+
self.current_hour = self.game_date_time.clock.hour
1927
self.observers: List[DayCycleEventObserver] = []
20-
mud_context.driver.register_periodicals(self)
28+
driver.register_periodicals(self)
29+
self._time_of_day = TimeOfDay.DAY
2130

2231
def register_observer(self, observer: DayCycleEventObserver):
2332
self.observers.append(observer)
@@ -50,16 +59,25 @@ def hour(self):
5059

5160
def dawn(self):
5261
self.notify_observers('on_dawn')
62+
self._time_of_day = TimeOfDay.DAWN
5363

5464
def dusk(self):
5565
self.notify_observers('on_dusk')
66+
self._time_of_day = TimeOfDay.DUSK
5667

5768
def day(self):
5869
self.notify_observers('on_day')
70+
self._time_of_day = TimeOfDay.DAY
5971

6072
def night(self):
6173
self.notify_observers('on_night')
74+
self._time_of_day = TimeOfDay.NIGHT
6275

6376
def midnight(self):
6477
self.current_hour = 0
6578
self.notify_observers('on_midnight')
79+
self._time_of_day = TimeOfDay.NIGHT
80+
81+
@property
82+
def time_of_day(self) -> TimeOfDay:
83+
return self._time_of_day

tale/json_story.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from tale.items import generic
44
from tale.llm.llm_ext import DynamicStory
55
from tale.player import Player
6+
from tale.random_event import RandomEvent
67
from tale.story import GameMode, StoryConfig
78
import tale.parse_utils as parse_utils
89
import tale.llm.llm_cache as llm_cache
@@ -54,9 +55,12 @@ def init(self, driver) -> None:
5455
self._catalogue.add_item(item)
5556

5657
if self.config.day_night:
57-
self.day_cycle = DayCycle(self.driver.game_clock)
58+
self.day_cycle = DayCycle(driver)
5859
if self.config.server_mode == GameMode.IF:
5960
self.day_cycle.register_observer(LlmDayCycleListener(self.driver))
61+
62+
if self.config.random_events:
63+
self.random_events = RandomEvent(driver)
6064

6165
def welcome(self, player: Player) -> str:
6266
player.tell("<bright>Welcome to `%s'.</>" % self.config.name, end=True)

tale/llm/LivingNpc.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ def __init__(self, name: str, gender: str, *,
3737
self.example_voice = '' # type: str
3838
self.autonomous = False
3939
self.output_thoughts = False
40-
self.last_reaction_time = None
4140

4241
def notify_action(self, parsed: ParseResult, actor: Living) -> None:
4342
# store even our own events.
@@ -64,10 +63,6 @@ def notify_action(self, parsed: ParseResult, actor: Living) -> None:
6463
elif (targeted and parsed.verb == "idle-action") or parsed.verb == "location-event":
6564
event_hash = llm_cache.cache_event(unpad_text(parsed.unparsed))
6665
self._observed_events.append(event_hash)
67-
if self.last_reaction_time and mud_context.driver.game_clock.clock == self.last_reaction_time:
68-
# only react once per tick
69-
return
70-
self.last_reaction_time = mud_context.driver.game_clock.clock
7166
self._do_react(parsed, actor)
7267
elif targeted and parsed.verb == "give":
7368
parsed_split = parsed.unparsed.split(" to ")
@@ -162,7 +157,7 @@ def _do_react(self, parsed: ParseResult, actor: Living) -> None:
162157
sentiment=self.sentiments.get(actor.name, '') if actor else '')
163158
if action:
164159
self.action_history.append(action)
165-
self._defer_result(action, verb='idle-action')
160+
self._defer_result(action, verb='reaction')
166161

167162
def handle_item_result(self, result: ItemHandlingResult, actor: Living) -> bool:
168163
if result.to == self.title:
@@ -305,11 +300,11 @@ def _defer_result(self, action: str, verb: str="idle-action"):
305300
else:
306301
action = f"{self.title} : {action}"
307302
self.deferred_actions.add(action)
308-
self.tell_action_deferred()
303+
self.tell_action_deferred(verb)
309304

310-
def tell_action_deferred(self):
305+
def tell_action_deferred(self, verb: str):
311306
actions = '\n'.join(self.deferred_actions) + '\n'
312-
deferred_action = ParseResult(verb='idle-action', unparsed=actions, who_info=None)
307+
deferred_action = ParseResult(verb=verb, unparsed=actions, who_info=None)
313308
self.tell_others(actions)
314309
self.location._notify_action_all(deferred_action, actor=self)
315310
self.deferred_actions.clear()

tale/llm/contexts/EvokeContext.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
class EvokeContext(BaseContext):
44

5-
def __init__(self, story_context: str, history: str, extra_context: str = '') -> None:
5+
def __init__(self, story_context: str, history: str, time_of_day: str = 'day', extra_context: str = '') -> None:
66
super().__init__(story_context)
77
self.history = history
8+
self.time_of_day = time_of_day
89
self.extra_context = extra_context
910

1011
def to_prompt_string(self) -> str:
11-
return f"Story context:{self.story_context}; History:{self.history}; " + (f"{self.extra_context};" if self.extra_context else '')
12+
return f"Story context:{self.story_context}; History:{self.history}; " + (f"{self.extra_context};" if self.extra_context else '' + f"Time of day:{self.time_of_day};" if self.time_of_day else '')

tale/llm/llm_ext.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def find_location(self, name: str) -> Location:
4444
return location
4545

4646
def find_zone(self, location: str) -> Zone:
47-
""" Find a zone by location."""
47+
""" Find a zone by location name."""
4848
for zone in self._zones.values():
4949
if zone.get_location(location):
5050
return zone

tale/llm/llm_utils.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def __init__(self, io_util: IoUtil = None):
5353
self.short_word_limit = llm_config.params['SHORT_WORD_LIMIT']
5454
self.story_background_prompt = llm_config.params['STORY_BACKGROUND_PROMPT'] # type: str
5555
self.day_cycle_event_prompt = llm_config.params['DAY_CYCLE_EVENT_PROMPT'] # type: str
56+
self.narrative_event_prompt = llm_config.params['NARRATIVE_EVENT_PROMPT']
5657
self.__story = None # type: DynamicStory
5758
self.io_util = io_util or IoUtil(config=llm_config.params, backend_config=backend_config)
5859
self.stream = backend_config['STREAM']
@@ -92,13 +93,19 @@ def evoke(self, message: str, short_len: bool=False, rolling_prompt: str = '', a
9293

9394
rolling_prompt = self.update_memory(rolling_prompt, message)
9495

95-
text_hash_value = llm_cache.generate_hash(message)
96+
text_hash_value = llm_cache.generate_hash(message + extra_context)
9697

9798
cached_look = llm_cache.get_looks([text_hash_value])
9899
if cached_look:
99100
return output_template.format(message=message, text=cached_look), rolling_prompt
100101
trimmed_message = parse_utils.remove_special_chars(str(message))
101-
story_context = EvokeContext(story_context=self.__story_context, history=rolling_prompt if not (skip_history or alt_prompt) else '', extra_context=extra_context)
102+
time_of_day = ''
103+
if self.__story.config.day_night:
104+
time_of_day = self.__story.day_cycle.time_of_day.__str__
105+
story_context = EvokeContext(story_context=self.__story_context,
106+
time_of_day=time_of_day,
107+
history=rolling_prompt if not (skip_history or alt_prompt) else '',
108+
extra_context=extra_context)
102109
prompt = self.pre_prompt
103110
prompt += (alt_prompt or self.evoke_prompt).format(
104111
context = '{context}',
@@ -296,12 +303,9 @@ def request_follow(self, actor: MudObject, character_name: str, character_card:
296303
def describe_day_cycle_transition(self, player: PlayerConnection, from_time: str, to_time: str) -> str:
297304
prompt = self.pre_prompt
298305
location = player.player.location
299-
context = WorldGenerationContext(story_context=self.__story_context,
300-
story_type=self.__story_type,
301-
world_info=self.__world_info,
302-
world_mood=self.__story.config.world_mood)
306+
context = self._get_world_context()
303307
prompt += self.day_cycle_event_prompt.format(
304-
context= '',
308+
context= '{context}',
305309
location_name=location.name,
306310
from_time=from_time,
307311
to_time=to_time)
@@ -313,6 +317,18 @@ def describe_day_cycle_transition(self, player: PlayerConnection, from_time: str
313317
return text
314318
text = self.io_util.stream_request(request_body=request_body, prompt=prompt, context=context.to_prompt_string() + f'Location: {location.name, location.description};', io=player)
315319
return text
320+
321+
def generate_narrative_event(self, location: Location) -> str:
322+
prompt = self.pre_prompt
323+
context = self._get_world_context()
324+
prompt += self.narrative_event_prompt.format(
325+
context= '{context}',
326+
location_name=location.name)
327+
request_body = deepcopy(self.default_body)
328+
329+
text = self.io_util.synchronous_request(request_body, prompt=prompt, context=context.to_prompt_string() + f'Location: {location.name, location.description};')
330+
location.tell(text, evoke=False)
331+
return text
316332

317333
def set_story(self, story: DynamicStory):
318334
""" Set the story object."""

tale/llm/responses/LocationResponse.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ def _validate_location(self, json_result: dict,
5151
# some models don't generate description, sometimes
5252
json_result['description'] = exit_location_name
5353
location_to_build.description = json_result['description']
54-
54+
55+
if json_result.get('indoors', False):
56+
location_to_build.indoors = True
5557

5658
self._add_items(location_to_build, json_result, world_items)
5759

tale/parse_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ def load_story_config(json_file: dict):
236236
config.image_gen = json_file.get('image_gen', config.image_gen)
237237
config.epoch = json_file.get('epoch', config.epoch)
238238
config.day_night = json_file.get('day_night', config.day_night)
239+
config.random_events = json_file.get('random_events', config.random_events)
239240
return config
240241

241242
def save_story_config(config: StoryConfig) -> dict:
@@ -271,6 +272,7 @@ def save_story_config(config: StoryConfig) -> dict:
271272
json_file['image_gen'] = config.image_gen
272273
json_file['epoch'] = config.epoch
273274
json_file['day_night'] = config.day_night
275+
json_file['random_events'] = config.random_events
274276
return json_file
275277

276278

@@ -371,7 +373,7 @@ def sanitize_json(result: str) -> str:
371373
result = result.strip()
372374
result = result.replace('```json', '') #.replace('\\"', '"').replace('"\\n"', '","').replace('\\n', '').replace('}\n{', '},{').replace('}{', '},{').replace('\\r', '').replace('\\t', '').replace('"{', '{').replace('}"', '}').replace('"\\', '"').replace('\\”', '"').replace('" "', '","').replace(':,',':').replace('},]', '}]').replace('},}', '}}')
373375
result = result.split('```')[0]
374-
result = result.replace('False', 'false').replace('True', 'true').replace('None', 'null')
376+
result = result.replace('False', 'false').replace('True', 'true').replace('None', 'null').replace('\\r', '')
375377
if not result.endswith('}') and not result.endswith(']'):
376378
result = result + '}'
377379
#print('sanitized json: ' + result)

0 commit comments

Comments
 (0)