fix conversation events, rename look to examine, generate indoor/outdoor attributes
This commit is contained in:
parent
66c2c20031
commit
80e98482e0
|
@ -119,6 +119,32 @@ export function SnapshotEventItem(props: EventItemProps) {
|
|||
}
|
||||
|
||||
export function ReplyEventItem(props: EventItemProps) {
|
||||
const { event } = props;
|
||||
const { audience, speaker, text } = event;
|
||||
|
||||
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
||||
<ListItemAvatar>
|
||||
<Avatar alt="System">
|
||||
<Settings />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="System"
|
||||
secondary={
|
||||
<Typography
|
||||
sx={{ display: 'block' }}
|
||||
component="span"
|
||||
variant="body2"
|
||||
color="text.primary"
|
||||
>
|
||||
{speaker.name} replies to {audience.name}: {text}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>;
|
||||
}
|
||||
|
||||
export function StatusEventItem(props: EventItemProps) {
|
||||
const { event } = props;
|
||||
const { text } = event;
|
||||
|
||||
|
@ -295,8 +321,9 @@ export function EventItem(props: EventItemProps) {
|
|||
case 'result':
|
||||
return <ActionEventItem {...props} />;
|
||||
case 'reply':
|
||||
case 'status': // TODO: should have a different component
|
||||
return <ReplyEventItem {...props} />;
|
||||
case 'status':
|
||||
return <StatusEventItem {...props} />;
|
||||
case 'player':
|
||||
return <PlayerEventItem {...props} />;
|
||||
case 'render':
|
||||
|
|
|
@ -13,6 +13,7 @@ from taleweave.utils.search import (
|
|||
find_character_in_room,
|
||||
find_item_in_character,
|
||||
find_item_in_room,
|
||||
find_portal_in_room,
|
||||
find_room,
|
||||
)
|
||||
from taleweave.utils.string import normalize_name
|
||||
|
@ -20,12 +21,12 @@ from taleweave.utils.world import describe_entity
|
|||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
MAX_CONVERSATION_STEPS = 3
|
||||
MAX_CONVERSATION_STEPS = 2
|
||||
|
||||
|
||||
def action_look(target: str) -> str:
|
||||
def action_examine(target: str) -> str:
|
||||
"""
|
||||
Look at a target in the room or your inventory.
|
||||
Examine the room, a character, or an item (in the room or in your inventory).
|
||||
|
||||
Args:
|
||||
target: The name of the target to look at.
|
||||
|
@ -71,14 +72,7 @@ def action_move(direction: str) -> str:
|
|||
"""
|
||||
|
||||
with world_context() as (action_world, action_room, action_character):
|
||||
portal = next(
|
||||
(
|
||||
p
|
||||
for p in action_room.portals
|
||||
if normalize_name(p.name) == normalize_name(direction)
|
||||
),
|
||||
None,
|
||||
)
|
||||
portal = find_portal_in_room(action_room, direction)
|
||||
if not portal:
|
||||
raise ActionError(f"You cannot move {direction} from here.")
|
||||
|
||||
|
@ -151,7 +145,7 @@ def action_ask(character: str, question: str) -> str:
|
|||
)
|
||||
|
||||
action_agent = get_agent_for_character(action_character)
|
||||
answer = loop_conversation(
|
||||
result = loop_conversation(
|
||||
action_room,
|
||||
[question_character, action_character],
|
||||
[question_agent, action_agent],
|
||||
|
@ -165,9 +159,8 @@ def action_ask(character: str, question: str) -> str:
|
|||
max_length=MAX_CONVERSATION_STEPS,
|
||||
)
|
||||
|
||||
if answer:
|
||||
broadcast(f"{character} responds to {action_character.name}: {answer}")
|
||||
return f"{character} responds: {answer}"
|
||||
if result:
|
||||
return result
|
||||
|
||||
return f"{character} does not respond."
|
||||
|
||||
|
@ -209,7 +202,7 @@ def action_tell(character: str, message: str) -> str:
|
|||
)
|
||||
|
||||
action_agent = get_agent_for_character(action_character)
|
||||
answer = loop_conversation(
|
||||
result = loop_conversation(
|
||||
action_room,
|
||||
[question_character, action_character],
|
||||
[question_agent, action_agent],
|
||||
|
@ -223,9 +216,8 @@ def action_tell(character: str, message: str) -> str:
|
|||
max_length=MAX_CONVERSATION_STEPS,
|
||||
)
|
||||
|
||||
if answer:
|
||||
broadcast(f"{character} responds to {action_character.name}: {answer}")
|
||||
return f"{character} responds: {answer}"
|
||||
if result:
|
||||
return result
|
||||
|
||||
return f"{character} does not respond."
|
||||
|
||||
|
|
|
@ -18,7 +18,10 @@ def take_note(fact: str):
|
|||
|
||||
with action_context() as (_, action_character):
|
||||
if fact in action_character.planner.notes:
|
||||
raise ActionError("You already have a note about that fact.")
|
||||
raise ActionError(
|
||||
"You already have a note about that fact. You do not need to take duplicate notes. "
|
||||
"If you have too many notes, consider erasing, replacing, or summarizing them."
|
||||
)
|
||||
|
||||
if len(action_character.planner.notes) >= character_config.note_limit:
|
||||
raise ActionError(
|
||||
|
@ -103,7 +106,8 @@ def summarize_notes(limit: int) -> str:
|
|||
"If a newer note contradicts an older note, keep the newer note. "
|
||||
"Clean up your notes so you can focus on the most important facts. "
|
||||
"Respond with one note per line. You can have up to {limit} notes, "
|
||||
"so make sure you reply with less than {limit} lines. "
|
||||
"so make sure you reply with less than {limit} lines. Do not number the lines "
|
||||
"in your response. Do not include any JSON or other information. "
|
||||
"Your notes are:\n{notes}",
|
||||
limit=limit,
|
||||
notes=notes,
|
||||
|
|
|
@ -330,7 +330,7 @@ def embed_from_event(event: GameEvent) -> Embed | None:
|
|||
|
||||
|
||||
def embed_from_action(event: ActionEvent | ReplyEvent):
|
||||
action_embed = Embed(title=event.room.name, description=event.character.name)
|
||||
action_embed = Embed(title=event.room.name, description=event.speaker.name)
|
||||
|
||||
if isinstance(event, ActionEvent):
|
||||
action_name = event.action.replace("action_", "").title()
|
||||
|
|
|
@ -200,11 +200,14 @@ def load_or_initialize_system_data(args, systems: List[GameSystem], world: World
|
|||
logger.info(f"loading system data from {system_data_file}")
|
||||
data = system.data.load(system_data_file)
|
||||
set_system_data(system.name, data)
|
||||
continue
|
||||
else:
|
||||
logger.info(f"no system data found at {system_data_file}")
|
||||
if system.initialize:
|
||||
data = system.initialize(world)
|
||||
set_system_data(system.name, data)
|
||||
|
||||
if system.initialize:
|
||||
logger.info(f"initializing system data for {system.name}")
|
||||
data = system.initialize(world)
|
||||
set_system_data(system.name, data)
|
||||
|
||||
|
||||
def save_system_data(args, systems: List[GameSystem]):
|
||||
|
|
|
@ -71,22 +71,15 @@ class PromptEvent(BaseModel):
|
|||
class ReplyEvent(BaseModel):
|
||||
"""
|
||||
A character has replied with text.
|
||||
|
||||
This is the non-JSON version of an ActionEvent.
|
||||
|
||||
TODO: add the character being replied to.
|
||||
"""
|
||||
|
||||
text: str
|
||||
room: Room
|
||||
character: Character
|
||||
speaker: Character
|
||||
audience: Character | Room
|
||||
text: str
|
||||
id: str = Field(default_factory=uuid)
|
||||
type: Literal["reply"] = "reply"
|
||||
|
||||
@staticmethod
|
||||
def from_text(text: str, room: Room, character: Character) -> "ReplyEvent":
|
||||
return ReplyEvent(text=text, room=room, character=character)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResultEvent(BaseModel):
|
||||
|
|
|
@ -193,6 +193,7 @@ class RemotePlayer(BasePlayer):
|
|||
logger.exception("error getting reply from remote player")
|
||||
|
||||
if self.fallback_agent:
|
||||
logger.info("prompting fallback agent: {self.fallback_agent.name}")
|
||||
return self.fallback_agent(prompt, **kwargs)
|
||||
|
||||
return ""
|
||||
|
|
|
@ -230,7 +230,7 @@ def get_image_prefix(event: GameEvent | WorldEntity) -> str:
|
|||
|
||||
if isinstance(event, ReplyEvent):
|
||||
return sanitize_name(
|
||||
f"event-reply-{event.character.name}-{fast_hash(event.text)}"
|
||||
f"event-reply-{event.speaker.name}-{fast_hash(event.text)}"
|
||||
)
|
||||
|
||||
if isinstance(event, ResultEvent):
|
||||
|
|
|
@ -97,7 +97,7 @@ def scene_from_event(event: GameEvent) -> str | None:
|
|||
)
|
||||
|
||||
if isinstance(event, ReplyEvent):
|
||||
return f"{event.character.name} replies: {event.text}. {describe_entity(event.character)}. {describe_entity(event.room)}."
|
||||
return f"{event.speaker.name} replies: {event.text}. {describe_entity(event.speaker)}. {describe_entity(event.room)}."
|
||||
|
||||
if isinstance(event, ResultEvent):
|
||||
return f"{event.result}. {describe_entity(event.character)}. {describe_entity(event.room)}."
|
||||
|
|
|
@ -8,14 +8,14 @@ from typing import Callable, Sequence
|
|||
from packit.agent import Agent
|
||||
from packit.conditions import condition_or, condition_threshold
|
||||
from packit.loops import loop_retry
|
||||
from packit.results import multi_function_or_str_result
|
||||
from packit.results import function_result
|
||||
from packit.toolbox import Toolbox
|
||||
from packit.utils import could_be_json
|
||||
|
||||
from taleweave.actions.base import (
|
||||
action_ask,
|
||||
action_examine,
|
||||
action_give,
|
||||
action_look,
|
||||
action_move,
|
||||
action_take,
|
||||
action_tell,
|
||||
|
@ -45,11 +45,11 @@ from taleweave.context import (
|
|||
from taleweave.game_system import GameSystem
|
||||
from taleweave.models.config import DEFAULT_CONFIG
|
||||
from taleweave.models.entity import Character, Room, World
|
||||
from taleweave.models.event import ActionEvent, ReplyEvent, ResultEvent
|
||||
from taleweave.models.event import ActionEvent, ResultEvent
|
||||
from taleweave.utils.conversation import make_keyword_condition, summarize_room
|
||||
from taleweave.utils.effect import expire_effects
|
||||
from taleweave.utils.planning import expire_events, get_upcoming_events
|
||||
from taleweave.utils.search import find_room_with_character
|
||||
from taleweave.utils.search import find_containing_room
|
||||
from taleweave.utils.world import describe_entity, format_attributes
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
@ -76,7 +76,7 @@ def world_result_parser(value, agent, **kwargs):
|
|||
set_current_room(current_room)
|
||||
set_current_character(current_character)
|
||||
|
||||
return multi_function_or_str_result(value, agent=agent, **kwargs)
|
||||
return function_result(value, agent=agent, **kwargs)
|
||||
|
||||
|
||||
def prompt_character_action(
|
||||
|
@ -94,7 +94,7 @@ def prompt_character_action(
|
|||
character_items = [item.name for item in character.items]
|
||||
|
||||
# set up a result parser for the agent
|
||||
def result_parser(value, agent, **kwargs):
|
||||
def result_parser(value, **kwargs):
|
||||
if not room or not character:
|
||||
raise ValueError("Room and character must be set before parsing results")
|
||||
|
||||
|
@ -117,11 +117,12 @@ def prompt_character_action(
|
|||
if could_be_json(value):
|
||||
event = ActionEvent.from_json(value, room, character)
|
||||
else:
|
||||
event = ReplyEvent.from_text(value, room, character)
|
||||
# TODO: this should be removed and throw
|
||||
event = ResultEvent(value, room, character)
|
||||
|
||||
broadcast(event)
|
||||
|
||||
return world_result_parser(value, agent, **kwargs)
|
||||
return world_result_parser(value, **kwargs)
|
||||
|
||||
# prompt and act
|
||||
logger.info("starting turn for character: %s", character.name)
|
||||
|
@ -255,7 +256,7 @@ def simulate_world(
|
|||
[
|
||||
action_ask,
|
||||
action_give,
|
||||
action_look,
|
||||
action_examine,
|
||||
action_move,
|
||||
action_take,
|
||||
action_tell,
|
||||
|
@ -288,7 +289,7 @@ def simulate_world(
|
|||
logger.error(f"agent or character not found for name {character_name}")
|
||||
continue
|
||||
|
||||
room = find_room_with_character(world, character)
|
||||
room = find_containing_room(world, character)
|
||||
if not room:
|
||||
logger.error(f"character {character_name} is not in a room")
|
||||
continue
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
from functools import partial
|
||||
from typing import List
|
||||
from taleweave.context import get_dungeon_master
|
||||
from taleweave.models.base import dataclass
|
||||
from taleweave.models.entity import World
|
||||
from taleweave.systems.logic import load_logic
|
||||
from taleweave.game_system import GameSystem
|
||||
from packit.agent import Agent
|
||||
from taleweave.models.entity import Room, WorldEntity
|
||||
from taleweave.utils.string import or_list
|
||||
from packit.results import enum_result
|
||||
from packit.loops import loop_retry
|
||||
from logging import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
LOGIC_FILES = [
|
||||
"./taleweave/systems/weather/weather_logic.yaml",
|
||||
|
@ -39,10 +50,36 @@ def get_time_of_day(turn: int) -> TimeOfDay:
|
|||
def initialize_weather(world: World):
|
||||
time_of_day = get_time_of_day(0)
|
||||
for room in world.rooms:
|
||||
logger.info(f"initializing weather for {room.name}")
|
||||
room.attributes["time"] = time_of_day.name
|
||||
|
||||
if "environment" not in room.attributes:
|
||||
dungeon_master = get_dungeon_master()
|
||||
generate_room_weather(dungeon_master, world.theme, room)
|
||||
|
||||
# TODO: generate indoor/outdoor attributes
|
||||
|
||||
def generate_room_weather(agent: Agent, theme: str, entity: Room) -> None:
|
||||
environment_options = ["indoor", "outdoor"]
|
||||
environment_result = partial(enum_result, enum=environment_options)
|
||||
environment = loop_retry(
|
||||
agent,
|
||||
"Is this room indoors or outdoors?"
|
||||
"Reply with a single word: {environment_list}.\n\n"
|
||||
"{description}",
|
||||
context={
|
||||
"environment_list": or_list(environment_options),
|
||||
"description": entity.description,
|
||||
},
|
||||
result_parser=environment_result,
|
||||
)
|
||||
entity.attributes["environment"] = environment
|
||||
logger.info(f"generated environment for {entity.name}: {environment}")
|
||||
|
||||
|
||||
def generate_weather(agent: Agent, theme: str, entity: WorldEntity) -> None:
|
||||
if isinstance(entity, Room):
|
||||
if "environment" not in entity.attributes:
|
||||
generate_room_weather(agent, theme, entity)
|
||||
|
||||
|
||||
def simulate_weather(world: World, turn: int, data: None = None):
|
||||
|
@ -55,5 +92,10 @@ def init():
|
|||
logic_systems = [load_logic(filename) for filename in LOGIC_FILES]
|
||||
return [
|
||||
*logic_systems,
|
||||
GameSystem("weather", initialize=initialize_weather, simulate=simulate_weather),
|
||||
GameSystem(
|
||||
"weather",
|
||||
generate=generate_weather,
|
||||
initialize=initialize_weather,
|
||||
simulate=simulate_weather,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -3,7 +3,7 @@ rules:
|
|||
- group: weather
|
||||
match:
|
||||
type: room
|
||||
outdoor: true
|
||||
environment: outdoor
|
||||
weather: clear
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -12,7 +12,7 @@ rules:
|
|||
- group: weather
|
||||
match:
|
||||
type: room
|
||||
outdoor: true
|
||||
environment: outdoor
|
||||
weather: clouds
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -21,7 +21,7 @@ rules:
|
|||
- group: weather
|
||||
match:
|
||||
type: room
|
||||
outdoor: true
|
||||
environment: outdoor
|
||||
weather: rain
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -30,7 +30,7 @@ rules:
|
|||
- group: weather
|
||||
match:
|
||||
type: room
|
||||
outdoor: true
|
||||
environment: outdoor
|
||||
weather: clouds
|
||||
chance: 0.1
|
||||
set:
|
||||
|
@ -40,7 +40,7 @@ rules:
|
|||
- group: weather
|
||||
match:
|
||||
type: room
|
||||
outdoor: true
|
||||
environment: outdoor
|
||||
rule: |
|
||||
"weather" not in attributes
|
||||
set:
|
||||
|
|
|
@ -148,11 +148,11 @@ def loop_conversation(
|
|||
response = result_parser(response)
|
||||
|
||||
logger.info(f"{character.name} responds: {response}")
|
||||
reply_event = ReplyEvent.from_text(response, room, character)
|
||||
reply_event = ReplyEvent(room, character, last_character, response)
|
||||
broadcast(reply_event)
|
||||
|
||||
# increment the step counter
|
||||
i += 1
|
||||
last_character = character
|
||||
|
||||
return response
|
||||
return f"{last_character.name} ends the conversation for now"
|
||||
|
|
|
@ -23,9 +23,9 @@ def find_room(world: World, room_name: str) -> Room | None:
|
|||
|
||||
def find_portal(world: World, portal_name: str) -> Portal | None:
|
||||
for room in world.rooms:
|
||||
for portal in room.portals:
|
||||
if normalize_name(portal.name) == normalize_name(portal_name):
|
||||
return portal
|
||||
portal = find_portal_in_room(room, portal_name)
|
||||
if portal:
|
||||
return portal
|
||||
|
||||
return None
|
||||
|
||||
|
@ -47,6 +47,14 @@ def find_character_in_room(room: Room, character_name: str) -> Character | None:
|
|||
return None
|
||||
|
||||
|
||||
def find_portal_in_room(room: Room, portal_name: str) -> Portal | None:
|
||||
for portal in room.portals:
|
||||
if normalize_name(portal.name) == normalize_name(portal_name):
|
||||
return portal
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# TODO: allow item or str
|
||||
def find_item(
|
||||
world: World,
|
||||
|
@ -109,15 +117,6 @@ def find_item_in_room(
|
|||
return None
|
||||
|
||||
|
||||
def find_room_with_character(world: World, character: Character) -> Room | None:
|
||||
for room in world.rooms:
|
||||
for room_character in room.characters:
|
||||
if normalize_name(character.name) == normalize_name(room_character.name):
|
||||
return room
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_containing_room(world: World, entity: Room | Character | Item) -> Room | None:
|
||||
if isinstance(entity, Room):
|
||||
return entity
|
||||
|
|
Loading…
Reference in New Issue