From 3f983913ba28fe1e8a66f508247cb077b564716a Mon Sep 17 00:00:00 2001 From: Kaveen Kumarasinghe Date: Sun, 19 Feb 2023 20:02:28 -0500 Subject: [PATCH] Pre-moderations, language detection --- README.md | 1 + cogs/image_service_cog.py | 7 +++ cogs/index_service_cog.py | 7 +++ cogs/prompt_optimizer_cog.py | 7 +++ cogs/search_service_cog.py | 8 +++- cogs/text_service_cog.py | 39 ++++++++++++++++- detailed_guides/AI-MODERATION.md | 4 +- detailed_guides/LANGUAGE-DETECTION.md | 7 +++ gpt3discord.py | 2 +- language_detection_pretext.txt | 63 +++++++++++++++++++++++++++ models/openai_model.py | 56 ++++++++++++++++++++++++ sample.env | 8 +++- services/environment_service.py | 20 +++++++++ services/moderations_service.py | 63 +++++++++++++++++++++++++++ services/text_service.py | 9 ++++ 15 files changed, 296 insertions(+), 5 deletions(-) create mode 100644 detailed_guides/LANGUAGE-DETECTION.md create mode 100644 language_detection_pretext.txt diff --git a/README.md b/README.md index 77a79cf..0cc2c60 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ SUPPORT SERVER FOR BOT SETUP: https://discord.gg/WvAHXDMS7Q (You can try out the - [Translations](https://github.com/Kav-K/GPT3Discord/blob/main/detailed_guides/TRANSLATIONS.md) - [User-Input API Keys](https://github.com/Kav-K/GPT3Discord/blob/main/detailed_guides/USER-INPUT-KEYS.md) - [Permissions](https://github.com/Kav-K/GPT3Discord/blob/main/detailed_guides/PERMISSIONS.md) +- [Language Detection](https://github.com/Kav-K/GPT3Discord/blob/main/detailed_guides/LANGUAGE-DETECTION.md) - [Other Minor Features](https://github.com/Kav-K/GPT3Discord/blob/main/detailed_guides/OTHER-MINOR-FEATURES.md) diff --git a/cogs/image_service_cog.py b/cogs/image_service_cog.py index 8ab4b1e..c8d7124 100644 --- a/cogs/image_service_cog.py +++ b/cogs/image_service_cog.py @@ -9,6 +9,7 @@ from sqlitedict import SqliteDict from services.environment_service import EnvService from services.image_service import ImageService +from services.moderations_service import Moderation from services.text_service import TextService users_to_interactions = {} @@ -16,6 +17,7 @@ ALLOWED_GUILDS = EnvService.get_allowed_guilds() USER_INPUT_API_KEYS = EnvService.get_user_input_api_keys() USER_KEY_DB = EnvService.get_api_db() +PRE_MODERATE = EnvService.get_premoderate() class DrawDallEService(discord.Cog, name="DrawDallEService"): @@ -48,6 +50,11 @@ class DrawDallEService(discord.Cog, name="DrawDallEService"): await ctx.defer() + # Check the opener for bad content. + if PRE_MODERATE: + if await Moderation.simple_moderate_and_respond(prompt, ctx): + return + user = ctx.user if user == self.bot.user: diff --git a/cogs/index_service_cog.py b/cogs/index_service_cog.py index 004343c..635ecae 100644 --- a/cogs/index_service_cog.py +++ b/cogs/index_service_cog.py @@ -3,11 +3,13 @@ import traceback import discord from services.environment_service import EnvService +from services.moderations_service import Moderation from services.text_service import TextService from models.index_model import Index_handler USER_INPUT_API_KEYS = EnvService.get_user_input_api_keys() USER_KEY_DB = EnvService.get_api_db() +PRE_MODERATE = EnvService.get_premoderate() class IndexService(discord.Cog, name="IndexService"): @@ -146,6 +148,11 @@ class IndexService(discord.Cog, name="IndexService"): if not user_api_key: return + # Check the opener for bad content. + if PRE_MODERATE: + if await Moderation.simple_moderate_and_respond(query, ctx): + return + await self.index_handler.query(ctx, query, response_mode, nodes, user_api_key) async def compose_command(self, ctx, name): diff --git a/cogs/prompt_optimizer_cog.py b/cogs/prompt_optimizer_cog.py index 5aabd4d..e575f3a 100644 --- a/cogs/prompt_optimizer_cog.py +++ b/cogs/prompt_optimizer_cog.py @@ -8,12 +8,14 @@ from models.openai_model import Override from services.environment_service import EnvService from models.user_model import RedoUser from services.image_service import ImageService +from services.moderations_service import Moderation from services.text_service import TextService ALLOWED_GUILDS = EnvService.get_allowed_guilds() USER_INPUT_API_KEYS = EnvService.get_user_input_api_keys() USER_KEY_DB = EnvService.get_api_db() +PRE_MODERATE = EnvService.get_premoderate() class ImgPromptOptimizer(discord.Cog, name="ImgPromptOptimizer"): @@ -76,6 +78,11 @@ class ImgPromptOptimizer(discord.Cog, name="ImgPromptOptimizer"): if not final_prompt.endswith("."): final_prompt += "." + # Check the opener for bad content. + if PRE_MODERATE: + if await Moderation.simple_moderate_and_respond(prompt, ctx): + return + # Get the token amount for the prompt # tokens = self.usage_service.count_tokens(final_prompt) diff --git a/cogs/search_service_cog.py b/cogs/search_service_cog.py index 91ea63a..720af58 100644 --- a/cogs/search_service_cog.py +++ b/cogs/search_service_cog.py @@ -8,12 +8,13 @@ from discord.ext import pages from models.deepl_model import TranslationModel from models.search_model import Search from services.environment_service import EnvService +from services.moderations_service import Moderation from services.text_service import TextService ALLOWED_GUILDS = EnvService.get_allowed_guilds() USER_INPUT_API_KEYS = EnvService.get_user_input_api_keys() USER_KEY_DB = EnvService.get_api_db() - +PRE_MODERATE = EnvService.get_premoderate() class RedoSearchUser: def __init__(self, ctx, query, search_scope, nodes): @@ -93,6 +94,11 @@ class SearchService(discord.Cog, name="SearchService"): """Command handler for the search command""" await ctx.defer() if not redo else None + # Check the opener for bad content. + if PRE_MODERATE: + if await Moderation.simple_moderate_and_respond(query, ctx): + return + user_api_key = None if USER_INPUT_API_KEYS: user_api_key = await TextService.get_user_api_key( diff --git a/cogs/text_service_cog.py b/cogs/text_service_cog.py index b7a6e95..befacee 100644 --- a/cogs/text_service_cog.py +++ b/cogs/text_service_cog.py @@ -10,6 +10,7 @@ import json import discord +from models.deepl_model import TranslationModel from models.embed_statics_model import EmbedStatics from models.openai_model import Override from services.environment_service import EnvService @@ -34,6 +35,8 @@ else: USER_INPUT_API_KEYS = EnvService.get_user_input_api_keys() USER_KEY_DB = EnvService.get_api_db() CHAT_BYPASS_ROLES = EnvService.get_bypass_roles() +PRE_MODERATE = EnvService.get_premoderate() +FORCE_ENGLISH = EnvService.get_force_english() # # Obtain the Moderation table and the General table, these are two SQLite tables that contain @@ -77,6 +80,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): self.bot = bot self.usage_service = usage_service self.model = model + self.translation_model = TranslationModel() self.deletion_queue = deletion_queue # Data specific to all text based GPT interactions @@ -111,6 +115,17 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): ) assert self.CONVERSATION_STARTER_TEXT is not None + language_detect_file_path = EnvService.find_shared_file( + "language_detection_pretext.txt" + ) + # Attempt to read a conversation starter text string from the file. + with language_detect_file_path.open("r") as f: + self.LANGUAGE_DETECT_STARTER_TEXT = f.read() + print( + f"Language detection starter text loaded from {language_detect_file_path}." + ) + assert self.LANGUAGE_DETECT_STARTER_TEXT is not None + conversation_file_path_minimal = EnvService.find_shared_file( "conversation_starter_pretext_minimal.txt" ) @@ -541,6 +556,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): if message.author == self.bot.user: return + # Moderations service is done here. if ( hasattr(message, "guild") @@ -560,7 +576,13 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): ).timestamp() await Moderation.moderation_queues[message.guild.id].put( Moderation(message, timestamp) - ) # TODO Don't proceed to conversation processing if the message is deleted by moderations. + ) + + # Language check + if FORCE_ENGLISH and len(message.content.split(" ")) > 3: + if not await Moderation.force_english_and_respond(message.content, self.LANGUAGE_DETECT_STARTER_TEXT, message): + await message.delete() + return # Process the message if the user is in a conversation if await TextService.process_conversation_message( @@ -778,6 +800,11 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): await ctx.defer(ephemeral=private) if is_context else None + # If premoderation is enabled, check + if PRE_MODERATE: + if await Moderation.simple_moderate_and_respond(prompt, ctx): + return + overrides = Override(temperature, top_p, frequency_penalty, presence_penalty) await TextService.encapsulated_send( @@ -826,6 +853,10 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): await ctx.defer(ephemeral=private) + if PRE_MODERATE: + if await Moderation.simple_moderate_and_respond(instruction + text, ctx): + return + overrides = Override(temperature, top_p, 0, 0) await TextService.encapsulated_send( @@ -894,6 +925,11 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): elif not private: await ctx.defer() + # Check the opener for bad content. + if PRE_MODERATE and opener is not None: + if await Moderation.simple_moderate_and_respond(opener, ctx): + return + # if user.id in self.conversation_thread_owners: # await ctx.respond( # "You've already created a thread, end it before creating a new one", @@ -989,6 +1025,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): # Append the starter text for gpt3 to the user's history so it gets concatenated with the prompt later if minimal or opener_file or opener: + self.conversation_threads[thread.id].history.append( EmbeddedConversationItem(self.CONVERSATION_STARTER_TEXT_MINIMAL, 0) ) diff --git a/detailed_guides/AI-MODERATION.md b/detailed_guides/AI-MODERATION.md index a090803..1f9f507 100644 --- a/detailed_guides/AI-MODERATION.md +++ b/detailed_guides/AI-MODERATION.md @@ -21,6 +21,8 @@ There are two thresholds for the bot, there are instances in which the bot will If you'd like to help us test and fine tune our thresholds for the moderation service, please join this test server: https://discord.gg/CWhsSgNdrP. You can let off some steam in a controlled environment ;) To set a certain role immune to moderations, add the line `CHAT_BYPASS_ROLES="Role1,Role2,etc"` to your `.env file. - + +If you want to have the bot pre-moderate things sent to commands like /gpt ask, /gpt edit, /dalle draw, etc, you can set `PRE_MODERATE="True"` in the `.env` file. + **The above server is NOT for support or discussions about GPT3Discord** \ No newline at end of file diff --git a/detailed_guides/LANGUAGE-DETECTION.md b/detailed_guides/LANGUAGE-DETECTION.md new file mode 100644 index 0000000..c7f7ff3 --- /dev/null +++ b/detailed_guides/LANGUAGE-DETECTION.md @@ -0,0 +1,7 @@ +## Language Detection + +Using GPT, this bot can force everybody on your server to speak English. Simply add the environment variable `FORCE_ENGLISH="True"` to your `.env` file and the bot will automatically delete foreign language messages. + +This feature is in beta and may be buggy, so we're looking for your feedback on it to help us improve. + +This feature currently using `text-davinci-003`, so this is certainly an expensive feature to run if you have a lot of people in your server. Be careful. \ No newline at end of file diff --git a/gpt3discord.py b/gpt3discord.py index fa1a3a1..1d0010d 100644 --- a/gpt3discord.py +++ b/gpt3discord.py @@ -31,7 +31,7 @@ from services.environment_service import EnvService from models.openai_model import Model -__version__ = "10.5.0" +__version__ = "10.6.0" PID_FILE = Path("bot.pid") diff --git a/language_detection_pretext.txt b/language_detection_pretext.txt new file mode 100644 index 0000000..6670c12 --- /dev/null +++ b/language_detection_pretext.txt @@ -0,0 +1,63 @@ +You are to be given some text in an unspecified language. Detect if the primary language of the text is english. Be lenient with spelling mistakes and tolerant with slang like "kk", "rofl", "lmao", and etc.. The text will likely be informal, with slurring of text and slang. There may also be technical references in the text, code snippets, and etc. These are usually already in English. Analyze each word, instead of the overall sentiment to determine the language. Return 'True' if the text is in English, otherwise return 'False'. + +Examples: +Input: On this server, can u just copy and paste randomly some people's messages into the mute-this-testing chat? +Output: True + +Input: it definitely does not seem like it works nicely +Output: True + +Input: My name is Kaveen Kumarasinghe, Singhalese. +Output: True + +Input: heeeeeeeeeeeeeeeey guys my name is Kaveen +Output: True + +Input: but it could have something due with how long she waits before releasing the crack +Output: True + +Input: me mande uma index.html com sistema de Login +Output: False + +Input: oi tudo bem? +Output: False + +Input: bonsoir, je m'appelle Kav +Output: False + +Input: create a basic phyton code +Output: True + +Input: not helping ukraine is nati patriotism, ure actively going agaisnt the idea of being a chad nato country legit walking up to russias borders +Output: True + +Input: torch==1.9.1+cpu torchvision==0.10.1+cpu +Output: True + +Input: sounds good kk, lmao +Output: True + +Input: where tf is the pricing for text-davinci-002 +Output: True + +Input: https://clips.twitch.tv/GrossAdorableWolfStrawBeary-m2cXYk0Z89_UPojL +Output: True + +Input: +def detect_language(text): + """Detects the text's language.""" + from google.cloud import translate_v2 as translate + + translate_client = translate.Client() + + # Text can also be a sequence of strings, in which case this method + # will return a sequence of results for each text. + result = translate_client.detect_language(text) + + print("Text: {}".format(text)) + print("Confidence: {}".format(result["confidence"])) + print("Language: {}".format(result["language"])) +Output: True + +Now, detect the language. +Input: \ No newline at end of file diff --git a/models/openai_model.py b/models/openai_model.py index 6f6f3c9..bee6eaa 100644 --- a/models/openai_model.py +++ b/models/openai_model.py @@ -204,6 +204,10 @@ class Model: else 5 ) + print("Building language detector") + #self.detector = LanguageDetectorBuilder.from_languages(*Language.all()).build() + print("Language detector built") + def reset_settings(self): keys = [ "temp", @@ -750,6 +754,58 @@ class Model: return response + # async def send_language_detect_request_local(self, text): + # detected = await asyncio.get_running_loop().run_in_executor( + # None, self.detector.compute_language_confidence, text, Language.ENGLISH + # ) + # if detected < 0.03: + # return False + # return True + + @backoff.on_exception( + backoff.expo, + ValueError, + factor=3, + base=5, + max_tries=4, + on_backoff=backoff_handler_request, + ) + async def send_language_detect_request( + self, + text, + pretext, + ) -> ( + Tuple[dict, bool] + ): # The response, and a boolean indicating whether or not the context limit was reached. + # Validate that all the parameters are in a good state before we send the request + + prompt = f"{pretext}{text}\nOutput:" + + max_tokens = Models.get_max_tokens(Models.DAVINCI) - self.usage_service.count_tokens(prompt) + + print(f"Language detection request for {text}") + + async with aiohttp.ClientSession(raise_for_status=False) as session: + payload = { + "model": Models.DAVINCI, + "prompt": prompt, + "temperature": 0, + "top_p": 1, + "max_tokens": max_tokens + } + headers = { + "Authorization": f"Bearer {self.openai_key}" + } + async with session.post( + "https://api.openai.com/v1/completions", json=payload, headers=headers + ) as resp: + response = await resp.json() + + await self.valid_text_request(response) + print(f"Response -> {response}") + + return response + @backoff.on_exception( backoff.expo, ValueError, diff --git a/sample.env b/sample.env index b50a2d4..236dffd 100644 --- a/sample.env +++ b/sample.env @@ -22,4 +22,10 @@ USER_INPUT_API_KEYS="False" # If True, users must use their own API keys for Ope MODERATIONS_ALERT_CHANNEL = "977697652147892304" # User API key db path configuration. This is where the user API keys will be stored. -USER_KEY_DB_PATH = "user_key_db.sqlite" \ No newline at end of file +USER_KEY_DB_PATH = "user_key_db.sqlite" + +# Moderate things sent to /gpt ask and etc +PRE_MODERATE = "False" + +# Force only english to be spoken in the server +FORCE_ENGLISH = "False" \ No newline at end of file diff --git a/services/environment_service.py b/services/environment_service.py index ee0334a..80cac2a 100644 --- a/services/environment_service.py +++ b/services/environment_service.py @@ -248,6 +248,26 @@ class EnvService: except Exception: return False + @staticmethod + def get_premoderate(): + try: + pre_moderate = os.getenv("PRE_MODERATE") + if pre_moderate.lower().strip() == "true": + return True + return False + except Exception: + return False + + @staticmethod + def get_force_english(): + try: + force_english = os.getenv("FORCE_ENGLISH") + if force_english.lower().strip() == "true": + return True + return False + except Exception: + return False + @staticmethod def get_custom_bot_name(): try: diff --git a/services/moderations_service.py b/services/moderations_service.py index f8f5e6b..57c88b0 100644 --- a/services/moderations_service.py +++ b/services/moderations_service.py @@ -106,6 +106,69 @@ class Moderation: ) return embed + @staticmethod + def build_safety_blocked_message(): + # Create a discord embed to send to the user when their message gets moderated + embed = discord.Embed( + title="Your request was blocked by the safety system", + description="Our automatic moderation systems detected that your request was inappropriate and it has not been sent. Please review the usage guidelines.", + colour=discord.Colour.red(), + ) + # Set the embed thumbnail + embed.set_thumbnail(url="https://i.imgur.com/2oL8JSp.png") + embed.set_footer( + text="If you think this was a mistake, please contact the server admins." + ) + return embed + + @staticmethod + def build_non_english_message(): + # Create a discord embed to send to the user when their message gets moderated + embed = discord.Embed( + title="Your message was moderated", + description="Our automatic moderation systems detected that your message was not in English and has been deleted. Please review the rules.", + colour=discord.Colour.red(), + ) + # Set the embed thumbnail + embed.set_thumbnail(url="https://i.imgur.com/2oL8JSp.png") + embed.set_footer( + text="If you think this was a mistake, please contact the server admins." + ) + return embed + + @staticmethod + async def force_english_and_respond(text, pretext, ctx): + response = await model.send_language_detect_request(text, pretext) + response_text = response['choices'][0]['text'] + + if "false" in response_text.lower().strip(): + if isinstance(ctx, discord.Message): + await ctx.reply(embed=Moderation.build_non_english_message()) + else: + await ctx.respond(embed=Moderation.build_non_english_message()) + return False + return True + + @staticmethod + async def simple_moderate(text): + return await model.send_moderations_request(text) + + @staticmethod + async def simple_moderate_and_respond(text, ctx): + pre_mod_set = ThresholdSet(0.26, 0.26, 0.1, 0.95, 0.03, 0.95, 0.4) + + response = await Moderation.simple_moderate(text) + print(response) + flagged = True if Moderation.determine_moderation_result(text, response, pre_mod_set, pre_mod_set) == ModerationResult.DELETE else False + + if flagged: + if isinstance(ctx, discord.Message): + await ctx.reply(embed=Moderation.build_safety_blocked_message()) + else: + await ctx.respond(embed=Moderation.build_safety_blocked_message()) + return True + return False + @staticmethod def build_admin_warning_message( moderated_message, deleted_message=None, timed_out=None diff --git a/services/text_service.py b/services/text_service.py index 9cca096..5a7e209 100644 --- a/services/text_service.py +++ b/services/text_service.py @@ -11,8 +11,10 @@ from services.deletion_service import Deletion from models.openai_model import Model, Override from models.user_model import EmbeddedConversationItem, RedoUser from services.environment_service import EnvService +from services.moderations_service import Moderation BOT_NAME = EnvService.get_custom_bot_name() +PRE_MODERATE = EnvService.get_premoderate() class TextService: @@ -261,6 +263,7 @@ class TextService: await converser_cog.end_conversation(ctx) return + # Send the request to the model if from_edit_command: response = await converser_cog.model.send_edit_request( @@ -533,6 +536,12 @@ class TextService: return if conversing: + # Pre-moderation check + if PRE_MODERATE: + if await Moderation.simple_moderate_and_respond(message.content, message): + await message.delete() + return + user_api_key = None if USER_INPUT_API_KEYS: user_api_key = await TextService.get_user_api_key(