diff --git a/Dockerfile b/Dockerfile index 905a723..f3bb6d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,6 @@ COPY requirements/base.txt /taleweave/requirements/base.txt RUN pip install --no-cache-dir -r requirements/base.txt RUN pip install --no-cache-dir --index-url https://test.pypi.org/simple/ packit_llm==0.1.0 -COPY adventure/ /taleweave/adventure/ +COPY taleweave/ /taleweave/taleweave/ -CMD ["python", "-m", "adventure.main"] +CMD ["python", "-m", "taleweave.main"] diff --git a/README.md b/README.md index 88cf7e7..2c0f7cd 100644 --- a/README.md +++ b/README.md @@ -107,19 +107,16 @@ server: ```bash # Start the TaleWeave AI engine -python3 -m adventure.main \ +python3 -m taleweave.main \ --world worlds/outback-animals-1 \ - --world-prompt ./adventure/prompts.yml:outback-animals \ + --world-prompt ./taleweave/prompts.yml:outback-animals \ --discord=true \ --server=true \ --rooms 3 \ --turns 30 \ --optional-actions=true \ - --actions adventure.sim_systems:init_actions \ - --systems adventure.sim_systems:init_logic - -# --actions adventure.custom_systems:init_actions -# --systems adventure.custom_systems:init_logic + --actions taleweave.systems.sim:init_actions \ + --systems taleweave.systems.sim:init_logic ``` This will generate a relatively small world with 3 rooms or areas, run for 30 steps, then shut down. diff --git a/client/src/app.tsx b/client/src/app.tsx index cf15c11..4a11ae9 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -54,9 +54,12 @@ export function App(props: AppProps) { function setPlayer(character: Maybe) { // do not call setCharacter until the server confirms the player change + // eslint-disable-next-line no-null/no-null + let become = null; if (doesExist(character)) { - sendMessage(JSON.stringify({ type: 'player', become: character.name })); + become = character.name; } + sendMessage(JSON.stringify({ type: 'player', become })); } function sendInput(input: string) { @@ -96,10 +99,20 @@ export function App(props: AppProps) { setActiveTurn(event.client === clientId); break; case 'player': - if (event.status === 'join' && doesExist(world) && event.client === clientId) { - const { character: characterName } = event; - const character = world.rooms.flatMap((room) => room.characters).find((a) => a.name === characterName); - setCharacter(character); + if (doesExist(world) && event.client === clientId) { + switch (event.status) { + case 'join': { + const { character: characterName } = event; + const character = world.rooms.flatMap((room) => room.characters).find((a) => a.name === characterName); + setCharacter(character); + break; + } + case 'leave': + setCharacter(undefined); + break; + default: + // ignore other player events + } } break; case 'players': diff --git a/config.yml b/config.yml index 256c9f8..f348b63 100644 --- a/config.yml +++ b/config.yml @@ -8,7 +8,7 @@ render: checkpoints: [ "diffusion-sdxl-dynavision-0-5-5-7.safetensors", ] - path: /tmp/adventure-images + path: /tmp/taleweave-images sizes: landscape: width: 1280 diff --git a/taleweave/bot/discord.py b/taleweave/bot/discord.py index 7d81d08..9bad720 100644 --- a/taleweave/bot/discord.py +++ b/taleweave/bot/discord.py @@ -81,14 +81,18 @@ class AdventureClient(Client): channel = message.channel user_name = author.name # include nick - if message.content.startswith("!adventure"): + if message.content.startswith( + bot_config.command_prefix + bot_config.name_command + ): world = get_current_world() if world: active_world = f"Active world: {world.name} (theme: {world.theme})" else: active_world = "No active world" - await message.channel.send(f"Hello! Welcome to Adventure! {active_world}") + await message.channel.send( + f"Hello! Welcome to {bot_config.name_title}! {active_world}" + ) return if message.content.startswith("!help"): diff --git a/taleweave/context.py b/taleweave/context.py index 75ee0ba..54af28e 100644 --- a/taleweave/context.py +++ b/taleweave/context.py @@ -19,7 +19,7 @@ from pyee.base import EventEmitter from taleweave.game_system import GameSystem from taleweave.models.entity import Character, Room, World -from taleweave.models.event import GameEvent +from taleweave.models.event import GameEvent, StatusEvent from taleweave.utils.string import normalize_name logger = getLogger(__name__) @@ -49,12 +49,17 @@ def get_event_name(event: GameEvent | Type[GameEvent]): def broadcast(message: str | GameEvent): if isinstance(message, GameEvent): - event_name = get_event_name(message) - logger.debug(f"broadcasting {event_name}") - event_emitter.emit(event_name, message) + event = message else: - logger.warning("broadcasting a string message is deprecated") - event_emitter.emit(STRING_EVENT_TYPE, message) + logger.warning( + "broadcasting a string message is deprecated, converting to status event: %s", + message, + ) + event = StatusEvent(text=message) + + event_name = get_event_name(event) + logger.debug(f"broadcasting {event_name}: {event}") + event_emitter.emit(event_name, event) def is_union(type_: Type | UnionType): diff --git a/taleweave/generate.py b/taleweave/generate.py index 9ef111a..a89826b 100644 --- a/taleweave/generate.py +++ b/taleweave/generate.py @@ -286,6 +286,7 @@ def generate_character( dest_room: Room, additional_prompt: str = "", detail_prompt: str = "", + add_to_world_order: bool = True, ) -> Character: existing_characters = [character.name for character in list_characters(world)] + [ character.name for character in list_characters_in_room(dest_room) @@ -350,6 +351,10 @@ def generate_character( except Exception: logger.exception("error generating item") + if add_to_world_order: + logger.info(f"adding character {name} to end of world turn order") + world.order.append(name) + return character diff --git a/taleweave/models/config.py b/taleweave/models/config.py index 68041f4..648e7f7 100644 --- a/taleweave/models/config.py +++ b/taleweave/models/config.py @@ -12,6 +12,9 @@ class Size: @dataclass class DiscordBotConfig: channels: List[str] + command_prefix: str + name_command: str + name_title: str content_intent: bool = False @@ -24,6 +27,7 @@ class BotConfig: class RenderConfig: cfg: int | IntRange checkpoints: List[str] + count: int path: str sizes: Dict[str, Size] steps: int | IntRange @@ -80,19 +84,27 @@ class Config: DEFAULT_CONFIG = Config( - bot=BotConfig(discord=DiscordBotConfig(channels=["adventure"])), + bot=BotConfig( + discord=DiscordBotConfig( + channels=["taleweave"], + command_prefix="!", + name_command="taleweave", + name_title="Taleweave AI", + ), + ), render=RenderConfig( cfg=IntRange(min=5, max=8), checkpoints=[ "diffusion-sdxl-dynavision-0-5-5-7.safetensors", ], - path="/tmp/adventure-images", + count=2, + path="/tmp/taleweave-images", sizes={ "landscape": Size(width=1024, height=768), "portrait": Size(width=768, height=1024), "square": Size(width=768, height=768), }, - steps=IntRange(min=30, max=30), + steps=30, ), server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)), world=WorldConfig( diff --git a/taleweave/render/comfy.py b/taleweave/render/comfy.py index 41e189f..fffb039 100644 --- a/taleweave/render/comfy.py +++ b/taleweave/render/comfy.py @@ -9,7 +9,6 @@ from random import choice, randint from re import sub from threading import Thread from typing import List -from uuid import uuid4 import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client) from fnvhash import fnv1a_32 @@ -17,6 +16,7 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape from PIL import Image from taleweave.context import broadcast +from taleweave.models.base import uuid from taleweave.models.config import DEFAULT_CONFIG, RenderConfig from taleweave.models.entity import WorldEntity from taleweave.models.event import ( @@ -35,7 +35,7 @@ from .prompt import prompt_from_entity, prompt_from_event logger = getLogger(__name__) server_address = environ["COMFY_API"] -client_id = uuid4().hex +client_id = uuid() render_config: RenderConfig = DEFAULT_CONFIG.render @@ -265,12 +265,18 @@ def render_loop(): logger.info( "using existing images for event %s: %s", event, existing_images ) + + if isinstance(event, WorldEntity): + title = event.name # TODO: generate a real title + else: + title = event.type + broadcast( RenderEvent( paths=existing_images, - prompt="", + prompt="reusing existing images", source=event, - title="Existing Images", + title=title, ) ) continue @@ -288,7 +294,7 @@ def render_loop(): # render or not if prompt: logger.debug("rendering prompt for event %s: %s", event, prompt) - image_paths = generate_images(prompt, 2, prefix=prefix) + image_paths = generate_images(prompt, render_config.count, prefix=prefix) broadcast( RenderEvent(paths=image_paths, prompt=prompt, source=event, title=title) ) diff --git a/taleweave/server/websocket.py b/taleweave/server/websocket.py index 2e3afd4..e77b8b1 100644 --- a/taleweave/server/websocket.py +++ b/taleweave/server/websocket.py @@ -20,7 +20,7 @@ from taleweave.context import ( subscribe, ) from taleweave.models.config import DEFAULT_CONFIG, WebsocketServerConfig -from taleweave.models.entity import Character, Item, Room, World +from taleweave.models.entity import World, WorldEntity from taleweave.models.event import ( GameEvent, PlayerEvent, @@ -128,14 +128,39 @@ async def handler(websocket): elif "become" in data: character_name = data["become"] - if has_player(character_name): + if character_name is not None and has_player(character_name): logger.error( f"character {character_name} is already in use" ) continue - # TODO: should this always remove? - remove_player(id) + player = get_player(id) + if player and isinstance(player, RemotePlayer): + remove_player(id) + + # TODO: deduplicate this leaving block + if character_name is None: + player_name = get_player_name(id) + logger.info( + "disconnecting player %s from %s", + player_name, + player.name, + ) + broadcast_player_event( + player.name, player_name, "leave" + ) + broadcast_player_list() + + character, _ = get_character_agent_for_name(player.name) + if character and player.fallback_agent: + logger.info( + "restoring LLM agent for %s", player.name + ) + set_character_agent( + player.name, character, player.fallback_agent + ) + + continue character, llm_agent = get_character_agent_for_name( character_name @@ -263,7 +288,7 @@ socket_thread = None def server_json(obj): - if isinstance(obj, (Character, Item, Room)): + if isinstance(obj, WorldEntity): return obj.name return world_json(obj) diff --git a/taleweave/systems/sim/environment_logic.yaml b/taleweave/systems/sim/environment_logic.yaml index e69be1a..d9cd032 100644 --- a/taleweave/systems/sim/environment_logic.yaml +++ b/taleweave/systems/sim/environment_logic.yaml @@ -22,14 +22,14 @@ rules: type: room temperature: hot chance: 0.2 - trigger: [adventure.systems.sim.environment_triggers:hot_room] + trigger: [taleweave.systems.sim.environment_triggers:hot_room] - group: environment-temperature match: type: room temperature: cold chance: 0.2 - trigger: [adventure.systems.sim.environment_triggers:cold_room] + trigger: [taleweave.systems.sim.environment_triggers:cold_room] labels: - match: diff --git a/taleweave/systems/weather/__init__.py b/taleweave/systems/weather/__init__.py index 15b28d9..3d74ee8 100644 --- a/taleweave/systems/weather/__init__.py +++ b/taleweave/systems/weather/__init__.py @@ -42,6 +42,9 @@ def initialize_weather(world: World): room.attributes["time"] = time_of_day.name +# TODO: generate indoor/outdoor attributes + + def simulate_weather(world: World, turn: int, data: None = None): time_of_day = get_time_of_day(turn) for room in world.rooms: diff --git a/taleweave/systems/weather/weather_logic.yaml b/taleweave/systems/weather/weather_logic.yaml index 905cb09..d4f1a4b 100644 --- a/taleweave/systems/weather/weather_logic.yaml +++ b/taleweave/systems/weather/weather_logic.yaml @@ -38,6 +38,9 @@ rules: # weather initial state - group: weather + match: + type: room + outdoor: true rule: | "weather" not in attributes set: