diff --git a/README.md b/README.md index 4d5cf76..1969abf 100644 --- a/README.md +++ b/README.md @@ -43,36 +43,41 @@ python -m fjerkroa_bot --config config.toml Create a `config.toml` file with the following configuration options: ```toml -openai-token = "your_openai_api_key" +openai-key = "OPENAIKEY" +discord-token = "DISCORDTOKEN" model = "gpt-3.5-turbo" -temperature = 0.3 -max-tokens = 100 -top-p = 0.9 -presence-penalty = 0 -frequency-penalty = 0 -history-limit = 50 -history-per-channel = 3 -history-directory = "./history" -system = "You are conversing with an AI assistant designed to answer questions and provide helpful information." -short-path = [ - ["channel_regex_1", "user_regex_1"], - ["channel_regex_2", "user_regex_2"], -] -fix-model = "text-davinci-002" -fix-description = "Please fix the text to a valid JSON format." +max-tokens = 1024 +temperature = 0.9 +top-p = 1.0 +presence-penalty = 1.0 +frequency-penalty = 1.0 +history-limit = 10 +welcome-channel = "welcome" +staff-channel = "staff" +join-message = "Hi! I am {name}, and I am new here." +short-path = [['^news$', '^news-bot$'], ['^mod$', '.*']] +system = "You are an smart AI" ``` -- `openai-token`: Your OpenAI API key. -- `model`: The OpenAI GPT model to use. -- `temperature`: Controls the randomness of the generated responses. -- `max-tokens`: Maximum number of tokens allowed in a response. -- `top-p`: Controls the diversity of the generated responses. -- `presence-penalty`: Controls the penalty for new token occurrences. -- `frequency-penalty`: Controls the penalty for frequent token occurrences. -- `history-limit`: Maximum number of messages to store in the conversation history. -- `history-per-channel`: Maximum number of messages per channel in the conversation history. -- `history-directory`: Directory to store the conversation history as a file. -- `system`: System message to be included in the conversation. +- `discord-token`: The token for the Discord bot account. +- `openai-token`: The API key for the OpenAI API. +- `model`: The OpenAI model name to be used for generating AI responses. +- `temperature`: The temperature for the AI model's output. +- `history-limit`: The maximum number of messages to maintain in the conversation history. +- `history-directory`: The directory where the conversation history will be stored. +- `history-per-channel`: The number of history items to keep per channel. +- `max-tokens`: The maximum number of tokens in the generated AI response. +- `top-p`: The top-p sampling value for the AI model's output. +- `presence-penalty`: The presence penalty value for the AI model's output. +- `frequency-penalty`: The frequency penalty value for the AI model's output. +- `staff-channel`: The name of the channel where staff messages will be sent. +- `welcome-channel`: The name of the channel where welcome messages will be sent. +- `join-message`: The message template to be sent to AI when a user joins the server, triggers that way the AI to write something to the user. +- `ignore-channels`: A list of channels to be ignored by the bot. +- `additional-responders`: A list of channels that should have a separate AI responder with separated history. - `short-path`: List of channel and user regex patterns to apply short path (skip sending message to AI, just fill the history). -- `fix-model`: OpenAI GPT model to use for fixing invalid JSON responses. -- `fix-description`: Description of the fixing process. \ No newline at end of file +- `system`: The system message template for the AI conversation. +- `fix-model`: The OpenAI model name to be used for fixing the AI responses. +- `fix-description`: The description for the fix-model's conversation. + +register-python-argcomplete \ No newline at end of file diff --git a/fjerkroa_bot/discord_bot.py b/fjerkroa_bot/discord_bot.py index 336110c..df2b56b 100644 --- a/fjerkroa_bot/discord_bot.py +++ b/fjerkroa_bot/discord_bot.py @@ -27,39 +27,28 @@ class FjerkroaBot(commands.Bot): intents.message_content = True intents.members = True - self.observer = Observer() - self.file_handler = ConfigFileHandler(self.on_config_file_modified) - self.observer.schedule(self.file_handler, path=config_file, recursive=False) - self.observer.start() - - self.airesponder = AIResponder(self.config) - self.aichannels = {} - for chan_name in self.config['additional-responders']: - self.aichannels[chan_name] = AIResponder(self.config, chan_name) + self.init_observer() + self.init_aichannels() super().__init__(command_prefix="!", case_insensitive=True, intents=intents) - @classmethod - def load_config(self, config_file: str = "config.toml"): - with open(config_file, encoding='utf-8') as file: - return toml.load(file) + def init_observer(self): + self.observer = Observer() + self.file_handler = ConfigFileHandler(self.on_config_file_modified) + self.observer.schedule(self.file_handler, path=self.config_file, recursive=False) + self.observer.start() - def on_config_file_modified(self, event): - if event.src_path == self.config_file: - new_config = self.load_config(self.config_file) - if repr(new_config) != repr(self.config): - logging.info(f"config file {self.config_file} changed, reloading.") - self.config = new_config - self.airesponder.config = self.config - for responder in self.aichannels.values(): - responder.config = self.config + def init_aichannels(self): + self.airesponder = AIResponder(self.config) + self.aichannels = {chan_name: AIResponder(self.config, chan_name) for chan_name in self.config['additional-responders']} + + def init_channels(self): + self.staff_channel = self.fetch_channel_by_name(self.config['staff-channel'], no_ignore=True) + self.welcome_channel = self.fetch_channel_by_name(self.config['welcome-channel'], no_ignore=True) async def on_ready(self): - print(f"We have logged in as {self.user}") - self.staff_channel = self.channel_by_name(self.config['staff-channel'], no_ignore=True) - logging.info(f'staff channel {repr(self.config.get("staff-channel"))}: {repr(self.staff_channel)}') - self.welcome_channel = self.channel_by_name(self.config['welcome-channel'], no_ignore=True) - logging.info(f'welcome channel {repr(self.config.get("welcome-channel"))}: {repr(self.welcome_channel)}') + self.init_channels() + logging.info(f"We have logged in as {self.user} ({repr(self.staff_channel)}, {repr(self.welcome_channel)})") async def on_member_join(self, member): logging.info(f"User {member.name} joined") @@ -72,18 +61,29 @@ class FjerkroaBot(commands.Bot): return if not isinstance(message.channel, (TextChannel, DMChannel)): return - message_content = str(message.content).strip() - if len(message_content) < 1: - return - channel_name = self.get_channel_name(message.channel) - msg = AIMessage(message.author.name, message_content, channel_name, self.user in message.mentions) - await self.respond(msg, message.channel) + await self.handle_message_through_responder(message) + + def on_config_file_modified(self, event): + if event.src_path == self.config_file: + new_config = self.load_config(self.config_file) + if repr(new_config) != repr(self.config): + logging.info(f"config file {self.config_file} changed, reloading.") + self.config = new_config + self.airesponder.config = self.config + for responder in self.aichannels.values(): + responder.config = self.config + + @classmethod + def load_config(self, config_file: str = "config.toml"): + with open(config_file, encoding='utf-8') as file: + return toml.load(file) def channel_by_name(self, channel_name: Optional[str], fallback_channel: Optional[Union[TextChannel, DMChannel]] = None, no_ignore: bool = False ) -> Optional[Union[TextChannel, DMChannel]]: + """Fetch a channel by name, or return the fallback channel if not found.""" if channel_name is None: return fallback_channel if channel_name.startswith("#"): @@ -110,24 +110,36 @@ class FjerkroaBot(commands.Bot): def get_ai_responder(self, channel_name): return self.aichannels[channel_name] if channel_name in self.aichannels else self.airesponder + async def handle_message_through_responder(self, message): + """Handle a message through the AI responder""" + message_content = str(message.content).strip() + if len(message_content) < 1: + return + channel_name = self.get_channel_name(message.channel) + msg = AIMessage(message.author.name, message_content, channel_name, self.user in message.mentions) + await self.respond(msg, message.channel) + async def send_message_with_typing(self, airesponder, channel, message): + """Send the user message to the AI responder with typing animation in discord""" async with channel.typing(): return await airesponder.send(message) async def send_answer_with_typing(self, response, answer_channel, airesponder): + """Send an answer from AI to discord channel with typing animation""" async with answer_channel.typing(): if response.picture is not None: + # Generate the image with the AI and send it with the answer images = [discord.File(fp=await airesponder.draw(response.picture), filename="image.png")] await answer_channel.send(response.answer, files=images, suppress_embeds=True) else: await answer_channel.send(response.answer, suppress_embeds=True) - # This is an asynchronous function to generate AI responses async def respond( self, message: AIMessage, # Incoming message object with user message and metadata channel: Union[TextChannel, DMChannel] # Channel (Text or Direct Message) the message is coming from ) -> None: + """Handle a message from a user with an AI responder""" # Get the name of the channel where the message was sent channel_name = self.get_channel_name(channel)