diff --git a/README.md b/README.md index a5e2737..797e3a0 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,20 @@ probably add more in the future, so the name is a bit misleading. ## Features -- AI-generated responses to all messages in a Matrix room (chatbot) +- AI-generated responses to messages in a Matrix room (chatbot) - Currently supports OpenAI (tested with `gpt-3.5-turbo` and `gpt-4`) - AI-generated pictures via the `!gptbot imagine` command - Currently supports OpenAI (DALL-E) - Mathematical calculations via the `!gptbot calculate` command - Currently supports WolframAlpha +- Automatic classification of messages (for `imagine`, `calculate`, etc.) + - Beta feature, see Usage section for details - Really useful commands like `!gptbot help` and `!gptbot coin` - DuckDB database to store room context ## Planned features - End-to-end encryption support (partly implemented, but not yet working) -- Automatic classification of messages (for `imagine`, `calculate`, etc.) - - Beta feature, enable for a room using `!gptbot roomsettings classification true` ## Installation @@ -57,12 +57,48 @@ to messages. If you want to create a new room, you can use the `!gptbot newroom` command at any time, which will cause the bot to create a new room and invite you to it. You may also specify a room name, e.g. `!gptbot newroom My new room`. -Note that the bot will currently respond to _all_ messages in the room. So you -shouldn't invite it to a room with other people in it. +### Reply generation -It also supports the `!gptbot help` command, which will print a list of available -commands. Messages starting with `!` are considered commands and will not be -considered for response generation. +Note that the bot will respond to _all_ messages in the room by default. If you +don't want this, for example because you want to use the bot in a room with +other people, you can use the `!gptbot roomsettings` command to change the +settings for the current room. For example, you can disable response generation +with `!gptbot roomsettings always_reply false`. + +With this setting, the bot will only be triggered if a message begins with +`!gptbot chat`. For example, `!gptbot chat Hello, how are you?` will cause the +bot to generate a response to the message `Hello, how are you?`. The bot will +still get previous messages in the room as context for generating the response. + +### Commands + +There are a few commands that you can use to interact with the bot. For example, +if you want to generate an image from a text prompt, you can use the +`!gptbot imagine` command. For example, `!gptbot imagine a cat` will cause the +bot to generate an image of a cat. + +To learn more about the available commands, `!gptbot help` will print a list of +available commands. + +### Automatic classification + +As a beta feature, the bot can automatically classify messages and use the +appropriate API to generate a response. For example, if you send a message +like "Draw me a picture of a cat", the bot will automatically use the +`imagine` command to generate an image of a cat. + +This feature is disabled by default. To enable it, use the `!gptbot roomsettings` +command to change the settings for the current room. `!gptbot roomsettings classification true` +will enable automatic classification, and `!gptbot roomsettings classification false` +will disable it again. + +Note that this feature is still in beta and may not work as expected. You can +always use the commands manually if the automatic classification doesn't work +for you (including `!gptbot chat` for a regular chat message). + +Also note that this feature conflicts with the `always_reply false` setting - +or rather, it doesn't make sense then because you already have to explicitly +specify the command to use. ## Troubleshooting diff --git a/classes/bot.py b/classes/bot.py index ce40706..b216c0c 100644 --- a/classes/bot.py +++ b/classes/bot.py @@ -22,7 +22,9 @@ from nio import ( RoomMessageText, RoomSendResponse, SyncResponse, - RoomMessageNotice + RoomMessageNotice, + JoinError, + RoomLeaveError, ) from nio.crypto import Olm @@ -58,6 +60,7 @@ class GPTBot: image_api: Optional[OpenAI] = None classification_api: Optional[OpenAI] = None operator: Optional[str] = None + room_ignore_list: List[str] = [] # List of rooms to ignore invites from @classmethod def from_config(cls, config: ConfigParser): @@ -276,8 +279,25 @@ class GPTBot: invites = self.matrix_client.invited_rooms for invite in invites.keys(): + if invite in self.room_ignore_list: + self.logger.log( + f"Ignoring invite to room {invite} (room is in ignore list)") + continue + self.logger.log(f"Accepting invite to room {invite}") - await self.matrix_client.join(invite) + + response = await self.matrix_client.join(invite) + + if isinstance(response, JoinError): + self.logger.log( + f"Error joining room {invite}: {response.message}. Not trying again.", "error") + + leave_response = await self.matrix_client.room_leave(invite) + + if isinstance(leave_response, RoomLeaveError): + self.logger.log( + f"Error leaving room {invite}: {leave_response.message}", "error") + self.room_ignore_list.append(invite) async def send_image(self, room: MatrixRoom, image: bytes, message: Optional[str] = None): """Send an image to a room. @@ -487,13 +507,13 @@ class GPTBot: await self.matrix_client.sync(timeout=30000) def respond_to_room_messages(self, room: MatrixRoom | str) -> bool: - """Check whether the bot should respond to messages sent in a room. + """Check whether the bot should respond to all messages sent in a room. Args: room (MatrixRoom | str): The room to check. Returns: - bool: Whether the bot should respond to messages sent in the room. + bool: Whether the bot should respond to all messages sent in the room. """ if isinstance(room, MatrixRoom): @@ -501,7 +521,7 @@ class GPTBot: with self.database.cursor() as cursor: cursor.execute( - "SELECT value FROM room_settings WHERE room_id = ? AND setting = ?", (room, "respond_to_messages")) + "SELECT value FROM room_settings WHERE room_id = ? AND setting = ?", (room, "always_reply")) result = cursor.fetchone() return True if not result else bool(int(result[0])) diff --git a/commands/newroom.py b/commands/newroom.py index 0b1b25a..7e77699 100644 --- a/commands/newroom.py +++ b/commands/newroom.py @@ -28,4 +28,4 @@ async def command_newroom(room: MatrixRoom, event: RoomMessageText, bot): await bot.matrix_client.joined_rooms() await bot.send_message(room, f"Alright, I've created a new room called '{room_name}' and invited you to it. You can find it at {new_room.room_id}", True) - await bot.send_message(new_room.room_id, f"Welcome to the new room! What can I do for you?") \ No newline at end of file + await bot.send_message(new_room, f"Welcome to the new room! What can I do for you?") \ No newline at end of file diff --git a/commands/roomsettings.py b/commands/roomsettings.py index 4f9df66..999b226 100644 --- a/commands/roomsettings.py +++ b/commands/roomsettings.py @@ -3,10 +3,15 @@ from nio.rooms import MatrixRoom async def command_roomsettings(room: MatrixRoom, event: RoomMessageText, bot): - setting = event.body.split()[2] + setting = event.body.split()[2] if len(event.body.split()) > 2 else None value = " ".join(event.body.split()[3:]) if len( event.body.split()) > 3 else None + if setting == "classification": + setting = "use_classification" + if setting == "systemmessage": + setting = "system_message" + if setting == "system_message": if value: bot.logger.log("Adding system message...") @@ -28,38 +33,52 @@ async def command_roomsettings(room: MatrixRoom, event: RoomMessageText, bot): await bot.send_message(room, f"The current system message is: '{system_message}'.", True) return - if setting == "classification": + if setting in ("use_classification", "always_reply"): if value: if value.lower() in ["true", "false"]: value = value.lower() == "true" - bot.logger.log("Setting classification status...") + bot.logger.log(f"Setting {setting} status for {room.room_id} to {value}...") with bot.database.cursor() as cur: cur.execute( """INSERT INTO room_settings (room_id, setting, value) VALUES (?, ?, ?) ON CONFLICT (room_id, setting) DO UPDATE SET value = ?;""", - (room.room_id, "use_classification", "1" if value else "0", "1" if value else "0") + (room.room_id, setting, "1" if value else "0", "1" if value else "0") ) - await bot.send_message(room, f"Alright, I've set use_classification to: '{value}'.", True) + await bot.send_message(room, f"Alright, I've set {setting} to: '{value}'.", True) return await bot.send_message(room, "You need to provide a boolean value (true/false).", True) return - bot.logger.log("Retrieving classification status...") + bot.logger.log(f"Retrieving {setting} status for {room.room_id}...") - use_classification = bot.room_uses_classification(room) + with bot.database.cursor() as cur: + cur.execute( + """SELECT value FROM room_settings WHERE room_id = ? AND setting = ?;""", + (room.room_id, setting) + ) - await bot.send_message(room, f"The current classification status is: '{use_classification}'.", True) + value = cur.fetchone()[0] + + if not value: + if setting == "use_classification": + value = False + elif setting == "always_reply": + value = True + else: + value = bool(int(value)) + + await bot.send_message(room, f"The current {setting} status is: '{value}'.", True) return - message = f""" - The following settings are available: + message = f"""The following settings are available: - - system_message [message]: Get or set the system message to be sent to the chat model - - classification [true/false]: Get or set whether the room uses classification - """ +- system_message [message]: Get or set the system message to be sent to the chat model +- classification [true/false]: Get or set whether the room uses classification +- always_reply [true/false]: Get or set whether the bot should reply to all messages (if false, only reply to mentions and commands) +""" await bot.send_message(room, message, True)