Skip to content

Commit

Permalink
Merge pull request #98 from neph1/update-v0.37.0
Browse files Browse the repository at this point in the history
Update v0.37.0
  • Loading branch information
neph1 authored Nov 4, 2024
2 parents 4212f07 + f09b284 commit 6a19be8
Show file tree
Hide file tree
Showing 15 changed files with 96 additions and 50 deletions.
3 changes: 2 additions & 1 deletion backend_kobold_cpp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ JSON_GRAMMAR_KEY: "grammar"
STREAM_ENDPOINT: "/api/extra/generate/stream"
DATA_ENDPOINT: "/api/extra/generate/check"
DEFAULT_BODY: '{"stop_sequence": "", "max_length":1500, "max_context_length":4096, "temperature":0.5, "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}'
GENERATION_BODY: '{"stop_sequence": "", "max_length":1500, "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}'
GENERATION_BODY: '{"stop_sequence": "", "max_length":1500, "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}'
API_PASSWORD: "" # if koboldcpp is run with the --password flag, this must be set to the same password
2 changes: 1 addition & 1 deletion llm_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ 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. 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}'
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 describe their health status without mentioning numbers: 1.0 is highest, 0.0 is dead. <combat result> {input_text}</combat result>'
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.'
CREATE_CHARACTER_PROMPT: '<context>{context}</context>\n[USER_START] Create a diverse character with rich personality that can be interacted with using the story context and keywords. {{quest_prompt}} Do not mention height. Keywords: {keywords}. Fill in the blanks in this JSON template and write nothing else: {character_template}'
CREATE_LOCATION_PROMPT: '<context>{context}</context>\nZone info: {zone_info}; Exit json example: {exit_template}; Npc or mob example: {npc_template}. Existing connected locations: {exit_locations}. [USER_START] Using the information supplied inside the <context> tags, describe the following location: {location_name}. {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: {location_template}. Write the response in valid JSON.'
Expand Down
2 changes: 2 additions & 0 deletions tale/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ def __init__(self, name: str, title: str = "", *, descr: str = "", short_descr:
self.rent = 0.0 # price to keep in store / day
self.weight = 0.0 # some abstract unit
self.takeable = True # can this item be taken/picked up?
self.durability = 100 # how long can this item last?
super().__init__(name, title=title, descr=descr, short_descr=short_descr)

def init(self) -> None:
Expand All @@ -459,6 +460,7 @@ def to_dict(self) -> Dict[str, Any]:
"rent": self.rent,
"weight": self.weight,
"takeable": self.takeable,
"durability": self.durability,
"location" : self.location.name if self.location else ''
}

Expand Down
6 changes: 0 additions & 6 deletions tale/cmds/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,9 +904,3 @@ def do_create_item(player: Player, parsed: base.ParseResult, ctx: util.Context)
player.tell(item.name + ' added.', evoke=False)
else:
raise ParseError("Item could not be added")

@wizcmd("restart_story")
def do_restart(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None:
"""Restart the game."""
player.tell("Restarting the game... Please reconnect")
os.execv(sys.executable, ['python3'] + sys.argv)
51 changes: 31 additions & 20 deletions tale/combat.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ def _calculate_armor_bonus(self, actor: 'base.Living', body_part: WearLocation =
return wearable.ac if wearable else 1
return actor.stats.ac + 1

def _subtract_armor_durability(self, actor: 'base.Living', body_part: WearLocation, amount: int):
wearable = actor.get_wearable(body_part)
if wearable:
wearable.durability -= amount

def resolve_body_part(self, defender: 'base.Living', size_factor: float, target_part: WearLocation = None) -> WearLocation:
""" Resolve the body part that was hit. """
body_parts = body_parts_for_bodytype(defender.stats.bodytype)
Expand Down Expand Up @@ -94,49 +99,55 @@ def resolve_attack(self) -> str:
for attacker in self.attackers:
random_defender = random.choice(self.defenders)
text_result, damage_to_defender = self._round(attacker, random_defender)
texts.extend(text_result)
texts.append(text_result)
random_defender.stats.hp -= damage_to_defender
if random_defender.stats.hp < 1:
texts.append(f'{random_defender.title} dies')
texts.append(f'{random_defender.title} dies from their injuries.')

for defender in self.defenders:
if defender.stats.hp < 1:
continue
random_attacker = random.choice(self.attackers)
text_result, damage_to_attacker = self._round(defender, random_attacker)
texts.extend(text_result)
texts.append(text_result)

random_attacker.stats.hp -= damage_to_attacker
if random_attacker.stats.hp < 1:
texts.append(f'{random_attacker.title} dies')
texts.append(f'{random_attacker.title} dies from their injuries.')

return ', '.join(texts)
return '\n'.join(texts)

def _round(self, actor1: 'base.Living', actor2: 'base.Living') -> Tuple[List[str], int]:
attack_result = self._calculate_attack_success(actor1)
texts = []
attack_text = f'{actor1.title} attacks {actor2.title} with their {actor1.wielding.name}'
if attack_result < 0:
if attack_result < -actor1.stats.weapon_skills.get(actor1.wielding.type) + 5:
texts.append(f'{actor1.title} performs a critical hit on {actor2.title}')
attack_text += ' and hits critically'
block_result = 100
else:
texts.append(f'{actor1.title} hits {actor2.title}')
attack_text += ' and hits'
block_result = self._calculate_block_success(actor1, actor2)

if block_result < 0:
texts.append(f'but {actor2.title} blocks')
attack_text += f', but {actor2.title} blocks with their {actor2.wielding.name}'
actor2.wielding.durability -= random.randint(1, 10)
else:
actor1_strength = self._calculate_weapon_bonus(actor1) * actor1.stats.size.order
actor1_attack = self._calculate_weapon_bonus(actor1) * actor1.stats.size.order
body_part = self.resolve_body_part(actor2, actor1.stats.size.order / actor2.stats.size.order, target_part=self.target_body_part)
actor2_strength = self._calculate_armor_bonus(actor2, body_part) * actor2.stats.size.order
damage_to_defender = int(max(0, actor1_strength - actor2_strength))
actor2_defense = self._calculate_armor_bonus(actor2, body_part) * actor2.stats.size.order
damage_to_defender = int(max(0, actor1_attack - actor2_defense))
if damage_to_defender > 0:
texts.append(f', {actor2.title} is injured in the {body_part.name.lower()}')
attack_text += f', and {actor2.title} is injured in the {body_part.name.lower()}.'
elif actor1_attack < actor2_defense:
attack_text += f', but {actor2.title}\' armor protects them.'
self._subtract_armor_durability(actor2, body_part, actor1_attack)
else:
texts.append(f', {actor2.title} is unharmed')
return texts, damage_to_defender
attack_text += f', but {actor2.title} is unharmed.'
return attack_text, damage_to_defender
elif attack_result > 50:
texts.append(f'{actor1.title} misses {actor2.title} completely')
attack_text + f', but misses completely.'
elif attack_result > 25:
texts.append(f'{actor1.title} misses {actor2.title}')
attack_text + f', but misses.'
else:
texts.append(f'{actor1.title} barely misses {actor2.title}')
return texts, 0
attack_text + f', and barely misses.'
return attack_text, 0
4 changes: 3 additions & 1 deletion tale/json_story.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from tale import load_items
from tale import load_items, wearable
from tale.items import generic
from tale.llm.dynamic_story import DynamicStory
from tale.player import Player
Expand Down Expand Up @@ -34,6 +34,8 @@ def init(self, driver) -> None:
self._catalogue._creatures = world['catalogue']['creatures']
if world['catalogue']['items']:
self._catalogue._items = world['catalogue']['items']
if world['catalogue'].get('wearables', None):
wearable.add_story_wearables(world['catalogue']['wearables'])
if world.get('world', None):
if world['world']['items']:
# Keep this so that saved items in worlds will transfer to locations. But don't save them.
Expand Down
16 changes: 9 additions & 7 deletions tale/llm/io_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(self, url: str, stream_endpoint: str, user_start_prompt: str = '',
self.prompt_end = prompt_end

@abstractmethod
def stream_request(self, request_body: dict, io = None, wait: bool = False) -> str:
def stream_request(self, headers: dict, request_body: dict, io = None, wait: bool = False) -> str:
pass

@abstractmethod
Expand All @@ -44,8 +44,8 @@ def __init__(self, url: str, stream_endpoint: str, data_endpoint: str, user_star
self.data_endpoint = data_endpoint
self.place_context_in_memory = False

def stream_request(self, request_body: dict, io: PlayerConnection = None, wait: bool = False) -> str:
result = asyncio.run(self._do_stream_request(self.url + self.stream_endpoint, request_body))
def stream_request(self, headers: dict, request_body: dict, io: PlayerConnection = None, wait: bool = False) -> str:
result = asyncio.run(self._do_stream_request(self.url + self.stream_endpoint, headers, request_body))

try:
if result:
Expand All @@ -54,9 +54,10 @@ def stream_request(self, request_body: dict, io: PlayerConnection = None, wait:
print("Error parsing response from backend - ", exc)
return ''

async def _do_stream_request(self, url: str, request_body: dict,) -> bool:
async def _do_stream_request(self, url: str, headers: dict, request_body: dict,) -> bool:
""" Send request to stream endpoint async to not block the main thread"""
async with aiohttp.ClientSession() as session:
session.headers.update(headers)
async with session.post(url, data=json.dumps(request_body)) as response:
if response.status == 200:
return True
Expand Down Expand Up @@ -103,14 +104,15 @@ def set_prompt(self, request_body: dict, prompt: str, context: str = '') -> dict

class LlamaCppAdapter(AbstractIoAdapter):

def stream_request(self, request_body: dict, io: PlayerConnection = None, wait: bool = False) -> str:
return asyncio.run(self._do_stream_request(self.url + self.stream_endpoint, request_body, io = io))
def stream_request(self, headers: dict, request_body: dict, io: PlayerConnection = None, wait: bool = False) -> str:
return asyncio.run(self._do_stream_request(self.url + self.stream_endpoint, headers, request_body, io = io))

async def _do_stream_request(self, url: str, request_body: dict, io: PlayerConnection) -> str:
async def _do_stream_request(self, url: str, headers: dict, request_body: dict, io: PlayerConnection) -> str:
""" Send request to stream endpoint async to not block the main thread"""
request_body['stream'] = True
text = ''
async with aiohttp.ClientSession() as session:
session.headers.update(headers)
async with session.post(url, data=json.dumps(request_body)) as response:
if response.status != 200:
print("Error occurred:", response.status)
Expand Down
7 changes: 5 additions & 2 deletions tale/llm/llm_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ def __init__(self, config: dict = None, backend_config: dict = None):
self.backend = config['BACKEND']
self.url = backend_config['URL']
self.endpoint = backend_config['ENDPOINT']
headers = {}
if self.backend != 'kobold_cpp':
headers = json.loads(backend_config['OPENAI_HEADERS'])
headers['Authorization'] = f"Bearer {backend_config['OPENAI_API_KEY']}"
self.openai_json_format = json.loads(backend_config['OPENAI_JSON_FORMAT'])
self.headers = headers
self.io_adapter = LlamaCppAdapter(self.url, backend_config['STREAM_ENDPOINT'], config.get('USER_START', ''), config.get('USER_END', ''), config.get('SYSTEM_START', ''), config.get('PROMPT_END', ''))
else:
if 'API_PASSWORD' in backend_config and backend_config['API_PASSWORD']:
headers['Authorization'] = f"Bearer {backend_config['API_PASSWORD']}"
self.headers = headers
self.io_adapter = KoboldCppAdapter(self.url, backend_config['STREAM_ENDPOINT'], backend_config['DATA_ENDPOINT'], config.get('USER_START', ''), config.get('USER_END', ''), config.get('SYSTEM_START', ''), config.get('PROMPT_END', ''))
self.headers = {}

self.stream = backend_config['STREAM']

Expand All @@ -46,7 +49,7 @@ def asynchronous_request(self, request_body: dict, prompt: str, context: str = '
def stream_request(self, request_body: dict, prompt: str, context: str = '', io = None, wait: bool = False) -> str:
if self.io_adapter:
request_body = self.io_adapter.set_prompt(request_body, prompt, context)
return self.io_adapter.stream_request(request_body, io, wait)
return self.io_adapter.stream_request(self.headers, request_body, io, wait)
# fall back if no io adapter
return self.synchronous_request(request_body=request_body, prompt=prompt, context=context)

15 changes: 7 additions & 8 deletions tale/wearable.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,8 @@ def load_wearables_from_json(file_path):

wearables_fantasy = load_wearables_from_json('../items/wearables_fantasy.json')
wearables_modern = load_wearables_from_json('../items/wearables_modern.json')
wearbles_story = []

# Disclaimer: Not to limit the player, but to give the generator some hints
female_clothing_modern = {'dress', 'dress_shirt', 'blouse', 'skirt', 'bra', 'panties', 'thong', 'stockings', 'top'}
male_clothing_modern = {'suit', 'boxers', 'briefs', 'shirt'}
neutral_clothing_modern = {'t-shirt', 'shirt', 'jeans', 'sneakers', 'belt', 'dress_shoes', 'hat', 'coveralls', 'sweater', 'socks', 'coat', 'jacket'}



dressable_body_types = [BodyType.HUMANOID, BodyType.SEMI_BIPEDAL, BodyType.WINGED_MAN]

def body_parts_for_bodytype(bodytype: BodyType) -> list:
Expand All @@ -53,13 +47,18 @@ def body_parts_for_bodytype(bodytype: BodyType) -> list:
return None

def random_wearable_for_body_part(bodypart: WearLocation, setting: str = 'fantasy', armor_only = False) -> dict:
wearables = []
if setting == 'fantasy':
wearables = wearables_fantasy
else:
elif setting == 'modern' or setting == 'sci-fi' or setting == 'post-apocalyptic':
wearables = wearables_modern
wearables.extend(wearbles_story)
available_wearables = [item for item in wearables if item['location'] == bodypart and (not armor_only or item.get('ac', 0) > 0)]
if not available_wearables:
return None
wearable = random.choice(available_wearables)
wearable['short_descr'] = f"{random.choice(wearable_colors)} {wearable['name']}"
return wearable

def add_story_wearables(wearables: list):
wearbles_story.extend(wearables)
10 changes: 10 additions & 0 deletions tests/files/world_story/world.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@
"descr": "A wolf",
"type": "Mob"
}
],
"wearables": [
{
"name": "fur jacket",
"short_descr": "A fur jacket",
"descr": "A warm fur jacket",
"location": "TORSO",
"type": "Wearable",
"ac": 1
}
]
}
}
7 changes: 4 additions & 3 deletions tests/test_combat.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ def test_resolve_attack(self):
combat = Combat([attacker], [defender])

text = combat.resolve_attack()
assert('attacker hits' in text or 'attacker performs a critical hit' in text)
assert('hits' in text)
assert('defender is injured' in text)
assert('defender dies' in text)
assert not 'defender attacks' in text


def test_block_ranged_fails(self):
Expand Down Expand Up @@ -235,8 +236,8 @@ def test_resolve_attack_group(self):
text = combat.resolve_attack()

self._assert_combat(attacker, defender, text)
assert('attacker hits' in text or 'attacker performs a critical hit' in text)
assert('attacker2 hits' in text or 'attacker2 performs a critical hit' in text)
assert('attacker attacks')
assert('attacker2 attacks')

def test_start_attack_no_combat_points(self):
attacker = Player(name='att', gender='m')
Expand Down
5 changes: 4 additions & 1 deletion tests/test_json_story.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
from tale.coord import Coord
from tale.items import generic
import tale.parse_utils as parse_utils
from tale import util
from tale import util, wearable
from tale.base import Location
from tale.driver_if import IFDriver
from tale.json_story import JsonStory
from tale.mob_spawner import MobSpawner

class TestJsonStory():
wearable.wearbles_story = []
driver = IFDriver(screen_delay=99, gui=False, web=True, wizard_override=True)
driver.game_clock = util.GameDateTime(datetime.datetime(year=2023, month=1, day=1), 1)
story = JsonStory('tests/files/world_story/', parse_utils.load_story_config(parse_utils.load_json('tests/files/world_story/story_config.json')))
Expand Down Expand Up @@ -74,6 +75,8 @@ def test_load_story(self):

assert self.story.day_cycle
assert self.story.random_events

assert wearable.wearbles_story[0]['name'] == 'fur jacket'


def test_add_location(self):
Expand Down
1 change: 1 addition & 0 deletions tests/test_living_npc.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class TestLivingNpcActions():
'OPENAI_HEADERS': '',
'OPENAI_API_KEY': '',
'OPENAI_JSON_FORMAT': '',
'API_PASSWORD': ''
}
driver = FakeDriver()
driver.story = DynamicStory()
Expand Down
Loading

0 comments on commit 6a19be8

Please sign in to comment.