1
0
Fork 0
taleweave-ai/adventure/discord_bot.py

277 lines
8.4 KiB
Python
Raw Normal View History

2024-05-08 01:42:10 +00:00
# from functools import cache
from json import loads
from logging import getLogger
from os import environ
from queue import Queue
from re import sub
from threading import Thread
from typing import Literal
from discord import Client, Embed, File, Intents
from packit.utils import could_be_json
from adventure.context import (
get_actor_agent_for_name,
get_current_world,
set_actor_agent_for_name,
)
from adventure.models import Actor, Room
from adventure.player import RemotePlayer, get_player, has_player, set_player
from adventure.render_comfy import generate_image_tool
logger = getLogger(__name__)
client = None
prompt_queue: Queue = Queue()
def remove_tags(text: str) -> str:
"""
Remove any <foo> tags.
"""
return sub(r"<[^>]*>", "", text)
class AdventureClient(Client):
async def on_ready(self):
logger.info(f"Logged in as {self.user}")
async def on_reaction_add(self, reaction, user):
if user == self.user:
return
logger.info(f"Reaction added: {reaction} by {user}")
if reaction.emoji == "📷":
# message_id = reaction.message.id
# TODO: look up event that caused this message, get the room and actors
if len(reaction.message.embeds) > 0:
embed = reaction.message.embeds[0]
room_name = embed.title
actor_name = embed.description
prompt = f"{room_name}. {actor_name}."
await reaction.message.channel.send(f"Generating image for: {prompt}")
world = get_current_world()
if not world:
return
room = next(
(room for room in world.rooms if room.name == room_name), None
)
if not room:
return
actor = next(
(actor for actor in room.actors if actor.name == actor_name), None
)
if not actor:
return
prompt = f"{room.name}. {actor.name}."
else:
prompt = remove_tags(reaction.message.content)
paths = generate_image_tool(prompt, 2)
logger.info(f"Generated images: {paths}")
files = [File(filename) for filename in paths]
await reaction.message.channel.send(files=files, reference=reaction.message)
async def on_message(self, message):
if message.author == self.user:
return
author = message.author
channel = message.channel
user_name = author.name # include nick
world = get_current_world()
if world:
active_world = f"Active world: {world.name} (theme: {world.theme})"
else:
active_world = "No active world"
if message.content.startswith("!adventure"):
await message.channel.send(f"Hello! Welcome to Adventure! {active_world}")
return
if message.content.startswith("!help"):
await message.channel.send("Type `!join` to start playing!")
return
if message.content.startswith("!join"):
character_name = remove_tags(message.content).replace("!join", "").strip()
if has_player(character_name):
await channel.send(f"{character_name} has already been taken!")
return
actor, agent = get_actor_agent_for_name(character_name)
if not actor:
await channel.send(f"Character `{character_name}` not found!")
return
def prompt_player(character: str, prompt: str):
logger.info(
"append prompt for character %s (user %s) to queue: %s",
character,
user_name,
prompt,
)
prompt_queue.put((character, prompt))
return True
player = RemotePlayer(
actor.name, actor.backstory, prompt_player, fallback_agent=agent
)
set_actor_agent_for_name(character_name, actor, player)
set_player(user_name, player)
logger.info(f"{user_name} has joined the game as {actor.name}!")
await message.channel.send(
f"{user_name} has joined the game as {actor.name}!"
)
return
if message.content.startswith("!leave"):
# TODO: revert to LLM agent
logger.info(f"{user_name} has left the game!")
await message.channel.send(f"{user_name} has left the game!")
return
player = get_player(user_name)
if player and isinstance(player, RemotePlayer):
content = remove_tags(message.content)
player.input_queue.put(content)
logger.info(
f"Received message from {user_name} for {player.name}: {content}"
)
return
await message.channel.send(
"You are not currently playing Adventure! Type `!join` to start playing!"
)
return
active_tasks = set()
def launch_bot():
def bot_main():
global client
intents = Intents.default()
# intents.message_content = True
client = AdventureClient(intents=intents)
client.run(environ["DISCORD_TOKEN"])
def prompt_main():
from time import sleep
while True:
sleep(0.5)
if prompt_queue.empty():
continue
if len(active_tasks) > 0:
continue
character, prompt = prompt_queue.get()
logger.info("Prompting character %s: %s", character, prompt)
if client:
prompt_task = client.loop.create_task(broadcast_event(prompt))
active_tasks.add(prompt_task)
prompt_task.add_done_callback(active_tasks.discard)
bot_thread = Thread(target=bot_main)
bot_thread.start()
prompt_thread = Thread(target=prompt_main)
prompt_thread.start()
def stop_bot():
global client
if client:
client.close()
client = None
# @cache
def get_active_channels():
if not client:
return []
# return client.private_channels
return [
channel
for guild in client.guilds
for channel in guild.text_channels
if channel.name == "bots"
]
async def broadcast_event(message: str | Embed):
if not client:
logger.warning("No Discord client available")
return
active_channels = get_active_channels()
if not active_channels:
logger.warning("No active channels")
return
for channel in active_channels:
if isinstance(message, str):
logger.info("Broadcasting to channel %s: %s", channel, message)
await channel.send(content=message)
elif isinstance(message, Embed):
logger.info(
"Broadcasting to channel %s: %s - %s",
channel,
message.title,
message.description,
)
await channel.send(embed=message)
def bot_action(room: Room, actor: Actor, message: str):
try:
action_embed = Embed(title=room.name, description=actor.name)
if could_be_json(message):
action_data = loads(message)
action_name = action_data["function"].replace("action_", "").title()
action_parameters = action_data.get("parameters", {})
action_embed.add_field(name="Action", value=action_name)
for key, value in action_parameters.items():
action_embed.add_field(name=key.replace("_", " ").title(), value=value)
else:
action_embed.add_field(name="Message", value=message)
prompt_queue.put((actor.name, action_embed))
except Exception as e:
logger.error("Failed to broadcast action: %s", e)
def bot_event(message: str):
prompt_queue.put((None, message))
def bot_result(room: Room, actor: Actor, action: str):
result_embed = Embed(title=room.name, description=actor.name)
result_embed.add_field(name="Result", value=action)
prompt_queue.put((actor.name, result_embed))
def player_event(character: str, id: str, event: Literal["join", "leave"]):
if event == "join":
prompt_queue.put((character, f"{character} has joined the game!"))
elif event == "leave":
prompt_queue.put((character, f"{character} has left the game!"))