From 8a5058dbec3698d2a220bb16c7e511aa15b03701 Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Sun, 19 May 2024 15:27:56 -0500 Subject: [PATCH] start using promptgen --- adventure/actions/base.py | 54 +++---- adventure/models/event.py | 2 + adventure/render/comfy.py | 36 +---- adventure/render/prompt.py | 180 ++++++++++++++++++++++ adventure/systems/rpg/crafting_actions.py | 20 ++- adventure/systems/rpg/language_actions.py | 18 +-- adventure/utils/search.py | 1 + 7 files changed, 230 insertions(+), 81 deletions(-) create mode 100644 adventure/render/prompt.py diff --git a/adventure/actions/base.py b/adventure/actions/base.py index 3e18141..7ee2e22 100644 --- a/adventure/actions/base.py +++ b/adventure/actions/base.py @@ -91,22 +91,22 @@ def action_move(direction: str) -> str: return f"You move {direction} and arrive at {destination_room.name}." -def action_take(item_name: str) -> str: +def action_take(item: str) -> str: """ Take an item from the room and put it in your inventory. Args: - item_name: The name of the item to take. + item: The name of the item to take. """ with action_context() as (action_room, action_actor): - item = find_item_in_room(action_room, item_name) - if not item: - return "The {item_name} item is not in the room." + action_item = find_item_in_room(action_room, item) + if not action_item: + return f"The {item} item is not in the room." - broadcast(f"{action_actor.name} takes the {item_name} item") - action_room.items.remove(item) - action_actor.items.append(item) - return "You take the {item_name} item and put it in your inventory." + broadcast(f"{action_actor.name} takes the {item} item") + action_room.items.remove(action_item) + action_actor.items.append(action_item) + return f"You take the {item} item and put it in your inventory." def action_ask(character: str, question: str) -> str: @@ -184,45 +184,45 @@ def action_tell(character: str, message: str) -> str: return f"{character} does not respond." -def action_give(character: str, item_name: str) -> str: +def action_give(character: str, item: str) -> str: """ Give an item to another character in the room. Args: character: The name of the character to give the item to. - item_name: The name of the item to give. + item: The name of the item to give. """ with action_context() as (action_room, action_actor): destination_actor = find_actor_in_room(action_room, character) if not destination_actor: return f"The {character} character is not in the room." - item = find_item_in_actor(action_actor, item_name) - if not item: - return f"You do not have the {item_name} item in your inventory." + action_item = find_item_in_actor(action_actor, item) + if not action_item: + return f"You do not have the {item} item in your inventory." - broadcast(f"{action_actor.name} gives {character} the {item_name} item.") - action_actor.items.remove(item) - destination_actor.items.append(item) + broadcast(f"{action_actor.name} gives {character} the {item} item.") + action_actor.items.remove(action_item) + destination_actor.items.append(action_item) - return f"You give the {item_name} item to {character}." + return f"You give the {item} item to {character}." -def action_drop(item_name: str) -> str: +def action_drop(item: str) -> str: """ Drop an item from your inventory into the room. Args: - item_name: The name of the item to drop. + item: The name of the item to drop. """ with action_context() as (action_room, action_actor): - item = find_item_in_actor(action_actor, item_name) - if not item: - return f"You do not have the {item_name} item in your inventory." + action_item = find_item_in_actor(action_actor, item) + if not action_item: + return f"You do not have the {item} item in your inventory." - broadcast(f"{action_actor.name} drops the {item_name} item") - action_actor.items.remove(item) - action_room.items.append(item) + broadcast(f"{action_actor.name} drops the {item} item") + action_actor.items.remove(action_item) + action_room.items.append(action_item) - return f"You drop the {item_name} item." + return f"You drop the {item} item." diff --git a/adventure/models/event.py b/adventure/models/event.py index cbf8d3f..c59d063 100644 --- a/adventure/models/event.py +++ b/adventure/models/event.py @@ -73,6 +73,8 @@ class ReplyEvent(BaseModel): An actor has replied with text. This is the non-JSON version of an ActionEvent. + + TODO: add the actor being replied to. """ text: str diff --git a/adventure/render/comfy.py b/adventure/render/comfy.py index a8aa95a..26821bf 100644 --- a/adventure/render/comfy.py +++ b/adventure/render/comfy.py @@ -28,7 +28,8 @@ from adventure.models.event import ( ResultEvent, StatusEvent, ) -from adventure.utils.world import describe_entity + +from .prompt import prompt_from_entity, prompt_from_event logger = getLogger(__name__) @@ -211,39 +212,6 @@ def generate_images( return paths -def prompt_from_event(event: GameEvent) -> str | None: - if isinstance(event, ActionEvent): - if event.item: - return ( - f"{event.actor.name} uses the {event.item.name}. {describe_entity(event.item)}. " - f"{describe_entity(event.actor)}. {describe_entity(event.room)}." - ) - - action_name = event.action.removeprefix("action_") - return f"{event.actor.name} uses {action_name}. {describe_entity(event.actor)}. {describe_entity(event.room)}." - - if isinstance(event, ReplyEvent): - return event.text - - if isinstance(event, ResultEvent): - return f"{event.result}. {describe_entity(event.actor)}. {describe_entity(event.room)}." - - if isinstance(event, StatusEvent): - if event.room: - if event.actor: - return f"{event.text}. {describe_entity(event.actor)}. {describe_entity(event.room)}." - - return f"{event.text}. {describe_entity(event.room)}." - - return event.text - - return None - - -def prompt_from_entity(entity: WorldEntity) -> str: - return describe_entity(entity) - - def sanitize_name(name: str) -> str: def valid_char(c: str) -> str: if c.isalnum() or c in ["-", "_"]: diff --git a/adventure/render/prompt.py b/adventure/render/prompt.py new file mode 100644 index 0000000..7135f1c --- /dev/null +++ b/adventure/render/prompt.py @@ -0,0 +1,180 @@ +import re +from logging import getLogger +from random import shuffle +from typing import List + +from adventure.context import get_current_world, get_dungeon_master +from adventure.models.entity import Room, WorldEntity +from adventure.models.event import ( + ActionEvent, + GameEvent, + ReplyEvent, + ResultEvent, + StatusEvent, +) +from adventure.utils.search import find_actor_in_room, find_item_in_room, find_room +from adventure.utils.world import describe_entity + +logger = getLogger(__name__) + + +def prompt_from_parameters( + action_room: Room, parameters: dict[str, bool | float | int | str] +) -> tuple[str, str]: + pre = [] + post = [] + + if "character" in parameters: + # look up the character + character_name = str(parameters["character"]) + logger.debug("searching for parameter character: %s", character_name) + target_actor = find_actor_in_room(action_room, character_name) + if target_actor: + logger.debug("adding actor to prompt: %s", target_actor.name) + pre.append(f"with {target_actor.name}") + post.append(describe_entity(target_actor)) + + if "item" in parameters: + # look up the item + item_name = str(parameters["item"]) + logger.debug("searching for parameter item: %s", item_name) + target_item = find_item_in_room( + action_room, + item_name, + include_actor_inventory=True, + include_item_inventory=True, + ) + if target_item: + logger.debug("adding item to prompt: %s", target_item.name) + pre.append(f"using the {target_item.name}") + post.append(describe_entity(target_item)) + + if "target" in parameters: + # could be a room, actor, or item + target_name = str(parameters["target"]) + logger.debug("searching for parameter target: %s", target_name) + + world = get_current_world() + if world: + target_room = find_room(world, target_name) + if target_room: + logger.debug("adding room to prompt: %s", target_room.name) + pre.append(f"in the {target_room.name}") + post.append(describe_entity(target_room)) + + target_actor = find_actor_in_room(action_room, target_name) + if target_actor: + logger.debug("adding actor to prompt: %s", target_actor.name) + pre.append(f"with {target_actor.name}") + post.append(describe_entity(target_actor)) + + target_item = find_item_in_room( + action_room, + target_name, + include_actor_inventory=True, + include_item_inventory=True, + ) + if target_item: + logger.debug("adding item to prompt: %s", target_item.name) + pre.append(f"using the {target_item.name}") + post.append(describe_entity(target_item)) + + return (" and ".join(pre) if pre else "", " and ".join(post) if post else "") + + +def scene_from_event(event: GameEvent) -> str | None: + logger.debug("generating scene from event: %s", event) + + if isinstance(event, ActionEvent): + action_name = event.action.removeprefix("action_") + parameter_pre, parameter_post = prompt_from_parameters( + event.room, event.parameters + ) + + return ( + f"{event.actor.name} uses the {action_name} action {parameter_pre}. " + "{describe_entity(event.actor)}. {describe_entity(event.room)}. {parameter_post}." + ) + + if isinstance(event, ReplyEvent): + return f"{event.actor.name} replies: {event.text}. {describe_entity(event.actor)}. {describe_entity(event.room)}." + + if isinstance(event, ResultEvent): + return f"{event.result}. {describe_entity(event.actor)}. {describe_entity(event.room)}." + + if isinstance(event, StatusEvent): + if event.room: + if event.actor: + return f"{event.text}. {describe_entity(event.actor)}. {describe_entity(event.room)}." + + return f"{event.text}. {describe_entity(event.room)}." + + return event.text + + return None + + +def scene_from_entity(entity: WorldEntity) -> str: + logger.debug("generating scene from entity: %s", entity) + + return f"Describe the {entity.type} called {entity.name}. {describe_entity(entity)}" + + +def make_example_prompts(keywords: List[str], k=5, q=10) -> List[str]: + logger.debug("generating %s example prompts from keywords: %s", k, keywords) + + examples = [] + for _ in range(k): + example = list(keywords) + shuffle(example) + examples.append(", ".join(example[:q])) + + return examples + + +def generate_keywords_from_scene(scene: str) -> List[str]: + logger.debug("generating keywords from scene: %s", scene) + + # TODO: use a gpt2 model to generate keywords from scene + # hack for now: split on punctuation and whitespace + return re.split(r"\W+", scene) + + +def generate_prompt_from_scene(scene: str, example_prompts: List[str]) -> str: + logger.debug( + "generating prompt from scene and example prompts: %s, %s", + scene, + example_prompts, + ) + # generate prompt from scene and example prompts + dungeon_master = get_dungeon_master() + return dungeon_master( + "Generate a prompt for the following scene:\n" + "{scene}\n" + "Here are some example prompts:\n" + "{examples}\n" + "Reply with a comma-separated list of keywords that summarize the visual details of the scene." + "Make sure you describe the location, characters, and any items present. Be creative with the details." + "Do not include the question or any JSON. Only include the list of keywords on a single line.", + examples=example_prompts, + scene=scene, + ) + + +def prompt_from_event(event: GameEvent) -> str | None: + scene = scene_from_event(event) + if not scene: + return None + + keywords = generate_keywords_from_scene(scene) + example_prompts = make_example_prompts(keywords) + result = generate_prompt_from_scene(scene, example_prompts) + return result + + +def prompt_from_entity(entity: WorldEntity) -> str: + scene = scene_from_entity(entity) + keywords = generate_keywords_from_scene(scene) + example_prompts = make_example_prompts(keywords) + result = generate_prompt_from_scene(scene, example_prompts) + return result diff --git a/adventure/systems/rpg/crafting_actions.py b/adventure/systems/rpg/crafting_actions.py index dd3d154..b75f0f2 100644 --- a/adventure/systems/rpg/crafting_actions.py +++ b/adventure/systems/rpg/crafting_actions.py @@ -24,23 +24,23 @@ recipes = { } -def action_craft(item_name: str) -> str: +def action_craft(item: str) -> str: """ Craft an item using available recipes and inventory items. Args: - item_name: The name of the item to craft. + item: The name of the item to craft. """ with world_context() as (action_world, _, action_actor): - if item_name not in recipes: - return f"There is no recipe to craft a {item_name}." + if item not in recipes: + return f"There is no recipe to craft a {item}." - recipe = recipes[item_name] + recipe = recipes[item] # Check if the actor has the required skill level skill = randint(1, 20) if skill < recipe.difficulty: - return f"You need a crafting skill level of {recipe.difficulty} to craft {item_name}." + return f"You need a crafting skill level of {recipe.difficulty} to craft {item}." # Collect inventory items names inventory_items = {item.name for item in action_actor.items} @@ -50,9 +50,7 @@ def action_craft(item_name: str) -> str: item for item in recipe.ingredients if item not in inventory_items ] if missing_items: - return ( - f"You are missing {' and '.join(missing_items)} to craft {item_name}." - ) + return f"You are missing {' and '.join(missing_items)} to craft {item}." # Deduct the ingredients from inventory for ingredient in recipe.ingredients: @@ -76,5 +74,5 @@ def action_craft(item_name: str) -> str: action_actor.items.append(new_item) - broadcast(f"{action_actor.name} crafts a {item_name}.") - return f"You successfully craft a {item_name}." + broadcast(f"{action_actor.name} crafts a {item}.") + return f"You successfully craft a {item}." diff --git a/adventure/systems/rpg/language_actions.py b/adventure/systems/rpg/language_actions.py index 1146e0c..fd32a24 100644 --- a/adventure/systems/rpg/language_actions.py +++ b/adventure/systems/rpg/language_actions.py @@ -2,20 +2,20 @@ from adventure.context import action_context, broadcast from adventure.utils.search import find_item_in_actor -def action_read(item_name: str) -> str: +def action_read(item: str) -> str: """ Read an item like a book or a sign. Args: - item_name: The name of the item to read. + item: The name of the item to read. """ with action_context() as (_, action_actor): - item = find_item_in_actor(action_actor, item_name) - if not item: - return f"You do not have a {item_name} to read." + action_item = find_item_in_actor(action_actor, item) + if not action_item: + return f"You do not have a {item} to read." - if "text" in item.attributes: - broadcast(f"{action_actor.name} reads {item_name}") - return str(item.attributes["text"]) + if "text" in action_item.attributes: + broadcast(f"{action_actor.name} reads {item}") + return str(action_item.attributes["text"]) - return f"The {item_name} has nothing to read." + return f"The {item} has nothing to read." diff --git a/adventure/utils/search.py b/adventure/utils/search.py index d2c2912..adfce7f 100644 --- a/adventure/utils/search.py +++ b/adventure/utils/search.py @@ -39,6 +39,7 @@ def find_actor_in_room(room: Room, actor_name: str) -> Actor | None: return None +# TODO: allow item or str def find_item( world: World, item_name: str,