Skip to content

Commit

Permalink
Merge pull request #96 from neph1/update-v0.36.1
Browse files Browse the repository at this point in the history
Update v0.36.1
  • Loading branch information
neph1 authored Oct 20, 2024
2 parents 1c878a1 + d8fc907 commit 9350be3
Show file tree
Hide file tree
Showing 37 changed files with 370 additions and 149 deletions.
3 changes: 2 additions & 1 deletion llm_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ DUNGEON_LOCATION_TEMPLATE: '{"index": (int), "name": "", "description": 25 words
CHARACTER_TEMPLATE: '{"name":"", "description": "50 words", "appearance": "25 words", "personality": "50 words", "money":(int), "level":"", "gender":"m/f/n", "age":(int), "race":"", "occupation":""}'
FOLLOW_TEMPLATE: '{{"response":"yes or no", "reason":"50 words"}}'
ITEM_TYPES: ["Weapon", "Wearable", "Health", "Money", "Trash"]
PRE_PROMPT: 'You are a creative game keeper for an interactive fiction story telling session. You craft detailed worlds and interesting characters with unique and deep personalities for the player to interact with. Do not acknowledge the task or speak directly to the user, or respond with anything besides the request..'
PRE_PROMPT: 'You are a creative game keeper for an interactive fiction story telling session. You craft detailed worlds and interesting characters with unique and deep personalities for the player to interact with. Always follow the instructions given, never acknowledge the task or speak directly to the user or respond with anything besides the request.'
BASE_PROMPT: '<context>{context}</context>\n[USER_START] Rewrite [{input_text}] in your own words. The information inside the <context> tags should be used to ensure it fits the story. Use about {max_words} words.'
DIALOGUE_PROMPT: '<context>{context}</context>\nThe following is a conversation between {character1} and {character2}; {character2}s sentiment towards {character1}: {sentiment}. Write a single response as {character2} in third person pov, using {character2} description and other information found inside the <context> tags. If {character2} has a quest active, they will discuss it based on its status. Respond in JSON using this template: """{dialogue_template}""". [USER_START]Continue the following conversation as {character2}: {previous_conversation}'
COMBAT_PROMPT: '<context>{context}</context>\nThe following is a combat scene between {attackers} and {defenders} in {location}. [USER_START] Describe the following combat result in about 150 words in vivid language, using the characters weapons and their health status: 1.0 is highest, 0.0 is lowest. Combat Result: {input_text}'
Expand Down Expand Up @@ -46,3 +46,4 @@ REQUEST_FOLLOW_PROMPT: '<context>{context}</context>\n[USER_START]Act as as {cha
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.'
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.'
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}'
ADVANCE_STORY_PROMPT: '<context>{context}</context>\n[USER_START] Advance the high-level plot outline in the current story, hidden to the player of this roleplaying game. The story progress is a value between 0 and 10. Consider the pacing of the plot when writing the section. A value below 3 is considered early. An inciting event should happen during this phase. Between 3 and 8 is the middle of the story. A climactic event should happen at 8 or 9. Above 9 is considered to be the ending. Use the information supplied inside the <context> tags to write a short paragraph that progresses the plot outline. Use up to 100 words.'
2 changes: 1 addition & 1 deletion stories/combat_sandbox/story.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from tale.base import Location
from tale.driver import Driver
from tale.json_story import JsonStory
from tale.llm.llm_ext import DynamicStory
from tale.llm.dynamic_story import DynamicStory
from tale.main import run_from_cmdline
from tale.player import Player, PlayerConnection
from tale.charbuilder import PlayerNaming
Expand Down
10 changes: 8 additions & 2 deletions stories/demo/story.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
Copyright by Irmen de Jong ([email protected])
"""
import datetime
import pathlib
import sys
from typing import Optional, Generator

from tale.driver import Driver
from tale.main import run_from_cmdline
from tale.player import Player, PlayerConnection
from tale.charbuilder import PlayerNaming
from tale.story import *
Expand Down Expand Up @@ -85,5 +87,9 @@ def goodbye(self, player: Player) -> None:

if __name__ == "__main__":
# story is invoked as a script, start it.
from tale.main import run_from_cmdline
run_from_cmdline(["--game", sys.path[0]])
gamedir = pathlib.Path(__file__).parent
if gamedir.is_dir() or gamedir.is_file():
cmdline_args = sys.argv[1:]
cmdline_args.insert(0, "--game")
cmdline_args.insert(1, str(gamedir))
run_from_cmdline(cmdline_args)
13 changes: 7 additions & 6 deletions stories/prancingllama/story.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
from tale.base import Location
from tale.cmds import spells
from tale.driver import Driver
from tale.llm.llm_ext import DynamicStory
from tale.llm.dynamic_story import DynamicStory
from tale.skills.magic import MagicType
from tale.main import run_from_cmdline
from tale.player import Player, PlayerConnection
from tale.charbuilder import PlayerNaming
from tale.skills.skills import SkillType
from tale.story import *
from tale.skills.weapon_type import WeaponType
from tale.story import StoryContext
from tale.zone import Zone

class Story(DynamicStory):
Expand All @@ -34,15 +35,15 @@ class Story(DynamicStory):
config.startlocation_player = "prancingllama.entrance"
config.startlocation_wizard = "prancingllama.entrance"
config.zones = ["prancingllama"]
config.context = "The final outpost high up in a cold, craggy mountain range. It's frequented by adventurers and those seeking to avoid attention."
config.context = StoryContext(base_story="The final outpost high up in a cold, craggy mountain range. A drama unfolds between those avoiding the cold and those seeking the cold. And what is lurking underneath the snow covered peaks and uncharted valleys?")
config.type = "A low level fantasy adventure with focus of character building and interaction."
config.custom_resources = True


def init(self, driver: Driver) -> None:
"""Called by the game driver when it is done with its initial initialization."""
self.driver = driver
self._zones = dict() # type: dict(str, Zone)
self._zones = dict() # type: {str, Zone}
self._zones["The Prancing Llama"] = Zone("The Prancing Llama", description="A cold, craggy mountain range. Snow covered peaks and uncharted valleys hide and attract all manners of creatures.")
import zones.prancingllama
for location in zones.prancingllama.all_locations:
Expand Down Expand Up @@ -96,13 +97,13 @@ def goodbye(self, player: Player) -> None:
player.tell("Goodbye, %s. Please come back again soon." % player.title)
player.tell("\n")

def races_for_zone(self, zone: str) -> [str]:
def races_for_zone(self, zone: str) -> list[str]:
return self._catalogue._creatures

def items_for_zone(self, zone: str) -> [str]:
def items_for_zone(self, zone: str) -> list[str]:
return self._catalogue._items

def zone_info(self, zone_name: str, location: str) -> dict():
def zone_info(self, zone_name: str, location: str) -> dict:
zone_info = super.zone_info(zone_name, location)
zone_info['races'] = self.races_for_zone(zone_name)
zone_info['items'] = self.items_for_zone(zone_name)
Expand Down
2 changes: 1 addition & 1 deletion tale/cmds/normal.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from typing import Iterable, List, Dict, Generator, Union, Optional
from tale.llm.LivingNpc import LivingNpc

from tale.llm.llm_ext import DynamicStory
from tale.llm.dynamic_story import DynamicStory
from tale.skills.skills import SkillType

from . import abbreviations, cmd, disabled_in_gamemode, disable_notify_action, overrides_soul, no_soul_parse
Expand Down
9 changes: 6 additions & 3 deletions tale/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@

from . import __version__ as tale_version_str, _check_required_libraries
from . import mud_context, errors, util, cmds, player, pubsub, charbuilder, lang, verbdefs, vfs, base
from .story import TickMethod, GameMode, MoneyType, StoryBase
from .story import StoryContext, TickMethod, GameMode, MoneyType, StoryBase
from .tio import DEFAULT_SCREEN_WIDTH
from .races import playable_races
from .errors import StoryCompleted
from tale.load_character import CharacterLoader, CharacterV2
from tale.llm.llm_ext import DynamicStory
from tale.llm.dynamic_story import DynamicStory
from tale.llm.llm_utils import LlmUtil
from tale.web.web_utils import clear_resources, copy_web_resources

Expand Down Expand Up @@ -535,6 +535,9 @@ def _server_tick(self) -> None:
events, idle_time, subbers = topicinfo[topicname]
if events == 0 and not subbers and idle_time > 30:
pubsub.topic(topicname).destroy()
progress = self.story.increase_progress(0.0001)
if progress:
self.llm_util.advance_story_section(self.story)

def disconnect_idling(self, conn: player.PlayerConnection) -> None:
raise NotImplementedError
Expand Down Expand Up @@ -933,7 +936,7 @@ def build_location(self, targetLocation: base.Location, zone: Zone, player: play
# try to add location, and if it fails, remove exit to it
result = dynamic_story.add_location(location, zone=zone.name)
if not result:
for exit in exits: # type: Exit
for exit in exits:
if exit.name == location.name:
exits.remove(exit)
for exit in exits:
Expand Down
2 changes: 1 addition & 1 deletion tale/dungeon/dungeon_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from tale.coord import Coord
from tale.item_spawner import ItemSpawner
from tale.items.basic import Money
from tale.llm.llm_ext import DynamicStory
from tale.llm.dynamic_story import DynamicStory
from tale.mob_spawner import MobSpawner
from tale.zone import Zone

Expand Down
2 changes: 1 addition & 1 deletion tale/json_story.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from tale.day_cycle.day_cycle import DayCycle
from tale.day_cycle.llm_day_cycle_listener import LlmDayCycleListener
from tale.items import generic
from tale.llm.llm_ext import DynamicStory
from tale.llm.dynamic_story import DynamicStory
from tale.player import Player
from tale.random_event import RandomEvent
from tale.story import GameMode, StoryConfig
Expand Down
12 changes: 12 additions & 0 deletions tale/llm/contexts/AdvanceStoryContext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@


from tale.llm.contexts.BaseContext import BaseContext


class AdvanceStoryContext(BaseContext):

def __init__(self, story_context: str):
super().__init__(story_context)

def to_prompt_string(self) -> str:
return f"{self.story_context}"
12 changes: 8 additions & 4 deletions tale/llm/contexts/BaseContext.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@


from abc import ABC, abstractmethod
from typing import Union

from tale.story import StoryContext


class BaseContext(ABC):

def __init__(self, story_context: str) -> None:
self.story_context = story_context
def __init__(self, story_context: Union[StoryContext, str]) -> None:
if isinstance(story_context, StoryContext):
self.story_context = story_context.to_context_with_past()
else:
self.story_context = story_context

@abstractmethod
def to_prompt_string(self) -> str:
Expand Down
4 changes: 3 additions & 1 deletion tale/llm/llm_ext.py → tale/llm/dynamic_story.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from tale.llm.LivingNpc import LivingNpc
from tale.quest import Quest, QuestType
from tale.mob_spawner import MobSpawner
from tale.story import StoryBase
from tale.story import StoryBase, StoryContext

from tale.zone import Zone
import tale.llm.llm_cache as llm_cache
Expand All @@ -21,6 +21,8 @@ def __init__(self) -> None:
self._zones = dict() # type: dict[str, Zone]
self._world = WorldInfo()
self._catalogue = Catalogue()
if isinstance(self.config.context, str):
self.config.context = StoryContext(self.config.context)

def get_zone(self, name: str) -> Zone:
""" Find a zone by name."""
Expand Down
4 changes: 2 additions & 2 deletions tale/llm/llm_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def cache_event(event: str, event_hash: int = -1) -> int:
event_cache[event_hash] = event
return event_hash

def get_events(event_hashes: [int]) -> str:
def get_events(event_hashes: list[int]) -> str:
""" Gets events from the cache. """
return "<break>".join([event_cache.get(event_hash, '') for event_hash in event_hashes])

Expand All @@ -37,7 +37,7 @@ def cache_look(look: str, look_hash: int = -1) -> int:
look_cache[look_hash] = look
return look_hash

def get_looks(look_hashes: [int]) -> str:
def get_looks(look_hashes: list[int]) -> str:
""" Gets an event from the cache. """
return ", ".join([look_cache.get(look_hash, '') for look_hash in look_hashes])

Expand Down
6 changes: 5 additions & 1 deletion tale/llm/llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from tale.llm.contexts.EvokeContext import EvokeContext
from tale.llm.contexts.FollowContext import FollowContext
from tale.llm.contexts.WorldGenerationContext import WorldGenerationContext
from tale.llm.llm_ext import DynamicStory
from tale.llm.dynamic_story import DynamicStory
from tale.llm.llm_io import IoUtil
from tale.llm.contexts.DialogueContext import DialogueContext
from tale.llm.quest_building import QuestBuilding
Expand Down Expand Up @@ -342,6 +342,10 @@ def set_story(self, story: DynamicStory):
if story.config.image_gen:
self._init_image_gen(story.config.image_gen)

def advance_story_section(self, story: DynamicStory) -> str:
""" Increase the story progress"""
return self._story_building.advance_story_section(story or self.__story)

def _init_image_gen(self, image_gen: str):
""" Initialize the image generator"""
clazz = getattr(sys.modules['tale.image_gen.' + image_gen.lower()], image_gen)
Expand Down
13 changes: 12 additions & 1 deletion tale/llm/story_building.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# This file contains the StoryBuilding class, which is responsible for generating the story background
from tale import parse_utils
from tale.llm import llm_config
from tale.llm.contexts.AdvanceStoryContext import AdvanceStoryContext
from tale.llm.dynamic_story import DynamicStory
from tale.llm.llm_io import IoUtil


Expand All @@ -11,12 +13,21 @@ def __init__(self, io_util: IoUtil, default_body: dict, backend: str = 'kobold_c
self.io_util = io_util
self.default_body = default_body
self.story_background_prompt = llm_config.params['STORY_BACKGROUND_PROMPT'] # Type: str
self.advance_story_prompt = llm_config.params['ADVANCE_STORY_PROMPT'] # Type: str

def generate_story_background(self, world_mood: int, world_info: str, story_type: str):
def generate_story_background(self, world_mood: int, world_info: str, story_type: str) -> str:
prompt = self.story_background_prompt.format(
story_type=story_type,
world_mood=parse_utils.mood_string_from_int(world_mood),
world_info=world_info)
request_body = self.default_body
return self.io_util.synchronous_request(request_body, prompt=prompt)

def advance_story_section(self, story: DynamicStory) -> str:
story_context = AdvanceStoryContext(story.config.context)
prompt = self.advance_story_prompt.format(context=story_context.to_prompt_string())
request_body = self.default_body
result = self.io_util.synchronous_request(request_body, prompt=prompt)
story.config.context.set_current_section(result)
return result

2 changes: 1 addition & 1 deletion tale/llm/world_building.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from tale.llm import llm_config
from tale.llm.contexts.DungeonLocationsContext import DungeonLocationsContext
from tale.llm.contexts.WorldGenerationContext import WorldGenerationContext
from tale.llm.llm_ext import DynamicStory
from tale.llm.dynamic_story import DynamicStory
from tale.llm.llm_io import IoUtil
from tale.llm.requests.generate_zone import GenerateZone
from tale.llm.requests.start_location import StartLocation
Expand Down
19 changes: 10 additions & 9 deletions tale/parse_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,16 @@
from tale.equip_npcs import equip_npc
from tale.item_spawner import ItemSpawner
from tale.items import generic
from tale.items.basic import Boxlike, Drink, Food, Health, Money, Note
from tale.llm.LivingNpc import LivingNpc
from tale.load_items import load_item
from tale.skills.magic import MagicType
from tale.npc_defs import StationaryMob, StationaryNpc, Trader
from tale.races import BodyType, UnarmedAttack
from tale.mob_spawner import MobSpawner
from tale.story import GameMode, MoneyType, TickMethod, StoryConfig
from tale.skills.weapon_type import WeaponSkills, WeaponType
from tale.wearable import WearLocation
from tale.story import GameMode, MoneyType, StoryContext, TickMethod, StoryConfig
from tale.skills.weapon_type import WeaponType
import json
import re
import sys
import os


Expand Down Expand Up @@ -236,7 +233,11 @@ def load_story_config(json_file: dict):
config.server_mode = GameMode[json_file['server_mode']]
config.npcs = json_file.get('npcs', '')
config.items = json_file.get('items', '')
config.context = json_file.get('context', '')
context = json_file.get('context', '')
if isinstance(context, dict):
config.context = StoryContext().from_json(context)
else:
config.context = context
config.type = json_file.get('type', '')
config.world_info = json_file.get('world_info', '')
config.world_mood = json_file.get('world_mood', config.world_mood)
Expand Down Expand Up @@ -275,7 +276,7 @@ def save_story_config(config: StoryConfig) -> dict:
json_file['type'] = config.type
json_file['world_info'] = config.world_info
json_file['world_mood'] = config.world_mood
json_file['context'] = config.context
json_file['context'] = config.context if isinstance(config.context, str) else config.context.to_json()
json_file['custom_resources'] = config.custom_resources
json_file['image_gen'] = config.image_gen
json_file['epoch'] = 0 # TODO: fix later
Expand All @@ -295,7 +296,7 @@ def _insert(new_item: Item, locations, location: str):
loc.insert(new_item, None)

def remove_special_chars(message: str):
re.sub('[^A-Za-z0-9 .,_\-\'\"]+', '', message)
re.sub('[^A-Za-z0-9 .,_\'\"]+', '', message)
return message

def trim_response(message: str):
Expand Down Expand Up @@ -646,7 +647,7 @@ def load_stats(json_stats: dict) -> Stats:
stats.skills[WeaponType(int_skill)] = json
return stats

def save_items(items: List[Item]) -> []:
def save_items(items: List[Item]) -> list[dict]:
json_items = []
for item in items:
json_item = item.to_dict()
Expand Down
2 changes: 1 addition & 1 deletion tale/random_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import random
from tale import _MudContext
from tale.driver import Driver
from tale.llm.llm_ext import DynamicStory
from tale.llm.dynamic_story import DynamicStory
from tale.player import PlayerConnection
from tale.util import call_periodically

Expand Down
Loading

0 comments on commit 9350be3

Please sign in to comment.