From ae252ba57b9e300e2d8c88acdc204211122e4e96 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Tue, 13 Dec 2022 17:21:41 +0900 Subject: [PATCH] Adding a slash command based discord bot --- bot/.gitignore | 3 + bot/Makefile | 13 +++++ bot/README.md | 11 ++++ bot/bot.py | 131 ++++++++++++++++++++++++++++++++++++++++++ bot/requirements.txt | 2 + bot/setup.py | 34 +++++++++++ bot/test_prompts.json | 10 ++++ 7 files changed, 204 insertions(+) create mode 100644 bot/.gitignore create mode 100644 bot/Makefile create mode 100644 bot/README.md create mode 100644 bot/bot.py create mode 100644 bot/requirements.txt create mode 100644 bot/setup.py create mode 100644 bot/test_prompts.json diff --git a/bot/.gitignore b/bot/.gitignore new file mode 100644 index 00000000..a7982d60 --- /dev/null +++ b/bot/.gitignore @@ -0,0 +1,3 @@ +.env +*.egg-info/ +__pycache__/ diff --git a/bot/Makefile b/bot/Makefile new file mode 100644 index 00000000..87f07026 --- /dev/null +++ b/bot/Makefile @@ -0,0 +1,13 @@ +install: + python -m pip install -U pip + python -m pip install -e . + +lint: ## [Local development] Run pylint and black + python -m pylint app + python -m black --check -l 120 app + +black: ## [Local development] Auto-format python code using black + python -m black -l 120 . + +run: + python -m bot diff --git a/bot/README.md b/bot/README.md new file mode 100644 index 00000000..09de56fe --- /dev/null +++ b/bot/README.md @@ -0,0 +1,11 @@ +# open-chat-gpt +This is the github repo for the open-chat-gpt project. +We are currently building a discord bot in order to make everyone contribute with great prompts and answers. +Join us! +https://discord.gg/ZUfPw6jP + +## Project description +We are calling the community for help to collect ChatGPT-like Instruction-Fulfillment datasamples via Discord. People can post Instructions they think would make sense for ChatGPT-like systems & also provide a good reference answer for it. + +## Todo +Figure out ouath flow for the app to work inside the open-chat-gpt testing channel here. https://discord.gg/JJSKtRhv diff --git a/bot/bot.py b/bot/bot.py new file mode 100644 index 00000000..ab0519c1 --- /dev/null +++ b/bot/bot.py @@ -0,0 +1,131 @@ +bot_url = "https://discord.com/api/oauth2/authorize?client_id=1051614245940375683&permissions=8&scope=bot" + +import discord +import json +import os +import requests + +from discord import app_commands +from dotenv import load_dotenv + +# Load up all the important environment variables. +load_dotenv() + +# For authentication. +TOKEN = os.getenv("DISCORD_TOKEN") + +# For Backends. +API_SERVER_URL = os.getenv("API_SERVER_URL") +API_SERVER_KEY = os.getenv("API_SERVER_KEY") + +labelers_url = f"{API_SERVER_URL}/api/v1/labelers/" +prompts_url = f"{API_SERVER_URL}/api/v1/prompts/" +headers = {"X-API-Key": API_SERVER_KEY} + +# For testing only. +TEST_GUILD = os.getenv("TEST_GUILD") + +# Initiate the client and command tree to create slash commands. +class OpenChatGPTClient(discord.Client): + def __init__(self, *, intents: discord.Intents): + super().__init__(intents=intents) + self.tree = app_commands.CommandTree(self) + + async def setup_hook(self): + if TEST_GUILD: + # When testing the bot it's handy to run in a single server (called a + # Guide in the API). This is relatively fast. + guild = discord.Object(id=TEST_GUILD) + self.tree.copy_global_to(guild=guild) + await self.tree.sync(guild=guild) + else: + # This can take up to an hour for the commands to be registered. + await tree.sync() + print("Ready!") + + +# List the set of intents needed for commands to operate properly. +intents = discord.Intents.default() +intents.message_content = True +client = OpenChatGPTClient(intents=intents) + + +@client.tree.command() +async def register(interaction: discord.Interaction): + """Registers the user for submissions.""" + labeler = { + "discord_username": f"{interaction.user.id}", + "display_name": interaction.user.name, + "is_enabled": True, + } + response = requests.post(labelers_url, headers=headers, json=labeler) + if response.status_code == 200: + await interaction.response.send_message(f"Added you {interaction.user.name}") + else: + print(response) + await interaction.response.send_message(f"Failed to add you") + + +@client.tree.command() +async def list_participants(interaction: discord.Interaction): + """Reports the set of registered participants.""" + response = requests.get(labelers_url, headers=headers) + if response.status_code == 200: + names = ",".join([labeler["display_name"] for labeler in response.json()]) + await interaction.response.send_message(f"Found these users: {names}") + else: + await interaction.response.send_message(f"Failed to fetch participants") + + +@client.tree.command() +async def add_prompt( + interaction: discord.Interaction, prompt: str, response: str, language: str = "en" +): + """Uploads a single prompt to the server.""" + prompt = { + "discord_username": f"{interaction.user.id}", + "labeler_id": 5, + "prompt": prompt, + "response": response, + "lang": language, + } + response = requests.post(prompts_url, headers=headers, json=prompt) + if response.status_code == 200: + await interaction.response.send_message(f"Added your prompt") + else: + await interaction.response.send_message(f"Failed to add the prompt") + + +@client.tree.command() +async def add_prompts_set( + interaction: discord.Interaction, prompts: discord.Attachment +): + """Uploads a batch of prompts to the server.""" + # Loading a bunch of prompts from a file can take a while. So first defer + # the response to ensure we're able to later tell the user what happened. + await interaction.response.defer(ephemeral=True) + + # Read the prompts and load them one by one. + # TODO: Upload a batch when the API supports it. + # TODO: Handle incorrect file types and parsing errors. + prompts_raw = await prompts.read() + prompts_loaded = json.loads(prompts_raw) + count = 0 + for entry in prompts_loaded: + for response in entry["responses"]: + prompt = { + "discord_username": f"{interaction.user.id}", + "labeler_id": 5, + "prompt": entry["prompt"], + "response": response, + "lang": "en", + } + response = requests.post(prompts_url, headers=headers, json=prompt) + if response.status_code != 200: + await interaction.followup.send(f"Failed to upload") + return + count += 1 + await interaction.followup.send(f"Loaded up {count} prompts") + + +client.run(TOKEN) diff --git a/bot/requirements.txt b/bot/requirements.txt new file mode 100644 index 00000000..617e7071 --- /dev/null +++ b/bot/requirements.txt @@ -0,0 +1,2 @@ +discord.py==2.1.0 +python-dotenv==0.21.0 diff --git a/bot/setup.py b/bot/setup.py new file mode 100644 index 00000000..be44789a --- /dev/null +++ b/bot/setup.py @@ -0,0 +1,34 @@ +from setuptools import setup, find_packages +from pathlib import Path +import os + +if __name__ == "__main__": + import os + + def _read_reqs(relpath): + fullpath = os.path.join(os.path.dirname(__file__), relpath) + with open(fullpath) as f: + return [ + s.strip() + for s in f.readlines() + if (s.strip() and not s.startswith("#")) + ] + + REQUIREMENTS = _read_reqs("requirements.txt") + + setup( + name="open-chat-gpt", + packages=find_packages(), + version="0.0.1", + license="Apache 2.0", + description="A Discord Bot for collecting and ranking prompts to train an Open ChatGPT", + keywords=["machine learning", "natural language processing", "discord"], + install_requires=REQUIREMENTS, + classifiers=[ + "Development Status :: Alpha", + "Intended Audience :: Developers", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "License :: OSI Approved :: Apache License", + "Programming Language :: Python :: 3.6", + ], + ) diff --git a/bot/test_prompts.json b/bot/test_prompts.json new file mode 100644 index 00000000..be892986 --- /dev/null +++ b/bot/test_prompts.json @@ -0,0 +1,10 @@ +[ + { + "prompt": "tell me the name of two dogs", + "responses": ["Charles", "bobby"] + }, + { + "prompt": "Name one type of cheese made in france", + "responses": ["Munster", "Gouda"] + } +]