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 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 08/10] 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 From 74ee711c0fb453f516ab2e50dd1b5b6cc7b80ca8 Mon Sep 17 00:00:00 2001 From: Yannic Kilcher Date: Fri, 23 Dec 2022 22:26:51 +0100 Subject: [PATCH 09/10] added logo svg --- .gitignore | 1 + assets/logo.svg | 95 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 assets/logo.svg diff --git a/.gitignore b/.gitignore index 293cd36b..84512e5a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.swp *.egg-info __pycache__ +.DS_Store diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 00000000..43f34441 --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,95 @@ + + From 39d97b6cf3e17e49be8012e1409dc009bd404cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Fri, 23 Dec 2022 23:02:36 +0100 Subject: [PATCH 10/10] optimize logo svg code, add monochrome and png versions --- assets/logo.png | Bin 0 -> 18263 bytes assets/logo.svg | 117 +++++++++---------------------------------- assets/logo_mono.png | Bin 0 -> 6766 bytes assets/logo_mono.svg | 16 ++++++ 4 files changed, 39 insertions(+), 94 deletions(-) create mode 100644 assets/logo.png create mode 100644 assets/logo_mono.png create mode 100644 assets/logo_mono.svg diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..34cdf878289fa8a8f176fe912edb714de4022613 GIT binary patch literal 18263 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 literal 0 HcmV?d00001 diff --git a/assets/logo.svg b/assets/logo.svg index 43f34441..8cc9a708 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,95 +1,24 @@ - -