Skip to content

Commit

Permalink
Merge pull request #80 from neph1/update-v0.29.0
Browse files Browse the repository at this point in the history
Update v0.29.0
  • Loading branch information
neph1 authored Jun 17, 2024
2 parents a103de8 + ba35d2a commit 19f2b37
Show file tree
Hide file tree
Showing 12 changed files with 196 additions and 2 deletions.
2 changes: 2 additions & 0 deletions llm_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ LOCATION_TEMPLATE: '{{"name": "", "exits":[], "items":[], "npcs":[]}}'
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)}}'
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":""}'
FOLLOW_TEMPLATE: '{{"response":"yes or no", "reason":"50 words"}}'
ITEM_TYPES: ["Weapon", "Wearable", "Health", "Money", "Trash", "Food", "Drink", "Key"]
PRE_PROMPT: 'You are a creative game keeper for a role playing game (RPG). You craft detailed worlds and interesting characters with unique and deep personalities for the player to interact with.'
BASE_PROMPT: '<context>{context}</context>\n[USER_START]Rewrite [{input_text}] in your own words using the information found inside the <context> tags to create a background for your text. Use about {max_words} words.'
Expand Down Expand Up @@ -41,5 +42,6 @@ QUEST_PROMPT: '<context>{context}</context> Zone info: {zone_info}; Character: {
NOTE_QUEST_PROMPT: '<context>{context}</context> Zone info: {zone_info};\n[USER_START]Using the information supplied inside the <context> tags, generate a quest that starts from reading a note. The reader must find and talk to a person. Fill in the following JSON template and write nothing else.: {{"reason": "only what the note says. 50 words.", "type":"talk", "target":"who to talk to", "location":"", "name":"name of quest"}}'
NOTE_LORE_PROMPT: '<context>{context}</context> Zone info: {zone_info};\n[USER_START]FUsing the information supplied inside the <context> tags, decide what is written on a note that has been found. Use the provided story and world information to generate a piece of lore. Use about 50 words.'
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}'
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}'
USER_START: '### Instruction:\n'
USER_END: '### Response:\n'
40 changes: 39 additions & 1 deletion tale/cmds/normal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1808,4 +1808,42 @@ def do_consume(player: Player, parsed: base.ParseResult, ctx: util.Context) -> N
result = player.locate_item(item, include_location=False)
if not result:
raise ActionRefused("You don't have that item")
result[0].consume(player)
result[0].consume(player)

@cmd("request_follow")
def do_request_follow(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None:
"""Request a living entity to follow you."""
if len(parsed.args) < 1:
raise ParseError("You need to specify the entity to follow you")
try:
entity = str(parsed.args[0])
except ValueError as x:
raise ActionRefused(str(x))
result = player.location.search_living(entity) # type: LivingNpc
if not result or not isinstance(result, LivingNpc):
raise ActionRefused("Can't follow")
if result.following is player:
raise ActionRefused("Already following you")
text = ''
if len(parsed.args) == 2:
text = str(parsed.args[1])
result.notify_action(base.ParseResult(verb='request_to_follow', who_list=[result], args=[text]), actor=player)

@cmd("unfollow")
def do_unfollow(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None:
"""Make a living entity not follow you."""
if len(parsed.args) < 1:
raise ParseError("You need to specify the entity to follow you")
try:
entity = str(parsed.args[0])
except ValueError as x:
raise ActionRefused(str(x))
result = player.location.search_living(entity) # type: LivingNpc
if not result or not isinstance(result, LivingNpc):
raise ActionRefused("Not found")
if result.following is not player:
raise ActionRefused("Not following you")
result.following = None
player.tell("%s stops following you" % result.title)
result.tell("You stop following %s" % player.title)

19 changes: 19 additions & 0 deletions tale/llm/LivingNpc.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from tale.llm.contexts.FollowContext import FollowContext
from tale.llm.item_handling_result import ItemHandlingResult
from tale.llm import llm_config
import tale.llm.llm_cache as llm_cache
from tale import lang, mud_context
from tale.base import ContainingType, Living, ParseResult
from tale.errors import LlmResponseException
from tale.llm.responses.ActionResponse import ActionResponse
from tale.llm.responses.FollowResponse import FollowResponse
from tale.player import Player


Expand Down Expand Up @@ -90,6 +92,23 @@ def notify_action(self, parsed: ParseResult, actor: Living) -> None:
target = self.location.search_living(parsed.who_1) if parsed.who_1 else None
if target:
self.start_attack(target)
elif parsed.verb == 'request_to_follow' and targeted:
result = mud_context.driver.llm_util.request_follow(actor=actor,
character_name=self.title,
character_card=self.character_card,
event_history=llm_cache.get_events(self._observed_events),
location=self.location,
asker_reason=parsed.args[0]) # type: FollowResponse
if result:
if result.follow:
self.following = actor
actor.tell(f"{self.title} starts following you.", evoke=False)
if result.reason:
response = '{actor.title} says: "{response}"'.format(actor=self, response=("Yes. " if result.follow else "No. ") + result.reason)
tell_hash = llm_cache.cache_event(response)
self._observed_events.append(tell_hash)
actor.tell(response, evoke=False)

else:
event_hash = llm_cache.cache_event(unpad_text(parsed.unparsed))
self._observed_events.append(event_hash)
Expand Down
20 changes: 20 additions & 0 deletions tale/llm/character.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
from tale.llm import llm_config
from tale.llm.contexts.ActionContext import ActionContext
from tale.llm.contexts.CharacterContext import CharacterContext
from tale.llm.contexts.FollowContext import FollowContext
from tale.llm.llm_io import IoUtil
from tale.llm.contexts.DialogueContext import DialogueContext
from tale.llm.responses.ActionResponse import ActionResponse
from tale.llm.responses.FollowResponse import FollowResponse
from tale.load_character import CharacterV2


Expand All @@ -35,6 +37,8 @@ def __init__(self, backend: str, io_util: IoUtil, default_body: dict, json_gramm
self.dialogue_template = llm_config.params['DIALOGUE_TEMPLATE']
self.action_template = llm_config.params['ACTION_TEMPLATE']
self.character_template = llm_config.params['CHARACTER_TEMPLATE']
self.request_follow_prompt = llm_config.params['REQUEST_FOLLOW_PROMPT']
self.follow_template = llm_config.params['FOLLOW_TEMPLATE']

def generate_dialogue(self,
context: DialogueContext,
Expand Down Expand Up @@ -172,4 +176,20 @@ def free_form_action(self, action_context: ActionContext) -> list:
print('Failed to parse action ' + str(exc))
print(text)
return None

def request_follow(self, follow_context: FollowContext) -> FollowResponse:
prompt = self.pre_prompt
prompt += self.request_follow_prompt.format(
context=follow_context.to_prompt_string(),
character_name=follow_context.character_name,
target=follow_context.asker_name,
follow_template=self.follow_template,
target_reason=follow_context.asker_reason)
request_body = deepcopy(self.default_body)
if self.json_grammar_key:
request_body[self.json_grammar_key] = self.json_grammar
text = self.io_util.synchronous_request(request_body, prompt=prompt)
if not text:
return None
return FollowResponse(json.loads(parse_utils.sanitize_json(text)))

2 changes: 1 addition & 1 deletion tale/llm/contexts/DialogueContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def __init__(self,
self.speaker_name = speaker_name
self.target_name = target_name
self.target_description = target_description
self.conversation = conversation.replace('<break>', '\n')#llm_config.params['USER_END'] + '\n' + llm_config.params['USER_START'])
self.conversation = conversation.replace('<break>', '\n') # Added last in actual prompt


def to_prompt_string(self) -> str:
Expand Down
17 changes: 17 additions & 0 deletions tale/llm/contexts/FollowContext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@


from tale.base import Location
from tale.llm.contexts.ActionContext import ActionContext
from tale.llm.contexts.BaseContext import BaseContext


class FollowContext(ActionContext):

def __init__(self, story_context: str, story_type: str, character_name: str, character_card: str, event_history: str, location: Location, asker_name: str, asker_card: str, asker_reason: str):
super().__init__(story_context, story_type, character_name, character_card, event_history, location, [])
self.asker_name = asker_name
self.asker_card = asker_card
self.asker_reason = asker_reason # Added last in actual prompt

def to_prompt_string(self) -> str:
return f"Story context:{self.story_context}; Story type:{self.story_type}; Location:{self.location.name}, {self.location.description}; Self({self.character_name}): {self.character_card}; Asker({self.asker_name}): {self.asker_card} ; History:{self.event_history};"
11 changes: 11 additions & 0 deletions tale/llm/llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from tale.llm.contexts.CharacterContext import CharacterContext
from tale.llm.contexts.DungeonLocationsContext import DungeonLocationsContext
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.llm_io import IoUtil
Expand Down Expand Up @@ -280,6 +281,16 @@ def free_form_action(self, location: Location, character_name: str, character_c
actions=self.action_list)
return self._character.free_form_action(action_context)

def request_follow(self, actor: MudObject, character_name: str, character_card: str, event_history: str, location: Location, asker_reason: str):
return self._character.request_follow(FollowContext(story_context=self.__story_context,
story_type=self.__story_type,
character_name=character_name,
character_card=character_card,
event_history=event_history,
location=location,
asker_name=actor.title,
asker_card=actor.short_description,
asker_reason=asker_reason))

def set_story(self, story: DynamicStory):
""" Set the story object."""
Expand Down
8 changes: 8 additions & 0 deletions tale/llm/responses/FollowResponse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@


class FollowResponse:

def __init__(self, response: dict):
yes_responses = ['yes', 'y', 'true', 'True']
self.follow = any(r in response.get('response', 'no').lower() for r in yes_responses) # type: bool
self.reason = response.get('reason', '') # type: str
33 changes: 33 additions & 0 deletions tests/test_contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from tale.llm.contexts.ActionContext import ActionContext
from tale.llm.contexts.DungeonLocationsContext import DungeonLocationsContext
from tale.llm.contexts.EvokeContext import EvokeContext
from tale.llm.contexts.FollowContext import FollowContext
from tale.llm.contexts.WorldGenerationContext import WorldGenerationContext


Expand Down Expand Up @@ -72,3 +73,35 @@ def test_action_context(self):
assert "say" in result


def test_follow_context(self):
story_context = "test_context"
story_type = "test type"
character_name = "Test character"
character_card = "{actions}"
history = "[history]"
location = Location("TestLocation", descr="Test description")
asker_name = "Asker"
asker_card = "{actions}"
asker_reason = "Asker reason"
follow_context = FollowContext(story_context=story_context,
story_type=story_type,
character_name=character_name,
character_card=character_card,
event_history=history,
location=location,
asker_name=asker_name,
asker_card=asker_card,
asker_reason=asker_reason)

result = follow_context.to_prompt_string()

assert asker_name in result
assert asker_card in result
assert asker_reason not in result
assert character_name in result
assert character_card in result
assert history in result
assert location.name in result
assert location.description in result
assert story_context in result
assert story_type in result
10 changes: 10 additions & 0 deletions tests/test_llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import yaml
from tale.image_gen.automatic1111 import Automatic1111
from tale.llm.contexts.CharacterContext import CharacterContext
from tale.llm.contexts.FollowContext import FollowContext
from tale.llm.contexts.WorldGenerationContext import WorldGenerationContext
import tale.llm.llm_cache as llm_cache
from tale import mud_context, weapon_type
Expand All @@ -16,6 +17,7 @@
from tale.llm.llm_io import IoUtil
from tale.llm.llm_utils import LlmUtil
from tale.llm.responses.ActionResponse import ActionResponse
from tale.llm.responses.FollowResponse import FollowResponse
from tale.npc_defs import StationaryMob
from tale.races import UnarmedAttack
from tale.util import MoneyFormatterFantasy
Expand Down Expand Up @@ -156,6 +158,14 @@ def test_init_image_gen(self):
self.llm_util._init_image_gen("Automatic1111")
assert self.llm_util._image_gen.__class__ == Automatic1111().__class__

def test_request_follow_true(self):
self.llm_util._character.io_util.response = '{"response":"yes", "reason":"test reason"}'
location = Location(name='Test Location')
follow_context = FollowContext(story_context='test context', story_type='test type', character_name='Norhardt', character_card='{}', event_history='{}', location=location, asker_name='Arto', asker_card='{}', asker_reason='{}')
result = self.llm_util._character.request_follow(follow_context) # type: FollowResponse
assert(result.follow == True)
assert(result.reason == 'test reason')

class TestWorldBuilding():

driver = IFDriver(screen_delay=99, gui=False, web=True, wizard_override=True)
Expand Down
16 changes: 16 additions & 0 deletions tests/test_normal_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,19 @@ def test_wield(self):
parse_result = ParseResult(verb='unwield', args=['test sword'])
normal.do_unwield(self.test_player, parse_result, self.context)
assert self.test_player.wielding != item

def test_request_follow(self):
self.io_util.set_response('{"response":"no"')
test_npc = LivingNpc('test_npc', 'f')
location = Location('test_room')
location.init_inventory([self.test_player, test_npc])
normal.do_request_follow(self.test_player, ParseResult(verb='request_follow', args=['test_npc', 'to infinity, and beyond']), self.context)
assert test_npc.notify_action

def test_unfollow(self):
test_npc = LivingNpc('test_npc', 'f')
test_npc.following = self.test_player
location = Location('test_room')
location.init_inventory([self.test_player, test_npc])
normal.do_unfollow(self.test_player, ParseResult(verb='unfollow', args=['test_npc']), self.context)
assert not test_npc.following
20 changes: 20 additions & 0 deletions tests/test_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@


import json
from tale.llm.responses.FollowResponse import FollowResponse


class TestFollowResponse:

def test_follow_response(self):
response = '{"response":"yes", "reason":"because"}'
follow_response = FollowResponse(json.loads(response))
assert follow_response.follow == True

response = '{"response":"no", "reason":"because"}'
follow_response = FollowResponse(json.loads(response))
assert follow_response.follow == False

response = '{"response":"Yes, I will follow you.", "reason":"because"}'
follow_response = FollowResponse(json.loads(response))
assert follow_response.follow == True

0 comments on commit 19f2b37

Please sign in to comment.