From d20828e759722fecd52ddc41f2d114a68e7e7cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Wed, 21 Dec 2022 20:15:38 +0100 Subject: [PATCH 1/8] add first bits of jinja template support --- bot/__main__.py | 2 + bot/bot.py | 96 ++++++++++++++++++++++++++++------ bot/bot_settings.py | 2 + bot/requirements.txt | 3 ++ bot/templates/boot.msg | 14 +++++ bot/templates/rate_summary.msg | 7 +++ bot/templates/welcome.msg | 6 +++ bot/utils.py | 17 ++++++ pyproject.toml | 1 + 9 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 bot/templates/boot.msg create mode 100644 bot/templates/rate_summary.msg create mode 100644 bot/templates/welcome.msg create mode 100644 bot/utils.py diff --git a/bot/__main__.py b/bot/__main__.py index 362b16f0..0047bce7 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -12,5 +12,7 @@ if __name__ == "__main__": backend_url=settings.BACKEND_URL, api_key=settings.API_KEY, owner_id=settings.OWNER_ID, + template_dir=settings.TEMPLATE_DIR, + debug=settings.DEBUG, ) bot.run() diff --git a/bot/bot.py b/bot/bot.py index e6e90770..6ab11ed0 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -1,12 +1,20 @@ # -*- coding: utf-8 -*- import asyncio -from typing import Optional, Union +from datetime import timedelta +from pathlib import Path +from typing import Any, Optional, Union import discord +import discord.ui as ui +import jinja2 from api_client import ApiClient, TaskType from discord import app_commands from loguru import logger from oasst_shared.schemas import protocol as protocol_schema +from utils import get_git_head_hash, utcnow + +__version__ = "0.0.1" +BOT_NAME = "Open-Assistant Junior" class RatingButton(discord.ui.Button): @@ -26,6 +34,26 @@ def generate_rating_view(lo: int, hi: int, response_handler) -> discord.ui.View: return view +class Questionnaire(ui.Modal, title="Questionnaire Response"): + name = ui.TextInput(label="Name") + answer = ui.TextInput(label="Answer", style=discord.TextStyle.paragraph) + + async def on_submit(self, interaction: discord.Interaction): + await interaction.response.send_message(f"Thanks for your response, {self.name}!", ephemeral=True) + + +class MessageTemplates: + def __init__(self, template_dir="./templates"): + self.env = jinja2.Environment( + loader=jinja2.FileSystemLoader(template_dir), + autoescape=jinja2.select_autoescape(disabled_extensions=("msg",), default=False, default_for_string=False), + ) + + def render(self, template_name, **kwargs): + template = self.env.get_template(template_name) + return template.render(kwargs) + + class OpenAssistantBot: def __init__( self, @@ -34,7 +62,14 @@ class OpenAssistantBot: backend_url: str, api_key: str, owner_id: Optional[Union[int, str]] = None, + template_dir: str = "./templates", + debug: bool = False, ): + self.template_dir = Path(template_dir) + self.bot_channel_name = bot_channel_name + self.templates = MessageTemplates(template_dir) + self.debug = debug + intents = discord.Intents.default() intents.message_content = True @@ -59,6 +94,11 @@ class OpenAssistantBot: client.loop.create_task(self.background_timer(), name="OpenAssistantBot.background_timer()") logger.info(f"{client.user} is now running!") + await self.delete_all_old_bot_messages() + if self.debug: + await self.post_boot_message() + await self.post_welcome_message() + @client.event async def on_message(message: discord.Message): # ignore own messages @@ -78,7 +118,42 @@ class OpenAssistantBot: @self.tree.command() async def work(interaction: discord.Interaction): """Request a new personalized task""" - await interaction.response.send_message(f"work command by {interaction.user.name}") + # task = self.backend.fetch_task(protocol_schema.TaskRequestType.rate_summary, user=None) + # task = self.backend.fetch_random_task(user=None) + q = Questionnaire() + await interaction.response.send_modal(q) + + def ensure_bot_channel(self) -> None: + if self.bot_channel is None: + raise RuntimeError(f"bot channel '{self.bot_channel_name}' not found") + + async def post(self, content: str, view: discord.ui.View = None) -> discord.Message: + self.ensure_bot_channel() + return await self.bot_channel.send(content=content) + + async def post_template(self, name: str, view: discord.ui.View = None, **kwargs: Any) -> discord.Message: + logger.info(f"rendering {name}") + text = self.templates.render(name, **kwargs) + return await self.post(text, view) + + async def post_boot_message(self) -> discord.Message: + return await self.post_template( + "boot.msg", bot_name=BOT_NAME, version=__version__, git_hash=get_git_head_hash(), debug=self.debug + ) + + async def post_welcome_message(self) -> discord.Message: + return await self.post_template("welcome.msg") + + async def delete_all_old_bot_messages(self) -> None: + logger.info("Begin deleting old bot messages.") + look_until = utcnow() - timedelta(days=365) + async for msg in self.bot_channel.history(limit=None): + msg: discord.Message + if msg.created_at < look_until: + break + if msg.author.id == self.client.user.id: + await msg.delete() + logger.info("Completed deleting old bot messages.") async def print_separtor(self, title: str) -> discord.Message: msg: discord.Message = await self.bot_channel.send(f"\n:point_right: {title} :point_left:\n") @@ -100,21 +175,12 @@ class OpenAssistantBot: return msg async def generate_rate_summary(self, task: protocol_schema.RateSummaryTask): - s = [ - "Rate the following summary:", - task.summary, - "Full text:", - task.full_text, - f"Rating scale: {task.scale.min} - {task.scale.max}", - ] - text = "\n".join(s) - async def rating_response_handler(score, interaction: discord.Interaction): logger.info("rating_response_handler", score) await interaction.response.send_message(f"got your feedback: {score}") view = generate_rating_view(task.scale.min, task.scale.max, rating_response_handler) - msg: discord.Message = await self.bot_channel.send(text, view=view) + msg: discord.Message = await self.post_template("rate_summary", task=task, view=view) async def on_reply(message: discord.Message): logger.info("on_summary_reply", message) @@ -235,8 +301,8 @@ class OpenAssistantBot: return msg async def next_task(self): - # task = self.backend.fetch_task(protocol_schema.TaskRequestType.user_reply, user=None) - task = self.backend.fetch_random_task(user=None) + task = self.backend.fetch_task(protocol_schema.TaskRequestType.summarize_story, user=None) + # task = self.backend.fetch_random_task(user=None) await self.print_separtor("New Task") @@ -269,7 +335,7 @@ class OpenAssistantBot: await self.next_task() except Exception: logger.exception("fetching next task failed") - await asyncio.sleep(30) + await asyncio.sleep(60) async def _sync(self, command: str, message: discord.Message): diff --git a/bot/bot_settings.py b/bot/bot_settings.py index b7a46aa6..c976d7cd 100644 --- a/bot/bot_settings.py +++ b/bot/bot_settings.py @@ -8,6 +8,8 @@ class BotSettings(BaseSettings): BOT_TOKEN: str BOT_CHANNEL_NAME: str = "bot" OWNER_ID: int = None + TEMPLATE_DIR: str = "./templates" + DEBUG: bool = True settings = BotSettings(_env_file=".env") diff --git a/bot/requirements.txt b/bot/requirements.txt index da4762a6..927ebcf2 100644 --- a/bot/requirements.txt +++ b/bot/requirements.txt @@ -1,4 +1,7 @@ discord.py==2.1.0 +Jinja2==3.1.2 pydantic==1.9.1 python-dotenv==0.21.0 +pytz==2022.7 requests==2.28.1 +schedule==1.1.0 diff --git a/bot/templates/boot.msg b/bot/templates/boot.msg new file mode 100644 index 00000000..1d212e99 --- /dev/null +++ b/bot/templates/boot.msg @@ -0,0 +1,14 @@ +``` +________ __ +\_____ \ _____ _____ _______/ |_ + / | \\__ \ \__ \ / ___/\ __\ +/ | \/ __ \_/ __ \_\___ \ | | +\_______ (____ (____ /____ > |__| + \/ \/ \/ \/ + +{{bot_name}} {{version}} +git hash: {{git_hash}} +debug_mode: {{debug}} +``` + +https://github.com/LAION-AI/Open-Assistant diff --git a/bot/templates/rate_summary.msg b/bot/templates/rate_summary.msg new file mode 100644 index 00000000..31007c40 --- /dev/null +++ b/bot/templates/rate_summary.msg @@ -0,0 +1,7 @@ +Rate the following summary: +{{task.summary}} + +Full text: +{{task.full_text}} + +Rating scale: {{task.scale.min}} - {{task.scale.max}} diff --git a/bot/templates/welcome.msg b/bot/templates/welcome.msg new file mode 100644 index 00000000..553f7925 --- /dev/null +++ b/bot/templates/welcome.msg @@ -0,0 +1,6 @@ +Hi there, + +I am the **Open-Assistant Junior Bot** 🤖. I would love to get your feedback 🤗! +Currently I am still learning from human demonstrations how to reply to instructions. When I am grown up I want to become a fully functional AI Assistant language model that is fully open-sourced and assists millions of humans all over the world. + +Type `/tutorial` to start the tutorial or `/help` to see a list of all my commands. diff --git a/bot/utils.py b/bot/utils.py new file mode 100644 index 00000000..1a06b833 --- /dev/null +++ b/bot/utils.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +import subprocess +from datetime import datetime + +import pytz + + +def get_git_head_hash(): + # get current git hash + x = subprocess.run(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, universal_newlines=True) + if x.returncode == 0: + return x.stdout.replace("\n", "") + return None + + +def utcnow() -> datetime: + return datetime.now(pytz.UTC) diff --git a/pyproject.toml b/pyproject.toml index 83b614a2..30541eec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,3 +11,4 @@ line_length = 120 [tool.black] line-length = 120 target-version = ['py310'] +exclude = ["bot/templates"] From df62ee0f983bbfbe5a3f28fd2ccdb9031b5560d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Thu, 22 Dec 2022 00:59:00 +0100 Subject: [PATCH 2/8] add jinja templates for all current tasks --- .pre-commit-config.yaml | 2 +- bot/bot.py | 91 ++++++------------- bot/templates/boot.msg | 11 +-- bot/templates/task_assistant_reply.msg | 12 +++ bot/templates/task_initial_prompt.msg | 4 + .../task_rank_conversation_replies.msg | 13 +++ bot/templates/task_rank_initial_prompts.msg | 5 + ...rate_summary.msg => task_rate_summary.msg} | 0 bot/templates/task_user_reply.msg | 12 +++ 9 files changed, 78 insertions(+), 72 deletions(-) create mode 100644 bot/templates/task_assistant_reply.msg create mode 100644 bot/templates/task_initial_prompt.msg create mode 100644 bot/templates/task_rank_conversation_replies.msg create mode 100644 bot/templates/task_rank_initial_prompts.msg rename bot/templates/{rate_summary.msg => task_rate_summary.msg} (100%) create mode 100644 bot/templates/task_user_reply.msg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 90279415..f2775e86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -exclude: "build|stubs" +exclude: "build|stubs|bot/templates/.*msg" default_language_version: python: python3 diff --git a/bot/bot.py b/bot/bot.py index 6ab11ed0..a420b6c1 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -51,7 +51,9 @@ class MessageTemplates: def render(self, template_name, **kwargs): template = self.env.get_template(template_name) - return template.render(kwargs) + txt = template.render(kwargs) + logger.info(txt) + return txt class OpenAssistantBot: @@ -86,12 +88,9 @@ class OpenAssistantBot: self.reply_handlers = {} # handlers by msg_id self.tree = app_commands.CommandTree(self.client, fallback_to_global=True) - self.auto_archive_minutes = 60 # ToDo: add to bot config - @client.event async def on_ready(): self.bot_channel = self.get_text_channel_by_name(bot_channel_name) - client.loop.create_task(self.background_timer(), name="OpenAssistantBot.background_timer()") logger.info(f"{client.user} is now running!") await self.delete_all_old_bot_messages() @@ -99,6 +98,8 @@ class OpenAssistantBot: await self.post_boot_message() await self.post_welcome_message() + client.loop.create_task(self.background_timer(), name="OpenAssistantBot.background_timer()") + @client.event async def on_message(message: discord.Message): # ignore own messages @@ -131,11 +132,14 @@ class OpenAssistantBot: self.ensure_bot_channel() return await self.bot_channel.send(content=content) - async def post_template(self, name: str, view: discord.ui.View = None, **kwargs: Any) -> discord.Message: + async def post_template_view(self, name: str, *, view: discord.ui.View, **kwargs: Any) -> discord.Message: logger.info(f"rendering {name}") text = self.templates.render(name, **kwargs) return await self.post(text, view) + async def post_template(self, name: str, **kwargs: Any) -> discord.Message: + return await self.post_template_view(name=name, view=None, **kwargs) + async def post_boot_message(self) -> discord.Message: return await self.post_template( "boot.msg", bot_name=BOT_NAME, version=__version__, git_hash=get_git_head_hash(), debug=self.debug @@ -162,9 +166,7 @@ class OpenAssistantBot: async def generate_summarize_story(self, task: protocol_schema.SummarizeStoryTask): text = f"Summarize to the following story:\n{task.story}" msg: discord.Message = await self.bot_channel.send(text) - await self.bot_channel.create_thread( - message=discord.Object(msg.id), name="Summaries", auto_archive_duration=self.auto_archive_minutes - ) + await self.bot_channel.create_thread(message=discord.Object(msg.id), name="Summaries") async def on_reply(message: discord.Message): logger.info("on_summarize_story_reply", message) @@ -180,24 +182,20 @@ class OpenAssistantBot: await interaction.response.send_message(f"got your feedback: {score}") view = generate_rating_view(task.scale.min, task.scale.max, rating_response_handler) - msg: discord.Message = await self.post_template("rate_summary", task=task, view=view) + msg = await self.post_template_view("task_rate_summary.msg", view=view, task=task) async def on_reply(message: discord.Message): logger.info("on_summary_reply", message) - await message.add_reaction("") + await message.add_reaction("✅") self.reply_handlers[msg.id] = on_reply return msg async def generate_initial_prompt(self, task: protocol_schema.InitialPromptTask): - text = "Please provide an initial prompt to the assistant." - if task.hint: - text += f"\nHint: {task.hint}" - msg: discord.Message = await self.bot_channel.send(text) - await self.bot_channel.create_thread( - message=discord.Object(msg.id), name="Prompts", auto_archive_duration=self.auto_archive_minutes - ) + msg = await self.post_template("task_initial_prompt.msg", task=task) + + await self.bot_channel.create_thread(message=discord.Object(msg.id), name="Prompts") async def on_reply(message: discord.Message): logger.info("on_initial_prompt_reply", message) @@ -215,17 +213,8 @@ class OpenAssistantBot: return f":person_red_hair: User:\n**{message.text}**" async def generate_user_reply(self, task: protocol_schema.UserReplyTask): - s = ["Please provide a reply to the assistant.", "Here is the conversation so far:\n"] - for message in task.conversation.messages: - s.append(self._render_message(message)) - s.append("") - if task.hint: - s.append(f"Hint: {task.hint}") - text = "\n".join(s) - msg: discord.Message = await self.bot_channel.send(text) - await self.bot_channel.create_thread( - message=discord.Object(msg.id), name="User responses", auto_archive_duration=self.auto_archive_minutes - ) + msg = await self.post_template("task_user_reply.msg", task=task) + await self.bot_channel.create_thread(message=discord.Object(msg.id), name="User responses") async def on_reply(message: discord.Message): logger.info("on_user_reply_reply", message) @@ -236,16 +225,8 @@ class OpenAssistantBot: return msg async def generate_assistant_reply(self, task: protocol_schema.AssistantReplyTask): - s = ["Act as the assistant and reply to the user.", "Here is the conversation so far\n:"] - for message in task.conversation.messages: - s.append(self._render_message(message)) - s.append("") - s.append(":robot: Assistant: { human, pls help me! ... }") - text = "\n".join(s) - msg: discord.Message = await self.bot_channel.send(text) - await self.bot_channel.create_thread( - message=discord.Object(msg.id), name="Agent responses", auto_archive_duration=self.auto_archive_minutes - ) + msg = await self.post_template("task_assistant_reply.msg", task=task) + await self.bot_channel.create_thread(message=discord.Object(msg.id), name="Agent responses") async def on_reply(message: discord.Message): logger.info("on_assistant_reply_reply", message) @@ -256,16 +237,8 @@ class OpenAssistantBot: return msg async def generate_rank_initial_prompts(self, task: protocol_schema.RankInitialPromptsTask): - s = ["Rank the following prompts:"] - for idx, prompt in enumerate(task.prompts, start=1): - s.append(f"{idx}: {prompt}") - s.append("") - s.append(':scroll: Reply with the numbers of best to worst prompts separated by commas (example: "4,1,3,2").') - text = "\n".join(s) - msg: discord.Message = await self.bot_channel.send(text) - await self.bot_channel.create_thread( - message=discord.Object(msg.id), name="User responses", auto_archive_duration=self.auto_archive_minutes - ) + msg = await self.post_template("task_rank_initial_prompts.msg", task=task) + await self.bot_channel.create_thread(message=discord.Object(msg.id), name="User responses") async def on_reply(message: discord.Message): logger.info("on_rank_initial_prompts_reply", message) @@ -276,20 +249,8 @@ class OpenAssistantBot: return msg async def generate_rank_conversation(self, task: protocol_schema.RankConversationRepliesTask): - s = ["Here is the conversation so far:"] - for message in task.conversation.messages: - s.append(self._render_message(message)) - s.append("") - s.append("Rank the following replies:") - for idx, reply in enumerate(task.replies, start=1): - s.append(f"{idx}: {reply}") - s.append("") - s.append(':scroll: Reply with the numbers of best to worst prompts separated by commas (example: "4,1,3,2").') - text = "\n".join(s) - msg: discord.Message = await self.bot_channel.send(text) - await self.bot_channel.create_thread( - message=discord.Object(msg.id), name="User responses", auto_archive_duration=self.auto_archive_minutes - ) + msg = await self.post_template("task_rank_conversation_replies.msg", task=task) + await self.bot_channel.create_thread(message=discord.Object(msg.id), name="User responses") async def on_reply(message: discord.Message): logger.info("on_rank_conversation_reply", message) @@ -301,8 +262,8 @@ class OpenAssistantBot: return msg async def next_task(self): - task = self.backend.fetch_task(protocol_schema.TaskRequestType.summarize_story, user=None) - # task = self.backend.fetch_random_task(user=None) + task_type = protocol_schema.TaskRequestType.random + task = self.backend.fetch_task(task_type, user=None) await self.print_separtor("New Task") @@ -335,7 +296,7 @@ class OpenAssistantBot: await self.next_task() except Exception: logger.exception("fetching next task failed") - await asyncio.sleep(60) + await asyncio.sleep(5) async def _sync(self, command: str, message: discord.Message): diff --git a/bot/templates/boot.msg b/bot/templates/boot.msg index 1d212e99..0561c8be 100644 --- a/bot/templates/boot.msg +++ b/bot/templates/boot.msg @@ -1,14 +1,13 @@ ``` ________ __ -\_____ \ _____ _____ _______/ |_ - / | \\__ \ \__ \ / ___/\ __\ -/ | \/ __ \_/ __ \_\___ \ | | -\_______ (____ (____ /____ > |__| +\_____ \ _____ ______ _______/ |_ + / | \\__ \ / ___// ___/\ __\ +/ | \/ __ \_\___ \ \___ \ | | +\_______ (____ /____ >____ > |__| \/ \/ \/ \/ {{bot_name}} {{version}} git hash: {{git_hash}} debug_mode: {{debug}} ``` - -https://github.com/LAION-AI/Open-Assistant +https://github.com/LAION-AI/Open-Assistant \ No newline at end of file diff --git a/bot/templates/task_assistant_reply.msg b/bot/templates/task_assistant_reply.msg new file mode 100644 index 00000000..3dfe84a3 --- /dev/null +++ b/bot/templates/task_assistant_reply.msg @@ -0,0 +1,12 @@ +Act as the assistant and reply to the user. +Here is the conversation so far: +{% for message in task.conversation.messages %} +{% if message.is_assistant %} +:robot: Assistant: +{{ message.text }} +{% else %} +:person_red_hair: User: +**{{ message.text }}**" +{% endif %} +{% endfor %} +:robot: Assistant: { human, pls help me! ... } \ No newline at end of file diff --git a/bot/templates/task_initial_prompt.msg b/bot/templates/task_initial_prompt.msg new file mode 100644 index 00000000..dc3b10d3 --- /dev/null +++ b/bot/templates/task_initial_prompt.msg @@ -0,0 +1,4 @@ +Please provide an initial prompt to the assistant. +{% if task.hint %} +Hint: {task.hint}" +{% endif %} \ No newline at end of file diff --git a/bot/templates/task_rank_conversation_replies.msg b/bot/templates/task_rank_conversation_replies.msg new file mode 100644 index 00000000..c0c8bc80 --- /dev/null +++ b/bot/templates/task_rank_conversation_replies.msg @@ -0,0 +1,13 @@ +Here is the conversation so far: +{% for message in task.conversation.messages %}{% if message.is_assistant %} +:robot: Assistant: +{{ message.text }} +{% else %} +:person_red_hair: User: +**{{ message.text }}**" +{% endif %}{% endfor %} +Rank the following replies: +{% for reply in task.replies %} +{{loop.index}}: {{reply}}{% endfor %} + +:scroll: Reply with the numbers of best to worst prompts separated by commas (example: "4,1,3,2"). \ No newline at end of file diff --git a/bot/templates/task_rank_initial_prompts.msg b/bot/templates/task_rank_initial_prompts.msg new file mode 100644 index 00000000..5a75cbd1 --- /dev/null +++ b/bot/templates/task_rank_initial_prompts.msg @@ -0,0 +1,5 @@ +Rank the following prompts: +{% for prompt in task.prompts %} +{{loop.index}}: {{prompt}}{% endfor %} + +:scroll: Reply with the numbers of best to worst prompts separated by commas (example: "4,1,3,2"). \ No newline at end of file diff --git a/bot/templates/rate_summary.msg b/bot/templates/task_rate_summary.msg similarity index 100% rename from bot/templates/rate_summary.msg rename to bot/templates/task_rate_summary.msg diff --git a/bot/templates/task_user_reply.msg b/bot/templates/task_user_reply.msg new file mode 100644 index 00000000..c247daa5 --- /dev/null +++ b/bot/templates/task_user_reply.msg @@ -0,0 +1,12 @@ +Please provide a reply to the assistant. +Here is the conversation so far: +{% for message in task.conversation.messages %}{% if message.is_assistant %} +:robot: Assistant: +{{ message.text }} +{% else %} +:person_red_hair: User: +**{{ message.text }}**" +{% endif %}{% endfor %} +{% if task.hint %} +Hint: {{ task.hint }} +{% endif %} \ No newline at end of file From 3205491166e190512608bf01754815cadae47a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Thu, 22 Dec 2022 14:51:12 +0100 Subject: [PATCH 3/8] add channel handler async msg routing --- .pre-commit-config.yaml | 2 +- bot/bot.py | 258 ++++++++----------------- bot/bot_base.py | 53 +++++ bot/channel_handlers.py | 83 ++++++++ bot/message_templates.py | 18 ++ bot/task_handlers.py | 153 +++++++++++++++ bot/templates/boot.msg | 2 +- bot/templates/task_initial_prompt.msg | 4 +- bot/templates/task_summarize_story.msg | 2 + pyproject.toml | 1 - 10 files changed, 389 insertions(+), 187 deletions(-) create mode 100644 bot/bot_base.py create mode 100644 bot/channel_handlers.py create mode 100644 bot/message_templates.py create mode 100644 bot/task_handlers.py create mode 100644 bot/templates/task_summarize_story.msg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2775e86..cccb2167 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -exclude: "build|stubs|bot/templates/.*msg" +exclude: "build|stubs|^bot/templates/" default_language_version: python: python3 diff --git a/bot/bot.py b/bot/bot.py index a420b6c1..b3e2e309 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -1,62 +1,26 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + import asyncio from datetime import timedelta from pathlib import Path -from typing import Any, Optional, Union +from typing import Optional, Union import discord -import discord.ui as ui -import jinja2 +import task_handlers from api_client import ApiClient, TaskType +from bot_base import BotBase from discord import app_commands from loguru import logger +from message_templates import MessageTemplates from oasst_shared.schemas import protocol as protocol_schema from utils import get_git_head_hash, utcnow -__version__ = "0.0.1" +__version__ = "0.0.2" BOT_NAME = "Open-Assistant Junior" -class RatingButton(discord.ui.Button): - def __init__(self, label, value, response_handler): - super().__init__(label=label, style=discord.ButtonStyle.green) - self.value = value - self.response_handler = response_handler - - async def callback(self, interaction): - await self.response_handler(self.value, interaction) - - -def generate_rating_view(lo: int, hi: int, response_handler) -> discord.ui.View: - view = discord.ui.View() - for i in range(lo, hi + 1): - view.add_item(RatingButton(str(i), i, response_handler)) - return view - - -class Questionnaire(ui.Modal, title="Questionnaire Response"): - name = ui.TextInput(label="Name") - answer = ui.TextInput(label="Answer", style=discord.TextStyle.paragraph) - - async def on_submit(self, interaction: discord.Interaction): - await interaction.response.send_message(f"Thanks for your response, {self.name}!", ephemeral=True) - - -class MessageTemplates: - def __init__(self, template_dir="./templates"): - self.env = jinja2.Environment( - loader=jinja2.FileSystemLoader(template_dir), - autoescape=jinja2.select_autoescape(disabled_extensions=("msg",), default=False, default_for_string=False), - ) - - def render(self, template_name, **kwargs): - template = self.env.get_template(template_name) - txt = template.render(kwargs) - logger.info(txt) - return txt - - -class OpenAssistantBot: +class OpenAssistantBot(BotBase): def __init__( self, bot_token: str, @@ -67,6 +31,8 @@ class OpenAssistantBot: template_dir: str = "./templates", debug: bool = False, ): + super().__init__() + self.template_dir = Path(template_dir) self.bot_channel_name = bot_channel_name self.templates = MessageTemplates(template_dir) @@ -82,10 +48,11 @@ class OpenAssistantBot: self.bot_token = bot_token client = discord.Client(intents=intents) self.client = client + self.loop = client.loop self.bot_channel: discord.TextChannel = None self.backend = ApiClient(backend_url, api_key) - self.reply_handlers = {} # handlers by msg_id + self.tree = app_commands.CommandTree(self.client, fallback_to_global=True) @client.event @@ -109,6 +76,9 @@ class OpenAssistantBot: @self.tree.command() async def tutorial(interaction: discord.Interaction): """Start the Open-Assistant tutorial via DMs.""" + + dm = await self.client.create_dm(discord.Object(interaction.user.id)) + await dm.send("Tutorial coming soon... :-)") await interaction.response.send_message(f"tutorial command by {interaction.user.name}") @self.tree.command() @@ -119,27 +89,12 @@ class OpenAssistantBot: @self.tree.command() async def work(interaction: discord.Interaction): """Request a new personalized task""" + # task = self.backend.fetch_task(protocol_schema.TaskRequestType.rate_summary, user=None) # task = self.backend.fetch_random_task(user=None) - q = Questionnaire() + q = task_handlers.Questionnaire() await interaction.response.send_modal(q) - def ensure_bot_channel(self) -> None: - if self.bot_channel is None: - raise RuntimeError(f"bot channel '{self.bot_channel_name}' not found") - - async def post(self, content: str, view: discord.ui.View = None) -> discord.Message: - self.ensure_bot_channel() - return await self.bot_channel.send(content=content) - - async def post_template_view(self, name: str, *, view: discord.ui.View, **kwargs: Any) -> discord.Message: - logger.info(f"rendering {name}") - text = self.templates.render(name, **kwargs) - return await self.post(text, view) - - async def post_template(self, name: str, **kwargs: Any) -> discord.Message: - return await self.post_template_view(name=name, view=None, **kwargs) - async def post_boot_message(self) -> discord.Message: return await self.post_template( "boot.msg", bot_name=BOT_NAME, version=__version__, git_hash=get_git_head_hash(), debug=self.debug @@ -163,140 +118,64 @@ class OpenAssistantBot: msg: discord.Message = await self.bot_channel.send(f"\n:point_right: {title} :point_left:\n") return msg - async def generate_summarize_story(self, task: protocol_schema.SummarizeStoryTask): - text = f"Summarize to the following story:\n{task.story}" - msg: discord.Message = await self.bot_channel.send(text) - await self.bot_channel.create_thread(message=discord.Object(msg.id), name="Summaries") - - async def on_reply(message: discord.Message): - logger.info("on_summarize_story_reply", message) - await message.add_reaction("✅") - - self.reply_handlers[msg.id] = on_reply - - return msg - - async def generate_rate_summary(self, task: protocol_schema.RateSummaryTask): - async def rating_response_handler(score, interaction: discord.Interaction): - logger.info("rating_response_handler", score) - await interaction.response.send_message(f"got your feedback: {score}") - - view = generate_rating_view(task.scale.min, task.scale.max, rating_response_handler) - msg = await self.post_template_view("task_rate_summary.msg", view=view, task=task) - - async def on_reply(message: discord.Message): - logger.info("on_summary_reply", message) - await message.add_reaction("✅") - - self.reply_handlers[msg.id] = on_reply - - return msg - - async def generate_initial_prompt(self, task: protocol_schema.InitialPromptTask): - msg = await self.post_template("task_initial_prompt.msg", task=task) - - await self.bot_channel.create_thread(message=discord.Object(msg.id), name="Prompts") - - async def on_reply(message: discord.Message): - logger.info("on_initial_prompt_reply", message) - await message.add_reaction("✅") - - self.reply_handlers[msg.id] = on_reply - - return msg - - def _render_message(self, message: protocol_schema.ConversationMessage) -> str: - """Render a message to the user.""" - if message.is_assistant: - return f":robot: Assistant:\n{message.text}" - else: - return f":person_red_hair: User:\n**{message.text}**" - - async def generate_user_reply(self, task: protocol_schema.UserReplyTask): - msg = await self.post_template("task_user_reply.msg", task=task) - await self.bot_channel.create_thread(message=discord.Object(msg.id), name="User responses") - - async def on_reply(message: discord.Message): - logger.info("on_user_reply_reply", message) - await message.add_reaction("✅") - - self.reply_handlers[msg.id] = on_reply - - return msg - - async def generate_assistant_reply(self, task: protocol_schema.AssistantReplyTask): - msg = await self.post_template("task_assistant_reply.msg", task=task) - await self.bot_channel.create_thread(message=discord.Object(msg.id), name="Agent responses") - - async def on_reply(message: discord.Message): - logger.info("on_assistant_reply_reply", message) - await message.add_reaction("✅") - - self.reply_handlers[msg.id] = on_reply - - return msg - - async def generate_rank_initial_prompts(self, task: protocol_schema.RankInitialPromptsTask): - msg = await self.post_template("task_rank_initial_prompts.msg", task=task) - await self.bot_channel.create_thread(message=discord.Object(msg.id), name="User responses") - - async def on_reply(message: discord.Message): - logger.info("on_rank_initial_prompts_reply", message) - await message.add_reaction("✅") - - self.reply_handlers[msg.id] = on_reply - - return msg - - async def generate_rank_conversation(self, task: protocol_schema.RankConversationRepliesTask): - msg = await self.post_template("task_rank_conversation_replies.msg", task=task) - await self.bot_channel.create_thread(message=discord.Object(msg.id), name="User responses") - - async def on_reply(message: discord.Message): - logger.info("on_rank_conversation_reply", message) - await message.add_reaction("✅") - message - - self.reply_handlers[msg.id] = on_reply - - return msg - async def next_task(self): - task_type = protocol_schema.TaskRequestType.random + task_type = protocol_schema.TaskRequestType.rate_summary task = self.backend.fetch_task(task_type, user=None) await self.print_separtor("New Task") - msg: discord.Message = None + handler: task_handlers.ChannelTaskBase = None match task.type: case TaskType.summarize_story: - msg = await self.generate_summarize_story(task) + handler = task_handlers.SummarizeStoryHandler() case TaskType.rate_summary: - msg = await self.generate_rate_summary(task) + handler = task_handlers.RateSummaryHandler() case TaskType.initial_prompt: - msg = await self.generate_initial_prompt(task) + handler = task_handlers.InitialPromptHandler() case TaskType.user_reply: - msg = await self.generate_user_reply(task) + handler = task_handlers.UserReplyHandler() case TaskType.assistant_reply: - msg = await self.generate_assistant_reply(task) + handler = task_handlers.AssistantReplyHandler() case TaskType.rank_initial_prompts: - msg = await self.generate_rank_initial_prompts(task) + handler = task_handlers.RankInitialPromptsHandler() case TaskType.rank_user_replies | TaskType.rank_assistant_replies: - msg = await self.generate_rank_conversation(task) + handler = task_handlers.RankConversationsHandler() + case _: + logger.warning(f"Unsupported task type received: {task.type}") + self.backend.nack_task(task.id, "not supported") - if msg is not None: - self.backend.ack_task(task.id, msg.id) - else: - self.backend.nack_task(task.id, "not supported") + if handler: + try: + logger.info(f"strarting task {task.id}") + msg = await handler.start(self, task) + self.backend.ack_task(task.id, msg.id) + except Exception: + logger.exception("Starting task failed.") + self.backend.nack_task(task.id, "faled") async def background_timer(self): + next_remove_completed = utcnow() + timedelta(seconds=10) + next_fetch_task = utcnow() + timedelta(seconds=1) while True: + now = utcnow() + if self.bot_channel: - try: - await self.next_task() - except Exception: - logger.exception("fetching next task failed") - await asyncio.sleep(5) + if now > next_fetch_task: + next_fetch_task = utcnow() + timedelta(seconds=600) + + try: + await self.next_task() + except Exception: + logger.exception("fetching next task failed") + + for x in self.reply_handlers.values(): + x.handler.tick(now) + + if now > next_remove_completed: + next_remove_completed = utcnow() + timedelta(seconds=10) + await self.remove_completed_handlers() + + await asyncio.sleep(1) async def _sync(self, command: str, message: discord.Message): @@ -341,18 +220,33 @@ class OpenAssistantBot: if isinstance(message.channel, discord.Thread): handler = self.reply_handlers.get(message.channel.id) - if handler: - await handler(message) + if handler and not handler.handler.completed: + handler.handler.on_reply(message) if message.reference: handler = self.reply_handlers.get(message.reference.message_id) - if handler: - await handler(message) + if handler and not handler.handler.completed: + handler.handler.on_reply(message) logger.debug( f"{message.type} {message.channel.type} from ({user_display_name}) {user_id}: {message.content} ({type(message.content)})" ) + async def remove_completed_handlers(self): + completed = [k for k, v in self.reply_handlers.items() if v.handler is None or v.handler.completed] + if len(completed) == 0: + return + + for c in completed: + handler = self.reply_handlers[c] + del self.reply_handlers[c] + try: + await handler.handler.finalize() + except Exception: + logger.exception("handler finalize failed") + + logger.info(f"removed {len(completed)} completed handlers (remaining: {len(self.reply_handlers)})") + def get_text_channel_by_name(self, channel_name) -> discord.TextChannel: for channel in self.client.get_all_channels(): if channel.type == discord.ChannelType.text and channel.name == channel_name: diff --git a/bot/bot_base.py b/bot/bot_base.py new file mode 100644 index 00000000..52b98f2c --- /dev/null +++ b/bot/bot_base.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import asyncio +from abc import ABC +from dataclasses import dataclass +from typing import Any + +import discord +from channel_handlers import ChannelHandlerBase +from loguru import logger +from message_templates import MessageTemplates + + +@dataclass +class ReplyHandlerInfo: + msg_id: int + handler_task: asyncio.Task + handler: ChannelHandlerBase + + +class BotBase(ABC): + bot_channel_name: str + debug: bool + client: discord.Client + loop: asyncio.BaseEventLoop + owner_id: int + bot_channel: discord.TextChannel + templates: MessageTemplates + reply_handlers: dict[int, ReplyHandlerInfo] + + def __init__(self): + self.reply_handlers = {} # handlers by msg_id + + def ensure_bot_channel(self) -> None: + if self.bot_channel is None: + raise RuntimeError(f"bot channel '{self.bot_channel_name}' not found") + + async def post(self, content: str, view: discord.ui.View = None) -> discord.Message: + self.ensure_bot_channel() + return await self.bot_channel.send(content=content, view=view) + + async def post_template(self, name: str, *, view: discord.ui.View = None, **kwargs: Any) -> discord.Message: + logger.debug(f"rendering {name}") + text = self.templates.render(name, **kwargs) + return await self.post(text, view) + + def register_reply_handler(self, msg_id: int, handler: ChannelHandlerBase): + if msg_id in self.reply_handlers: + raise RuntimeError(f"Handler already registered for msg_id: {msg_id}") + task = asyncio.create_task(coro=handler.handler_loop(), name=f"reply_handler(msg_id={msg_id})") + task.add_done_callback(lambda t: handler.on_completed()) + self.reply_handlers[msg_id] = ReplyHandlerInfo(msg_id=msg_id, handler_task=task, handler=handler) diff --git a/bot/channel_handlers.py b/bot/channel_handlers.py new file mode 100644 index 00000000..74d88414 --- /dev/null +++ b/bot/channel_handlers.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +import asyncio +from abc import ABC, abstractmethod +from datetime import datetime + +import discord +from loguru import logger + + +class ChannelExpiredException(Exception): + pass + + +class ChannelHandlerBase(ABC): + queue: asyncio.Queue + completed: bool + expiry_date: datetime + expired: bool + + def __init__(self, *, expiry_date: datetime = None): + self.expiry_date = expiry_date + self.expired = False + self.queue = asyncio.Queue() + self.completed = False + + async def read(self) -> discord.Message: + """Call this method to read the next message from the user in the handler method.""" + msg = await self.queue.get() + if msg is None and self.expired: + raise ChannelExpiredException() + return msg + + def on_reply(self, message: discord.Message) -> None: + self.queue.put_nowait(message) + + def on_expire(self) -> None: + logger.info("ChannelHandler: on_expire") + self.expired = True + self.queue.put_nowait(None) + + def on_completed(self) -> None: + logger.info("ChannelHandler: on_completed") + self.completed = True + + def tick(self, now: datetime): + if now > self.expiry_date and not self.expired: + self.on_expire() + + @abstractmethod + async def handler_loop(self): + ... + + async def finalize(self): + pass + + +class AutoDestructThreadHandler(ChannelHandlerBase): + first_message: discord.Message + thread: discord.Thread + + def __init__(self, *, expiry_date: datetime = None): + super().__init__(expiry_date=expiry_date) + + async def read(self) -> discord.Message: + try: + return await super().read() + except ChannelExpiredException: + await self.cleanup() + raise + + async def cleanup(self): + if self.thread: + logger.debug(f"[expired] deleting thread: {self.thread.name}") + await self.thread.delete() + self.thread = None + if self.first_message: + logger.debug(f"[expired] deleting first_message: {self.first_message.content}") + await self.first_message.delete() + self.first_message = None + + async def finalize(self): + await self.cleanup() + return await super().finalize() diff --git a/bot/message_templates.py b/bot/message_templates.py new file mode 100644 index 00000000..df3ef1ac --- /dev/null +++ b/bot/message_templates.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +import jinja2 +from loguru import logger + + +class MessageTemplates: + def __init__(self, template_dir="./templates"): + self.env = jinja2.Environment( + loader=jinja2.FileSystemLoader(template_dir), + autoescape=jinja2.select_autoescape(disabled_extensions=("msg",), default=False, default_for_string=False), + ) + + def render(self, template_name, **kwargs): + template = self.env.get_template(template_name) + txt = template.render(kwargs) + logger.debug(txt) + + return txt diff --git a/bot/task_handlers.py b/bot/task_handlers.py new file mode 100644 index 00000000..a18f666d --- /dev/null +++ b/bot/task_handlers.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from abc import abstractmethod +from datetime import timedelta + +import discord +from bot_base import BotBase +from channel_handlers import AutoDestructThreadHandler +from loguru import logger +from oasst_shared.schemas import protocol as protocol_schema +from utils import utcnow + + +class Questionnaire(discord.ui.Modal, title="Questionnaire Response"): + name = discord.ui.TextInput(label="Name") + answer = discord.ui.TextInput(label="Answer", style=discord.TextStyle.paragraph) + + async def on_submit(self, interaction: discord.Interaction): + await interaction.response.send_message(f"Thanks for your response, {self.name}!", ephemeral=True) + + +class ChannelTaskBase(AutoDestructThreadHandler): + thread_name: str = "Replies" + expires_after: timedelta = timedelta(minutes=5) + + async def start(self, bot: BotBase, task: protocol_schema.Task) -> discord.Message: + self.bot = bot + self.task = task + msg = await self.send_first_message() + self.first_message = msg + self.thread = await bot.bot_channel.create_thread(message=discord.Object(msg.id), name=self.thread_name) + self.expiry_date = utcnow() + self.expires_after if self.expires_after else None + bot.register_reply_handler(msg_id=msg.id, handler=self) + return msg + + @abstractmethod + async def send_first_message(self) -> discord.message: + ... + + +class SummarizeStoryHandler(ChannelTaskBase): + task: protocol_schema.SummarizeStoryTask + thread_name: str = "Summaries" + + async def send_first_message(self) -> discord.message: + return await self.bot.post_template("task_summarize_story.msg", task=self.task) + + async def handler_loop(self): + msg = await self.read() + print("received: ", msg, type(msg)) + logger.info("on_summarize_story_reply") + await msg.add_reaction("✅") + + +class InitialPromptHandler(ChannelTaskBase): + task: protocol_schema.InitialPromptTask + thread_name: str = "Prompts" + + async def send_first_message(self) -> discord.message: + return await self.bot.post_template("task_initial_prompt.msg", task=self.task) + + async def handler_loop(self): + msg = await self.read() + logger.info("on_initial_prompt_reply") + await msg.add_reaction("✅") + + +class UserReplyHandler(ChannelTaskBase): + task: protocol_schema.UserReplyTask + thread_name: str = "User replies" + + async def send_first_message(self) -> discord.message: + return await self.bot.post_template("task_user_reply.msg", task=self.task) + + async def handler_loop(self): + msg = await self.read() + logger.info("on_user_reply_reply") + await msg.add_reaction("✅") + + +class AssistantReplyHandler(ChannelTaskBase): + task: protocol_schema.AssistantReplyTask + thread_name: str = "Assistant replies" + + async def send_first_message(self) -> discord.message: + return await self.bot.post_template("task_assistant_reply.msg", task=self.task) + + async def handler_loop(self): + msg = await self.read() + logger.info("on_assistant_reply_reply") + await msg.add_reaction("✅") + + +class RankInitialPromptsHandler(ChannelTaskBase): + task: protocol_schema.RankInitialPromptsTask + thread_name: str = "User Responses" + + async def send_first_message(self) -> discord.message: + return await self.bot.post_template("task_rank_initial_prompts.msg", task=self.task) + + async def handler_loop(self): + msg = await self.read() + logger.info("on_rank_initial_prompts_reply") + await msg.add_reaction("✅") + + +class RankConversationsHandler(ChannelTaskBase): + task: protocol_schema.RankConversationRepliesTask + thread_name: str = "Rankings" + + async def send_first_message(self) -> discord.message: + return await self.bot.post_template("task_rank_conversation_replies.msg", task=self.task) + + async def handler_loop(self): + msg = await self.read() + logger.info("on_rank_conversation_reply") + await msg.add_reaction("✅") + + +class RatingButton(discord.ui.Button): + def __init__(self, label, value, response_handler): + super().__init__(label=label, style=discord.ButtonStyle.green) + self.value = value + self.response_handler = response_handler + + async def callback(self, interaction): + await self.response_handler(self.value, interaction) + + +def generate_rating_view(lo: int, hi: int, response_handler) -> discord.ui.View: + view = discord.ui.View() + for i in range(lo, hi + 1): + view.add_item(RatingButton(str(i), i, response_handler)) + return view + + +class RateSummaryHandler(ChannelTaskBase): + task: protocol_schema.RateSummaryTask + thread_name: str = "Rate" + + async def send_first_message(self) -> discord.message: + async def rating_response_handler(score, interaction: discord.Interaction): + logger.info("rating_response_handler", score) + await interaction.response.send_message(f"got your feedback: {score}") + + view = generate_rating_view(self.task.scale.min, self.task.scale.max, rating_response_handler) + return await self.bot.post_template("task_rate_summary.msg", view=view, task=self.task) + + async def handler_loop(self): + msg = await self.read() + logger.info("on_rate_summary_reply") + await msg.add_reaction("✅") diff --git a/bot/templates/boot.msg b/bot/templates/boot.msg index 0561c8be..a3629715 100644 --- a/bot/templates/boot.msg +++ b/bot/templates/boot.msg @@ -10,4 +10,4 @@ ________ __ git hash: {{git_hash}} debug_mode: {{debug}} ``` -https://github.com/LAION-AI/Open-Assistant \ No newline at end of file +https://github.com/LAION-AI/Open-Assistant diff --git a/bot/templates/task_initial_prompt.msg b/bot/templates/task_initial_prompt.msg index dc3b10d3..47cf0f45 100644 --- a/bot/templates/task_initial_prompt.msg +++ b/bot/templates/task_initial_prompt.msg @@ -1,4 +1,4 @@ Please provide an initial prompt to the assistant. -{% if task.hint %} -Hint: {task.hint}" +{% if task.hint is not none %} +Hint: {{task.hint}} {% endif %} \ No newline at end of file diff --git a/bot/templates/task_summarize_story.msg b/bot/templates/task_summarize_story.msg new file mode 100644 index 00000000..24753841 --- /dev/null +++ b/bot/templates/task_summarize_story.msg @@ -0,0 +1,2 @@ +Summarize to the following story: +{{task.story}} diff --git a/pyproject.toml b/pyproject.toml index 30541eec..83b614a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,4 +11,3 @@ line_length = 120 [tool.black] line-length = 120 target-version = ['py310'] -exclude = ["bot/templates"] From cad6a450c01f1daedf820247d7124222e5895649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Thu, 22 Dec 2022 16:11:41 +0100 Subject: [PATCH 4/8] add simple /help and !help command handling --- bot/bot.py | 58 +++++++++++++++++++++++++-------- bot/bot_base.py | 16 ++++++--- bot/channel_handlers.py | 6 ++-- bot/task_handlers.py | 72 +++++++++++++++++++++++++---------------- bot/templates/help.msg | 15 +++++++++ 5 files changed, 118 insertions(+), 49 deletions(-) create mode 100644 bot/templates/help.msg diff --git a/bot/bot.py b/bot/bot.py index b3e2e309..a8df4cf5 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -84,7 +84,8 @@ class OpenAssistantBot(BotBase): @self.tree.command() async def help(interaction: discord.Interaction): """Sends the user a list of all available commands""" - await interaction.response.send_message(f"help command by {interaction.user.name}") + await self.post_help(interaction.user) + await interaction.response.send_message(f"@{interaction.user.display_name}, I've sent you a PM.") @self.tree.command() async def work(interaction: discord.Interaction): @@ -95,6 +96,10 @@ class OpenAssistantBot(BotBase): q = task_handlers.Questionnaire() await interaction.response.send_modal(q) + async def post_help(self, user: discord.abc.User) -> discord.Message: + is_bot_owner = user.id == self.owner_id + return await self.post_template("help.msg", channel=user, is_bot_owner=is_bot_owner) + async def post_boot_message(self) -> discord.Message: return await self.post_template( "boot.msg", bot_name=BOT_NAME, version=__version__, git_hash=get_git_head_hash(), debug=self.debug @@ -104,7 +109,13 @@ class OpenAssistantBot(BotBase): return await self.post_template("welcome.msg") async def delete_all_old_bot_messages(self) -> None: - logger.info("Begin deleting old bot messages.") + logger.info("Deleting old threads...") + for thread in self.bot_channel.threads: + if thread.owner_id == self.client.user.id: + await thread.delete() + logger.info("Completed deleting old theards.") + + logger.info("Deleting old bot messages...") look_until = utcnow() - timedelta(days=365) async for msg in self.bot_channel.history(limit=None): msg: discord.Message @@ -119,7 +130,7 @@ class OpenAssistantBot(BotBase): return msg async def next_task(self): - task_type = protocol_schema.TaskRequestType.rate_summary + task_type = protocol_schema.TaskRequestType.random task = self.backend.fetch_task(task_type, user=None) await self.print_separtor("New Task") @@ -199,22 +210,47 @@ class OpenAssistantBot(BotBase): command_text: str = message.content command_text = command_text[1:] match command_text: - case "sync" | "sync.guild" | "sync.copy_global" | "sync.clear_guild" | "sync.clear_guild": + case "help" | "?": + await self.post_help(user=message.author) + case "sync" | "sync.guild" | "sync.copy_global" | "sync.clear_guild": if is_owner: await self._sync(command_text, message) case _: await message.reply(f"unknown command: {command_text}") + def recipient_filter(self, message: discord.Message) -> bool: + channel = message.channel + + if ( + message.channel.type == discord.ChannelType.private + or message.channel.type == discord.ChannelType.private_thread + ): + return True + + if ( + message.channel.type == discord.ChannelType.text + or message.channel.type == discord.ChannelType.public_thread + ): + while channel: + if self.bot_channel and channel.id == self.bot_channel.id: + return True + channel = channel.parent + + return False + async def handle_message(self, message: discord.Message): + if not self.recipient_filter(message): + return + user_id = message.author.id user_display_name = message.author.name + logger.debug( + f"{message.type} {message.channel.type} from ({user_display_name}) {user_id}: {message.content} ({type(message.content)})" + ) + command_prefix = "!" - if ( - message.channel.type == discord.ChannelType.private - and message.type == discord.MessageType.default - and message.content.startswith(command_prefix) - ): + if message.type == discord.MessageType.default and message.content.startswith(command_prefix): is_owner = self.owner_id and user_id == self.owner_id await self.handle_command(message, is_owner) @@ -228,10 +264,6 @@ class OpenAssistantBot(BotBase): if handler and not handler.handler.completed: handler.handler.on_reply(message) - logger.debug( - f"{message.type} {message.channel.type} from ({user_display_name}) {user_id}: {message.content} ({type(message.content)})" - ) - async def remove_completed_handlers(self): completed = [k for k, v in self.reply_handlers.items() if v.handler is None or v.handler.completed] if len(completed) == 0: diff --git a/bot/bot_base.py b/bot/bot_base.py index 52b98f2c..7ac2e2ac 100644 --- a/bot/bot_base.py +++ b/bot/bot_base.py @@ -36,14 +36,20 @@ class BotBase(ABC): if self.bot_channel is None: raise RuntimeError(f"bot channel '{self.bot_channel_name}' not found") - async def post(self, content: str, view: discord.ui.View = None) -> discord.Message: - self.ensure_bot_channel() - return await self.bot_channel.send(content=content, view=view) + async def post( + self, content: str, *, view: discord.ui.View = None, channel: discord.abc.Messageable = None + ) -> discord.Message: + if channel is None: + self.ensure_bot_channel() + channel = self.bot_channel + return await channel.send(content=content, view=view) - async def post_template(self, name: str, *, view: discord.ui.View = None, **kwargs: Any) -> discord.Message: + async def post_template( + self, name: str, *, view: discord.ui.View = None, channel: discord.abc.Messageable = None, **kwargs: Any + ) -> discord.Message: logger.debug(f"rendering {name}") text = self.templates.render(name, **kwargs) - return await self.post(text, view) + return await self.post(text, view=view, channel=channel) def register_reply_handler(self, msg_id: int, handler: ChannelHandlerBase): if msg_id in self.reply_handlers: diff --git a/bot/channel_handlers.py b/bot/channel_handlers.py index 74d88414..b92d2273 100644 --- a/bot/channel_handlers.py +++ b/bot/channel_handlers.py @@ -66,15 +66,15 @@ class AutoDestructThreadHandler(ChannelHandlerBase): return await super().read() except ChannelExpiredException: await self.cleanup() - raise async def cleanup(self): + logger.debug("AutoDestructThreadHandler.cleanup") if self.thread: - logger.debug(f"[expired] deleting thread: {self.thread.name}") + logger.debug(f"deleting thread: {self.thread.name}") await self.thread.delete() self.thread = None if self.first_message: - logger.debug(f"[expired] deleting first_message: {self.first_message.content}") + logger.debug(f"deleting first_message: {self.first_message.content}") await self.first_message.delete() self.first_message = None diff --git a/bot/task_handlers.py b/bot/task_handlers.py index a18f666d..dd81bbde 100644 --- a/bot/task_handlers.py +++ b/bot/task_handlers.py @@ -30,10 +30,14 @@ class ChannelTaskBase(AutoDestructThreadHandler): msg = await self.send_first_message() self.first_message = msg self.thread = await bot.bot_channel.create_thread(message=discord.Object(msg.id), name=self.thread_name) + await self.on_thread_created(self.thread) self.expiry_date = utcnow() + self.expires_after if self.expires_after else None bot.register_reply_handler(msg_id=msg.id, handler=self) return msg + async def on_thread_created(self, thread: discord.Thread) -> None: + pass + @abstractmethod async def send_first_message(self) -> discord.message: ... @@ -47,10 +51,11 @@ class SummarizeStoryHandler(ChannelTaskBase): return await self.bot.post_template("task_summarize_story.msg", task=self.task) async def handler_loop(self): - msg = await self.read() - print("received: ", msg, type(msg)) - logger.info("on_summarize_story_reply") - await msg.add_reaction("✅") + while True: + msg = await self.read() + print("received: ", msg, type(msg)) + logger.info("on_summarize_story_reply") + await msg.add_reaction("✅") class InitialPromptHandler(ChannelTaskBase): @@ -61,9 +66,10 @@ class InitialPromptHandler(ChannelTaskBase): return await self.bot.post_template("task_initial_prompt.msg", task=self.task) async def handler_loop(self): - msg = await self.read() - logger.info("on_initial_prompt_reply") - await msg.add_reaction("✅") + while True: + msg = await self.read() + logger.info("on_initial_prompt_reply") + await msg.add_reaction("✅") class UserReplyHandler(ChannelTaskBase): @@ -74,9 +80,10 @@ class UserReplyHandler(ChannelTaskBase): return await self.bot.post_template("task_user_reply.msg", task=self.task) async def handler_loop(self): - msg = await self.read() - logger.info("on_user_reply_reply") - await msg.add_reaction("✅") + while True: + msg = await self.read() + logger.info("on_user_reply_reply") + await msg.add_reaction("✅") class AssistantReplyHandler(ChannelTaskBase): @@ -87,9 +94,10 @@ class AssistantReplyHandler(ChannelTaskBase): return await self.bot.post_template("task_assistant_reply.msg", task=self.task) async def handler_loop(self): - msg = await self.read() - logger.info("on_assistant_reply_reply") - await msg.add_reaction("✅") + while True: + msg = await self.read() + logger.info("on_assistant_reply_reply") + await msg.add_reaction("✅") class RankInitialPromptsHandler(ChannelTaskBase): @@ -100,9 +108,10 @@ class RankInitialPromptsHandler(ChannelTaskBase): return await self.bot.post_template("task_rank_initial_prompts.msg", task=self.task) async def handler_loop(self): - msg = await self.read() - logger.info("on_rank_initial_prompts_reply") - await msg.add_reaction("✅") + while True: + msg = await self.read() + logger.info("on_rank_initial_prompts_reply") + await msg.add_reaction("✅") class RankConversationsHandler(ChannelTaskBase): @@ -113,9 +122,10 @@ class RankConversationsHandler(ChannelTaskBase): return await self.bot.post_template("task_rank_conversation_replies.msg", task=self.task) async def handler_loop(self): - msg = await self.read() - logger.info("on_rank_conversation_reply") - await msg.add_reaction("✅") + while True: + msg = await self.read() + logger.info("on_rank_conversation_reply") + await msg.add_reaction("✅") class RatingButton(discord.ui.Button): @@ -139,15 +149,21 @@ class RateSummaryHandler(ChannelTaskBase): task: protocol_schema.RateSummaryTask thread_name: str = "Rate" - async def send_first_message(self) -> discord.message: - async def rating_response_handler(score, interaction: discord.Interaction): - logger.info("rating_response_handler", score) - await interaction.response.send_message(f"got your feedback: {score}") + async def _rating_response_handler(self, score, interaction: discord.Interaction): + logger.info("rating_response_handler", score) + if self.thread: + await self.thread.send(f"{interaction.user.name} got your feedback: {score}") + await interaction.response.send_message(f"got your feedback: {score}") - view = generate_rating_view(self.task.scale.min, self.task.scale.max, rating_response_handler) - return await self.bot.post_template("task_rate_summary.msg", view=view, task=self.task) + async def send_first_message(self) -> discord.message: + return await self.bot.post("first message") + + async def on_thread_created(self, thread: discord.Thread) -> None: + view = generate_rating_view(self.task.scale.min, self.task.scale.max, self._rating_response_handler) + return await self.bot.post_template("task_rate_summary.msg", view=view, channel=thread, task=self.task) async def handler_loop(self): - msg = await self.read() - logger.info("on_rate_summary_reply") - await msg.add_reaction("✅") + while True: + msg = await self.read() + logger.info("on_rate_summary_reply") + await msg.add_reaction("✅") diff --git a/bot/templates/help.msg b/bot/templates/help.msg new file mode 100644 index 00000000..ca033c47 --- /dev/null +++ b/bot/templates/help.msg @@ -0,0 +1,15 @@ +**Open-Assistant Bot Help** + +Available slash-commands: + +`/work` Requests a new personalized human feedback task +`/help` Show this message + +{% if is_bot_owner %} +Commands for bot owners: + +`!sync` +`!sync.guild` +`!sync.copy_global` +`!sync.clear_guild` +{% endif %} \ No newline at end of file From 8a48722e7204e05926e7551dfb0f99bd472cacd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Thu, 22 Dec 2022 18:41:50 +0100 Subject: [PATCH 5/8] first api-interaction, fix auth_method unique-index --- ...f_add_auth_method_to_ix_person_username.py | 30 +++++ backend/oasst_backend/models/person.py | 2 +- backend/oasst_backend/prompt_repository.py | 7 +- bot/api_client.py | 2 +- bot/bot.py | 14 +-- bot/bot_base.py | 2 + bot/channel_handlers.py | 10 +- bot/task_handlers.py | 108 ++++++++++++++---- bot/templates/task_summarize_story_teaser.msg | 5 + bot/utils.py | 35 ++++++ 10 files changed, 172 insertions(+), 43 deletions(-) create mode 100644 backend/alembic/versions/2022_12_22_1835-0daec5f8135f_add_auth_method_to_ix_person_username.py create mode 100644 bot/templates/task_summarize_story_teaser.msg diff --git a/backend/alembic/versions/2022_12_22_1835-0daec5f8135f_add_auth_method_to_ix_person_username.py b/backend/alembic/versions/2022_12_22_1835-0daec5f8135f_add_auth_method_to_ix_person_username.py new file mode 100644 index 00000000..c65b8319 --- /dev/null +++ b/backend/alembic/versions/2022_12_22_1835-0daec5f8135f_add_auth_method_to_ix_person_username.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""add_auth_method_to_ix_person_username + +Revision ID: 0daec5f8135f +Revises: 6368515778c5 +Create Date: 2022-12-22 18:35:59.609013 + +""" +import sqlalchemy as sa # noqa: F401 +from alembic import op + +# revision identifiers, used by Alembic. +revision = "0daec5f8135f" +down_revision = "6368515778c5" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_person_username", table_name="person") + op.create_index("ix_person_username", "person", ["api_client_id", "username", "auth_method"], unique=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_person_username", table_name="person") + op.create_index("ix_person_username", "person", ["api_client_id", "username"], unique=False) + # ### end Alembic commands ### diff --git a/backend/oasst_backend/models/person.py b/backend/oasst_backend/models/person.py index 57f134a4..f01f85f0 100644 --- a/backend/oasst_backend/models/person.py +++ b/backend/oasst_backend/models/person.py @@ -10,7 +10,7 @@ from sqlmodel import Field, Index, SQLModel class Person(SQLModel, table=True): __tablename__ = "person" - __table_args__ = (Index("ix_person_username", "api_client_id", "username", unique=True),) + __table_args__ = (Index("ix_person_username", "api_client_id", "username", "auth_method", unique=True),) id: Optional[UUID] = Field( sa_column=sa.Column( diff --git a/backend/oasst_backend/prompt_repository.py b/backend/oasst_backend/prompt_repository.py index 9f7bb1dd..b0063cdf 100644 --- a/backend/oasst_backend/prompt_repository.py +++ b/backend/oasst_backend/prompt_repository.py @@ -32,7 +32,12 @@ class PromptRepository: ) if person is None: # user is unknown, create new record - person = Person(username=user.id, display_name=user.display_name, api_client_id=self.api_client.id) + person = Person( + username=user.id, + display_name=user.display_name, + api_client_id=self.api_client.id, + auth_method=user.auth_method, + ) self.db.add(person) self.db.commit() self.db.refresh(person) diff --git a/bot/api_client.py b/bot/api_client.py index 19a62188..1de6bb17 100644 --- a/bot/api_client.py +++ b/bot/api_client.py @@ -69,6 +69,6 @@ class ApiClient: req = protocol_schema.TaskNAck(reason=reason) return self.post(f"/api/v1/tasks/{task_id}/nack", req.dict()) - def post_interaction(self, interaction: protocol_schema.Interaction) -> protocol_schema.TaskDone: + def post_interaction(self, interaction: protocol_schema.Interaction) -> protocol_schema.Task: data = self.post("/api/v1/tasks/interaction", interaction.dict()) return self._parse_task(data) diff --git a/bot/bot.py b/bot/bot.py index a8df4cf5..9f2f9247 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -16,7 +16,7 @@ from message_templates import MessageTemplates from oasst_shared.schemas import protocol as protocol_schema from utils import get_git_head_hash, utcnow -__version__ = "0.0.2" +__version__ = "0.0.3" BOT_NAME = "Open-Assistant Junior" @@ -61,8 +61,8 @@ class OpenAssistantBot(BotBase): logger.info(f"{client.user} is now running!") await self.delete_all_old_bot_messages() - if self.debug: - await self.post_boot_message() + # if self.debug: + # await self.post_boot_message() await self.post_welcome_message() client.loop.create_task(self.background_timer(), name="OpenAssistantBot.background_timer()") @@ -125,16 +125,10 @@ class OpenAssistantBot(BotBase): await msg.delete() logger.info("Completed deleting old bot messages.") - async def print_separtor(self, title: str) -> discord.Message: - msg: discord.Message = await self.bot_channel.send(f"\n:point_right: {title} :point_left:\n") - return msg - async def next_task(self): - task_type = protocol_schema.TaskRequestType.random + task_type = protocol_schema.TaskRequestType.summarize_story task = self.backend.fetch_task(task_type, user=None) - await self.print_separtor("New Task") - handler: task_handlers.ChannelTaskBase = None match task.type: case TaskType.summarize_story: diff --git a/bot/bot_base.py b/bot/bot_base.py index 7ac2e2ac..76eca22d 100644 --- a/bot/bot_base.py +++ b/bot/bot_base.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from typing import Any import discord +from api_client import ApiClient from channel_handlers import ChannelHandlerBase from loguru import logger from message_templates import MessageTemplates @@ -22,6 +23,7 @@ class ReplyHandlerInfo: class BotBase(ABC): bot_channel_name: str debug: bool + backend: ApiClient client: discord.Client loop: asyncio.BaseEventLoop owner_id: int diff --git a/bot/channel_handlers.py b/bot/channel_handlers.py index b92d2273..deed5049 100644 --- a/bot/channel_handlers.py +++ b/bot/channel_handlers.py @@ -13,15 +13,13 @@ class ChannelExpiredException(Exception): class ChannelHandlerBase(ABC): queue: asyncio.Queue - completed: bool + completed: bool = False expiry_date: datetime - expired: bool + expired: bool = False def __init__(self, *, expiry_date: datetime = None): self.expiry_date = expiry_date - self.expired = False self.queue = asyncio.Queue() - self.completed = False async def read(self) -> discord.Message: """Call this method to read the next message from the user in the handler method.""" @@ -55,8 +53,8 @@ class ChannelHandlerBase(ABC): class AutoDestructThreadHandler(ChannelHandlerBase): - first_message: discord.Message - thread: discord.Thread + first_message: discord.Message = None + thread: discord.Thread = None def __init__(self, *, expiry_date: datetime = None): super().__init__(expiry_date=expiry_date) diff --git a/bot/task_handlers.py b/bot/task_handlers.py index dd81bbde..261860ab 100644 --- a/bot/task_handlers.py +++ b/bot/task_handlers.py @@ -5,11 +5,12 @@ from abc import abstractmethod from datetime import timedelta import discord +from api_client import ApiClient from bot_base import BotBase from channel_handlers import AutoDestructThreadHandler from loguru import logger from oasst_shared.schemas import protocol as protocol_schema -from utils import utcnow +from utils import DiscordTimestampStyle, discord_timestamp, utcnow class Questionnaire(discord.ui.Modal, title="Questionnaire Response"): @@ -23,15 +24,23 @@ class Questionnaire(discord.ui.Modal, title="Questionnaire Response"): class ChannelTaskBase(AutoDestructThreadHandler): thread_name: str = "Replies" expires_after: timedelta = timedelta(minutes=5) + backend: ApiClient async def start(self, bot: BotBase, task: protocol_schema.Task) -> discord.Message: - self.bot = bot - self.task = task - msg = await self.send_first_message() - self.first_message = msg - self.thread = await bot.bot_channel.create_thread(message=discord.Object(msg.id), name=self.thread_name) - await self.on_thread_created(self.thread) - self.expiry_date = utcnow() + self.expires_after if self.expires_after else None + try: + self.bot = bot + self.task = task + self.backend = bot.backend + self.expiry_date = utcnow() + self.expires_after if self.expires_after else None + msg = await self.send_first_message() + self.first_message = msg + self.thread = await bot.bot_channel.create_thread(message=discord.Object(msg.id), name=self.thread_name) + await self.on_thread_created(self.thread) + except Exception: + logger.exception("start task failed") + await self.cleanup() # try to cleanup messag or thread + raise + bot.register_reply_handler(msg_id=msg.id, handler=self) return msg @@ -42,20 +51,57 @@ class ChannelTaskBase(AutoDestructThreadHandler): async def send_first_message(self) -> discord.message: ... + def to_api_user(self, user: discord.User) -> protocol_schema.User: + return protocol_schema.User(auth_method="discord", id=user.id, display_name=user.display_name) + + async def post_interaction(self, interaction: protocol_schema.Interaction) -> protocol_schema.Task: + api_response = await self.backend.post_interaction(interaction) + if api_response.type != "task_done": + # multi-step tasks are not supported yet + logger.error(f"multi-step tasks are not supported yet (got response type: {api_response.type})") + raise RuntimeError("Unexpected response from backend received") + return api_response + + def post_text_reply_to_post(self, user_msg: discord.Message) -> protocol_schema.Task: + return self.backend.post_interaction( + protocol_schema.TextReplyToPost( + post_id=str(self.first_message.id), + user_post_id=str(user_msg.id), + user=self.to_api_user(user_msg.author), + text=user_msg.content, + ) + ) + + async def handle_text_reply_to_post(self, user_msg: discord.Member) -> protocol_schema.Task: + try: + self.post_text_reply_to_post(user_msg) + await user_msg.add_reaction("✅") + except Exception as e: + await user_msg.add_reaction("❌") + await user_msg.reply(f"❌ Error communicating with backend: {e}") + class SummarizeStoryHandler(ChannelTaskBase): task: protocol_schema.SummarizeStoryTask thread_name: str = "Summaries" async def send_first_message(self) -> discord.message: - return await self.bot.post_template("task_summarize_story.msg", task=self.task) + + expiry_time = discord_timestamp(self.expiry_date, DiscordTimestampStyle.long_time) + expiry_relatve = discord_timestamp(self.expiry_date, DiscordTimestampStyle.relative_time) + msg = await self.bot.post_template( + "task_summarize_story_teaser.msg", task=self.task, expiry_time=expiry_time, expiry_relatve=expiry_relatve + ) + self.backend.ack_task(self.task.id, str(msg.id)) + return msg + + async def on_thread_created(self, thread: discord.Thread) -> None: + await self.bot.post_template("task_summarize_story.msg", channel=thread, task=self.task) async def handler_loop(self): while True: msg = await self.read() - print("received: ", msg, type(msg)) - logger.info("on_summarize_story_reply") - await msg.add_reaction("✅") + await self.handle_text_reply_to_post(msg) class InitialPromptHandler(ChannelTaskBase): @@ -63,13 +109,14 @@ class InitialPromptHandler(ChannelTaskBase): thread_name: str = "Prompts" async def send_first_message(self) -> discord.message: - return await self.bot.post_template("task_initial_prompt.msg", task=self.task) + msg = await self.bot.post_template("task_initial_prompt.msg", task=self.task) + self.backend.ack_task(self.task.id, str(msg.id)) + return msg async def handler_loop(self): while True: msg = await self.read() - logger.info("on_initial_prompt_reply") - await msg.add_reaction("✅") + await self.handle_text_reply_to_post(msg) class UserReplyHandler(ChannelTaskBase): @@ -77,13 +124,14 @@ class UserReplyHandler(ChannelTaskBase): thread_name: str = "User replies" async def send_first_message(self) -> discord.message: - return await self.bot.post_template("task_user_reply.msg", task=self.task) + msg = await self.bot.post_template("task_user_reply.msg", task=self.task) + self.backend.ack_task(self.task.id, str(msg.id)) + return msg async def handler_loop(self): while True: msg = await self.read() - logger.info("on_user_reply_reply") - await msg.add_reaction("✅") + await self.handle_text_reply_to_post(msg) class AssistantReplyHandler(ChannelTaskBase): @@ -91,13 +139,19 @@ class AssistantReplyHandler(ChannelTaskBase): thread_name: str = "Assistant replies" async def send_first_message(self) -> discord.message: - return await self.bot.post_template("task_assistant_reply.msg", task=self.task) + msg = await self.bot.post_template("task_assistant_reply.msg", task=self.task) + self.backend.ack_task(self.task.id, str(msg.id)) + return msg async def handler_loop(self): while True: msg = await self.read() - logger.info("on_assistant_reply_reply") - await msg.add_reaction("✅") + try: + self.post_text_reply_to_post(msg) + await msg.add_reaction("✅") + except Exception as e: + await msg.add_reaction("❌") + await msg.reply(f"❌ Error communicating with backend: {e}") class RankInitialPromptsHandler(ChannelTaskBase): @@ -105,7 +159,9 @@ class RankInitialPromptsHandler(ChannelTaskBase): thread_name: str = "User Responses" async def send_first_message(self) -> discord.message: - return await self.bot.post_template("task_rank_initial_prompts.msg", task=self.task) + msg = await self.bot.post_template("task_rank_initial_prompts.msg", task=self.task) + self.backend.ack_task(self.task.id, str(msg.id)) + return msg async def handler_loop(self): while True: @@ -119,7 +175,9 @@ class RankConversationsHandler(ChannelTaskBase): thread_name: str = "Rankings" async def send_first_message(self) -> discord.message: - return await self.bot.post_template("task_rank_conversation_replies.msg", task=self.task) + msg = await self.bot.post_template("task_rank_conversation_replies.msg", task=self.task) + self.backend.ack_task(self.task.id, str(msg.id)) + return msg async def handler_loop(self): while True: @@ -156,7 +214,9 @@ class RateSummaryHandler(ChannelTaskBase): await interaction.response.send_message(f"got your feedback: {score}") async def send_first_message(self) -> discord.message: - return await self.bot.post("first message") + msg = await self.bot.post("first message") + self.backend.ack_task(self.task.id, str(msg.id)) + return msg async def on_thread_created(self, thread: discord.Thread) -> None: view = generate_rating_view(self.task.scale.min, self.task.scale.max, self._rating_response_handler) diff --git a/bot/templates/task_summarize_story_teaser.msg b/bot/templates/task_summarize_story_teaser.msg new file mode 100644 index 00000000..3493982b --- /dev/null +++ b/bot/templates/task_summarize_story_teaser.msg @@ -0,0 +1,5 @@ +:point_right: **Challenge: Summarize Story :books: ** :point_left: + +:point_down: Work on this in the theard. + +:fire: Message will self-destruct at {{ expiry_time }} UTC ({{ expiry_relatve }}). \ No newline at end of file diff --git a/bot/utils.py b/bot/utils.py index 1a06b833..968e4498 100644 --- a/bot/utils.py +++ b/bot/utils.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import enum import subprocess from datetime import datetime @@ -15,3 +16,37 @@ def get_git_head_hash(): def utcnow() -> datetime: return datetime.now(pytz.UTC) + + +class DiscordTimestampStyle(str, enum.Enum): + """ + Timestamp Styles + + t 16:20 Short Time + T 16:20:30 Long Time + d 20/04/2021 Short Date + D 20 April 2021 Long Date + f * 20 April 2021 16:20 Short Date/Time + F Tuesday, 20 April 2021 16:20 Long Date/Time + R 2 months ago Relative Time + + See https://discord.com/developers/docs/reference#message-formatting-timestamp-styles + """ + + default = "" + short_time = "t" + long_time = "T" + short_date = "d" + long_date = "D" + short_date_time = "f" + long_date_time = "F" + relative_time = "R" + + +def discord_timestamp(d: datetime, style: DiscordTimestampStyle = DiscordTimestampStyle.default): + parts = ["") + return "".join(parts) From 3a96cb062b498e1569892ec9543a8de998c08065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Thu, 22 Dec 2022 18:58:19 +0100 Subject: [PATCH 6/8] fix duplicate ack_task api-calls --- backend/oasst_backend/api/v1/tasks.py | 2 +- bot/task_handlers.py | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/backend/oasst_backend/api/v1/tasks.py b/backend/oasst_backend/api/v1/tasks.py index dac2a9bd..7ec5aa96 100644 --- a/backend/oasst_backend/api/v1/tasks.py +++ b/backend/oasst_backend/api/v1/tasks.py @@ -169,8 +169,8 @@ def acknowledge_task( pr = PromptRepository(db, api_client, user=None) # here we store the post id in the database for the task - pr.bind_frontend_post_id(task_id=task_id, post_id=ack_request.post_id) logger.info(f"Frontend acknowledges task {task_id=}, {ack_request=}.") + pr.bind_frontend_post_id(task_id=task_id, post_id=ack_request.post_id) except Exception: logger.exception("Failed to acknowledge task.") diff --git a/bot/task_handlers.py b/bot/task_handlers.py index 261860ab..6f38fa88 100644 --- a/bot/task_handlers.py +++ b/bot/task_handlers.py @@ -86,13 +86,11 @@ class SummarizeStoryHandler(ChannelTaskBase): thread_name: str = "Summaries" async def send_first_message(self) -> discord.message: - expiry_time = discord_timestamp(self.expiry_date, DiscordTimestampStyle.long_time) expiry_relatve = discord_timestamp(self.expiry_date, DiscordTimestampStyle.relative_time) msg = await self.bot.post_template( "task_summarize_story_teaser.msg", task=self.task, expiry_time=expiry_time, expiry_relatve=expiry_relatve ) - self.backend.ack_task(self.task.id, str(msg.id)) return msg async def on_thread_created(self, thread: discord.Thread) -> None: @@ -110,7 +108,6 @@ class InitialPromptHandler(ChannelTaskBase): async def send_first_message(self) -> discord.message: msg = await self.bot.post_template("task_initial_prompt.msg", task=self.task) - self.backend.ack_task(self.task.id, str(msg.id)) return msg async def handler_loop(self): @@ -125,7 +122,6 @@ class UserReplyHandler(ChannelTaskBase): async def send_first_message(self) -> discord.message: msg = await self.bot.post_template("task_user_reply.msg", task=self.task) - self.backend.ack_task(self.task.id, str(msg.id)) return msg async def handler_loop(self): @@ -140,7 +136,6 @@ class AssistantReplyHandler(ChannelTaskBase): async def send_first_message(self) -> discord.message: msg = await self.bot.post_template("task_assistant_reply.msg", task=self.task) - self.backend.ack_task(self.task.id, str(msg.id)) return msg async def handler_loop(self): @@ -160,7 +155,6 @@ class RankInitialPromptsHandler(ChannelTaskBase): async def send_first_message(self) -> discord.message: msg = await self.bot.post_template("task_rank_initial_prompts.msg", task=self.task) - self.backend.ack_task(self.task.id, str(msg.id)) return msg async def handler_loop(self): @@ -176,7 +170,6 @@ class RankConversationsHandler(ChannelTaskBase): async def send_first_message(self) -> discord.message: msg = await self.bot.post_template("task_rank_conversation_replies.msg", task=self.task) - self.backend.ack_task(self.task.id, str(msg.id)) return msg async def handler_loop(self): @@ -215,7 +208,6 @@ class RateSummaryHandler(ChannelTaskBase): async def send_first_message(self) -> discord.message: msg = await self.bot.post("first message") - self.backend.ack_task(self.task.id, str(msg.id)) return msg async def on_thread_created(self, thread: discord.Thread) -> None: From 81e08e9dd22299cfdffa5d657183cd7ea62e70f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Thu, 22 Dec 2022 21:13:05 +0100 Subject: [PATCH 7/8] add teaser msgs & remaining task handling --- bot/bot.py | 8 +- bot/channel_handlers.py | 11 +- bot/task_handlers.py | 116 ++++++++++++------ bot/templates/task_summarize_story_teaser.msg | 5 - bot/templates/teaser_assistant_reply.msg | 3 + bot/templates/teaser_initial_prompt.msg | 3 + .../teaser_rank_conversation_replies.msg | 3 + bot/templates/teaser_rank_initial_prompts.msg | 3 + bot/templates/teaser_rate_summary.msg | 3 + bot/templates/teaser_summarize_story.msg | 3 + bot/templates/teaser_user_reply.msg | 3 + 11 files changed, 115 insertions(+), 46 deletions(-) delete mode 100644 bot/templates/task_summarize_story_teaser.msg create mode 100644 bot/templates/teaser_assistant_reply.msg create mode 100644 bot/templates/teaser_initial_prompt.msg create mode 100644 bot/templates/teaser_rank_conversation_replies.msg create mode 100644 bot/templates/teaser_rank_initial_prompts.msg create mode 100644 bot/templates/teaser_rate_summary.msg create mode 100644 bot/templates/teaser_summarize_story.msg create mode 100644 bot/templates/teaser_user_reply.msg diff --git a/bot/bot.py b/bot/bot.py index 9f2f9247..a19fdfe1 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -115,7 +115,7 @@ class OpenAssistantBot(BotBase): await thread.delete() logger.info("Completed deleting old theards.") - logger.info("Deleting old bot messages...") + logger.info("Deleting old messages...") look_until = utcnow() - timedelta(days=365) async for msg in self.bot_channel.history(limit=None): msg: discord.Message @@ -123,10 +123,10 @@ class OpenAssistantBot(BotBase): break if msg.author.id == self.client.user.id: await msg.delete() - logger.info("Completed deleting old bot messages.") + logger.info("Completed deleting old messages.") async def next_task(self): - task_type = protocol_schema.TaskRequestType.summarize_story + task_type = protocol_schema.TaskRequestType.random task = self.backend.fetch_task(task_type, user=None) handler: task_handlers.ChannelTaskBase = None @@ -166,7 +166,7 @@ class OpenAssistantBot(BotBase): if self.bot_channel: if now > next_fetch_task: - next_fetch_task = utcnow() + timedelta(seconds=600) + next_fetch_task = utcnow() + timedelta(seconds=60) try: await self.next_task() diff --git a/bot/channel_handlers.py b/bot/channel_handlers.py index deed5049..75f03c0e 100644 --- a/bot/channel_handlers.py +++ b/bot/channel_handlers.py @@ -23,9 +23,15 @@ class ChannelHandlerBase(ABC): async def read(self) -> discord.Message: """Call this method to read the next message from the user in the handler method.""" - msg = await self.queue.get() - if msg is None and self.expired: + if self.expired: raise ChannelExpiredException() + + msg = await self.queue.get() + if msg is None: + if self.expired: + raise ChannelExpiredException() + else: + raise RuntimeError("Unexpected None message read") return msg def on_reply(self, message: discord.Message) -> None: @@ -64,6 +70,7 @@ class AutoDestructThreadHandler(ChannelHandlerBase): return await super().read() except ChannelExpiredException: await self.cleanup() + raise async def cleanup(self): logger.debug("AutoDestructThreadHandler.cleanup") diff --git a/bot/task_handlers.py b/bot/task_handlers.py index 6f38fa88..04fd1d9a 100644 --- a/bot/task_handlers.py +++ b/bot/task_handlers.py @@ -7,7 +7,7 @@ from datetime import timedelta import discord from api_client import ApiClient from bot_base import BotBase -from channel_handlers import AutoDestructThreadHandler +from channel_handlers import AutoDestructThreadHandler, ChannelExpiredException from loguru import logger from oasst_shared.schemas import protocol as protocol_schema from utils import DiscordTimestampStyle, discord_timestamp, utcnow @@ -54,6 +54,13 @@ class ChannelTaskBase(AutoDestructThreadHandler): def to_api_user(self, user: discord.User) -> protocol_schema.User: return protocol_schema.User(auth_method="discord", id=user.id, display_name=user.display_name) + async def post_teaser_msg(self, template_name: str): + expiry_time = discord_timestamp(self.expiry_date, DiscordTimestampStyle.long_time) + expiry_relatve = discord_timestamp(self.expiry_date, DiscordTimestampStyle.relative_time) + return await self.bot.post_template( + template_name, task=self.task, expiry_time=expiry_time, expiry_relatve=expiry_relatve + ) + async def post_interaction(self, interaction: protocol_schema.Interaction) -> protocol_schema.Task: api_response = await self.backend.post_interaction(interaction) if api_response.type != "task_done": @@ -72,11 +79,37 @@ class ChannelTaskBase(AutoDestructThreadHandler): ) ) - async def handle_text_reply_to_post(self, user_msg: discord.Member) -> protocol_schema.Task: + async def handle_text_reply_to_post(self, user_msg: discord.Message) -> protocol_schema.Task: try: self.post_text_reply_to_post(user_msg) await user_msg.add_reaction("✅") + except ChannelExpiredException: + raise except Exception as e: + logger.exception("Error in handle_text_reply_to_post()") + await user_msg.add_reaction("❌") + await user_msg.reply(f"❌ Error communicating with backend: {e}") + + def post_ranking(self, user_msg: discord.Message, ranking: list[int]) -> protocol_schema.Task: + return self.backend.post_interaction( + protocol_schema.PostRanking( + post_id=str(self.first_message.id), + user_post_id=str(user_msg.id), + user=self.to_api_user(user_msg.author), + ranking=ranking, + ) + ) + + async def handle_ranking(self, user_msg: discord.Message) -> protocol_schema.Task: + try: + ranking_str = user_msg.content + ranking = [int(x) - 1 for x in ranking_str.split(",")] + self.post_ranking(user_msg, ranking=ranking) + await user_msg.add_reaction("✅") + except ChannelExpiredException: + raise + except Exception as e: + logger.exception("Error in handle_ranking()") await user_msg.add_reaction("❌") await user_msg.reply(f"❌ Error communicating with backend: {e}") @@ -86,12 +119,7 @@ class SummarizeStoryHandler(ChannelTaskBase): thread_name: str = "Summaries" async def send_first_message(self) -> discord.message: - expiry_time = discord_timestamp(self.expiry_date, DiscordTimestampStyle.long_time) - expiry_relatve = discord_timestamp(self.expiry_date, DiscordTimestampStyle.relative_time) - msg = await self.bot.post_template( - "task_summarize_story_teaser.msg", task=self.task, expiry_time=expiry_time, expiry_relatve=expiry_relatve - ) - return msg + return await self.post_teaser_msg("teaser_summarize_story.msg") async def on_thread_created(self, thread: discord.Thread) -> None: await self.bot.post_template("task_summarize_story.msg", channel=thread, task=self.task) @@ -107,8 +135,10 @@ class InitialPromptHandler(ChannelTaskBase): thread_name: str = "Prompts" async def send_first_message(self) -> discord.message: - msg = await self.bot.post_template("task_initial_prompt.msg", task=self.task) - return msg + return await self.post_teaser_msg("teaser_initial_prompt.msg") + + async def on_thread_created(self, thread: discord.Thread) -> None: + await self.bot.post_template("task_initial_prompt.msg", channel=thread, task=self.task) async def handler_loop(self): while True: @@ -121,8 +151,10 @@ class UserReplyHandler(ChannelTaskBase): thread_name: str = "User replies" async def send_first_message(self) -> discord.message: - msg = await self.bot.post_template("task_user_reply.msg", task=self.task) - return msg + return await self.post_teaser_msg("teaser_user_reply.msg") + + async def on_thread_created(self, thread: discord.Thread) -> None: + await self.bot.post_template("task_user_reply.msg", channel=thread, task=self.task) async def handler_loop(self): while True: @@ -135,18 +167,15 @@ class AssistantReplyHandler(ChannelTaskBase): thread_name: str = "Assistant replies" async def send_first_message(self) -> discord.message: - msg = await self.bot.post_template("task_assistant_reply.msg", task=self.task) - return msg + return await self.post_teaser_msg("teaser_assistant_reply.msg") + + async def on_thread_created(self, thread: discord.Thread) -> None: + await self.bot.post_template("task_assistant_reply.msg", channel=thread, task=self.task) async def handler_loop(self): while True: msg = await self.read() - try: - self.post_text_reply_to_post(msg) - await msg.add_reaction("✅") - except Exception as e: - await msg.add_reaction("❌") - await msg.reply(f"❌ Error communicating with backend: {e}") + await self.handle_text_reply_to_post(msg) class RankInitialPromptsHandler(ChannelTaskBase): @@ -154,14 +183,15 @@ class RankInitialPromptsHandler(ChannelTaskBase): thread_name: str = "User Responses" async def send_first_message(self) -> discord.message: - msg = await self.bot.post_template("task_rank_initial_prompts.msg", task=self.task) - return msg + return await self.post_teaser_msg("teaser_rank_initial_prompts.msg") + + async def on_thread_created(self, thread: discord.Thread) -> None: + await self.bot.post_template("task_rank_initial_prompts.msg", channel=thread, task=self.task) async def handler_loop(self): while True: msg = await self.read() - logger.info("on_rank_initial_prompts_reply") - await msg.add_reaction("✅") + await self.handle_ranking(msg) class RankConversationsHandler(ChannelTaskBase): @@ -169,14 +199,15 @@ class RankConversationsHandler(ChannelTaskBase): thread_name: str = "Rankings" async def send_first_message(self) -> discord.message: - msg = await self.bot.post_template("task_rank_conversation_replies.msg", task=self.task) - return msg + return await self.post_teaser_msg("teaser_rank_conversation_replies.msg") + + async def on_thread_created(self, thread: discord.Thread) -> None: + await self.bot.post_template("task_rank_conversation_replies.msg", channel=thread, task=self.task) async def handler_loop(self): while True: msg = await self.read() - logger.info("on_rank_conversation_reply") - await msg.add_reaction("✅") + await self.handle_ranking(msg) class RatingButton(discord.ui.Button): @@ -198,17 +229,31 @@ def generate_rating_view(lo: int, hi: int, response_handler) -> discord.ui.View: class RateSummaryHandler(ChannelTaskBase): task: protocol_schema.RateSummaryTask - thread_name: str = "Rate" + thread_name: str = "Ratings" async def _rating_response_handler(self, score, interaction: discord.Interaction): logger.info("rating_response_handler", score) if self.thread: - await self.thread.send(f"{interaction.user.name} got your feedback: {score}") - await interaction.response.send_message(f"got your feedback: {score}") + try: + self.backend.post_interaction( + protocol_schema.PostRating( + post_id=str(self.first_message.id), + user_post_id=str(interaction.id), + user=self.to_api_user(interaction.user), + rating=score, + ) + ) + await interaction.response.send_message( + f"Thanks {interaction.user.display_name}, got your feedback: {score}!" + ) + except ChannelExpiredException: + raise + except Exception as e: + logger.exception("Error in _rating_response_handler()") + interaction.response.send_message(f"❌ Error communicating with backend: {e}") async def send_first_message(self) -> discord.message: - msg = await self.bot.post("first message") - return msg + return await self.post_teaser_msg("teaser_rate_summary.msg") async def on_thread_created(self, thread: discord.Thread) -> None: view = generate_rating_view(self.task.scale.min, self.task.scale.max, self._rating_response_handler) @@ -217,5 +262,6 @@ class RateSummaryHandler(ChannelTaskBase): async def handler_loop(self): while True: msg = await self.read() - logger.info("on_rate_summary_reply") - await msg.add_reaction("✅") + logger.info(f"on_rate_summary_reply: {msg.content}") + await msg.add_reaction("❌") + await msg.reply("❌ Text intput not supported.") diff --git a/bot/templates/task_summarize_story_teaser.msg b/bot/templates/task_summarize_story_teaser.msg deleted file mode 100644 index 3493982b..00000000 --- a/bot/templates/task_summarize_story_teaser.msg +++ /dev/null @@ -1,5 +0,0 @@ -:point_right: **Challenge: Summarize Story :books: ** :point_left: - -:point_down: Work on this in the theard. - -:fire: Message will self-destruct at {{ expiry_time }} UTC ({{ expiry_relatve }}). \ No newline at end of file diff --git a/bot/templates/teaser_assistant_reply.msg b/bot/templates/teaser_assistant_reply.msg new file mode 100644 index 00000000..ef13ee02 --- /dev/null +++ b/bot/templates/teaser_assistant_reply.msg @@ -0,0 +1,3 @@ +:robot: **Challenge: Assistant Reply** + +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file diff --git a/bot/templates/teaser_initial_prompt.msg b/bot/templates/teaser_initial_prompt.msg new file mode 100644 index 00000000..89fabd01 --- /dev/null +++ b/bot/templates/teaser_initial_prompt.msg @@ -0,0 +1,3 @@ +:microphone2: **Challenge: Initial Prompt** + +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file diff --git a/bot/templates/teaser_rank_conversation_replies.msg b/bot/templates/teaser_rank_conversation_replies.msg new file mode 100644 index 00000000..39c52539 --- /dev/null +++ b/bot/templates/teaser_rank_conversation_replies.msg @@ -0,0 +1,3 @@ +:bar_chart: **Challenge: Rank Replies** + +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file diff --git a/bot/templates/teaser_rank_initial_prompts.msg b/bot/templates/teaser_rank_initial_prompts.msg new file mode 100644 index 00000000..e9f9972a --- /dev/null +++ b/bot/templates/teaser_rank_initial_prompts.msg @@ -0,0 +1,3 @@ +:bar_chart: **Challenge: Rank Initial Prompts** + +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file diff --git a/bot/templates/teaser_rate_summary.msg b/bot/templates/teaser_rate_summary.msg new file mode 100644 index 00000000..c096973b --- /dev/null +++ b/bot/templates/teaser_rate_summary.msg @@ -0,0 +1,3 @@ +:ballot_box: **Challenge: Rate Summary** + +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file diff --git a/bot/templates/teaser_summarize_story.msg b/bot/templates/teaser_summarize_story.msg new file mode 100644 index 00000000..57585198 --- /dev/null +++ b/bot/templates/teaser_summarize_story.msg @@ -0,0 +1,3 @@ +:books: **Challenge: Summarize Story** + +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file diff --git a/bot/templates/teaser_user_reply.msg b/bot/templates/teaser_user_reply.msg new file mode 100644 index 00000000..e7d10996 --- /dev/null +++ b/bot/templates/teaser_user_reply.msg @@ -0,0 +1,3 @@ +:person_red_hair: **Challenge: User Reply** + +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file From 1276365679e23a32244d17e1d4e7a795ca33b54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Fri, 23 Dec 2022 01:02:02 +0100 Subject: [PATCH 8/8] fix typo --- bot/task_handlers.py | 4 ++-- bot/templates/teaser_assistant_reply.msg | 2 +- bot/templates/teaser_initial_prompt.msg | 2 +- bot/templates/teaser_rank_conversation_replies.msg | 2 +- bot/templates/teaser_rank_initial_prompts.msg | 2 +- bot/templates/teaser_rate_summary.msg | 2 +- bot/templates/teaser_summarize_story.msg | 2 +- bot/templates/teaser_user_reply.msg | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bot/task_handlers.py b/bot/task_handlers.py index 04fd1d9a..1434d17c 100644 --- a/bot/task_handlers.py +++ b/bot/task_handlers.py @@ -56,9 +56,9 @@ class ChannelTaskBase(AutoDestructThreadHandler): async def post_teaser_msg(self, template_name: str): expiry_time = discord_timestamp(self.expiry_date, DiscordTimestampStyle.long_time) - expiry_relatve = discord_timestamp(self.expiry_date, DiscordTimestampStyle.relative_time) + expiry_relative = discord_timestamp(self.expiry_date, DiscordTimestampStyle.relative_time) return await self.bot.post_template( - template_name, task=self.task, expiry_time=expiry_time, expiry_relatve=expiry_relatve + template_name, task=self.task, expiry_time=expiry_time, expiry_relative=expiry_relative ) async def post_interaction(self, interaction: protocol_schema.Interaction) -> protocol_schema.Task: diff --git a/bot/templates/teaser_assistant_reply.msg b/bot/templates/teaser_assistant_reply.msg index ef13ee02..6975d417 100644 --- a/bot/templates/teaser_assistant_reply.msg +++ b/bot/templates/teaser_assistant_reply.msg @@ -1,3 +1,3 @@ :robot: **Challenge: Assistant Reply** -:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relative }}). \ No newline at end of file diff --git a/bot/templates/teaser_initial_prompt.msg b/bot/templates/teaser_initial_prompt.msg index 89fabd01..e9ae5c7a 100644 --- a/bot/templates/teaser_initial_prompt.msg +++ b/bot/templates/teaser_initial_prompt.msg @@ -1,3 +1,3 @@ :microphone2: **Challenge: Initial Prompt** -:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relative }}). \ No newline at end of file diff --git a/bot/templates/teaser_rank_conversation_replies.msg b/bot/templates/teaser_rank_conversation_replies.msg index 39c52539..744f7a76 100644 --- a/bot/templates/teaser_rank_conversation_replies.msg +++ b/bot/templates/teaser_rank_conversation_replies.msg @@ -1,3 +1,3 @@ :bar_chart: **Challenge: Rank Replies** -:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relative }}). \ No newline at end of file diff --git a/bot/templates/teaser_rank_initial_prompts.msg b/bot/templates/teaser_rank_initial_prompts.msg index e9f9972a..07399f56 100644 --- a/bot/templates/teaser_rank_initial_prompts.msg +++ b/bot/templates/teaser_rank_initial_prompts.msg @@ -1,3 +1,3 @@ :bar_chart: **Challenge: Rank Initial Prompts** -:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relative }}). \ No newline at end of file diff --git a/bot/templates/teaser_rate_summary.msg b/bot/templates/teaser_rate_summary.msg index c096973b..41357b06 100644 --- a/bot/templates/teaser_rate_summary.msg +++ b/bot/templates/teaser_rate_summary.msg @@ -1,3 +1,3 @@ :ballot_box: **Challenge: Rate Summary** -:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relative }}). \ No newline at end of file diff --git a/bot/templates/teaser_summarize_story.msg b/bot/templates/teaser_summarize_story.msg index 57585198..6e5ee5e5 100644 --- a/bot/templates/teaser_summarize_story.msg +++ b/bot/templates/teaser_summarize_story.msg @@ -1,3 +1,3 @@ :books: **Challenge: Summarize Story** -:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relative }}). \ No newline at end of file diff --git a/bot/templates/teaser_user_reply.msg b/bot/templates/teaser_user_reply.msg index e7d10996..47ec8a2d 100644 --- a/bot/templates/teaser_user_reply.msg +++ b/bot/templates/teaser_user_reply.msg @@ -1,3 +1,3 @@ :person_red_hair: **Challenge: User Reply** -:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relatve }}). \ No newline at end of file +:point_down: Work on it here (:fire: Thread will self-destruct at {{ expiry_time }}, {{ expiry_relative }}). \ No newline at end of file