My bot is based on a database that is updated frequently. The bot of one of the teams provides a choice of lesson topics (taken from the table). It seems to work, but if changes are made to the table while the bot is running, then without restarting it, new selection options will not appear
the command:
@app_commands.command()
@app_commands.default_permissions(administrator=True)
@app_commands.choices(lesson=lesson_choices())
async def create_or_update_mark(self, interaction: Interaction,
student: discord.Member,
lesson: Choice[str],
logiks: app_commands.Range[int, 0, 8]):
with MarkRepository(student_id=student.id, lesson_title=lesson.value) as lr:
lr.create_or_update(mark_dto=MarkCreateDTO(logiks=logiks))
await interaction.response.send_message("logiks has been changed", ephemeral=True)
The selection list is taken from the function lesson_choices
def lesson_choices() -> list[Choice[str]]:
return [
Choice(name=lesson.title, value=lesson.title)
for lesson in LessonRepository.get_all_lessons()
]
It seems clear that the decorator determines the choices when running the code, but this does not suit me. Are there any ways to help change the selection options without restarting the bot?
I wanted to use autocomplete
but I didn't even have basic options there. Maybe I didn't implement it correctly
Autocomplete command
@app_commands.command()
@app_commands.default_permissions(administrator=True)
@app_commands.autocomplete(lesson=lesson_autocomplete)
async def create_or_update_mark(self, interaction: Interaction,
student: discord.Member,
lesson: str,
logiks: app_commands.Range[int, 0, 8]):
with MarkRepository(student_id=student.id, lesson_title=lesson) as lr:
lr.create_or_update(mark_dto=MarkCreateDTO(logiks=logiks))
await interaction.response.send_message("logiks has been changed", ephemeral=True)
lesson_autocomplete
:
async def lesson_autocomplete(interaction: Interaction, current: str) -> list[app_commands.Choice[str]]:
lessons = [lesson_dto.title for lesson_dto in LessonRepository.get_all_lessons()]
print(lessons)
return [
app_commands.Choice(name=lesson, value=lesson)
for lesson in lessons if current.lower() in lesson.lower()
]
I see that you want to show a dynamic list of choices dependent on the current state of your Database and that you are having some trouble displaying them. Here's what I have noticed and here is what I recommend you do:
for lesson in lessons if current.lower() in lesson.lower()
-> this means that the autocomplete will only display something if the user has typed a lesson title completely, it will not suggest lessons based on what they're currently typing.LessonRepository.get_all_lessons()
-> The autocomplete method is called every single time the user types a new character (or deletes one), so this method is being called MANY times per second. You want to move this to a cache so that you do not need to make any such calls. This is vital because you only have 3 seconds to respond to the interaction with an autocomplete.Making these suggested changes while moving our logic to a Transformer (handles complex logic for transforming arguments and providing autocomplete support), would result in the following change to our code:
from typing import TYPE_CHECKING, Dict, List, Any, Optional, TypeAlias, Union
import discord
from discord.ext import commands
from discord import app_commands
import difflib
GUILD_ID: TypeAlias = int
MEMBER_ID: TypeAlias = int
LESSON_ID: TypeAlias = int
# Some class definition that encapsulates the information from your database.
class Lesson:
id: int
title: str
# ... some other data here
class MyBot(commands.Bot):
if TYPE_CHECKING:
some_function_for_loading_the_lessons_cache: Any
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
# We want to keep track of all the lessons your bot is
# holding here for each student in each guild. To achieve this, let's store a nested dictionary of
# {guild_id: {student_id: {lesson_name: lesson_information}}}. Of course, you can change this to your needs!
self.lessons_cache: Dict[GUILD_ID, Dict[MEMBER_ID, Dict[LESSON_ID, Lesson]]] = {}
async def setup_hook(self):
# This async function is called once during your bots
# startup, and can be used to load your cache and sync your application commands.
await self.tree.sync()
await self.some_function_for_loading_the_lessons_cache()
class LessonTransformer(app_commands.Transformer):
async def find_similar_lesson_titles(self, lessons: Dict[LESSON_ID, Lesson], title: str) -> Dict[LESSON_ID, Lesson]:
# We can use difflib to find similar lesson titles, a standard python library utility!
similar = difflib.get_close_matches(title, map(lambda l: l.title, lessons.values()), n=15, cutoff=1)
return {lesson.id: lesson for lesson in lessons.values() if lesson.title in similar}
async def autocomplete(self, interaction: discord.Interaction[MyBot], value: str, /) -> List[app_commands.Choice[str]]:
# Prerequisite: We know that this command can ONLY be invoked in a server (guild)
assert interaction.guild is not None
# Check if the invoker of this command has already filled out the "student" argument of this command,
# if they have, then we can use that information to filter to all lessons from only that student
student: Optional[discord.Member] = interaction.namespace.get('student')
if student is None:
# This person skipped the student argument before going to the lesson argument, so we can only show them
# so much.
lessons: Dict[MEMBER_ID, Dict[LESSON_ID, Lesson]] = interaction.client.lessons_cache.get(
interaction.guild.id, {}
)
# Flatten this dict into just a list of lessons
flat_lessons: Dict[LESSON_ID, Lesson] = {
s_lesson.id: s_lesson for student_lessons in lessons.values() for s_lesson in student_lessons.values()
}
similar_lessons = await self.find_similar_lesson_titles(flat_lessons, value)
else:
# We can get more specific!
student_lessons: Dict[LESSON_ID, Lesson] = interaction.client.lessons_cache.get(interaction.guild.id, {}).get(
student.id, {}
)
similar_lessons = await self.find_similar_lesson_titles(student_lessons, value)
return [
app_commands.Choice(name=lesson.title, value=str(lesson_id)) for lesson_id, lesson in similar_lessons.items()
]
async def transform(self, interaction: discord.Interaction[MyBot], value: str, /) -> Union[Lesson, LESSON_ID]:
# Prerequisite: We know that this command can ONLY be invoked in a server (guild)
assert interaction.guild is not None
# It should be noted that autocompletes are mere SUGGESTIONS, we need to make sure the user entered okay data.
# Find the lesson from the cache, where value should be a lesson id
if not value.isdigit():
raise Exception("Invalid lesson id") # Can raise better exception here
lesson_id = int(value)
student: Optional[discord.Member] = interaction.namespace.get('student')
if student is None:
return lesson_id # We will check that this lesson ID is to the actual student later
# We can get more specific!
lesson = interaction.client.lessons_cache.get(interaction.guild.id, {}).get(student.id, {}).get(lesson_id)
if lesson is None:
raise Exception("Invalid lesson id")
return lesson
class SomeCog(commands.Cog):
@app_commands.command()
@app_commands.guild_only()
@app_commands.default_permissions(administrator=True)
async def create_or_update_mark(
self,
interaction: discord.Interaction[MyBot],
student: discord.Member,
lesson: app_commands.Transform[Union[Lesson, LESSON_ID], LessonTransformer],
logiks: app_commands.Range[int, 0, 8],
):
# Prerequisite: We know that this command can ONLY be invoked in a server (guild)
assert interaction.guild is not None
reveal_type(lesson) # Type of "lesson" is "Lesson | int"
if isinstance(lesson, int):
# This is a lesson ID, but we need to check that it belongs to the student (or complain)
potential_lesson = interaction.client.lessons_cache.get(interaction.guild.id, {}).get(student.id, {}).get(lesson)
if potential_lesson is None:
raise Exception("Invalid lesson id")
lesson = potential_lesson
reveal_type(lesson) # Type of "lesson" is "Lesson"
# ... perform our operations with the lesson object here
So, to recap, this potential example:
Lesson
type that represents the data stored in our Database.With these changes, your problem will be fixed. Note that I have not tested this code, it is just an example of how you could go about refactoring your setup. If you have any questions don't hesitate to ask. Also, I highly recommend you check out the discord.py Discord Server, which has help channels for faster responses.