1
0
Fork 0

encourage agents to retry after action errors, allow constant numeric values in config, fix up some common JSON errors
Run Docker Build / build (push) Failing after 10s Details
Run Python Build / build (push) Failing after 17s Details

This commit is contained in:
Sean Sube 2024-05-26 17:03:39 -05:00
parent 200615ab2b
commit 949cd5687a
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
10 changed files with 129 additions and 101 deletions

View File

@ -7,6 +7,7 @@ from adventure.context import (
get_agent_for_actor, get_agent_for_actor,
world_context, world_context,
) )
from adventure.errors import ActionError
from adventure.utils.conversation import loop_conversation from adventure.utils.conversation import loop_conversation
from adventure.utils.search import ( from adventure.utils.search import (
find_actor_in_room, find_actor_in_room,
@ -79,11 +80,11 @@ def action_move(direction: str) -> str:
None, None,
) )
if not portal: if not portal:
return f"You cannot move {direction} from here." raise ActionError(f"You cannot move {direction} from here.")
destination_room = find_room(action_world, portal.destination) destination_room = find_room(action_world, portal.destination)
if not destination_room: if not destination_room:
return f"The {portal.destination} room does not exist." raise ActionError(f"The {portal.destination} room does not exist.")
broadcast( broadcast(
f"{action_actor.name} moves through {direction} to {destination_room.name}" f"{action_actor.name} moves through {direction} to {destination_room.name}"
@ -106,7 +107,7 @@ def action_take(item: str) -> str:
with action_context() as (action_room, action_actor): with action_context() as (action_room, action_actor):
action_item = find_item_in_room(action_room, item) action_item = find_item_in_room(action_room, item)
if not action_item: if not action_item:
return f"The {item} item is not in the room." raise ActionError(f"The {item} item is not in the room.")
broadcast(f"{action_actor.name} takes the {item} item") broadcast(f"{action_actor.name} takes the {item} item")
action_room.items.remove(action_item) action_room.items.remove(action_item)
@ -127,13 +128,15 @@ def action_ask(character: str, question: str) -> str:
# sanity checks # sanity checks
question_actor, question_agent = get_actor_agent_for_name(character) question_actor, question_agent = get_actor_agent_for_name(character)
if question_actor == action_actor: if question_actor == action_actor:
return "You cannot ask yourself a question. Stop talking to yourself. Try another action." raise ActionError(
"You cannot ask yourself a question. Stop talking to yourself. Try another action."
)
if not question_actor: if not question_actor:
return f"The {character} character is not in the room." raise ActionError(f"The {character} character is not in the room.")
if not question_agent: if not question_agent:
return f"The {character} character does not exist." raise ActionError(f"The {character} character does not exist.")
broadcast(f"{action_actor.name} asks {character}: {question}") broadcast(f"{action_actor.name} asks {character}: {question}")
first_prompt = ( first_prompt = (
@ -183,13 +186,15 @@ def action_tell(character: str, message: str) -> str:
# sanity checks # sanity checks
question_actor, question_agent = get_actor_agent_for_name(character) question_actor, question_agent = get_actor_agent_for_name(character)
if question_actor == action_actor: if question_actor == action_actor:
return "You cannot tell yourself a message. Stop talking to yourself. Try another action." raise ActionError(
"You cannot tell yourself a message. Stop talking to yourself. Try another action."
)
if not question_actor: if not question_actor:
return f"The {character} character is not in the room." raise ActionError(f"The {character} character is not in the room.")
if not question_agent: if not question_agent:
return f"The {character} character does not exist." raise ActionError(f"The {character} character does not exist.")
broadcast(f"{action_actor.name} tells {character}: {message}") broadcast(f"{action_actor.name} tells {character}: {message}")
first_prompt = ( first_prompt = (
@ -236,11 +241,16 @@ def action_give(character: str, item: str) -> str:
with action_context() as (action_room, action_actor): with action_context() as (action_room, action_actor):
destination_actor = find_actor_in_room(action_room, character) destination_actor = find_actor_in_room(action_room, character)
if not destination_actor: if not destination_actor:
return f"The {character} character is not in the room." raise ActionError(f"The {character} character is not in the room.")
if destination_actor == action_actor:
raise ActionError(
"You cannot give an item to yourself. Try another action."
)
action_item = find_item_in_actor(action_actor, item) action_item = find_item_in_actor(action_actor, item)
if not action_item: if not action_item:
return f"You do not have the {item} item in your inventory." raise ActionError(f"You do not have the {item} item in your inventory.")
broadcast(f"{action_actor.name} gives {character} the {item} item.") broadcast(f"{action_actor.name} gives {character} the {item} item.")
action_actor.items.remove(action_item) action_actor.items.remove(action_item)
@ -260,7 +270,7 @@ def action_drop(item: str) -> str:
with action_context() as (action_room, action_actor): with action_context() as (action_room, action_actor):
action_item = find_item_in_actor(action_actor, item) action_item = find_item_in_actor(action_actor, item)
if not action_item: if not action_item:
return f"You do not have the {item} item in your inventory." raise ActionError(f"You do not have the {item} item in your inventory.")
broadcast(f"{action_actor.name} drops the {item} item") broadcast(f"{action_actor.name} drops the {item} item")
action_actor.items.remove(action_item) action_actor.items.remove(action_item)

View File

@ -13,6 +13,7 @@ from adventure.context import (
set_dungeon_master, set_dungeon_master,
world_context, world_context,
) )
from adventure.errors import ActionError
from adventure.generate import generate_item, generate_room, link_rooms from adventure.generate import generate_item, generate_room, link_rooms
from adventure.utils.effect import apply_effects from adventure.utils.effect import apply_effects
from adventure.utils.search import find_actor_in_room from adventure.utils.search import find_actor_in_room
@ -47,7 +48,10 @@ def action_explore(direction: str) -> str:
if direction in action_room.portals: if direction in action_room.portals:
dest_room = action_room.portals[direction] dest_room = action_room.portals[direction]
return f"You cannot explore {direction} from here, that direction already leads to {dest_room}. Please use the move action to go there." raise ActionError(
f"You cannot explore {direction} from here, that direction already leads to {dest_room}. "
"Please use the move action to go there."
)
try: try:
systems = get_game_systems() systems = get_game_systems()
@ -118,7 +122,7 @@ def action_use(item: str, target: str) -> str:
None, None,
) )
if not action_item: if not action_item:
return f"The {item} item is not available to use." raise ActionError(f"The {item} item is not available to use.")
if target == "self": if target == "self":
target_actor = action_actor target_actor = action_actor
@ -148,13 +152,15 @@ def action_use(item: str, target: str) -> str:
) )
if not chosen_effect: if not chosen_effect:
# TODO: should retry the question if the effect is not found # TODO: should retry the question if the effect is not found
return f"The {chosen_name} effect is not available to apply." raise ValueError(f"The {chosen_name} effect is not available to apply.")
try: try:
apply_effects(target_actor, [chosen_effect]) apply_effects(target_actor, [chosen_effect])
except Exception: except Exception:
logger.exception("error applying effect: %s", chosen_effect) logger.exception("error applying effect: %s", chosen_effect)
return f"There was a problem applying the {chosen_name} effect." raise ValueError(
f"There was a problem applying the {chosen_name} effect while using the {item} item."
)
broadcast( broadcast(
f"{action_actor.name} uses the {chosen_name} effect of {item} on {target}" f"{action_actor.name} uses the {chosen_name} effect of {item} on {target}"

2
adventure/errors.py Normal file
View File

@ -0,0 +1,2 @@
class ActionError(Exception):
pass

View File

@ -1,5 +1,5 @@
from logging import getLogger from logging import getLogger
from random import choice, randint from random import choice
from typing import List, Tuple from typing import List, Tuple
from packit.agent import Agent from packit.agent import Agent
@ -19,6 +19,7 @@ from adventure.models.effect import (
from adventure.models.entity import Actor, Item, Portal, Room, World, WorldEntity from adventure.models.entity import Actor, Item, Portal, Room, World, WorldEntity
from adventure.models.event import GenerateEvent from adventure.models.event import GenerateEvent
from adventure.utils import try_parse_float, try_parse_int from adventure.utils import try_parse_float, try_parse_int
from adventure.utils.effect import resolve_int_range
from adventure.utils.search import ( from adventure.utils.search import (
list_actors, list_actors,
list_actors_in_room, list_actors_in_room,
@ -108,9 +109,7 @@ def generate_room(
actions = {} actions = {}
room = Room(name=name, description=desc, items=[], actors=[], actions=actions) room = Room(name=name, description=desc, items=[], actors=[], actions=actions)
item_count = randint( item_count = resolve_int_range(world_config.size.room_items) or 0
world_config.size.room_items.min, world_config.size.room_items.max
)
broadcast_generated(f"Generating {item_count} items for room: {name}") broadcast_generated(f"Generating {item_count} items for room: {name}")
for _ in range(item_count): for _ in range(item_count):
@ -127,9 +126,7 @@ def generate_room(
except Exception: except Exception:
logger.exception("error generating item") logger.exception("error generating item")
actor_count = randint( actor_count = resolve_int_range(world_config.size.room_actors) or 0
world_config.size.room_actors.min, world_config.size.room_actors.max
)
broadcast_generated(message=f"Generating {actor_count} actors for room: {name}") broadcast_generated(message=f"Generating {actor_count} actors for room: {name}")
for _ in range(actor_count): for _ in range(actor_count):
@ -265,9 +262,7 @@ def generate_item(
item = Item(name=name, description=desc, actions=actions) item = Item(name=name, description=desc, actions=actions)
generate_system_attributes(agent, world, item, systems) generate_system_attributes(agent, world, item, systems)
effect_count = randint( effect_count = resolve_int_range(world_config.size.item_effects) or 0
world_config.size.item_effects.min, world_config.size.item_effects.max
)
broadcast_generated(message=f"Generating {effect_count} effects for item: {name}") broadcast_generated(message=f"Generating {effect_count} effects for item: {name}")
for _ in range(effect_count): for _ in range(effect_count):
@ -326,9 +321,7 @@ def generate_actor(
generate_system_attributes(agent, world, actor, systems) generate_system_attributes(agent, world, actor, systems)
# generate the actor's inventory # generate the actor's inventory
item_count = randint( item_count = resolve_int_range(world_config.size.actor_items) or 0
world_config.size.actor_items.min, world_config.size.actor_items.max
)
broadcast_generated(f"Generating {item_count} items for actor {name}") broadcast_generated(f"Generating {item_count} items for actor {name}")
for k in range(item_count): for k in range(item_count):
@ -470,9 +463,7 @@ def link_rooms(
rooms = rooms or world.rooms rooms = rooms or world.rooms
for room in rooms: for room in rooms:
num_portals = randint( num_portals = resolve_int_range(world_config.size.portals) or 0
world_config.size.portals.min, world_config.size.portals.max
)
if len(room.portals) >= num_portals: if len(room.portals) >= num_portals:
logger.info(f"room {room.name} already has enough portals") logger.info(f"room {room.name} already has enough portals")
@ -517,9 +508,7 @@ def generate_world(
systems: List[GameSystem], systems: List[GameSystem],
room_count: int | None = None, room_count: int | None = None,
) -> World: ) -> World:
room_count = room_count or randint( room_count = room_count or resolve_int_range(world_config.size.rooms) or 0
world_config.size.rooms.min, world_config.size.rooms.max
)
broadcast_generated(message=f"Generating a {theme} with {room_count} rooms") broadcast_generated(message=f"Generating a {theme} with {room_count} rooms")
world = World(name=name, rooms=[], theme=theme, order=[]) world = World(name=name, rooms=[], theme=theme, order=[])

View File

@ -22,11 +22,11 @@ class BotConfig:
@dataclass @dataclass
class RenderConfig: class RenderConfig:
cfg: IntRange cfg: int | IntRange
checkpoints: List[str] checkpoints: List[str]
path: str path: str
sizes: Dict[str, Size] sizes: Dict[str, Size]
steps: IntRange steps: int | IntRange
@dataclass @dataclass
@ -47,12 +47,12 @@ class WorldActorConfig:
@dataclass @dataclass
class WorldSizeConfig: class WorldSizeConfig:
actor_items: IntRange actor_items: int | IntRange
item_effects: IntRange item_effects: int | IntRange
portals: IntRange portals: int | IntRange
room_actors: IntRange room_actors: int | IntRange
room_items: IntRange room_items: int | IntRange
rooms: IntRange rooms: int | IntRange
@dataclass @dataclass
@ -87,11 +87,11 @@ DEFAULT_CONFIG = Config(
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)), server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)),
world=WorldConfig( world=WorldConfig(
actor=WorldActorConfig( actor=WorldActorConfig(
conversation_limit=3, conversation_limit=2,
), ),
size=WorldSizeConfig( size=WorldSizeConfig(
actor_items=IntRange(min=0, max=2), actor_items=IntRange(min=0, max=2),
item_effects=IntRange(min=1, max=2), item_effects=IntRange(min=1, max=1),
portals=IntRange(min=1, max=3), portals=IntRange(min=1, max=3),
rooms=IntRange(min=3, max=6), rooms=IntRange(min=3, max=6),
room_actors=IntRange(min=1, max=3), room_actors=IntRange(min=1, max=3),

View File

@ -28,6 +28,7 @@ from adventure.models.event import (
ResultEvent, ResultEvent,
StatusEvent, StatusEvent,
) )
from adventure.utils.random import resolve_int_range
from .prompt import prompt_from_entity, prompt_from_event from .prompt import prompt_from_entity, prompt_from_event
@ -44,17 +45,11 @@ render_thread: Thread | None = None
def generate_cfg(): def generate_cfg():
if render_config.cfg.min == render_config.cfg.max: return resolve_int_range(render_config.cfg)
return render_config.cfg.min
return randint(render_config.cfg.min, render_config.cfg.max)
def generate_steps(): def generate_steps():
if render_config.steps.min == render_config.steps.max: return resolve_int_range(render_config.steps)
return render_config.steps.min
return randint(render_config.steps.min, render_config.steps.max)
def generate_batches( def generate_batches(

View File

@ -1,5 +1,6 @@
from functools import partial from functools import partial
from itertools import count from itertools import count
from json import loads
from logging import getLogger from logging import getLogger
from math import inf from math import inf
from typing import Callable, Sequence from typing import Callable, Sequence
@ -91,6 +92,22 @@ def prompt_actor_action(
if not room or not actor: if not room or not actor:
raise ValueError("Room and actor must be set before parsing results") raise ValueError("Room and actor must be set before parsing results")
# trim suffixes that are used elsewhere
value = value.removesuffix("END").strip()
# fix unbalanced curly braces
if value.startswith("{") and not value.endswith("}"):
open_count = value.count("{")
close_count = value.count("}")
if open_count > close_count:
fixed_value = value + ("}" * (open_count - close_count))
try:
loads(fixed_value)
value = fixed_value
except Exception:
pass
if could_be_json(value): if could_be_json(value):
event = ActionEvent.from_json(value, room, actor) event = ActionEvent.from_json(value, room, actor)
else: else:

View File

@ -11,6 +11,7 @@ from packit.utils import could_be_json
from adventure.context import broadcast from adventure.context import broadcast
from adventure.models.config import DEFAULT_CONFIG from adventure.models.config import DEFAULT_CONFIG
from adventure.models.entity import Actor, Room from adventure.models.entity import Actor, Room
from adventure.models.event import ReplyEvent
from .string import normalize_name from .string import normalize_name
@ -125,10 +126,12 @@ def loop_conversation(
if len(actors) != len(agents): if len(actors) != len(agents):
raise ValueError("The number of actors and agents must match.") raise ValueError("The number of actors and agents must match.")
# set up the keyword or length-limit compound condition
_, condition_end, parse_end = make_keyword_condition(end_message) _, condition_end, parse_end = make_keyword_condition(end_message)
stop_length = partial(condition_threshold, max=max_length) stop_length = partial(condition_threshold, max=max_length)
stop_condition = condition_or(condition_end, stop_length) stop_condition = condition_or(condition_end, stop_length)
# prepare a result parser looking for the echo function
def result_parser(value: str, **kwargs) -> str: def result_parser(value: str, **kwargs) -> str:
value = parse_end(value, **kwargs) value = parse_end(value, **kwargs)
@ -140,6 +143,7 @@ def loop_conversation(
return value.strip() return value.strip()
# prepare the loop state
i = 0 i = 0
last_actor = first_actor last_actor = first_actor
response = first_message response = first_message
@ -163,8 +167,9 @@ def loop_conversation(
) )
response = result_parser(response) response = result_parser(response)
logger.info(f"{actor.name} response: {response}") logger.info(f"{actor.name} responds: {response}")
broadcast(f"{actor.name} responds to {last_actor.name}: {response}") reply_event = ReplyEvent.from_text(response, room, actor)
broadcast(reply_event)
# increment the step counter # increment the step counter
i += 1 i += 1

View File

@ -1,8 +1,6 @@
import random
from logging import getLogger from logging import getLogger
from typing import List from typing import List
from adventure.models.base import FloatRange, IntRange
from adventure.models.effect import ( from adventure.models.effect import (
BooleanEffectPattern, BooleanEffectPattern,
BooleanEffectResult, BooleanEffectResult,
@ -23,6 +21,8 @@ from adventure.utils.attribute import (
prepend_value, prepend_value,
) )
from .random import resolve_float_range, resolve_int_range, resolve_string_list
logger = getLogger(__name__) logger = getLogger(__name__)
@ -124,50 +124,6 @@ def effective_attributes(
return attributes return attributes
def resolve_float_range(range: float | FloatRange | None) -> float | None:
"""
Resolve a float range to a single value.
"""
if range is None:
return None
if isinstance(
range, (float, int)
): # int is not really necessary here, but mypy complains without it
return range
return random.uniform(range.min, range.max)
def resolve_int_range(range: int | IntRange | None) -> int | None:
"""
Resolve an integer range to a single value.
"""
if range is None:
return None
if isinstance(range, int):
return range
return random.randint(range.min, range.max)
def resolve_string_list(result: str | List[str] | None) -> str | None:
"""
Resolve a string result to a single value.
"""
if result is None:
return None
if isinstance(result, str):
return result
return random.choice(result)
def resolve_boolean_effect(effect: BooleanEffectPattern) -> BooleanEffectResult: def resolve_boolean_effect(effect: BooleanEffectPattern) -> BooleanEffectResult:
""" """
Apply a boolean effect pattern to a set of attributes. Apply a boolean effect pattern to a set of attributes.

48
adventure/utils/random.py Normal file
View File

@ -0,0 +1,48 @@
import random
from typing import List
from adventure.models.base import FloatRange, IntRange
def resolve_float_range(range: float | FloatRange | None) -> float | None:
"""
Resolve a float range to a single value.
"""
if range is None:
return None
if isinstance(
range, (float, int)
): # int is not really necessary here, but mypy complains without it
return range
return random.uniform(range.min, range.max)
def resolve_int_range(range: int | IntRange | None) -> int | None:
"""
Resolve an integer range to a single value.
"""
if range is None:
return None
if isinstance(range, int):
return range
return random.randint(range.min, range.max)
def resolve_string_list(result: str | List[str] | None) -> str | None:
"""
Resolve a string result to a single value.
"""
if result is None:
return None
if isinstance(result, str):
return result
return random.choice(result)