1
0
Fork 0

fix conversation events, rename look to examine, generate indoor/outdoor attributes
Run Docker Build / build (push) Failing after 10s Details
Run Python Build / build (push) Failing after 22s Details

This commit is contained in:
Sean Sube 2024-05-28 19:55:32 -05:00
parent 66c2c20031
commit 80e98482e0
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
14 changed files with 131 additions and 69 deletions

View File

@ -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':

View File

@ -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."

View File

@ -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,

View File

@ -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()

View File

@ -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]):

View File

@ -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):

View File

@ -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 ""

View File

@ -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):

View File

@ -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)}."

View File

@ -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

View File

@ -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,
),
]

View File

@ -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:

View File

@ -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"

View File

@ -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