Add more settings and refactor other md files into README.md

This commit is contained in:
Alex Ott
2022-12-30 17:30:56 -08:00
parent aff3f18b07
commit 0fb7bfd27a
6 changed files with 111 additions and 516 deletions
+3
View File
@@ -2,3 +2,6 @@ TOKEN=<discord bot token>
DECLARE_GLOBAL_COMMANDS=<testing guild id>
OWNER_IDS=[<your user id>, <other user ids>]
PREFIX="./"
OASST_API_URL="http://localhost:8080" # No trailing '/'
OASST_API_KEY=""
-105
View File
@@ -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)
-408
View File
@@ -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)
````
+104 -2
View File
@@ -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)
+1 -1
View File
@@ -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()
+3
View File
@@ -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