Skip to content

Commit 19f2b37

Browse files
authored
Merge pull request #80 from neph1/update-v0.29.0
Update v0.29.0
2 parents a103de8 + ba35d2a commit 19f2b37

File tree

12 files changed

+196
-2
lines changed

12 files changed

+196
-2
lines changed

llm_config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ LOCATION_TEMPLATE: '{{"name": "", "exits":[], "items":[], "npcs":[]}}'
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":""}'
17+
FOLLOW_TEMPLATE: '{{"response":"yes or no", "reason":"50 words"}}'
1718
ITEM_TYPES: ["Weapon", "Wearable", "Health", "Money", "Trash", "Food", "Drink", "Key"]
1819
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.'
1920
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.'
@@ -41,5 +42,6 @@ QUEST_PROMPT: '<context>{context}</context> Zone info: {zone_info}; Character: {
4142
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"}}'
4243
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.'
4344
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}'
45+
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}'
4446
USER_START: '### Instruction:\n'
4547
USER_END: '### Response:\n'

tale/cmds/normal.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1808,4 +1808,42 @@ def do_consume(player: Player, parsed: base.ParseResult, ctx: util.Context) -> N
18081808
result = player.locate_item(item, include_location=False)
18091809
if not result:
18101810
raise ActionRefused("You don't have that item")
1811-
result[0].consume(player)
1811+
result[0].consume(player)
1812+
1813+
@cmd("request_follow")
1814+
def do_request_follow(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None:
1815+
"""Request a living entity to follow you."""
1816+
if len(parsed.args) < 1:
1817+
raise ParseError("You need to specify the entity to follow you")
1818+
try:
1819+
entity = str(parsed.args[0])
1820+
except ValueError as x:
1821+
raise ActionRefused(str(x))
1822+
result = player.location.search_living(entity) # type: LivingNpc
1823+
if not result or not isinstance(result, LivingNpc):
1824+
raise ActionRefused("Can't follow")
1825+
if result.following is player:
1826+
raise ActionRefused("Already following you")
1827+
text = ''
1828+
if len(parsed.args) == 2:
1829+
text = str(parsed.args[1])
1830+
result.notify_action(base.ParseResult(verb='request_to_follow', who_list=[result], args=[text]), actor=player)
1831+
1832+
@cmd("unfollow")
1833+
def do_unfollow(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None:
1834+
"""Make a living entity not follow you."""
1835+
if len(parsed.args) < 1:
1836+
raise ParseError("You need to specify the entity to follow you")
1837+
try:
1838+
entity = str(parsed.args[0])
1839+
except ValueError as x:
1840+
raise ActionRefused(str(x))
1841+
result = player.location.search_living(entity) # type: LivingNpc
1842+
if not result or not isinstance(result, LivingNpc):
1843+
raise ActionRefused("Not found")
1844+
if result.following is not player:
1845+
raise ActionRefused("Not following you")
1846+
result.following = None
1847+
player.tell("%s stops following you" % result.title)
1848+
result.tell("You stop following %s" % player.title)
1849+

tale/llm/LivingNpc.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
from tale.llm.contexts.FollowContext import FollowContext
12
from tale.llm.item_handling_result import ItemHandlingResult
23
from tale.llm import llm_config
34
import tale.llm.llm_cache as llm_cache
45
from tale import lang, mud_context
56
from tale.base import ContainingType, Living, ParseResult
67
from tale.errors import LlmResponseException
78
from tale.llm.responses.ActionResponse import ActionResponse
9+
from tale.llm.responses.FollowResponse import FollowResponse
810
from tale.player import Player
911

1012

@@ -90,6 +92,23 @@ def notify_action(self, parsed: ParseResult, actor: Living) -> None:
9092
target = self.location.search_living(parsed.who_1) if parsed.who_1 else None
9193
if target:
9294
self.start_attack(target)
95+
elif parsed.verb == 'request_to_follow' and targeted:
96+
result = mud_context.driver.llm_util.request_follow(actor=actor,
97+
character_name=self.title,
98+
character_card=self.character_card,
99+
event_history=llm_cache.get_events(self._observed_events),
100+
location=self.location,
101+
asker_reason=parsed.args[0]) # type: FollowResponse
102+
if result:
103+
if result.follow:
104+
self.following = actor
105+
actor.tell(f"{self.title} starts following you.", evoke=False)
106+
if result.reason:
107+
response = '{actor.title} says: "{response}"'.format(actor=self, response=("Yes. " if result.follow else "No. ") + result.reason)
108+
tell_hash = llm_cache.cache_event(response)
109+
self._observed_events.append(tell_hash)
110+
actor.tell(response, evoke=False)
111+
93112
else:
94113
event_hash = llm_cache.cache_event(unpad_text(parsed.unparsed))
95114
self._observed_events.append(event_hash)

tale/llm/character.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
from tale.llm import llm_config
1212
from tale.llm.contexts.ActionContext import ActionContext
1313
from tale.llm.contexts.CharacterContext import CharacterContext
14+
from tale.llm.contexts.FollowContext import FollowContext
1415
from tale.llm.llm_io import IoUtil
1516
from tale.llm.contexts.DialogueContext import DialogueContext
1617
from tale.llm.responses.ActionResponse import ActionResponse
18+
from tale.llm.responses.FollowResponse import FollowResponse
1719
from tale.load_character import CharacterV2
1820

1921

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

3943
def generate_dialogue(self,
4044
context: DialogueContext,
@@ -172,4 +176,20 @@ def free_form_action(self, action_context: ActionContext) -> list:
172176
print('Failed to parse action ' + str(exc))
173177
print(text)
174178
return None
179+
180+
def request_follow(self, follow_context: FollowContext) -> FollowResponse:
181+
prompt = self.pre_prompt
182+
prompt += self.request_follow_prompt.format(
183+
context=follow_context.to_prompt_string(),
184+
character_name=follow_context.character_name,
185+
target=follow_context.asker_name,
186+
follow_template=self.follow_template,
187+
target_reason=follow_context.asker_reason)
188+
request_body = deepcopy(self.default_body)
189+
if self.json_grammar_key:
190+
request_body[self.json_grammar_key] = self.json_grammar
191+
text = self.io_util.synchronous_request(request_body, prompt=prompt)
192+
if not text:
193+
return None
194+
return FollowResponse(json.loads(parse_utils.sanitize_json(text)))
175195

tale/llm/contexts/DialogueContext.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def __init__(self,
1919
self.speaker_name = speaker_name
2020
self.target_name = target_name
2121
self.target_description = target_description
22-
self.conversation = conversation.replace('<break>', '\n')#llm_config.params['USER_END'] + '\n' + llm_config.params['USER_START'])
22+
self.conversation = conversation.replace('<break>', '\n') # Added last in actual prompt
2323

2424

2525
def to_prompt_string(self) -> str:

tale/llm/contexts/FollowContext.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
3+
from tale.base import Location
4+
from tale.llm.contexts.ActionContext import ActionContext
5+
from tale.llm.contexts.BaseContext import BaseContext
6+
7+
8+
class FollowContext(ActionContext):
9+
10+
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):
11+
super().__init__(story_context, story_type, character_name, character_card, event_history, location, [])
12+
self.asker_name = asker_name
13+
self.asker_card = asker_card
14+
self.asker_reason = asker_reason # Added last in actual prompt
15+
16+
def to_prompt_string(self) -> str:
17+
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};"

tale/llm/llm_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from tale.llm.contexts.CharacterContext import CharacterContext
1313
from tale.llm.contexts.DungeonLocationsContext import DungeonLocationsContext
1414
from tale.llm.contexts.EvokeContext import EvokeContext
15+
from tale.llm.contexts.FollowContext import FollowContext
1516
from tale.llm.contexts.WorldGenerationContext import WorldGenerationContext
1617
from tale.llm.llm_ext import DynamicStory
1718
from tale.llm.llm_io import IoUtil
@@ -280,6 +281,16 @@ def free_form_action(self, location: Location, character_name: str, character_c
280281
actions=self.action_list)
281282
return self._character.free_form_action(action_context)
282283

284+
def request_follow(self, actor: MudObject, character_name: str, character_card: str, event_history: str, location: Location, asker_reason: str):
285+
return self._character.request_follow(FollowContext(story_context=self.__story_context,
286+
story_type=self.__story_type,
287+
character_name=character_name,
288+
character_card=character_card,
289+
event_history=event_history,
290+
location=location,
291+
asker_name=actor.title,
292+
asker_card=actor.short_description,
293+
asker_reason=asker_reason))
283294

284295
def set_story(self, story: DynamicStory):
285296
""" Set the story object."""
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
3+
class FollowResponse:
4+
5+
def __init__(self, response: dict):
6+
yes_responses = ['yes', 'y', 'true', 'True']
7+
self.follow = any(r in response.get('response', 'no').lower() for r in yes_responses) # type: bool
8+
self.reason = response.get('reason', '') # type: str

tests/test_contexts.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from tale.llm.contexts.ActionContext import ActionContext
55
from tale.llm.contexts.DungeonLocationsContext import DungeonLocationsContext
66
from tale.llm.contexts.EvokeContext import EvokeContext
7+
from tale.llm.contexts.FollowContext import FollowContext
78
from tale.llm.contexts.WorldGenerationContext import WorldGenerationContext
89

910

@@ -72,3 +73,35 @@ def test_action_context(self):
7273
assert "say" in result
7374

7475

76+
def test_follow_context(self):
77+
story_context = "test_context"
78+
story_type = "test type"
79+
character_name = "Test character"
80+
character_card = "{actions}"
81+
history = "[history]"
82+
location = Location("TestLocation", descr="Test description")
83+
asker_name = "Asker"
84+
asker_card = "{actions}"
85+
asker_reason = "Asker reason"
86+
follow_context = FollowContext(story_context=story_context,
87+
story_type=story_type,
88+
character_name=character_name,
89+
character_card=character_card,
90+
event_history=history,
91+
location=location,
92+
asker_name=asker_name,
93+
asker_card=asker_card,
94+
asker_reason=asker_reason)
95+
96+
result = follow_context.to_prompt_string()
97+
98+
assert asker_name in result
99+
assert asker_card in result
100+
assert asker_reason not in result
101+
assert character_name in result
102+
assert character_card in result
103+
assert history in result
104+
assert location.name in result
105+
assert location.description in result
106+
assert story_context in result
107+
assert story_type in result

tests/test_llm_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import yaml
66
from tale.image_gen.automatic1111 import Automatic1111
77
from tale.llm.contexts.CharacterContext import CharacterContext
8+
from tale.llm.contexts.FollowContext import FollowContext
89
from tale.llm.contexts.WorldGenerationContext import WorldGenerationContext
910
import tale.llm.llm_cache as llm_cache
1011
from tale import mud_context, weapon_type
@@ -16,6 +17,7 @@
1617
from tale.llm.llm_io import IoUtil
1718
from tale.llm.llm_utils import LlmUtil
1819
from tale.llm.responses.ActionResponse import ActionResponse
20+
from tale.llm.responses.FollowResponse import FollowResponse
1921
from tale.npc_defs import StationaryMob
2022
from tale.races import UnarmedAttack
2123
from tale.util import MoneyFormatterFantasy
@@ -156,6 +158,14 @@ def test_init_image_gen(self):
156158
self.llm_util._init_image_gen("Automatic1111")
157159
assert self.llm_util._image_gen.__class__ == Automatic1111().__class__
158160

161+
def test_request_follow_true(self):
162+
self.llm_util._character.io_util.response = '{"response":"yes", "reason":"test reason"}'
163+
location = Location(name='Test Location')
164+
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='{}')
165+
result = self.llm_util._character.request_follow(follow_context) # type: FollowResponse
166+
assert(result.follow == True)
167+
assert(result.reason == 'test reason')
168+
159169
class TestWorldBuilding():
160170

161171
driver = IFDriver(screen_delay=99, gui=False, web=True, wizard_override=True)

0 commit comments

Comments
 (0)