encourage agents to retry after action errors, allow constant numeric values in config, fix up some common JSON errors
This commit is contained in:
parent
200615ab2b
commit
949cd5687a
|
@ -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)
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
class ActionError(Exception):
|
||||
pass
|
|
@ -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=[])
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue