diff --git a/discord-bot/.env.example b/discord-bot/.env.example index d32e80d1..4fcb23b3 100644 --- a/discord-bot/.env.example +++ b/discord-bot/.env.example @@ -2,3 +2,6 @@ TOKEN= DECLARE_GLOBAL_COMMANDS= OWNER_IDS=[, ] PREFIX="./" + +OASST_API_URL="http://localhost:8080" # No trailing '/' +OASST_API_KEY="" diff --git a/discord-bot/CONTRIBUTING.md b/discord-bot/CONTRIBUTING.md deleted file mode 100644 index 33b2d435..00000000 --- a/discord-bot/CONTRIBUTING.md +++ /dev/null @@ -1,105 +0,0 @@ -# Contributing - -## Setup - -To run the bot - -``` -cp .env.example .env - -python -V # 3.10 - -pip install -r requirements.txt -python -m bot -``` - -Before you push, make sure the `pre-commit` hooks are installed and run successfully. - -``` -pip install pre-commit -pre-commit install -pre-commit run --all-files -``` - -To test the bot on your own discord server you need to register a discord application at the [Discord Developer Portal](https://discord.com/developers/applications) and get at bot token. - -1. Follow a tutorial on how to get a bot token, for example this one: [Creating a discord bot & getting a token](https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token) -2. The bot script expects the bot token to be in the `.env` file under the `TOKEN` variable. - -## Resources - -### Structure - -Important files - -```graphql -.env # Environment variables -.env.example # Example environment variables -CONTRIBUTING.md # This file -README.md # Project readme -EXAMPLES.md # Examples for commands and listeners -requirements.txt # Requirements - -bot/ -├─ __main__.py # Entrypoint -├─ api_client.py # API Client for interacting with the backend -├─ bot.py # Main bot class -├─ settings.py # Settings and secrets -├─ utils.py # Utility Functions -│ -├─ db/ # Database related code -│ ├─ database.db # SQLite database -│ ├─ schema.sql # SQL schema -│ └─ schemas.py # Python table schemas -│ -└── extensions/ # Application logic, see https://hikari-lightbulb.readthedocs.io/en/latest/guides/extensions.html - ├─ work.py # Task handling logic <-- most important file - ├─ guild_settings.py # Server specific settings - └─ hot_reload.py # Utility for hot reload extensions during development -``` - -### Adding a new command/listener - -1. Create a new file in the `extensions` folder -2. Copy the template below - -```py -# -*- coding: utf-8 -*- -"""My plugin.""" -import lightbulb - -plugin = lightbulb.Plugin("MyPlugin") - -# Add your commands here - -def load(bot: lightbulb.BotApp): - """Add the plugin to the bot.""" - bot.add_plugin(plugin) - - -def unload(bot: lightbulb.BotApp): - """Remove the plugin to the bot.""" - bot.remove_plugin(plugin) -``` - -For example commands and listeners, see [EXAMPLES.md](/discord-bot/EXAMPLES.md) - -### Docs - -Discord - -- [Discord API Reference](https://discord.com/developers/docs/intro) - -Main framework - -- [Hikari Repo](https://github.com/hikari-py/hikari) -- [Hikari Docs](https://docs.hikari-py.dev/en/latest/) - -Command handler - -- [Lightbulb Repo](https://github.com/tandemdude/hikari-lightbulb) -- [Lightbulb Docs](https://hikari-lightbulb.readthedocs.io/en/latest/) - -Component handler (buttons, modals, etc... ) - -- [Miru Repo](https://github.com/HyperGH/hikari-miru) diff --git a/discord-bot/EXAMPLES.md b/discord-bot/EXAMPLES.md deleted file mode 100644 index 29598fde..00000000 --- a/discord-bot/EXAMPLES.md +++ /dev/null @@ -1,408 +0,0 @@ -# `hikari`, `lightbulb`, and `muri` examples - -Example plugin for reference. - -````py -import asyncio - -import hikari -import lightbulb -import lightbulb.decorators -import miru -from miru.ext import nav - -plugin = lightbulb.Plugin("ExamplePlugin") - -# To add checks to a plugin, you can use the `@plugin.check` decorator -# or the `plugin.add_check` method. Lightbulb has some built-in checks. -# The check will be called before any command in the plugin is called. -plugin.add_checks(lightbulb.guild_only) - - -# To create a slash command, use the template below -@plugin.command -@lightbulb.command("example", "Example command.") -@lightbulb.implements(lightbulb.SlashCommand) -async def example(ctx: lightbulb.SlashContext): - """Example command.""" - # To send a message, use the `respond` method on `ctx`. - # !!! Be sure to use `await` when calling `respond` !!! - await ctx.respond("Hello, world!") - - -# To add arguments, use the `@lightbulb.option` decorator. -@plugin.command -@lightbulb.option( - "name", # The name of the option. This is what you will use to access the value in `ctx.options.name` - "Your name.", # The description of the option. This will be shown in the slash command menu. - # Whether or not the option is required. - # If `required` is `True`, the user will not be able to use the command without providing a value for this option. - required=False, - default=None, # The default value for the option. If `required` is `True`, this will be ignored. - type=str | None, # The type of the option. This is used to convert the value to the correct type. - # https://hikari-lightbulb.readthedocs.io/en/latest/guides/commands.html#converters-and-slash-command-option-types -) -@lightbulb.option( - "age", - "Your age.", - type=int, - # These are enforced on the client side, so the user won't be able to enter a value outside of the range. - min_value=0, - max_value=100, -) -@lightbulb.option( - "gender", - "Your gender.", - # You can also use `choices` to limit the user to a specific set of values. - # This can be a list of `str`, `int, or `float` - # choices=["Male", "Female", "Other"], - # or a list of `hikari.CommandChoice` objects to have separate option names and values - choices=[ - hikari.CommandChoice(name="male", value="M"), - hikari.CommandChoice(name="female", value="F"), - hikari.CommandChoice(name="other", value="Other"), - ], - type=str, -) -@lightbulb.command("args_example", "Example command with arguments.") -@lightbulb.implements(lightbulb.SlashCommand) -async def args_example(ctx: lightbulb.SlashContext): - """Example command with arguments.""" - name: str | None = ctx.options.name - if name is None: - name = ctx.author.username - age: int = ctx.options.age - gender: str = ctx.options.gender - - await ctx.respond( - f"Hello {ctx.author.mention}! Your name is {name}, you are {age} years old, and your gender is {gender}.", - # in order to actually mention the user, you must pass `user_mentions=True` - # otherwise, the user won't get a notification - user_mentions=True, - ) - - -# To have autocomplete options, add the -# pass `autocomplete=function` to `@lightbulb.option` -# or `autocomplete=True` and mark the function with `@command.autocomplete("option_name")`. -# @autocomplete_example.autocomplete("language") -async def _programming_language_autocomplete( - option: hikari.CommandInteractionOption, interaction: hikari.AutocompleteInteraction -) -> list[str]: - # The `option` argument is the current text that the user typed in. - if not isinstance(option.value, str): - # This will raise a TypeError if `option.value` cannot be converted - option.value = str(option.value) - - # You can query a database, fetch an api, or return any list of strings - # !!! You can return a max of 25 options !!! - langs = [ - "C", - "C++", - "C#", - "CSS", - "Go", - "HTML", - "Java", - "Javascript", - "Kotlin", - "Matlab", - "NoSQL", - "PHP", - "Perl", - "Python", - "R", - "Ruby", - "Rust", - "SQL", - "Scala", - "Swift", - "TypeScript", - "Zig", - ] - return [lang for lang in langs if option.value.lower() in lang.lower()] - - -@plugin.command -@lightbulb.option( - "language", - "Your favorite programming language.", - autocomplete=_programming_language_autocomplete, -) -@lightbulb.command("autocomplete_example", "Autocomplete example.") -@lightbulb.implements(lightbulb.SlashCommand) -async def autocomplete_example(ctx: lightbulb.SlashContext): - """Autocomplete example.""" - await ctx.respond("Your favorite programming language is " + ctx.options.language) - - -# Command groups are like trees -# You can have subcommands, subcommand groups, and subcommand groups with subcommands -# Here is an example diagram: -# /group_example (group) -# subcommand (executable) -# subcommand_group (group) -# subsubcommand (executable) - -# Because those are slash commands, only the leaves (/subcommand and /subsubcommand) are callable. - -# To create a group, use the template below -# 1. Create the command group -@plugin.command -@lightbulb.command("group_example", "Example command group.") -@lightbulb.implements(lightbulb.SlashCommandGroup) -async def group_example(_: lightbulb.SlashContext) -> None: - """Group example.""" - # This will never execute because it is a group - pass - - -# 2. Add a child command -@group_example.child -@lightbulb.command("subcommand", "Example subcommand.") -@lightbulb.implements(lightbulb.SlashSubCommand) -async def subcommand(ctx: lightbulb.SlashContext) -> None: - """An example subcommand.""" - await ctx.respond("invoked `/group_example subcommand`") - - -# 3. Add a sub-group -@group_example.child -@lightbulb.command("subcommand_group", "Example subcommand group.") -@lightbulb.implements(lightbulb.SlashSubGroup) -async def subcommand_group(_: lightbulb.SlashContext) -> None: - """Subcommand group example.""" - # This will never execute because it is a sub-group - pass - - -# 4. Add a child to the sub-group -@subcommand_group.child -@lightbulb.command("subsubcommand", "Example subsubcommand.") -@lightbulb.implements(lightbulb.SlashSubCommand) -async def subsubcommand(ctx: lightbulb.SlashContext) -> None: - """An example subsubcommand.""" - await ctx.respond("invoked `/group_example subcommand_group subsubcommand`") - - -# Event listeners are a way to listen to events from the gateway. -# You can have stand alone event listeners or use `wait_for` to wait for a specific event inside a command / listener. -@plugin.listener(hikari.MemberCreateEvent) -async def on_member_join(event: hikari.MemberCreateEvent) -> None: - """Event listener to welcome new members.""" - guild = event.get_guild() - await event.member.send(f"Welcome to {guild.name if guild else 'the server'}!") - - -# You can also use `wait_for` to wait for a specific event -@plugin.command -@lightbulb.command("wait_for_example", "Example command with `wait_for` and `stream`.") -@lightbulb.implements(lightbulb.SlashCommand) -async def wait_for_example(ctx: lightbulb.SlashContext) -> None: - """Wait for example.""" - await ctx.respond("Send a message!") - - # We can add a predicate to `wait_for` to filter out events - def author_check(e: hikari.MessageCreateEvent) -> bool: - return e.author_id == ctx.author.id - - # You need to wrap wait_for in a try/catch block because it can raise `asyncio.TimeoutError` - try: - event = await ctx.bot.wait_for(hikari.MessageCreateEvent, timeout=10, predicate=author_check) - await ctx.respond(f"You sent: {event.message.content}") - except asyncio.TimeoutError: - await ctx.respond("Too slow!") - # remember to use try/except/finally if you need to clean up any resources - - # You can also use `stream` to listen for events - await ctx.respond("Waiting for guild events...") - with ctx.bot.stream(hikari.Event, timeout=5).filter( - # Only listen for events that have a guild_id and are not bots - lambda e: getattr(e, "guild_id", None) == ctx.guild_id - and getattr(e, "is_human", False) - ) as stream: - async for event in stream: - await ctx.respond(f"New `{event.__class__.__name__}`") - - await ctx.respond("Done!") - - -# You can interact with discord's API using the `rest` attribute on the bot -# This allows you to -# - fetch information about users, channels, guilds, etc. -# - create, edit, and delete messages, channels, threads, roles, categories, etc. -# - add, remove, and edit reactions -@plugin.command -@lightbulb.command("rest_example", "Example command using the `rest` attribute.") -@lightbulb.implements(lightbulb.SlashCommand) -async def rest_example(ctx: lightbulb.SlashContext) -> None: - """Example command using the `rest` attribute.""" - rest = ctx.bot.rest - your_messages = await rest.fetch_messages(ctx.channel_id).filter(lambda m: m.author.id == ctx.author.id).count() - await ctx.respond(f"{your_messages} out of the last 10 messages in this channel were sent by you.") - - -# Context Menus are a way to attach a command to a user or a message. -# By right clicking a user or a User, you can select to execute a command under the "Apps" menu item. -@plugin.command -@lightbulb.command("user_context_menu_example", "Example context menu on a user.") -@lightbulb.implements(lightbulb.UserCommand) -async def user_context_menu_example(ctx: lightbulb.UserContext) -> None: - """User context menu example.""" - user: hikari.Member = ctx.options.target - await ctx.respond(f"Hello {user.mention}!", user_mentions=True) - - -# Same with messages -@plugin.command -@lightbulb.command("message_context_menu_example", "Example context menu on a message.") -@lightbulb.implements(lightbulb.MessageCommand) -async def message_context_menu_example(ctx: lightbulb.MessageContext) -> None: - """Message context menu example.""" - message: hikari.Message = ctx.options.target - await ctx.respond(f"The message length is: {len(message.content or '')}", flags=hikari.MessageFlag.EPHEMERAL) - - -# Components are a way to add interactive buttons to your slash commands. -# We use `miru` to manage components and their callbacks. - -# To create a component, use the template below -# 1. Create the view -class MyView(miru.View): - """An example view with buttons.""" - - @miru.button(label="Rock", emoji="\N{ROCK}", style=hikari.ButtonStyle.PRIMARY) - async def rock_button(self, button: miru.Button, ctx: miru.ViewContext) -> None: - await ctx.respond("Paper!") - - @miru.button(label="Paper", emoji="\N{SCROLL}", style=hikari.ButtonStyle.PRIMARY) - async def paper_button(self, button: miru.Button, ctx: miru.ViewContext) -> None: - await ctx.respond("Scissors!") - - @miru.button(label="Scissors", emoji="\N{BLACK SCISSORS}", style=hikari.ButtonStyle.PRIMARY) - async def scissors_button(self, button: miru.Button, ctx: miru.ViewContext): - await ctx.respond("Rock!") - - @miru.button(emoji="\N{BLACK SQUARE FOR STOP}", style=hikari.ButtonStyle.DANGER, row=2) - async def stop_button(self, button: miru.Button, ctx: miru.ViewContext) -> None: - self.stop() # Stop listening for interactions - - @miru.select( - options=[ - hikari.SelectMenuOption( - label="Thing 1", - value="1", - description="This is a thing", - emoji=hikari.UnicodeEmoji("🗿"), - is_default=True, - ), - hikari.SelectMenuOption( - label="Thing 2", - value="2", - description="This is another thing", - emoji=hikari.UnicodeEmoji("🗿"), - is_default=False, - ), - hikari.SelectMenuOption( - label="Thing 3", - value="3", - description="This is a different thing", - emoji=hikari.UnicodeEmoji("🗿"), - is_default=False, - ), - ], - placeholder="Select some stuff!", - min_values=0, - max_values=2, - row=3, - ) - async def select(self, select: miru.Select, ctx: miru.ViewContext) -> None: - await ctx.respond(f"You selected {select.values}") - - -# 2. Create a command to use the view -@plugin.command -@lightbulb.command("button_example", "Example command with buttons.") -@lightbulb.implements(lightbulb.SlashCommand) -async def button_example(ctx: lightbulb.SlashContext) -> None: - """Wait for example.""" - # 3. Create an instance of the view and start it - view = MyView(timeout=60) - resp = await ctx.respond("Rock Paper Scissors!", components=view) - msg = await resp.message() - await view.start(msg) - await view.wait() - - await ctx.respond("Thank you for playing!") - - -# You can use buttons to create a navigation menu -@plugin.command -@lightbulb.command("nav_example", "Example command with button navigation.", auto_defer=True) -@lightbulb.implements(lightbulb.SlashCommand) -async def navigation_example(ctx: lightbulb.SlashContext) -> None: - """Navigation example.""" - # await ctx.respond(response_type=hikari.ResponseType.DEFERRED_MESSAGE_UPDATE) - embed = hikari.Embed(title="I'm the second page!", description="Also an embed!") - pages = ["I'm the first page!", embed, "I'm the last page!"] - - navigator = nav.NavigatorView(pages=pages, timeout=10) - # You may also pass an interaction object to this function - await navigator.send(ctx.channel_id) - - await navigator.wait() # This is not necessary, but we want to wait anyway - await ctx.respond("Done!") - - -# Miru also has modal support -class MyModal(miru.Modal): - """An example modal.""" - - # Define our modal items - # You can also use Modal.add_item() to add items to the modal after instantiation, just like with views. - name = miru.TextInput(label="Name", placeholder="Enter your name!", required=True) - bio = miru.TextInput(label="Biography", value="Pre-filled content!", style=hikari.TextInputStyle.PARAGRAPH) - - # You can currently only use TextInputs - # https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-modal - - # The callback function is called after the user hits 'Submit' - async def callback(self, context: miru.ModalContext) -> None: - # You can also access the values using ctx.values, Modal.values, or use ctx.get_value_by_id() - await context.respond(f"Your name: `{self.name.value}`\nYour bio: ```{self.bio.value}```") - - -class ModalView(miru.View): - """An example view that opens a modal.""" - - # Create a new button that will invoke our modal - @miru.button(label="Click me!", style=hikari.ButtonStyle.PRIMARY) - async def modal_button(self, button: miru.Button, ctx: miru.ViewContext) -> None: - modal = MyModal(title="Example Title") - # You may also use Modal.send(interaction) if not working with a miru context object. (e.g. slash commands) - # Keep in mind that modals can only be sent in response to interactions. - await ctx.respond_with_modal(modal) - # OR - # await modal.send(ctx.interaction) - - -@plugin.command -@lightbulb.command("modal_example", "Example command with a modal.") -@lightbulb.implements(lightbulb.SlashCommand) -async def modal_example(ctx: lightbulb.SlashContext) -> None: - """Navigation example.""" - view = ModalView() - resp = await ctx.respond("This button triggers a modal!", components=view) - await view.start(await resp.message()) - - - -def load(bot: lightbulb.BotApp): - """Add the plugin to the bot.""" - bot.add_plugin(plugin) - - -def unload(bot: lightbulb.BotApp): - """Remove the plugin to the bot.""" - bot.remove_plugin(plugin) -```` diff --git a/discord-bot/README.md b/discord-bot/README.md index cde82025..f8b9e433 100644 --- a/discord-bot/README.md +++ b/discord-bot/README.md @@ -1,6 +1,6 @@ # Open-Assistant Data Collection Discord Bot -This bot collects human feedback to create a dataset for RLHF-alignment of an assistant chat bot based on a large langugae model. You and other people can teach the bot how to respond to user requests by demonstration and by garding and ranking the bot's outputs. If you want to learn more about RLHF please refer [to OpenAI's InstructGPT blog post](https://openai.com/blog/instruction-following/). +This bot collects human feedback to create a dataset for RLHF-alignment of an assistant chat bot based on a large language model. You and other people can teach the bot how to respond to user requests by demonstration and by ranking the bot's outputs. If you want to learn more about RLHF please refer [to OpenAI's InstructGPT blog post](https://openai.com/blog/instruction-following/). ## Invite official bot @@ -8,4 +8,106 @@ To add the official Open-Assistant data collection bot to your discord server [c ## Contributing -To contribute to the bot, please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file. +If you are unfamiliar with `hikari`, `lightbulb`, or `miru`, please refer to the [large list of examples](https://gist.github.com/AlexanderHOtt/7805843a7120f755938a3b75d680d2e7) + +### Setup + +To run the bot + +``` +cp .env.example .env + +python -V # 3.10 + +pip install -r requirements.txt +python -m bot +``` + +Before you push, make sure the `pre-commit` hooks are installed and run successfully. + +``` +pip install pre-commit +pre-commit install +pre-commit run --all-files +``` + +To test the bot on your own discord server you need to register a discord application at the [Discord Developer Portal](https://discord.com/developers/applications) and get at bot token. + +1. Follow a tutorial on how to get a bot token, for example this one: [Creating a discord bot & getting a token](https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token) +2. The bot script expects the bot token to be in the `.env` file under the `TOKEN` variable. + +### Resources + +#### Structure + +Important files + +```graphql +.env # Environment variables +.env.example # Example environment variables +CONTRIBUTING.md # This file +README.md # Project readme +EXAMPLES.md # Examples for commands and listeners +requirements.txt # Requirements + +bot/ +├─ __main__.py # Entrypoint +├─ api_client.py # API Client for interacting with the backend +├─ bot.py # Main bot class +├─ settings.py # Settings and secrets +├─ utils.py # Utility Functions +│ +├─ db/ # Database related code +│ ├─ database.db # SQLite database +│ ├─ schema.sql # SQL schema +│ └─ schemas.py # Python table schemas +│ +└── extensions/ # Application logic, see https://hikari-lightbulb.readthedocs.io/en/latest/guides/extensions.html + ├─ work.py # Task handling logic <-- most important file + ├─ guild_settings.py # Server specific settings + └─ hot_reload.py # Utility for hot reload extensions during development +``` + +#### Adding a new command/listener + +1. Create a new file in the `extensions` folder +2. Copy the template below + +```py +# -*- coding: utf-8 -*- +"""My plugin.""" +import lightbulb + +plugin = lightbulb.Plugin("MyPlugin") + +# Add your commands here + +def load(bot: lightbulb.BotApp): + """Add the plugin to the bot.""" + bot.add_plugin(plugin) + + +def unload(bot: lightbulb.BotApp): + """Remove the plugin to the bot.""" + bot.remove_plugin(plugin) +``` + +#### Docs + +Discord + +- [Discord API Reference](https://discord.com/developers/docs/intro) + +`hikari` (main framework) + +- [Hikari Repo](https://github.com/hikari-py/hikari) +- [Hikari Docs](https://docs.hikari-py.dev/en/latest/) + +`lightbulb` (command handler) + +- [Lightbulb Repo](https://github.com/tandemdude/hikari-lightbulb) +- [Lightbulb Docs](https://hikari-lightbulb.readthedocs.io/en/latest/) + +`miru` (component handler: buttons, modals, etc... ) + +- [Miru Repo](https://github.com/HyperGH/hikari-miru) diff --git a/discord-bot/bot/bot.py b/discord-bot/bot/bot.py index 2cf3d663..4e3bd12c 100644 --- a/discord-bot/bot/bot.py +++ b/discord-bot/bot/bot.py @@ -29,7 +29,7 @@ async def on_starting(event: hikari.StartingEvent): await bot.d.db.executescript(open("./bot/db/schema.sql").read()) await bot.d.db.commit() - bot.d.oasst_api = OasstApiClient("http://localhost:8080", "any_key") + bot.d.oasst_api = OasstApiClient(settings.oasst_api_url, settings.oasst_api_key) @bot.listen() diff --git a/discord-bot/bot/settings.py b/discord-bot/bot/settings.py index 41c6ae52..200ab54b 100644 --- a/discord-bot/bot/settings.py +++ b/discord-bot/bot/settings.py @@ -10,6 +10,9 @@ class Settings(BaseSettings): declare_global_commands: int = Field(env="DECLARE_GLOBAL_COMMANDS", default=0) owner_ids: list[int] = Field(env="OWNER_IDS", default_factory=list) prefix: str = Field(env="PREFIX", default="./") + oasst_api_url: str = Field(env="OASST_API_URL", default="http://localhost:8080") + oasst_api_key: str = Field(env="OASST_API_KEY", default="") class Config(BaseSettings.Config): env_file = ".env" + case_sensitive = False