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,
world_context,
)
from adventure.errors import ActionError
from adventure.utils.conversation import loop_conversation
from adventure.utils.search import (
find_actor_in_room,
@ -79,11 +80,11 @@ def action_move(direction: str) -> str:
None,
)
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)
if not destination_room:
return f"The {portal.destination} room does not exist."
raise ActionError(f"The {portal.destination} room does not exist.")
broadcast(
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):
action_item = find_item_in_room(action_room, 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")
action_room.items.remove(action_item)
@ -127,13 +128,15 @@ def action_ask(character: str, question: str) -> str:
# sanity checks
question_actor, question_agent = get_actor_agent_for_name(character)
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:
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:
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}")
first_prompt = (
@ -183,13 +186,15 @@ def action_tell(character: str, message: str) -> str:
# sanity checks
question_actor, question_agent = get_actor_agent_for_name(character)
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:
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:
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}")
first_prompt = (
@ -236,11 +241,16 @@ def action_give(character: str, item: str) -> str:
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."
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)
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.")
action_actor.items.remove(action_item)
@ -260,7 +270,7 @@ def action_drop(item: str) -> str:
with action_context() as (action_room, action_actor):
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."
raise ActionError(f"You do not have the {item} item in your inventory.")
broadcast(f"{action_actor.name} drops the {item} item")
action_actor.items.remove(action_item)

View File

@ -13,6 +13,7 @@ from adventure.context import (
set_dungeon_master,
world_context,
)
from adventure.errors import ActionError
from adventure.generate import generate_item, generate_room, link_rooms
from adventure.utils.effect import apply_effects
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:
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:
systems = get_game_systems()
@ -118,7 +122,7 @@ def action_use(item: str, target: str) -> str:
None,
)
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":
target_actor = action_actor
@ -148,13 +152,15 @@ def action_use(item: str, target: str) -> str:
)
if not chosen_effect:
# 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:
apply_effects(target_actor, [chosen_effect])
except Exception:
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(
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 random import choice, randint
from random import choice
from typing import List, Tuple
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.event import GenerateEvent
from adventure.utils import try_parse_float, try_parse_int
from adventure.utils.effect import resolve_int_range
from adventure.utils.search import (
list_actors,
list_actors_in_room,
@ -108,9 +109,7 @@ def generate_room(
actions = {}
room = Room(name=name, description=desc, items=[], actors=[], actions=actions)
item_count = randint(
world_config.size.room_items.min, world_config.size.room_items.max
)
item_count = resolve_int_range(world_config.size.room_items) or 0
broadcast_generated(f"Generating {item_count} items for room: {name}")
for _ in range(item_count):
@ -127,9 +126,7 @@ def generate_room(
except Exception:
logger.exception("error generating item")
actor_count = randint(
world_config.size.room_actors.min, world_config.size.room_actors.max
)
actor_count = resolve_int_range(world_config.size.room_actors) or 0
broadcast_generated(message=f"Generating {actor_count} actors for room: {name}")
for _ in range(actor_count):
@ -265,9 +262,7 @@ def generate_item(
item = Item(name=name, description=desc, actions=actions)
generate_system_attributes(agent, world, item, systems)
effect_count = randint(
world_config.size.item_effects.min, world_config.size.item_effects.max
)
effect_count = resolve_int_range(world_config.size.item_effects) or 0
broadcast_generated(message=f"Generating {effect_count} effects for item: {name}")
for _ in range(effect_count):
@ -326,9 +321,7 @@ def generate_actor(
generate_system_attributes(agent, world, actor, systems)
# generate the actor's inventory
item_count = randint(
world_config.size.actor_items.min, world_config.size.actor_items.max
)
item_count = resolve_int_range(world_config.size.actor_items) or 0
broadcast_generated(f"Generating {item_count} items for actor {name}")
for k in range(item_count):
@ -470,9 +463,7 @@ def link_rooms(
rooms = rooms or world.rooms
for room in rooms:
num_portals = randint(
world_config.size.portals.min, world_config.size.portals.max
)
num_portals = resolve_int_range(world_config.size.portals) or 0
if len(room.portals) >= num_portals:
logger.info(f"room {room.name} already has enough portals")
@ -517,9 +508,7 @@ def generate_world(
systems: List[GameSystem],
room_count: int | None = None,
) -> World:
room_count = room_count or randint(
world_config.size.rooms.min, world_config.size.rooms.max
)
room_count = room_count or resolve_int_range(world_config.size.rooms) or 0
broadcast_generated(message=f"Generating a {theme} with {room_count} rooms")
world = World(name=name, rooms=[], theme=theme, order=[])

View File

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

View File

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

View File

@ -1,5 +1,6 @@
from functools import partial
from itertools import count
from json import loads
from logging import getLogger
from math import inf
from typing import Callable, Sequence
@ -91,6 +92,22 @@ def prompt_actor_action(
if not room or not actor:
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):
event = ActionEvent.from_json(value, room, actor)
else:

View File

@ -11,6 +11,7 @@ from packit.utils import could_be_json
from adventure.context import broadcast
from adventure.models.config import DEFAULT_CONFIG
from adventure.models.entity import Actor, Room
from adventure.models.event import ReplyEvent
from .string import normalize_name
@ -125,10 +126,12 @@ def loop_conversation(
if len(actors) != len(agents):
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)
stop_length = partial(condition_threshold, max=max_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:
value = parse_end(value, **kwargs)
@ -140,6 +143,7 @@ def loop_conversation(
return value.strip()
# prepare the loop state
i = 0
last_actor = first_actor
response = first_message
@ -163,8 +167,9 @@ def loop_conversation(
)
response = result_parser(response)
logger.info(f"{actor.name} response: {response}")
broadcast(f"{actor.name} responds to {last_actor.name}: {response}")
logger.info(f"{actor.name} responds: {response}")
reply_event = ReplyEvent.from_text(response, room, actor)
broadcast(reply_event)
# increment the step counter
i += 1

View File

@ -1,8 +1,6 @@
import random
from logging import getLogger
from typing import List
from adventure.models.base import FloatRange, IntRange
from adventure.models.effect import (
BooleanEffectPattern,
BooleanEffectResult,
@ -23,6 +21,8 @@ from adventure.utils.attribute import (
prepend_value,
)
from .random import resolve_float_range, resolve_int_range, resolve_string_list
logger = getLogger(__name__)
@ -124,50 +124,6 @@ def effective_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:
"""
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)