From 8e5651347d57cc80536472c1c982d0176c8641ce Mon Sep 17 00:00:00 2001 From: rickard Date: Fri, 6 Oct 2023 21:18:24 +0200 Subject: [PATCH 1/6] fix missing location description cast gauss to int to avoid fractional items --- tale/llm/requests/build_location.py | 2 +- tale/parse_utils.py | 4 ++-- tests/test_parse_utils.py | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tale/llm/requests/build_location.py b/tale/llm/requests/build_location.py index 7b6e4652..7bd41101 100644 --- a/tale/llm/requests/build_location.py +++ b/tale/llm/requests/build_location.py @@ -38,7 +38,7 @@ def build_prompt(self, args: dict) -> str: spawn_prompt = self.spawn_prompt.format(alignment=mood_string, level=level) items_prompt = '' - item_amount = random.gauss(1, 2) + item_amount = (int) (random.gauss(1, 2)) if item_amount > 0: items_prompt = self.items_prompt.format(items=item_amount) diff --git a/tale/parse_utils.py b/tale/parse_utils.py index ff9100e7..d587e246 100644 --- a/tale/parse_utils.py +++ b/tale/parse_utils.py @@ -364,13 +364,13 @@ def parse_generated_exits(json_result: dict, exit_location_name: str, location: new_location.built = False new_location.generated = True - from_description = f'To the {directions_from[1]} you can see {location.name}' if len(directions_from) > 1 else f'You can see {location.name}' + from_description = f'To the {directions_from[1]} you see {location.name}.' if len(directions_from) > 1 else f'You see {location.name}.' exit_back = Exit(directions=directions_from, target_location=location, short_descr=from_description) new_location.add_exits([exit_back]) - to_description = f'To the {directions_to[1]} ' + exit.get('short_descr', 'description').lower() if len(directions_from) > 1 else exit.get('short_descr', 'description') + to_description = 'To the {direction} you see {location}.'.format(direction=directions_to[1], location=exit.get('short_descr', 'description').lower()) if len(directions_to) > 1 else exit.get('short_descr', 'description') exit_to = Exit(directions=directions_to, target_location=new_location, short_descr=to_description, diff --git a/tests/test_parse_utils.py b/tests/test_parse_utils.py index 16866f41..5a248c17 100644 --- a/tests/test_parse_utils.py +++ b/tests/test_parse_utils.py @@ -164,11 +164,21 @@ def test_parse_generated_exits_duplicate_direction(self): new_locations, parsed_exits = parse_utils.parse_generated_exits(json_result=exits, exit_location_name=exit_location_name, location=location) + location.add_exits(parsed_exits) assert(len(parsed_exits) == 2) assert(parsed_exits[0].names == ['glacier', 'north']) - assert(parsed_exits[0].short_description == 'To the north a treacherous path.') + assert(parsed_exits[0].short_description == 'To the north you see a treacherous path.') assert(parsed_exits[1].names == ['cave', 'south']) - assert(parsed_exits[1].short_description == 'To the south a dark opening.') + assert(parsed_exits[1].short_description == 'To the south you see a dark opening.') + + exits2 = json.loads('{"exits": [{"name": "The Ice Cliff", "direction": "north", "short_descr": "A steep fall."}, {"name": "The Icicle Forest", "direction": "east", "short_descr": "A forest of ice."}]}') + + new_locations, parsed_exits = parse_utils.parse_generated_exits(json_result=exits2, + exit_location_name='cave', + location=new_locations[1]) + + assert(parsed_exits[0].names == ['ice cliff', 'south']) + assert(parsed_exits[1].names == ['icicle forest', 'east']) def test_coordinates_from_direction(self): coord = Coord(0,0,0) From 7604dec681bd69d28db58f94b0d9a4b1ec155fd9 Mon Sep 17 00:00:00 2001 From: rickard Date: Sat, 7 Oct 2023 08:11:32 +0200 Subject: [PATCH 2/6] force sending prompt to request method replace and append start and end prompts --- llm_config.yaml | 38 +++++++++++++++++--------------- tale/llm/character.py | 26 +++++----------------- tale/llm/llm_io.py | 25 +++++++++++++-------- tale/llm/llm_utils.py | 8 ++----- tests/supportstuff.py | 2 +- tests/test_llm_io.py | 51 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 95 insertions(+), 55 deletions(-) create mode 100644 tests/test_llm_io.py diff --git a/llm_config.yaml b/llm_config.yaml index 3e30b2c0..938d46f2 100644 --- a/llm_config.yaml +++ b/llm_config.yaml @@ -10,28 +10,30 @@ GENERATION_BODY: '{"stop_sequence": "\n\n", "max_length":750, "max_context_lengt ANALYSIS_BODY: '{"banned_tokens":"\n\n", "stop_sequence": "\n\n\n\n", "max_length":500, "max_context_length":4096, "temperature":0.15, "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, "mirostat":2, "mirostat_tau":6.0, "mirostat_eta":0.1, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}' MEMORY_SIZE: 512 PRE_PROMPT: 'Below is an instruction that describes a task, paired with an input that provides further context. As an game keeper for an RPG, write a response that appropriately completes the request.\n\n' -BASE_PROMPT: "[Story context: {story_context}]; [History: {history}]; ### Instruction: Rewrite the following Text in your own words using the supplied Context and History to create a background for your text. Use about {max_words} words. [Text:{input_text}] \n\nEnd of text.\n\n ### Instruction: Rewrite [Text] in your own words. ### Response:\n" -ACTION_PROMPT: "[Story context: {story_context}]; [History: {history}]; ### Instruction: Rewrite the Action, and nothing else, in your own words using the supplied Context and Location. History is what happened before. Use less than {max_words} words. [Text: {input_text}] \n\nEnd of text.\n\n### Response:\n" -DIALOGUE_PROMPT: '[Story context: {story_context}]; [Location: {location}]; The following is a conversation between {character1} and {character2}; {character1}:{character1_description}; character2:{character2_description}; [Chat history: {previous_conversation}]\n\n [{character2}s sentiment towards {character1}: {sentiment}]. ### Instruction: Write a single response for {character2} in third person pov, using {character2} description. \n\n### Response:\n' -ITEM_PROMPT: 'Items:[{items}];Characters:[{character1},{character2}] \n\n ### Instruction: Decide if an item was explicitly given, taken, dropped or put somewhere in the following text:[Text:{text}]. Insert your thoughts about whether an item was explicitly given, taken, put somewhere or dropped in "my thoughts", and the results in "item", "from" and "to", or make them empty if . Insert {character1}s sentiment towards {character2} in a single word in "sentiment assessment". Fill in the following JSON template: {{ "thoughts":"my thoughts", "result": {{ "item":"", "from":"", "to":""}}, {{"sentiment":"sentiment assessment"}} }} End of example. \n\n Write your response in valid JSON\n\n### Response:\n' -COMBAT_PROMPT: 'Rewrite the following combat between user {attacker} using {attacker_weapon}, and {victim} using {victim_weapon} in {location}, {location_description} into a vivid description in less than 300 words. ### Instruction: Rewrite the following combat result in about 150 words, using the characters weapons . Combat Result: {attacker_msg} ### Response:\n\n' +BASE_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the following Text in your own words using the supplied Context and History to create a background for your text. Use about {max_words} words. [Text:{input_text}] \n\nEnd of text.\n\n [USER_START] Rewrite [Text] in your own words. " +ACTION_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the Action, and nothing else, in your own words using the supplied Context and Location. History is what happened before. Use less than {max_words} words. [Text: {input_text}] \n\nEnd of text.\n\n" +DIALOGUE_PROMPT: '[Story context: {story_context}]; [Location: {location}]; The following is a conversation between {character1} and {character2}; {character1}:{character1_description}; character2:{character2_description}; [Chat history: {previous_conversation}]\n\n [{character2}s sentiment towards {character1}: {sentiment}]. [USER_START] Write a single response for {character2} in third person pov, using {character2} description. \n\n' +ITEM_PROMPT: 'Items:[{items}];Characters:[{character1},{character2}] \n\n [USER_START] Decide if an item was explicitly given, taken, dropped or put somewhere in the following text:[Text:{text}]. Insert your thoughts about whether an item was explicitly given, taken, put somewhere or dropped in "my thoughts", and the results in "item", "from" and "to", or make them empty if . Insert {character1}s sentiment towards {character2} in a single word in "sentiment assessment". Fill in the following JSON template: {{ "thoughts":"my thoughts", "result": {{ "item":"", "from":"", "to":""}}, {{"sentiment":"sentiment assessment"}} }} End of example. \n\n Write your response in valid JSON\n\n' +COMBAT_PROMPT: 'Rewrite the following combat between user {attacker} using {attacker_weapon}, and {victim} using {victim_weapon} in {location}, {location_description} into a vivid description in less than 300 words. [USER_START] Rewrite the following combat result in about 150 words, using the characters weapons . Combat Result: {attacker_msg} \n' 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.\n\n' -CREATE_CHARACTER_PROMPT: '### Instruction: For a {story_type}, create a diverse character with rich personality that can be interacted with using the story context and keywords. Do not mention height. Story context: {story_context}; keywords: {keywords}. Fill in this JSON template and write nothing else: {{"name":"", "description": "50 words", "appearance": "25 words", "personality": "50 words", "money":(int), "level":"", "gender":"m/f/n", "age":(int), "race":""}}\n\n ### Response:\n' -CREATE_LOCATION_PROMPT: '[Story context: {story_context}]; World info: {world_info}; Zone info: {zone_info}; Exit json example: {{"direction":"", "name":"name of new location", "short_descr":"exit description"}}; Npc or mob example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words"}}. Existing connected locations: {exit_locations}. ### Instruction: For a {story_type}, 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: {{"description": "25 words", "exits":[], "items":[], "npcs":[]}}. Write the response in valid JSON.\n\n ### Response:\n' -CREATE_ZONE_PROMPT: '[Story context: {story_context}]; World info: {world_info}; ### Instruction: For a {story_type}, create an new area that can be further populated with locations. The area is about 25 square kilometers. It is connected in the {direction} to {zone_info}. The mood is {mood}. Add a name and brief description. Choose the names of 5 creatures from the supplied list likely to find in the area. Fill in "items" with the names and types of 5 common items in the area. Fill in this JSON template and do not write anything else: {{"name":"", "description": "75 words", "races":[], "items":[], "mood":"5 to -5, where 5 is extremely friendly and -5 is extremely hostile.", "level":(int)}}. Write the response in valid JSON.\n\n ### Response:\n' +CREATE_CHARACTER_PROMPT: '[USER_START] For a {story_type}, create a diverse character with rich personality that can be interacted with using the story context and keywords. Do not mention height. Story context: {story_context}; keywords: {keywords}. Fill in this JSON template and write nothing else: {{"name":"", "description": "50 words", "appearance": "25 words", "personality": "50 words", "money":(int), "level":"", "gender":"m/f/n", "age":(int), "race":""}}\n\n ' +CREATE_LOCATION_PROMPT: '[Story context: {story_context}]; World info: {world_info}; Zone info: {zone_info}; Exit json example: {{"direction":"", "name":"name of new location", "short_descr":"exit description"}}; Npc or mob example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words"}}. Existing connected locations: {exit_locations}. [USER_START] For a {story_type}, 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: {{"description": "25 words", "exits":[], "items":[], "npcs":[]}}. Write the response in valid JSON.\n\n ' +CREATE_ZONE_PROMPT: '[Story context: {story_context}]; World info: {world_info}; [USER_START] For a {story_type}, create an new area that can be further populated with locations. The area is about 25 square kilometers. It is connected in the {direction} to {zone_info}. The mood is {mood}. Add a name and brief description. Choose the names of 5 creatures from the supplied list likely to find in the area. Fill in "items" with the names and types of 5 common items in the area. Fill in this JSON template and do not write anything else: {{"name":"", "description": "75 words", "races":[], "items":[], "mood":"5 to -5, where 5 is extremely friendly and -5 is extremely hostile.", "level":(int)}}. Write the response in valid JSON.\n\n ' OPENAI_HEADERS: '{"Content-Type":"application/json", "Authorization":""}' OPENAI_BODY: '{"model":"gpt-3.5-turbo", "messages":[{"role":"system", "content":"You are an assistant game keeper for an RPG"}, {"role":"user", "content":""}], "temperature": 1.0, "max_tokens":500}' OPENAI_API_KEY: "OPENAI_API_KEY" ITEMS_PROMPT: "Add {items} of various type." SPAWN_PROMPT: "Add a level {level}, {alignment} npc or mob." -IDLE_ACTION_PROMPT: " [Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Acting character: {character}] ### Instruction: Choose an item from: {items}, or a character from:{characters}, to interact with, or perform a solo action. Do not make up new characters. Don't contradict or repeat Last Action. ### Instruction: Write what {character_name} does, in present tense third person point of view. Use less than 40 words. Don't write what {character_name} thinks, or what the player (You) or anyone else does.\n\n ### Response:\n" -TRAVEL_PROMPT: "### Instruction: For {character}: pick a location from [{locations}] they would like to travel to or a direction from [{directions}], or stay in the current location. Do not make up new locations. Write what {character_name} chooses. ### Instruction: Write only the location name, direction, or write nothing to stay in the same location. Write nothing else.\n\n ### Response:\n" -REACTION_PROMPT: "### Instruction: Act as {character}. {acting_character_name} has performed the following action that involves {character_name}: {action}. {character_name}'s sentiment towards {acting_character_name}: {sentiment}. ### Instruction: Respond with a suitable action for {character_name}, in present tense third person point of view. Use less than 40 words.\n\n ### Response:\n" -STORY_BACKGROUND_PROMPT: "### Instruction: For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, write a captivating background story that the player can interact with. Include a large scale plot conflict that the player will encounter. Use less than 400 words\n\n ### Response:\n" -START_LOCATION_PROMPT: '[Story context: {story_context}]; Zone info: {zone_info}; Item json example: {{"name":"", "type":"", "short_descr":"10 words"}}, type can be "Weapon", "Wearable", "Other" or "Money"; Npc example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words"}} ; Exit json example: {{"direction":"", "name":"name of new location", "short_descr":"exit description"}}; ### Instruction: For a {story_type}, come up with a name for the location with this description: {location_description}. {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: {{"name": "", "exits":[], "items":[], "npcs":[]}}.\n\n ### Response:\n' -STORY_PLOT_PROMPT: "### Instruction: For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}. Based on the following background: {story_background} write an innovative and engaging plot that the player can become part of. Use less than 400 words\n\n ### Response:\n" -WORLD_ITEMS: '[Story context: {story_context}]; For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, come up with 7 common items that can be found in the world. Item example: {{"name":"", "type":"", "short_descr":"", "level":int, "value":int}}, type is one of: {item_types}; Fill in this JSON template and do not write anything else: {{"items": []}}.\n\n ### Response:\n' -WORLD_CREATURES: '[Story context: {story_context}]; For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, come up with 5 creatures of various level and sentiment that can be found in the world. Creature example: {{"name":"", "body":"", "mass":int(kg), "hp":int, "type":"Npc or Mob", "level":int, "unarmed_attack":One of [FISTS, CLAWS, BITE, TAIL, HOOVES, HORN, TUSKS, BEAK, TALON], "short_descr":""}}. Fill in this JSON template and do not write anything else: {{"creatures": []}}.\n\n ### Response:\n' -GOAL_PROMPT: '[Characters:{characters}][Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Known locations: {locations}][Acting character: {character}] [Actions available:{actions}] ### Instruction: For {character_name}, come up with a goal that plays along with their character description that involves an item, a character or a location in the prompt. Then construct up to three tasks that will lead towards the achievement of said goal. Fill in the following JSON template: {{"goal":"", "tasks":[{"action":"", "what":""}, {"action":"", "what":""}, {"action":"", "what":""}]}}\n\n ### Response:\n' +IDLE_ACTION_PROMPT: " [Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Acting character: {character}] [USER_START] Choose an item from: {items}, or a character from:{characters}, to interact with, or perform a solo action. Do not make up new characters. Don't contradict or repeat Last Action. [USER_START] Write what {character_name} does, in present tense third person point of view. Use less than 40 words. Don't write what {character_name} thinks, or what the player (You) or anyone else does.\n\n " +TRAVEL_PROMPT: "[USER_START] For {character}: pick a location from [{locations}] they would like to travel to or a direction from [{directions}], or stay in the current location. Do not make up new locations. Write what {character_name} chooses. [USER_START] Write only the location name, direction, or write nothing to stay in the same location. Write nothing else.\n\n " +REACTION_PROMPT: "[USER_START] Act as {character}. {acting_character_name} has performed the following action that involves {character_name}: {action}. {character_name}'s sentiment towards {acting_character_name}: {sentiment}. [USER_START] Respond with a suitable action for {character_name}, in present tense third person point of view. Use less than 40 words.\n\n " +STORY_BACKGROUND_PROMPT: "[USER_START] For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, write a captivating background story that the player can interact with. Include a large scale plot conflict that the player will encounter. Use less than 400 words\n\n " +START_LOCATION_PROMPT: '[Story context: {story_context}]; Zone info: {zone_info}; Item json example: {{"name":"", "type":"", "short_descr":"10 words"}}, type can be "Weapon", "Wearable", "Other" or "Money"; Npc example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words"}} ; Exit json example: {{"direction":"", "name":"name of new location", "short_descr":"exit description"}}; [USER_START] For a {story_type}, come up with a name for the location with this description: {location_description}. {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: {{"name": "", "exits":[], "items":[], "npcs":[]}}.\n\n ' +STORY_PLOT_PROMPT: "[USER_START] For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}. Based on the following background: {story_background} write an innovative and engaging plot that the player can become part of. Use less than 400 words\n\n " +WORLD_ITEMS: '[Story context: {story_context}]; For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, come up with 7 common items that can be found in the world. Item example: {{"name":"", "type":"", "short_descr":"", "level":int, "value":int}}, type is one of: {item_types}; Fill in this JSON template and do not write anything else: {{"items": []}}.\n\n ' +WORLD_CREATURES: '[Story context: {story_context}]; For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, come up with 5 creatures of various level and sentiment that can be found in the world. Creature example: {{"name":"", "body":"", "mass":int(kg), "hp":int, "type":"Npc or Mob", "level":int, "unarmed_attack":One of [FISTS, CLAWS, BITE, TAIL, HOOVES, HORN, TUSKS, BEAK, TALON], "short_descr":""}}. Fill in this JSON template and do not write anything else: {{"creatures": []}}.\n\n ' +GOAL_PROMPT: '[Characters:{characters}][Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Known locations: {locations}][Acting character: {character}] [Actions available:{actions}] [USER_START] For {character_name}, come up with a goal that plays along with their character description that involves an item, a character or a location in the prompt. Then construct up to three tasks that will lead towards the achievement of said goal. Fill in the following JSON template: {{"goal":"", "tasks":[{"action":"", "what":""}, {"action":"", "what":""}, {"action":"", "what":""}]}}\n\n ' JSON_GRAMMAR: "root ::= object\nvalue ::= object | array | string | number | (\"true\" | \"false\" | \"null\") ws\n\nobject ::=\n \"{\" ws (\n string \":\" ws value\n (\",\" ws string \":\" ws value)*\n )? \"}\" ws\n\narray ::=\n \"[\" ws (\n value\n (\",\" ws value)*\n )? \"]\" ws\n\nstring ::=\n \"\\\"\" (\n [^\"\\\\] |\n \"\\\\\" ([\"\\\\/bfnrt] | \"u\" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) # escapes\n )* \"\\\"\" ws\n\nnumber ::= (\"-\"? ([0-9] | [1-9] [0-9]*)) (\".\" [0-9]+)? ([eE] [-+]? [0-9]+)? ws\n\n# Optional space: by convention, applied in this grammar after literal chars when allowed\nws ::= ([ \\t\\n] ws)?" -PLAYER_ENTER_PROMPT: '[Story context: {story_context}]; World info: {world_info}; Zone info: {zone_info}; ; Npc example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words", "memories":"25 words"}}. ### Instruction: The player has just re-entered this location: {location_info}. Consider whether any items, npcs or mobs should be spawned. For mobs, only enter the name of race. Fill in this JSON template and do not write anything else: {{"items":[], "npcs":[] "mobs":[]}}.\n\n ### Response:\n' \ No newline at end of file +PLAYER_ENTER_PROMPT: '[Story context: {story_context}]; World info: {world_info}; Zone info: {zone_info}; ; Npc example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words", "memories":"25 words"}}. [USER_START] The player has just re-entered this location: {location_info}. Consider whether any items, npcs or mobs should be spawned. For mobs, only enter the name of race. Fill in this JSON template and do not write anything else: {{"items":[], "npcs":[] "mobs":[]}}.\n\n ' +USER_START: '### Instruction:' +USER_END: '### Response:\n' \ No newline at end of file diff --git a/tale/llm/character.py b/tale/llm/character.py index b59db0c8..2f2a0580 100644 --- a/tale/llm/character.py +++ b/tale/llm/character.py @@ -79,12 +79,10 @@ def dialogue_analysis(self, text: str, character_card: str, character_name: str, if self.backend == 'kobold_cpp': request_body = self.analysis_body - request_body['prompt'] = prompt request_body['grammar'] = self.json_grammar elif self.backend == 'openai': request_body = self.default_body - request_body['messages'][1]['content'] = prompt - text = parse_utils.trim_response(self.io_util.synchronous_request(request_body)) + text = parse_utils.trim_response(self.io_util.synchronous_request(request_body, prompt=prompt)) try: json_result = json.loads(parse_utils.sanitize_json(text)) except JSONDecodeError as exc: @@ -139,10 +137,7 @@ def generate_character(self, story_context: str = '', keywords: list = [], story request_body['rep_pen'] = 1.0 request_body['banned_tokens'] = ['```'] request_body['grammar'] = self.json_grammar - request_body['prompt'] = prompt - elif self.backend == 'openai': - request_body['messages'][1]['content'] = prompt - result = self.io_util.synchronous_request(request_body) + result = self.io_util.synchronous_request(request_body, prompt=prompt) try: json_result = json.loads(parse_utils.sanitize_json(result)) except JSONDecodeError as exc: @@ -171,13 +166,10 @@ def perform_idle_action(self, character_name: str, location: Location, story_con sentiments=json.dumps(sentiments)) request_body = self.default_body if self.backend == 'kobold_cpp': - request_body['prompt'] = prompt request_body['seed'] = random.randint(0, 2147483647) request_body['banned_tokens'] = ['You'] - elif self.backend == 'openai': - request_body['messages'][1]['content'] = prompt - text = self.io_util.asynchronous_request(request_body) + text = self.io_util.asynchronous_request(request_body, prompt=prompt) return text.split(';') def perform_travel_action(self, character_name: str, location: Location, locations: list, directions: list, character_card: str = ''): @@ -192,11 +184,7 @@ def perform_travel_action(self, character_name: str, location: Location, locatio character=character_card, character_name=character_name) request_body = self.default_body - if self.backend == 'kobold_cpp': - request_body['prompt'] = prompt - elif self.backend == 'openai': - request_body['messages'][1]['content'] = prompt - text = self.io_util.asynchronous_request(request_body) + text = self.io_util.asynchronous_request(request_body, prompt=prompt) return text def perform_reaction(self, action: str, character_name: str, acting_character_name: str, location: Location, character_card: str = '', sentiment: str = ''): @@ -209,10 +197,6 @@ def perform_reaction(self, action: str, character_name: str, acting_character_na acting_character_name=acting_character_name, sentiment=sentiment) request_body = self.default_body - if self.backend == 'kobold_cpp': - request_body['prompt'] = prompt - elif self.backend == 'openai': - request_body['messages'][1]['content'] = prompt - text = self.io_util.asynchronous_request(request_body) + text = self.io_util.asynchronous_request(request_body, prompt=prompt) return text \ No newline at end of file diff --git a/tale/llm/llm_io.py b/tale/llm/llm_io.py index a591931f..e9608962 100644 --- a/tale/llm/llm_io.py +++ b/tale/llm/llm_io.py @@ -25,11 +25,12 @@ def __init__(self, config: dict = None): else: self.headers = {} self.stream = config['STREAM'] + self.user_start_prompt = config['USER_START'] + self.user_end_prompt = config['USER_END'] - def synchronous_request(self, request_body: dict, prompt: str = None) -> str: + def synchronous_request(self, request_body: dict, prompt: str) -> str: """ Send request to backend and return the result """ - if prompt: - self._set_prompt(request_body, prompt) + self._set_prompt(request_body, prompt) response = requests.post(self.url + self.endpoint, headers=self.headers, data=json.dumps(request_body)) if self.backend == 'openai': parsed_response = self._parse_openai_result(response.text) @@ -37,14 +38,15 @@ def synchronous_request(self, request_body: dict, prompt: str = None) -> str: parsed_response = self._parse_kobold_result(response.text) return parse_utils.trim_response(parsed_response) - def asynchronous_request(self, request_body: dict): + def asynchronous_request(self, request_body: dict, prompt: str) -> str: if self.backend == 'openai': - return self.synchronous_request(request_body) - return self.stream_request(request_body, wait=True) + return self.synchronous_request(request_body, prompt) + return self.stream_request(request_body, wait=True, prompt=prompt) - def stream_request(self, request_body: dict, player_io: TextBuffer = None, io = None, wait: bool = False) -> str: + def stream_request(self, request_body: dict, prompt: str, player_io: TextBuffer = None, io = None, wait: bool = False) -> str: if self.backend == 'openai': raise NotImplementedError("Currently does not support streaming requests for OpenAI") + self._set_prompt(request_body, prompt) result = asyncio.run(self._do_stream_request(self.url + self.stream_endpoint, request_body)) if result: return self._do_process_result(self.url + self.data_endpoint, player_io, io, wait) @@ -92,8 +94,13 @@ def _parse_openai_result(self, result: str) -> str: print("Error parsing result from OpenAI") print(result) - def _set_prompt(self, request_body: dict, prompt: str): + def _set_prompt(self, request_body: dict, prompt: str) -> dict: + if self.user_start_prompt: + prompt = prompt.replace('[USER_START]', self.user_start_prompt) + if self.user_end_prompt: + prompt = prompt + self.user_end_prompt if self.backend == 'kobold_cpp': request_body['prompt'] = prompt elif self.backend == 'openai': - request_body['messages'][1]['content'] = prompt \ No newline at end of file + request_body['messages'][1]['content'] = prompt + return request_body \ No newline at end of file diff --git a/tale/llm/llm_utils.py b/tale/llm/llm_utils.py index 45bed21d..11176632 100644 --- a/tale/llm/llm_utils.py +++ b/tale/llm/llm_utils.py @@ -73,18 +73,14 @@ def evoke(self, player_io: TextBuffer, message: str, short_len : bool=False, rol input_text=str(trimmed_message)) request_body = self.default_body - if self.backend == 'kobold_cpp': - request_body['prompt'] = prompt - elif self.backend == 'openai': - request_body['messages'][1]['content'] = prompt if not self.stream: - text = self.io_util.synchronous_request(request_body) + text = self.io_util.synchronous_request(request_body, prompt=prompt) self._store_hash(text_hash_value, text) return output_template.format(message=message, text=text), rolling_prompt player_io.print(output_template.format(message=message, text=text), end=False, format=True, line_breaks=False) - text = self.io_util.stream_request(request_body, player_io, self.connection) + text = self.io_util.stream_request(request_body, player_io, self.connection, prompt=prompt) self._store_hash(text_hash_value, text) return '\n', rolling_prompt diff --git a/tests/supportstuff.py b/tests/supportstuff.py index 5b4f16a0..36ed2ad5 100644 --- a/tests/supportstuff.py +++ b/tests/supportstuff.py @@ -68,7 +68,7 @@ def __init__(self, response: list = []) -> None: def synchronous_request(self, request_body: dict, prompt: str = None) -> str: return self.response.pop(0) if isinstance(self.response, list) > 0 else self.response - def asynchronous_request(self, request_body: dict): + def asynchronous_request(self, request_body: dict, prompt: str = None): return self.response.pop(0) if isinstance(self.response, list) > 0 else self.response def set_response(self, response: any): diff --git a/tests/test_llm_io.py b/tests/test_llm_io.py new file mode 100644 index 00000000..3d4d10cf --- /dev/null +++ b/tests/test_llm_io.py @@ -0,0 +1,51 @@ + + +import json +import os + +import yaml +from tale.llm import llm_config +from tale.llm.llm_io import IoUtil +from tests.supportstuff import FakeIoUtil + + +class TestLlmIo(): + + + + llm_io = IoUtil() + + def setup(self): + with open(os.path.realpath(os.path.join(os.path.dirname(__file__), "../llm_config.yaml")), "r") as stream: + try: + self.config_file = yaml.safe_load(stream) + except yaml.YAMLError as exc: + print(exc) + self.llm_io.user_start_prompt = self.config_file['USER_START'] + self.llm_io.user_end_prompt = self.config_file['USER_END'] + + def test_set_prompt_kobold_cpp(self): + self.llm_io.backend = 'kobold_cpp' + prompt = self.config_file['BASE_PROMPT'] + assert('### Instruction' not in prompt) + assert('### Response' not in prompt) + assert('USER_START' in prompt) + assert('USER_END' not in prompt) + request_body = json.loads(self.config_file['DEFAULT_BODY']) + + result = self.llm_io._set_prompt(request_body, prompt) + assert('### Instruction' in result['prompt']) + assert('### Response' in result['prompt']) + + def test_set_prompt_openai(self): + self.llm_io.backend = 'openai' + prompt = self.config_file['BASE_PROMPT'] + assert('### Instruction' not in prompt) + assert('### Response' not in prompt) + assert('USER_START' in prompt) + assert('USER_END' not in prompt) + request_body = json.loads(self.config_file['OPENAI_BODY']) + + result = self.llm_io._set_prompt(request_body, prompt) + assert('### Instruction' in result['messages'][1]['content']) + assert('### Response' in result['messages'][1]['content']) \ No newline at end of file From 06347ae05a68974f25646c308ba27bc972a24ff0 Mon Sep 17 00:00:00 2001 From: rickard Date: Sat, 7 Oct 2023 10:13:36 +0200 Subject: [PATCH 3/6] use deepcopy for request_body --- tale/llm/llm_ext.py | 9 ++++++--- tale/llm/world_building.py | 15 ++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tale/llm/llm_ext.py b/tale/llm/llm_ext.py index 394ab362..a4056eb2 100644 --- a/tale/llm/llm_ext.py +++ b/tale/llm/llm_ext.py @@ -62,7 +62,7 @@ def do_say(self, what_happened: str, actor: Living) -> None: self.update_conversation(f'{actor.title}:{what_happened}\n') short_len = False if isinstance(actor, Player) else True - response, item_result, sentiment = mud_context.driver.llm_util.generate_dialogue( + response, item_result, sentiment, summary = mud_context.driver.llm_util.generate_dialogue( conversation=self.conversation, character_card = self.character_card, character_name = self.title, @@ -71,8 +71,11 @@ def do_say(self, what_happened: str, actor: Living) -> None: sentiment = self.sentiments.get(actor.title, ''), location_description=self.location.look(exclude_living=self), short_len=short_len) - - self.update_conversation(f"{self.title} says: \"{response}\"") + + if summary: + self.update_conversation(f"{self.title} says: \"{summary}\"") + else: + self.update_conversation(f"{self.title} says: \"{response}\"") if len(self.conversation) > self.memory_size: self.conversation = self.conversation[self.memory_size+1:] diff --git a/tale/llm/world_building.py b/tale/llm/world_building.py index 787d731e..91eac982 100644 --- a/tale/llm/world_building.py +++ b/tale/llm/world_building.py @@ -1,4 +1,5 @@ +from copy import deepcopy import json from tale import parse_utils, races from tale import zone @@ -54,7 +55,7 @@ def build_location(self, location: Location, }) - request_body = self.default_body + request_body = deepcopy(self.default_body) if self.backend == 'kobold_cpp': request_body = self._kobold_generation_prompt(request_body) @@ -167,7 +168,7 @@ def _generate_zone(self, location_desc: str, story_config: StoryConfig, exit_loc 'story_context': story_config.context, }) - request_body = self.default_body + request_body = deepcopy(self.default_body) if self.backend == 'kobold_cpp': request_body['max_length'] = 750 elif self.backend == 'openai': @@ -202,7 +203,7 @@ def generate_start_location(self, location: Location, zone_info: dict, story_typ 'story_context': story_context, }) - request_body = self.default_body + request_body = deepcopy(self.default_body) if self.backend == 'kobold_cpp': request_body = self._kobold_generation_prompt(request_body) result = self.io_util.synchronous_request(request_body, prompt=prompt) @@ -223,7 +224,7 @@ def generate_start_zone(self, location_desc: str, story_type: str, story_context 'story_context': story_context, }) - request_body = self.default_body + request_body = deepcopy(self.default_body) if self.backend == 'kobold_cpp': request_body = self._kobold_generation_prompt(request_body) request_body['max_length'] = 750 @@ -244,7 +245,7 @@ def generate_world_items(self, story_context: str, story_type: str, world_info: world_info=world_info, world_mood=parse_utils.mood_string_from_int(world_mood), item_types=self.item_types) - request_body = self.default_body + request_body = deepcopy(self.default_body) if self.backend == 'kobold_cpp': request_body = self._kobold_generation_prompt(request_body) @@ -261,7 +262,7 @@ def generate_world_creatures(self, story_context: str, story_type: str, world_in story_type=story_type, world_info=world_info, world_mood=parse_utils.mood_string_from_int(world_mood)) - request_body = self.default_body + request_body = deepcopy(self.default_body) if self.backend == 'kobold_cpp': request_body = self._kobold_generation_prompt(request_body) @@ -284,7 +285,7 @@ def generate_random_spawn(self, location: Location, story_context: str, story_ty world_info=world_info, zone_info=zone_info, location_info=location_info) - request_body = self.default_body + request_body = deepcopy(self.default_body) if self.backend == 'kobold_cpp': request_body = self._kobold_generation_prompt(request_body) From ae5afbb20355070c94158d91d9353fe238dce085 Mon Sep 17 00:00:00 2001 From: rickard Date: Sat, 7 Oct 2023 12:35:34 +0200 Subject: [PATCH 4/6] json story loading --- llm_config.yaml | 6 +-- stories/test_story/__init__.py | 7 ++++ stories/test_story/story.py | 30 +++++++++++++++ stories/test_story/test_locations.json | 15 -------- stories/test_story/zones/test_locations.json | 7 +++- tale/driver.py | 8 +++- tale/json_story.py | 10 +++-- tale/llm/character.py | 39 ++++++++++---------- tale/llm/llm_ext.py | 2 +- tale/llm/llm_utils.py | 5 ++- tale/parse_utils.py | 4 +- tests/test_llm_utils.py | 31 ++++++++++++++++ 12 files changed, 115 insertions(+), 49 deletions(-) create mode 100644 stories/test_story/__init__.py create mode 100644 stories/test_story/story.py delete mode 100644 stories/test_story/test_locations.json diff --git a/llm_config.yaml b/llm_config.yaml index 938d46f2..2ba73386 100644 --- a/llm_config.yaml +++ b/llm_config.yaml @@ -5,15 +5,15 @@ STREAM_ENDPOINT: "/api/extra/generate/stream" DATA_ENDPOINT: "/api/extra/generate/check" WORD_LIMIT: 200 BACKEND: "kobold_cpp" # "openai" -DEFAULT_BODY: '{"stop_sequence": "", "max_length":750, "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}' +DEFAULT_BODY: '{"stop_sequence": "\n\n\n", "max_length":750, "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": "\n\n", "max_length":750, "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}' ANALYSIS_BODY: '{"banned_tokens":"\n\n", "stop_sequence": "\n\n\n\n", "max_length":500, "max_context_length":4096, "temperature":0.15, "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, "mirostat":2, "mirostat_tau":6.0, "mirostat_eta":0.1, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}' MEMORY_SIZE: 512 PRE_PROMPT: 'Below is an instruction that describes a task, paired with an input that provides further context. As an game keeper for an RPG, write a response that appropriately completes the request.\n\n' BASE_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the following Text in your own words using the supplied Context and History to create a background for your text. Use about {max_words} words. [Text:{input_text}] \n\nEnd of text.\n\n [USER_START] Rewrite [Text] in your own words. " ACTION_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the Action, and nothing else, in your own words using the supplied Context and Location. History is what happened before. Use less than {max_words} words. [Text: {input_text}] \n\nEnd of text.\n\n" -DIALOGUE_PROMPT: '[Story context: {story_context}]; [Location: {location}]; The following is a conversation between {character1} and {character2}; {character1}:{character1_description}; character2:{character2_description}; [Chat history: {previous_conversation}]\n\n [{character2}s sentiment towards {character1}: {sentiment}]. [USER_START] Write a single response for {character2} in third person pov, using {character2} description. \n\n' -ITEM_PROMPT: 'Items:[{items}];Characters:[{character1},{character2}] \n\n [USER_START] Decide if an item was explicitly given, taken, dropped or put somewhere in the following text:[Text:{text}]. Insert your thoughts about whether an item was explicitly given, taken, put somewhere or dropped in "my thoughts", and the results in "item", "from" and "to", or make them empty if . Insert {character1}s sentiment towards {character2} in a single word in "sentiment assessment". Fill in the following JSON template: {{ "thoughts":"my thoughts", "result": {{ "item":"", "from":"", "to":""}}, {{"sentiment":"sentiment assessment"}} }} End of example. \n\n Write your response in valid JSON\n\n' +DIALOGUE_PROMPT: '[Story context: {story_context}]; [Location: {location}]; The following is a conversation between {character1} and {character2}; {character1}:{character1_description}; character2:{character2_description}; [Chat history: {previous_conversation}]\n\n [{character2}s sentiment towards {character1}: {sentiment}].[USER_START] Write a single response for {character2} in third person pov, using {character2} description. \n\n' +ITEM_PROMPT: 'Items:[{items}];Characters:[{character1},{character2}] \n\n [USER_START] Decide if an item was explicitly given, taken, dropped or put somewhere in the following text:[Text:{text}]. Insert your thoughts about whether an item was explicitly given, taken, put somewhere or dropped in "my thoughts", and the results in "item", "from" and "to", or make them empty if . Insert {character1}s sentiment towards {character2} in a single word in "sentiment assessment". Fill in the following JSON template: {{ "thoughts":"", "result": {{ "item":"", "from":"", "to":""}}, {{"sentiment":"sentiment assessment"}} }} End of example. \n\n Write your response in valid JSON\n\n' COMBAT_PROMPT: 'Rewrite the following combat between user {attacker} using {attacker_weapon}, and {victim} using {victim_weapon} in {location}, {location_description} into a vivid description in less than 300 words. [USER_START] Rewrite the following combat result in about 150 words, using the characters weapons . Combat Result: {attacker_msg} \n' 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.\n\n' CREATE_CHARACTER_PROMPT: '[USER_START] For a {story_type}, create a diverse character with rich personality that can be interacted with using the story context and keywords. Do not mention height. Story context: {story_context}; keywords: {keywords}. Fill in this JSON template and write nothing else: {{"name":"", "description": "50 words", "appearance": "25 words", "personality": "50 words", "money":(int), "level":"", "gender":"m/f/n", "age":(int), "race":""}}\n\n ' diff --git a/stories/test_story/__init__.py b/stories/test_story/__init__.py new file mode 100644 index 00000000..733f0370 --- /dev/null +++ b/stories/test_story/__init__.py @@ -0,0 +1,7 @@ +""" +Package containing a demo room and some stuff, to easily check out +a running Tale framework (python -m tale.demo.story) + +'Tale' mud driver, mudlib and interactive fiction framework +Copyright by Irmen de Jong (irmen@razorvine.net) +""" diff --git a/stories/test_story/story.py b/stories/test_story/story.py new file mode 100644 index 00000000..98fd4f9a --- /dev/null +++ b/stories/test_story/story.py @@ -0,0 +1,30 @@ + +import pathlib +import sys +from tale import parse_utils +from tale.driver import Driver +from tale.driver_if import IFDriver +from tale.json_story import JsonStory +from tale.main import run_from_cmdline +from tale.zone import Zone + + +class Story(JsonStory): + + driver = None + + def __init__(self) -> None: + super(Story, self).__init__('', parse_utils.load_story_config(parse_utils.load_json('test_story_config.json'))) + + def init(self, driver: Driver) -> None: + super(Story, self).init(driver) + + +if __name__ == "__main__": + # story is invoked as a script, start it in the Tale Driver. + 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) \ No newline at end of file diff --git a/stories/test_story/test_locations.json b/stories/test_story/test_locations.json deleted file mode 100644 index 691b074b..00000000 --- a/stories/test_story/test_locations.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name":"Cave", - "rooms":[ - { - "name": "Cave entrance", - "descr": "This cave entrance gives shelter from the wind and rain.", - "exits": [{"name" : "Royal Grotto", "short_desc":"Exit to Grotto", "long_desc":"There's an opening that leads deeper into the cave'", "enter_msg":""}] - }, - { - "name": "Royal Grotto", - "descr": "This is Kobbo the Kings throne room. It's a dark, damp place with a log in one end", - "exits": [{"name" : "Cave entrance", "short_desc":"exit to Cave entrance", "long_desc":"", "enter_msg":""}] - } - ] -} diff --git a/stories/test_story/zones/test_locations.json b/stories/test_story/zones/test_locations.json index 691b074b..879d3d04 100644 --- a/stories/test_story/zones/test_locations.json +++ b/stories/test_story/zones/test_locations.json @@ -1,6 +1,7 @@ { "name":"Cave", - "rooms":[ + "description": "This is a cave. It's dark and damp.", + "locations":[ { "name": "Cave entrance", "descr": "This cave entrance gives shelter from the wind and rain.", @@ -11,5 +12,7 @@ "descr": "This is Kobbo the Kings throne room. It's a dark, damp place with a log in one end", "exits": [{"name" : "Cave entrance", "short_desc":"exit to Cave entrance", "long_desc":"", "enter_msg":""}] } - ] + ], + "races": [], + "items": [] } diff --git a/tale/driver.py b/tale/driver.py index b50481cb..7ce330b7 100644 --- a/tale/driver.py +++ b/tale/driver.py @@ -668,7 +668,11 @@ def lookup_location(self, location_name: str) -> base.Location: module = importlib.import_module(modulename) location = module except ImportError: - raise errors.TaleError("Location not found: " + location_name) + if isinstance(self.story, DynamicStory): + dynamic_story = typing.cast(DynamicStory, self.story) + location = dynamic_story.find_location(location_name.split('.')[-1]) + if not location: + raise errors.TaleError("Location not found: " + location_name) return location # type: ignore def _load_zones(self, zone_names: Sequence[str]) -> ModuleType: @@ -679,6 +683,8 @@ def _load_zones(self, zone_names: Sequence[str]) -> ModuleType: try: module = importlib.import_module("zones." + zone) except ImportError: + if isinstance(self.story, DynamicStory): + return [] raise errors.TaleError("zone not found: " + zone) if hasattr(module, "init"): # call the zone module initialization function diff --git a/tale/json_story.py b/tale/json_story.py index 768912fb..9397d130 100644 --- a/tale/json_story.py +++ b/tale/json_story.py @@ -13,6 +13,10 @@ def __init__(self, path: str, config: StoryConfig): super(JsonStory, self).__init__() self.config = config self.path = path + + + def init(self, driver) -> None: + self.driver = driver locs = {} zones = [] for zone in self.config.zones: @@ -24,15 +28,13 @@ def __init__(self, path: str, config: StoryConfig): zone = zones[name] for loc in zone.locations.values(): locs[loc.name] = loc + self.add_zone(zone) self._locations = locs - self._zones = zones # type: dict(str, dict) + if self.config.npcs: self._world["creatures"] = parse_utils.load_npcs(parse_utils.load_json(self.path +'npcs/'+self.config.npcs + '.json'), self._zones) if self.config.items: self._world["items"] = parse_utils.load_items(parse_utils.load_json(self.path + self.config.items + '.json'), self._zones) - - def init(self, driver) -> None: - pass def welcome(self, player: Player) -> str: diff --git a/tale/llm/character.py b/tale/llm/character.py index 2f2a0580..cae866a6 100644 --- a/tale/llm/character.py +++ b/tale/llm/character.py @@ -1,6 +1,6 @@ - +from copy import deepcopy from json import JSONDecodeError import json import random @@ -48,8 +48,7 @@ def generate_dialogue(self, conversation: str, character1=target, character1_description=target_description, sentiment=sentiment) - request_body = self.default_body - + request_body = deepcopy(self.default_body) #if not self.stream: text = parse_utils.trim_response(self.io_util.synchronous_request(request_body, prompt=prompt)) @@ -67,9 +66,9 @@ def generate_dialogue(self, conversation: str, # player_io = mud_context.pla # text = self.io_util.stream_request(self.url + self.stream_endpoint, self.url + self.data_endpoint, request_body, player_io, self.connection) - item_handling_result, new_sentiment = self.dialogue_analysis(text, character_card, character_name, target) - - return text, item_handling_result, new_sentiment + item_handling_result, new_sentiment, summary = self.dialogue_analysis(text, character_card, character_name, target) + + return text, item_handling_result, new_sentiment, summary def dialogue_analysis(self, text: str, character_card: str, character_name: str, target: str): """Parse the response from LLM and determine if there are any items to be handled.""" @@ -78,26 +77,28 @@ def dialogue_analysis(self, text: str, character_card: str, character_name: str, prompt = self.generate_item_prompt(text, items, character_name, target) if self.backend == 'kobold_cpp': - request_body = self.analysis_body + request_body = deepcopy(self.analysis_body) request_body['grammar'] = self.json_grammar elif self.backend == 'openai': - request_body = self.default_body + request_body = deepcopy(self.default_body) text = parse_utils.trim_response(self.io_util.synchronous_request(request_body, prompt=prompt)) try: json_result = json.loads(parse_utils.sanitize_json(text)) except JSONDecodeError as exc: print(exc) - return None, None + return None, None, None valid, item_result = self.validate_item_response(json_result, character_name, target, items) sentiment = self.validate_sentiment(json_result) + + summary = json_result.get('summary', '') - return item_result, sentiment + return item_result, sentiment, summary def validate_sentiment(self, json: dict): try: - return json.get('sentiment') + return json.get('sentiment', '') except: print(f'Exception while parsing sentiment {json}') return '' @@ -113,22 +114,22 @@ def generate_item_prompt(self, text: str, items: str, character1: str, character def validate_item_response(self, json_result: dict, character1: str, character2: str, items: str) -> bool: if 'result' not in json_result or not json_result.get('result'): - return False, None + return False, '' result = json_result['result'] if 'item' not in result or not result['item']: - return False, None + return False, '' if not result['from']: - return False, None + return False, '' if result['item'] in items: return True, result - return False, None + return False, '' def generate_character(self, story_context: str = '', keywords: list = [], story_type: str = ''): """ Generate a character card based on the current story context""" prompt = self.character_prompt.format(story_type=story_type if story_type else _MudContext.config.type, story_context=story_context, keywords=', '.join(keywords)) - request_body = self.default_body + request_body = deepcopy(self.default_body) if self.backend == 'kobold_cpp': # do some parameter tweaking for kobold_cpp request_body['stop_sequence'] = ['\n\n'] # to avoid text after the character card @@ -164,7 +165,7 @@ def perform_idle_action(self, character_name: str, location: Location, story_con items=items, characters=json.dumps(characters), sentiments=json.dumps(sentiments)) - request_body = self.default_body + request_body = deepcopy(self.default_body) if self.backend == 'kobold_cpp': request_body['seed'] = random.randint(0, 2147483647) request_body['banned_tokens'] = ['You'] @@ -183,7 +184,7 @@ def perform_travel_action(self, character_name: str, location: Location, locatio directions=directions, character=character_card, character_name=character_name) - request_body = self.default_body + request_body = deepcopy(self.default_body) text = self.io_util.asynchronous_request(request_body, prompt=prompt) return text @@ -196,7 +197,7 @@ def perform_reaction(self, action: str, character_name: str, acting_character_na character=character_card, acting_character_name=acting_character_name, sentiment=sentiment) - request_body = self.default_body + request_body = deepcopy(self.default_body) text = self.io_util.asynchronous_request(request_body, prompt=prompt) return text \ No newline at end of file diff --git a/tale/llm/llm_ext.py b/tale/llm/llm_ext.py index a4056eb2..4560a428 100644 --- a/tale/llm/llm_ext.py +++ b/tale/llm/llm_ext.py @@ -174,7 +174,7 @@ def get_zone(self, name: str) -> Zone: return self._zones[name] def add_zone(self, zone: Zone) -> bool: - """ Add a zone to the story. """ + if zone.name in self._zones: return False self._zones[zone.name] = zone diff --git a/tale/llm/llm_utils.py b/tale/llm/llm_utils.py index 11176632..85d20385 100644 --- a/tale/llm/llm_utils.py +++ b/tale/llm/llm_utils.py @@ -1,3 +1,4 @@ +from copy import deepcopy import json import os import yaml @@ -48,7 +49,7 @@ def evoke(self, player_io: TextBuffer, message: str, short_len : bool=False, rol """Evoke a response from LLM. Async if stream is True, otherwise synchronous. Update the rolling prompt with the latest message. Will put generated text in _look_hashes, and reuse it if same hash is passed in.""" - output_template = 'Original:[ {message} ] Generated:{text}' + output_template = 'Original:[ {message} ] Generated:{text}' if not message or str(message) == "\n": str(message), rolling_prompt @@ -72,7 +73,7 @@ def evoke(self, player_io: TextBuffer, message: str, short_len : bool=False, rol max_words=self.word_limit if not short_len else amount, input_text=str(trimmed_message)) - request_body = self.default_body + request_body = deepcopy(self.default_body) if not self.stream: text = self.io_util.synchronous_request(request_body, prompt=prompt) diff --git a/tale/parse_utils.py b/tale/parse_utils.py index d587e246..e58f9f81 100644 --- a/tale/parse_utils.py +++ b/tale/parse_utils.py @@ -134,7 +134,7 @@ def load_npcs(json_file: [], locations = {}) -> dict: new_npc.aliases.add(name.split(' ')[0].lower()) new_npc.stats.set_weapon_skill(WeaponType.UNARMED, random.randint(10, 30)) new_npc.stats.level = npc.get('level', 1) - elif npc_type == 'Mob': + else: new_npc = StationaryMob(name=npc['name'], gender=lang.validate_gender_mf(npc.get('gender', 'm')[0]), @@ -370,7 +370,7 @@ def parse_generated_exits(json_result: dict, exit_location_name: str, location: short_descr=from_description) new_location.add_exits([exit_back]) - to_description = 'To the {direction} you see {location}.'.format(direction=directions_to[1], location=exit.get('short_descr', 'description').lower()) if len(directions_to) > 1 else exit.get('short_descr', 'description') + to_description = 'To the {direction} you see {location}'.format(direction=directions_to[1], location=exit.get('short_descr', 'description').lower()) if len(directions_to) > 1 else exit.get('short_descr', 'description') exit_to = Exit(directions=directions_to, target_location=new_location, short_descr=to_description, diff --git a/tests/test_llm_utils.py b/tests/test_llm_utils.py index 5739802d..d1ec3085 100644 --- a/tests/test_llm_utils.py +++ b/tests/test_llm_utils.py @@ -221,6 +221,7 @@ def test_generate_start_location(self): assert(new_locations[0].name == 'Misty Meadows') assert(new_locations[1].name == 'Riverdale') + def test_generate_start_zone(self): # mostly for coverage self.llm_util._world_building.io_util.response = self.generated_zone @@ -429,5 +430,35 @@ def test_generate_random_spawn(self): assert(location.search_living('grumpy') is not None) assert(location.search_living('wolf') is not None) + def test_issue_overwriting_exits(self): + """ User walks west and enters Rocky Cliffs, When returning east, the exits are overwritten.""" + self.llm_util._world_building.io_util.response=['{"name": "Forest Path", "exits": [{"direction": "north", "name": "Mystic Woods", "short_descr": "A dense, misty forest teeming with ancient magic."}, {"direction": "south", "name": "Blooming Meadow", "short_descr": "A lush, vibrant meadow filled with wildflowers and gentle creatures."}, {"direction": "west", "name": "Rocky Cliffs", "short_descr": "A rugged, rocky terrain with breathtaking views of the surrounding landscape."}], "items": [{"name": "enchanted forest amulet", "type": "Wearable", "description": "A shimmering amulet infused with the magic of the forest, granting the wearer a moderate boost to their defense and resistance to harm."}], "npcs": [{"name": "Florabug", "sentiment": "neutral", "race": "florabug", "gender": "m", "level": 5, "description": "A friendly, curious creature who loves to make new friends."}]}', + '{"description": "A picturesque beach with soft, golden sand and crystal clear waters. The sun shines bright overhead, casting a warm glow over the area. The air is filled with the sound of gentle waves and the cries of seagulls. A few scattered palm trees provide shade and a sense of tranquility.", "exits": [{"direction": "north", "name": "Coastal Caves", "short_descr": "A network of dark, damp caves hidden behind the sandy shores."}, {"direction": "south", "name": "Rocky Cliffs", "short_descr": "A rugged, rocky coastline with steep drop-offs and hidden sea creatures."}, {"direction": "east", "name": "Mermaid\'s Grotto", "short_descr": "A hidden underwater cave system, rumored to be home to magical sea creatures."}], "items": [], "npcs": []}'] + location = Location(name='', descr='on a small road outside a forest') + new_locations, exits = self.llm_util._world_building.generate_start_location(location, + story_type='', + story_context='', + zone_info={}, + world_info='',) + + location = Location(name=location.name, descr=location.description) + + assert((len(exits) == 3)) + assert((len(new_locations) == 3)) + location.add_exits(exits) + assert((len(location.exits) == 6)) + rocky_cliffs = new_locations[2] # type: Location + assert(rocky_cliffs.name == 'Rocky Cliffs') + + new_locations, exits = self.llm_util._world_building.build_location(rocky_cliffs, + 'Rocky Cliffs', + story_type='', + story_context='', + world_info='', + zone_info={}) + rocky_cliffs.add_exits(exits) + assert((len(rocky_cliffs.exits) == 6)) + assert((len(new_locations) == 2)) + \ No newline at end of file From bd08a538362375d4126805d041cc4ba132165c17 Mon Sep 17 00:00:00 2001 From: rickard Date: Sat, 7 Oct 2023 13:51:47 +0200 Subject: [PATCH 5/6] update prompts parse more of config when json --- llm_config.yaml | 40 ++++++++++++++++++++-------------------- tale/parse_utils.py | 4 ++++ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/llm_config.yaml b/llm_config.yaml index 2ba73386..fd4aede7 100644 --- a/llm_config.yaml +++ b/llm_config.yaml @@ -9,31 +9,31 @@ DEFAULT_BODY: '{"stop_sequence": "\n\n\n", "max_length":750, "max_context_length GENERATION_BODY: '{"stop_sequence": "\n\n", "max_length":750, "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}' ANALYSIS_BODY: '{"banned_tokens":"\n\n", "stop_sequence": "\n\n\n\n", "max_length":500, "max_context_length":4096, "temperature":0.15, "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, "mirostat":2, "mirostat_tau":6.0, "mirostat_eta":0.1, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}' MEMORY_SIZE: 512 -PRE_PROMPT: 'Below is an instruction that describes a task, paired with an input that provides further context. As an game keeper for an RPG, write a response that appropriately completes the request.\n\n' +PRE_PROMPT: 'Below is an instruction that describes a task, paired with an input that provides further context. As an game keeper for an RPG, write a response that appropriately completes the request.' BASE_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the following Text in your own words using the supplied Context and History to create a background for your text. Use about {max_words} words. [Text:{input_text}] \n\nEnd of text.\n\n [USER_START] Rewrite [Text] in your own words. " -ACTION_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the Action, and nothing else, in your own words using the supplied Context and Location. History is what happened before. Use less than {max_words} words. [Text: {input_text}] \n\nEnd of text.\n\n" -DIALOGUE_PROMPT: '[Story context: {story_context}]; [Location: {location}]; The following is a conversation between {character1} and {character2}; {character1}:{character1_description}; character2:{character2_description}; [Chat history: {previous_conversation}]\n\n [{character2}s sentiment towards {character1}: {sentiment}].[USER_START] Write a single response for {character2} in third person pov, using {character2} description. \n\n' -ITEM_PROMPT: 'Items:[{items}];Characters:[{character1},{character2}] \n\n [USER_START] Decide if an item was explicitly given, taken, dropped or put somewhere in the following text:[Text:{text}]. Insert your thoughts about whether an item was explicitly given, taken, put somewhere or dropped in "my thoughts", and the results in "item", "from" and "to", or make them empty if . Insert {character1}s sentiment towards {character2} in a single word in "sentiment assessment". Fill in the following JSON template: {{ "thoughts":"", "result": {{ "item":"", "from":"", "to":""}}, {{"sentiment":"sentiment assessment"}} }} End of example. \n\n Write your response in valid JSON\n\n' -COMBAT_PROMPT: 'Rewrite the following combat between user {attacker} using {attacker_weapon}, and {victim} using {victim_weapon} in {location}, {location_description} into a vivid description in less than 300 words. [USER_START] Rewrite the following combat result in about 150 words, using the characters weapons . Combat Result: {attacker_msg} \n' -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.\n\n' -CREATE_CHARACTER_PROMPT: '[USER_START] For a {story_type}, create a diverse character with rich personality that can be interacted with using the story context and keywords. Do not mention height. Story context: {story_context}; keywords: {keywords}. Fill in this JSON template and write nothing else: {{"name":"", "description": "50 words", "appearance": "25 words", "personality": "50 words", "money":(int), "level":"", "gender":"m/f/n", "age":(int), "race":""}}\n\n ' -CREATE_LOCATION_PROMPT: '[Story context: {story_context}]; World info: {world_info}; Zone info: {zone_info}; Exit json example: {{"direction":"", "name":"name of new location", "short_descr":"exit description"}}; Npc or mob example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words"}}. Existing connected locations: {exit_locations}. [USER_START] For a {story_type}, 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: {{"description": "25 words", "exits":[], "items":[], "npcs":[]}}. Write the response in valid JSON.\n\n ' -CREATE_ZONE_PROMPT: '[Story context: {story_context}]; World info: {world_info}; [USER_START] For a {story_type}, create an new area that can be further populated with locations. The area is about 25 square kilometers. It is connected in the {direction} to {zone_info}. The mood is {mood}. Add a name and brief description. Choose the names of 5 creatures from the supplied list likely to find in the area. Fill in "items" with the names and types of 5 common items in the area. Fill in this JSON template and do not write anything else: {{"name":"", "description": "75 words", "races":[], "items":[], "mood":"5 to -5, where 5 is extremely friendly and -5 is extremely hostile.", "level":(int)}}. Write the response in valid JSON.\n\n ' +ACTION_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the Action, and nothing else, in your own words using the supplied Context and Location. History is what happened before. Use less than {max_words} words. [Text: {input_text}] \n\nEnd of text." +DIALOGUE_PROMPT: '[Story context: {story_context}]; [Location: {location}]; The following is a conversation between {character1} and {character2}; {character1}:{character1_description}; character2:{character2_description}; [Chat history: {previous_conversation}]\n\n [{character2}s sentiment towards {character1}: {sentiment}].[USER_START] Write a single response for {character2} in third person pov, using {character2} description.' +ITEM_PROMPT: 'Items:[{items}];Characters:[{character1},{character2}] \n\n [USER_START] Decide if an item was explicitly given, taken, dropped or put somewhere in the following text:[Text:{text}]. Insert your thoughts about whether an item was explicitly given, taken, put somewhere or dropped in "my thoughts", and the results in "item", "from" and "to", or make them empty if . Insert {character1}s sentiment towards {character2} in a single word in "sentiment assessment". Fill in the following JSON template: {{ "thoughts":"", "result": {{ "item":"", "from":"", "to":""}}, {{"sentiment":"sentiment assessment"}} }} End of example. \n\n Write your response in valid JSON' +COMBAT_PROMPT: 'Rewrite the following combat between user {attacker} using {attacker_weapon}, and {victim} using {victim_weapon} in {location}, {location_description} into a vivid description in less than 300 words. [USER_START] Rewrite the following combat result in about 150 words, using the characters weapons . Combat Result: {attacker_msg}' +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: '[USER_START] For a {story_type}, create a diverse character with rich personality that can be interacted with using the story context and keywords. Do not mention height. Story context: {story_context}; keywords: {keywords}. Fill in this JSON template and write nothing else: {{"name":"", "description": "50 words", "appearance": "25 words", "personality": "50 words", "money":(int), "level":"", "gender":"m/f/n", "age":(int), "race":""}}' +CREATE_LOCATION_PROMPT: '[Story context: {story_context}]; World info: {world_info}; Zone info: {zone_info}; Exit json example: {{"direction":"", "name":"name of new location", "short_descr":"exit description"}}; Npc or mob example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words"}}. Existing connected locations: {exit_locations}. [USER_START] For a {story_type}, 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: {{"description": "25 words", "exits":[], "items":[], "npcs":[]}}. Write the response in valid JSON.' +CREATE_ZONE_PROMPT: '[Story context: {story_context}]; World info: {world_info}; [USER_START] For a {story_type}, create an new area that can be further populated with locations. The area is about 25 square kilometers. It is connected in the {direction} to {zone_info}. The mood is {mood}. Add a name and brief description. Choose the names of 5 creatures from the supplied list likely to find in the area. Fill in "items" with the names and types of 5 common items in the area. Fill in this JSON template and do not write anything else: {{"name":"", "description": "75 words", "races":[], "items":[], "mood":"5 to -5, where 5 is extremely friendly and -5 is extremely hostile.", "level":(int)}}. Write the response in valid JSON.' OPENAI_HEADERS: '{"Content-Type":"application/json", "Authorization":""}' OPENAI_BODY: '{"model":"gpt-3.5-turbo", "messages":[{"role":"system", "content":"You are an assistant game keeper for an RPG"}, {"role":"user", "content":""}], "temperature": 1.0, "max_tokens":500}' OPENAI_API_KEY: "OPENAI_API_KEY" ITEMS_PROMPT: "Add {items} of various type." SPAWN_PROMPT: "Add a level {level}, {alignment} npc or mob." -IDLE_ACTION_PROMPT: " [Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Acting character: {character}] [USER_START] Choose an item from: {items}, or a character from:{characters}, to interact with, or perform a solo action. Do not make up new characters. Don't contradict or repeat Last Action. [USER_START] Write what {character_name} does, in present tense third person point of view. Use less than 40 words. Don't write what {character_name} thinks, or what the player (You) or anyone else does.\n\n " -TRAVEL_PROMPT: "[USER_START] For {character}: pick a location from [{locations}] they would like to travel to or a direction from [{directions}], or stay in the current location. Do not make up new locations. Write what {character_name} chooses. [USER_START] Write only the location name, direction, or write nothing to stay in the same location. Write nothing else.\n\n " -REACTION_PROMPT: "[USER_START] Act as {character}. {acting_character_name} has performed the following action that involves {character_name}: {action}. {character_name}'s sentiment towards {acting_character_name}: {sentiment}. [USER_START] Respond with a suitable action for {character_name}, in present tense third person point of view. Use less than 40 words.\n\n " -STORY_BACKGROUND_PROMPT: "[USER_START] For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, write a captivating background story that the player can interact with. Include a large scale plot conflict that the player will encounter. Use less than 400 words\n\n " -START_LOCATION_PROMPT: '[Story context: {story_context}]; Zone info: {zone_info}; Item json example: {{"name":"", "type":"", "short_descr":"10 words"}}, type can be "Weapon", "Wearable", "Other" or "Money"; Npc example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words"}} ; Exit json example: {{"direction":"", "name":"name of new location", "short_descr":"exit description"}}; [USER_START] For a {story_type}, come up with a name for the location with this description: {location_description}. {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: {{"name": "", "exits":[], "items":[], "npcs":[]}}.\n\n ' +IDLE_ACTION_PROMPT: " [Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Acting character: {character}] [USER_START] Choose an item from: {items}, or a character from:{characters}, to interact with, or perform a solo action. Do not make up new characters. Don't contradict or repeat Last Action. [USER_START] Write what {character_name} does, in present tense third person point of view. Use less than 40 words. Don't write what {character_name} thinks, or what the player (You) or anyone else does." +TRAVEL_PROMPT: "[USER_START] For {character}: pick a location from [{locations}] they would like to travel to or a direction from [{directions}], or stay in the current location. Do not make up new locations. Write what {character_name} chooses. [USER_START] Write only the location name, direction, or write nothing to stay in the same location. Write nothing else." +REACTION_PROMPT: "[USER_START] Act as {character}. {acting_character_name} has performed the following action that involves {character_name}: {action}. {character_name}'s sentiment towards {acting_character_name}: {sentiment}. [USER_START] Respond with a suitable action for {character_name}, in present tense third person point of view. Use less than 40 words." +STORY_BACKGROUND_PROMPT: "[USER_START] For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, write a captivating background story that the player can interact with. Include a large scale plot conflict that the player will encounter. Use less than 400 words" +START_LOCATION_PROMPT: '[Story context: {story_context}]; Zone info: {zone_info}; Item json example: {{"name":"", "type":"", "short_descr":"10 words"}}, type can be "Weapon", "Wearable", "Other" or "Money"; Npc example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words"}} ; Exit json example: {{"direction":"", "name":"name of new location", "short_descr":"exit description"}}; [USER_START] For a {story_type}, come up with a name for the location with this description: {location_description}. {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: {{"name": "", "exits":[], "items":[], "npcs":[]}}.' STORY_PLOT_PROMPT: "[USER_START] For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}. Based on the following background: {story_background} write an innovative and engaging plot that the player can become part of. Use less than 400 words\n\n " -WORLD_ITEMS: '[Story context: {story_context}]; For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, come up with 7 common items that can be found in the world. Item example: {{"name":"", "type":"", "short_descr":"", "level":int, "value":int}}, type is one of: {item_types}; Fill in this JSON template and do not write anything else: {{"items": []}}.\n\n ' -WORLD_CREATURES: '[Story context: {story_context}]; For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, come up with 5 creatures of various level and sentiment that can be found in the world. Creature example: {{"name":"", "body":"", "mass":int(kg), "hp":int, "type":"Npc or Mob", "level":int, "unarmed_attack":One of [FISTS, CLAWS, BITE, TAIL, HOOVES, HORN, TUSKS, BEAK, TALON], "short_descr":""}}. Fill in this JSON template and do not write anything else: {{"creatures": []}}.\n\n ' -GOAL_PROMPT: '[Characters:{characters}][Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Known locations: {locations}][Acting character: {character}] [Actions available:{actions}] [USER_START] For {character_name}, come up with a goal that plays along with their character description that involves an item, a character or a location in the prompt. Then construct up to three tasks that will lead towards the achievement of said goal. Fill in the following JSON template: {{"goal":"", "tasks":[{"action":"", "what":""}, {"action":"", "what":""}, {"action":"", "what":""}]}}\n\n ' +WORLD_ITEMS: '[Story context: {story_context}]; For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, come up with 7 common items that can be found in the world. Item example: {{"name":"", "type":"", "short_descr":"", "level":int, "value":int}}, type is one of: {item_types}; Fill in this JSON template and do not write anything else: {{"items": []}}.' +WORLD_CREATURES: '[Story context: {story_context}]; For an RPG described as {story_type} set in a world described as {world_mood}, {world_info}, come up with 5 creatures of various level and sentiment that can be found in the world. Creature example: {{"name":"", "body":"", "mass":int(kg), "hp":int, "type":"Npc or Mob", "level":int, "unarmed_attack":One of [FISTS, CLAWS, BITE, TAIL, HOOVES, HORN, TUSKS, BEAK, TALON], "short_descr":""}}. Fill in this JSON template and do not write anything else: {{"creatures": []}}.' +GOAL_PROMPT: '[Characters:{characters}][Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Known locations: {locations}][Acting character: {character}] [Actions available:{actions}] [USER_START] For {character_name}, come up with a goal that plays along with their character description that involves an item, a character or a location in the prompt. Then construct up to three tasks that will lead towards the achievement of said goal. Fill in the following JSON template: {{"goal":"", "tasks":[{"action":"", "what":""}, {"action":"", "what":""}, {"action":"", "what":""}]}}' JSON_GRAMMAR: "root ::= object\nvalue ::= object | array | string | number | (\"true\" | \"false\" | \"null\") ws\n\nobject ::=\n \"{\" ws (\n string \":\" ws value\n (\",\" ws string \":\" ws value)*\n )? \"}\" ws\n\narray ::=\n \"[\" ws (\n value\n (\",\" ws value)*\n )? \"]\" ws\n\nstring ::=\n \"\\\"\" (\n [^\"\\\\] |\n \"\\\\\" ([\"\\\\/bfnrt] | \"u\" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) # escapes\n )* \"\\\"\" ws\n\nnumber ::= (\"-\"? ([0-9] | [1-9] [0-9]*)) (\".\" [0-9]+)? ([eE] [-+]? [0-9]+)? ws\n\n# Optional space: by convention, applied in this grammar after literal chars when allowed\nws ::= ([ \\t\\n] ws)?" -PLAYER_ENTER_PROMPT: '[Story context: {story_context}]; World info: {world_info}; Zone info: {zone_info}; ; Npc example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words", "memories":"25 words"}}. [USER_START] The player has just re-entered this location: {location_info}. Consider whether any items, npcs or mobs should be spawned. For mobs, only enter the name of race. Fill in this JSON template and do not write anything else: {{"items":[], "npcs":[] "mobs":[]}}.\n\n ' -USER_START: '### Instruction:' -USER_END: '### Response:\n' \ No newline at end of file +PLAYER_ENTER_PROMPT: '[Story context: {story_context}]; World info: {world_info}; Zone info: {zone_info}; ; Npc example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words", "memories":"25 words"}}. [USER_START] The player has just re-entered this location: {location_info}. Consider whether any items, npcs or mobs should be spawned. For mobs, only enter the name of race. Fill in this JSON template and do not write anything else: {{"items":[], "npcs":[] "mobs":[]}}.' +USER_START: '\n### Instruction:\n' +USER_END: '\n### Response:\n' \ No newline at end of file diff --git a/tale/parse_utils.py b/tale/parse_utils.py index e58f9f81..b80c816e 100644 --- a/tale/parse_utils.py +++ b/tale/parse_utils.py @@ -181,6 +181,10 @@ 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', '') + config.type = json_file.get('story_type', '') + config.world_info = json_file.get('world_info', '') + config.world_mood = json_file.get('world_mood', '') return config From 6832bacd06ed8ae40cd9ffcfadd372ac915c0c29 Mon Sep 17 00:00:00 2001 From: rickard Date: Sun, 8 Oct 2023 09:05:59 +0200 Subject: [PATCH 6/6] adding tea party story cleaning up and fixing tests --- llm_config.yaml | 6 +-- stories/prancingllama/story.py | 2 - stories/teaparty/__init__.py | 7 +++ stories/teaparty/npcs/guests.json | 35 ++++++++++++++ stories/teaparty/story.py | 30 ++++++++++++ stories/teaparty/story_config.json | 35 ++++++++++++++ stories/teaparty/zones/mad_hatters_house.json | 13 +++++ tale/llm/character.py | 12 ++--- tale/llm/llm_ext.py | 48 +++++++++++-------- tale/llm/llm_utils.py | 2 +- tale/llm/requests/build_location.py | 4 +- tests/test_llm_utils.py | 2 + 12 files changed, 161 insertions(+), 35 deletions(-) create mode 100644 stories/teaparty/__init__.py create mode 100644 stories/teaparty/npcs/guests.json create mode 100644 stories/teaparty/story.py create mode 100644 stories/teaparty/story_config.json create mode 100644 stories/teaparty/zones/mad_hatters_house.json diff --git a/llm_config.yaml b/llm_config.yaml index fd4aede7..0c5debb6 100644 --- a/llm_config.yaml +++ b/llm_config.yaml @@ -10,7 +10,7 @@ GENERATION_BODY: '{"stop_sequence": "\n\n", "max_length":750, "max_context_lengt ANALYSIS_BODY: '{"banned_tokens":"\n\n", "stop_sequence": "\n\n\n\n", "max_length":500, "max_context_length":4096, "temperature":0.15, "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, "mirostat":2, "mirostat_tau":6.0, "mirostat_eta":0.1, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}' MEMORY_SIZE: 512 PRE_PROMPT: 'Below is an instruction that describes a task, paired with an input that provides further context. As an game keeper for an RPG, write a response that appropriately completes the request.' -BASE_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the following Text in your own words using the supplied Context and History to create a background for your text. Use about {max_words} words. [Text:{input_text}] \n\nEnd of text.\n\n [USER_START] Rewrite [Text] in your own words. " +BASE_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the following Text in your own words using the supplied Context and History to create a background for your text. Use about {max_words} words. [USER_START] Rewrite [{input_text}] in your own words." ACTION_PROMPT: "[Story context: {story_context}]; [History: {history}]; [USER_START] Rewrite the Action, and nothing else, in your own words using the supplied Context and Location. History is what happened before. Use less than {max_words} words. [Text: {input_text}] \n\nEnd of text." DIALOGUE_PROMPT: '[Story context: {story_context}]; [Location: {location}]; The following is a conversation between {character1} and {character2}; {character1}:{character1_description}; character2:{character2_description}; [Chat history: {previous_conversation}]\n\n [{character2}s sentiment towards {character1}: {sentiment}].[USER_START] Write a single response for {character2} in third person pov, using {character2} description.' ITEM_PROMPT: 'Items:[{items}];Characters:[{character1},{character2}] \n\n [USER_START] Decide if an item was explicitly given, taken, dropped or put somewhere in the following text:[Text:{text}]. Insert your thoughts about whether an item was explicitly given, taken, put somewhere or dropped in "my thoughts", and the results in "item", "from" and "to", or make them empty if . Insert {character1}s sentiment towards {character2} in a single word in "sentiment assessment". Fill in the following JSON template: {{ "thoughts":"", "result": {{ "item":"", "from":"", "to":""}}, {{"sentiment":"sentiment assessment"}} }} End of example. \n\n Write your response in valid JSON' @@ -35,5 +35,5 @@ WORLD_CREATURES: '[Story context: {story_context}]; For an RPG described as {sto GOAL_PROMPT: '[Characters:{characters}][Sentiments towards characters: {sentiments}] [Last action: {last_action}] [Location: {location}] [Known locations: {locations}][Acting character: {character}] [Actions available:{actions}] [USER_START] For {character_name}, come up with a goal that plays along with their character description that involves an item, a character or a location in the prompt. Then construct up to three tasks that will lead towards the achievement of said goal. Fill in the following JSON template: {{"goal":"", "tasks":[{"action":"", "what":""}, {"action":"", "what":""}, {"action":"", "what":""}]}}' JSON_GRAMMAR: "root ::= object\nvalue ::= object | array | string | number | (\"true\" | \"false\" | \"null\") ws\n\nobject ::=\n \"{\" ws (\n string \":\" ws value\n (\",\" ws string \":\" ws value)*\n )? \"}\" ws\n\narray ::=\n \"[\" ws (\n value\n (\",\" ws value)*\n )? \"]\" ws\n\nstring ::=\n \"\\\"\" (\n [^\"\\\\] |\n \"\\\\\" ([\"\\\\/bfnrt] | \"u\" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) # escapes\n )* \"\\\"\" ws\n\nnumber ::= (\"-\"? ([0-9] | [1-9] [0-9]*)) (\".\" [0-9]+)? ([eE] [-+]? [0-9]+)? ws\n\n# Optional space: by convention, applied in this grammar after literal chars when allowed\nws ::= ([ \\t\\n] ws)?" PLAYER_ENTER_PROMPT: '[Story context: {story_context}]; World info: {world_info}; Zone info: {zone_info}; ; Npc example: {{"name":"", "sentiment":"", "race":"", "gender":"m, f, or n", "level":(int), "description":"25 words", "memories":"25 words"}}. [USER_START] The player has just re-entered this location: {location_info}. Consider whether any items, npcs or mobs should be spawned. For mobs, only enter the name of race. Fill in this JSON template and do not write anything else: {{"items":[], "npcs":[] "mobs":[]}}.' -USER_START: '\n### Instruction:\n' -USER_END: '\n### Response:\n' \ No newline at end of file +USER_START: '### Instruction:\n' +USER_END: '### Response:\n' \ No newline at end of file diff --git a/stories/prancingllama/story.py b/stories/prancingllama/story.py index 6fb6da4c..43897709 100644 --- a/stories/prancingllama/story.py +++ b/stories/prancingllama/story.py @@ -61,8 +61,6 @@ def create_account_dialog(self, playerconnection: PlayerConnection, playernaming Ask questions using the yield "input", "question?" mechanism. Return True to declare all is well, and False to abort the player creation process. """ - age = yield "input", "Custom creation question: What is your age?" - playernaming.story_data["age"] = int(age) # will be stored in the database (mud) occupation = yield "input", "Custom creation question: What is your trade?" playernaming.story_data["occupation"] = str(occupation) # will be stored in the database (mud) return True diff --git a/stories/teaparty/__init__.py b/stories/teaparty/__init__.py new file mode 100644 index 00000000..733f0370 --- /dev/null +++ b/stories/teaparty/__init__.py @@ -0,0 +1,7 @@ +""" +Package containing a demo room and some stuff, to easily check out +a running Tale framework (python -m tale.demo.story) + +'Tale' mud driver, mudlib and interactive fiction framework +Copyright by Irmen de Jong (irmen@razorvine.net) +""" diff --git a/stories/teaparty/npcs/guests.json b/stories/teaparty/npcs/guests.json new file mode 100644 index 00000000..2a8b1235 --- /dev/null +++ b/stories/teaparty/npcs/guests.json @@ -0,0 +1,35 @@ +[ + { + "name":"Mad Hatter", + "gender":"m", + "race":"human", + "type":"LivingExt", + "title":"Mad Hatter", + "personality":"The Mad Hatter from Lewis Carrol's Alice in Wonderland. He is a bit crazy and has a strange sense of humor.", + "descr":"Wearing a big hat and a colorful costume.", + "short_descr":"A man with a big hat and colorful costume.", + "location":"Mad Hatters House.Living Room" + }, + { + "name":"Duchess", + "gender":"f", + "race":"human", + "type":"LivingExt", + "title":"Duchess", + "personality":"The Duchess from Lewis Carrol's Alice in Wonderland. Volatile and aggressive personality.", + "descr":"Very big head. Tall enough to rest her chin upon Alice’s shoulder, uncomfortably sharp chin.", + "short_descr": "Short woman with a very large head.", + "location":"Mad Hatters House.Living Room" + }, + { + "name":"Ace of Spades", + "gender":"m", + "race":"human", + "type":"LivingExt", + "title":"Ace of Spades", + "personality":"A drunken ex-rock star. Like a talkative Ozzy Osbourne if he were in Alice in Wonderland.", + "descr":"Thin man with eyeliner and a vacant expression. Wearing a black leather jacket and a black top hat.", + "short_descr": "A man with leather jacket and a black top hat.", + "location":"Mad Hatters House.Living Room" + } +] diff --git a/stories/teaparty/story.py b/stories/teaparty/story.py new file mode 100644 index 00000000..d27388e1 --- /dev/null +++ b/stories/teaparty/story.py @@ -0,0 +1,30 @@ + +import pathlib +import sys +from tale import parse_utils +from tale.driver import Driver +from tale.driver_if import IFDriver +from tale.json_story import JsonStory +from tale.main import run_from_cmdline +from tale.zone import Zone + + +class Story(JsonStory): + + driver = None + + def __init__(self) -> None: + super(Story, self).__init__('', parse_utils.load_story_config(parse_utils.load_json('story_config.json'))) + + def init(self, driver: Driver) -> None: + super(Story, self).init(driver) + + +if __name__ == "__main__": + # story is invoked as a script, start it in the Tale Driver. + 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) \ No newline at end of file diff --git a/stories/teaparty/story_config.json b/stories/teaparty/story_config.json new file mode 100644 index 00000000..33a028e3 --- /dev/null +++ b/stories/teaparty/story_config.json @@ -0,0 +1,35 @@ +{ + "name":"The Tea Party", + "type":"StoryConfig", + "author":"test author", + "author_address":"" , + "version":"1.00" , + "requires_tale":"4.0" , + "supported_modes":["IF"] , + "player_name":"alice" , + "player_gender":"f" , + "player_race":"human" , + "player_money":0.0 , + "playable_races":[] , + "money_type":"NOTHING" , + "server_tick_method":"TIMER" , + "server_tick_time":5.0 , + "gametime_to_realtime":1 , + "max_wait_hours":2 , + "display_gametime":false , + "display_race":false , + "epoch":null , + "startlocation_player":"Mad Hatters House.Living Room" , + "startlocation_wizard":"Mad Hatters House.Living Room" , + "savegames_enabled":true , + "show_exits_in_look":true , + "license_file":"", + "mud_host":"", + "mud_port":0, + "zones":["mad_hatters_house"], + "npcs":"guests", + "items":"", + "server_mode":"IF", + "story_type":"A whimsical and humoristic tale of tea and madness. Guests are so busy with their own problems that it's difficult to make yourself heard.", + "context":"The player is having tea in the Mad Hatter's house, as Alice. The guests are all mad in their own way, and making sense of anything is difficult." +} diff --git a/stories/teaparty/zones/mad_hatters_house.json b/stories/teaparty/zones/mad_hatters_house.json new file mode 100644 index 00000000..c59aced5 --- /dev/null +++ b/stories/teaparty/zones/mad_hatters_house.json @@ -0,0 +1,13 @@ +{ + "name":"Mad Hatters House", + "description": "The Mad Hatter's house. It's a bit of a mess.", + "locations":[ + { + "name": "Living Room", + "descr": "The Mad Hatter's cluttered living room. It's set up for a tea party.", + "exits": [] + } + ], + "races": [], + "items": [] +} diff --git a/tale/llm/character.py b/tale/llm/character.py index cae866a6..bed4b163 100644 --- a/tale/llm/character.py +++ b/tale/llm/character.py @@ -66,9 +66,9 @@ def generate_dialogue(self, conversation: str, # player_io = mud_context.pla # text = self.io_util.stream_request(self.url + self.stream_endpoint, self.url + self.data_endpoint, request_body, player_io, self.connection) - item_handling_result, new_sentiment, summary = self.dialogue_analysis(text, character_card, character_name, target) + item_handling_result, new_sentiment = self.dialogue_analysis(text, character_card, character_name, target) - return text, item_handling_result, new_sentiment, summary + return text, item_handling_result, new_sentiment def dialogue_analysis(self, text: str, character_card: str, character_name: str, target: str): """Parse the response from LLM and determine if there are any items to be handled.""" @@ -86,15 +86,15 @@ def dialogue_analysis(self, text: str, character_card: str, character_name: str, json_result = json.loads(parse_utils.sanitize_json(text)) except JSONDecodeError as exc: print(exc) - return None, None, None + return None, None valid, item_result = self.validate_item_response(json_result, character_name, target, items) sentiment = self.validate_sentiment(json_result) - summary = json_result.get('summary', '') + # summary = json_result.get('summary', '') - return item_result, sentiment, summary + return item_result, sentiment def validate_sentiment(self, json: dict): try: @@ -170,7 +170,7 @@ def perform_idle_action(self, character_name: str, location: Location, story_con request_body['seed'] = random.randint(0, 2147483647) request_body['banned_tokens'] = ['You'] - text = self.io_util.asynchronous_request(request_body, prompt=prompt) + text = self.io_util.synchronous_request(request_body, prompt=prompt) return text.split(';') def perform_travel_action(self, character_name: str, location: Location, locations: list, directions: list, character_card: str = ''): diff --git a/tale/llm/llm_ext.py b/tale/llm/llm_ext.py index 4560a428..8cbd7ddd 100644 --- a/tale/llm/llm_ext.py +++ b/tale/llm/llm_ext.py @@ -35,9 +35,7 @@ def notify_action(self, parsed: ParseResult, actor: Living) -> None: targeted = True if self.name in parsed.unparsed or self in parsed.who_info or self.title in parsed.unparsed: targeted = True - if parsed.verb in ("hi", "hello"): - greet = True - elif parsed.verb == "greet": + if parsed.verb in ("hi", "hello") or parsed.verb == "greet": greet = True if greet and targeted: self.tell_others("{Actor} says: \"Hi.\"", evoke=True) @@ -45,24 +43,13 @@ def notify_action(self, parsed: ParseResult, actor: Living) -> None: elif parsed.verb == "say" and targeted: self.do_say(parsed.unparsed, actor) elif targeted and parsed.verb == "idle-action": - action = mud_context.driver.llm_util.perform_reaction(action=parsed.unparsed, - character_card=self.character_card, - character_name=self.title, - location=self.location, - acting_character_name=actor.title, - sentiment=self.sentiments.get(actor.name, '')) - if action: - self.action_history.append(action) - result = ParseResult(verb='idle-action', unparsed=action, who_info=None) - self.tell_others(action) - self.location.notify_action(result, actor=self) - self.location._notify_action_all(result, actor=self) + self._do_react(parsed, actor) def do_say(self, what_happened: str, actor: Living) -> None: self.update_conversation(f'{actor.title}:{what_happened}\n') short_len = False if isinstance(actor, Player) else True - response, item_result, sentiment, summary = mud_context.driver.llm_util.generate_dialogue( + response, item_result, sentiment = mud_context.driver.llm_util.generate_dialogue( conversation=self.conversation, character_card = self.character_card, character_name = self.title, @@ -72,10 +59,10 @@ def do_say(self, what_happened: str, actor: Living) -> None: location_description=self.location.look(exclude_living=self), short_len=short_len) - if summary: - self.update_conversation(f"{self.title} says: \"{summary}\"") - else: - self.update_conversation(f"{self.title} says: \"{response}\"") + # if summary: + # self.update_conversation(f"{self.title} says: \"{summary}\"") + # else: + self.update_conversation(f"{self.title} says: \"{response}\"") if len(self.conversation) > self.memory_size: self.conversation = self.conversation[self.memory_size+1:] @@ -85,6 +72,19 @@ def do_say(self, what_happened: str, actor: Living) -> None: if sentiment: self.sentiments[actor.title] = sentiment + + def _do_react(self, parsed: ParseResult, actor: Living) -> None: + action = mud_context.driver.llm_util.perform_reaction(action=parsed.unparsed, + character_card=self.character_card, + character_name=self.title, + location=self.location, + acting_character_name=actor.title, + sentiment=self.sentiments.get(actor.name, '')) + if action: + self.action_history.append(action) + result = ParseResult(verb='idle-action', unparsed=action, who_info=None) + self.tell_others(action) + self.location._notify_action_all(result, actor=self) def handle_item_result(self, result: str, actor: Living): @@ -120,10 +120,15 @@ def idle_action(self): Currently handles planning several actions in advance, and then performing them in reverse order. """ if not self.planned_actions: + if self.action_history: + history_length = len(self.action_history) + previous_actions = self.action_history[-5:] if history_length > 4 else self.action_history[-history_length:] + else: + previous_actions = [] actions = mud_context.driver.llm_util.perform_idle_action(character_card=self.character_card, character_name=self.title, location=self.location, - last_action=self.action_history[-1] if self.action_history else None, + last_action=previous_actions, sentiments=self.sentiments) if actions: actions.reverse() @@ -160,6 +165,7 @@ def character_card(self) -> str: description=self.description, occupation=self.occupation, items=','.join(items)) + class DynamicStory(StoryBase): diff --git a/tale/llm/llm_utils.py b/tale/llm/llm_utils.py index 85d20385..d2784503 100644 --- a/tale/llm/llm_utils.py +++ b/tale/llm/llm_utils.py @@ -49,7 +49,7 @@ def evoke(self, player_io: TextBuffer, message: str, short_len : bool=False, rol """Evoke a response from LLM. Async if stream is True, otherwise synchronous. Update the rolling prompt with the latest message. Will put generated text in _look_hashes, and reuse it if same hash is passed in.""" - output_template = 'Original:[ {message} ] Generated:{text}' + output_template = 'Original:[ {message} ] Generated:{text}' if not message or str(message) == "\n": str(message), rolling_prompt diff --git a/tale/llm/requests/build_location.py b/tale/llm/requests/build_location.py index 7bd41101..134ad8aa 100644 --- a/tale/llm/requests/build_location.py +++ b/tale/llm/requests/build_location.py @@ -29,10 +29,10 @@ def build_prompt(self, args: dict) -> str: if spawn: mood = zone_info.get('mood', 0) if isinstance(mood, str): - num_mood = parse_utils.mood_int_from_string(mood) + num_mood = parse_utils.mood_string_to_int(mood) else: num_mood = mood - num_mood = (int) (random.gauss(zone_info.get('mood', 0), 2)) + num_mood = (int) (random.gauss(num_mood, 2)) level = (int) (random.gauss(zone_info.get('level', 1), 2)) mood_string = parse_utils.mood_string_from_int(num_mood) spawn_prompt = self.spawn_prompt.format(alignment=mood_string, level=level) diff --git a/tests/test_llm_utils.py b/tests/test_llm_utils.py index d1ec3085..c23e8d3a 100644 --- a/tests/test_llm_utils.py +++ b/tests/test_llm_utils.py @@ -169,6 +169,8 @@ class TestWorldBuilding(): generated_zone = '{"name":"Test Zone", "description":"A test zone", "level":10, "mood":-2, "races":["human", "elf", "dwarf"], "items":["sword", "shield"]}' story = JsonStory('tests/files/test_story/', parse_utils.load_story_config(parse_utils.load_json('tests/files/test_story_config_empty.json'))) + story.config.world_mood = 0 + story.config.world_info = "A test world" story.init(driver) def test_validate_location(self):