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