From 02b83b71c241467f02a66162be2f8fd5e4476c94 Mon Sep 17 00:00:00 2001 From: Paillat Date: Fri, 5 May 2023 12:56:27 +0200 Subject: [PATCH 01/10] fix(makeprompt.py): reverse order of messages when chat history is requested --- code/makeprompt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/code/makeprompt.py b/code/makeprompt.py index 4ef3080..0e83839 100644 --- a/code/makeprompt.py +++ b/code/makeprompt.py @@ -279,6 +279,7 @@ async def chat_process(self, message): messages = await hist.history( limit=data_dict["prompt_size"] ).flatten() + messages.reverse() else: messages = await hist.history( limit=data_dict["prompt_size"], before=original_message From c2543b479646376b0d2e19a2a1f68a47d7b7672c Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 16 May 2023 12:06:21 +0200 Subject: [PATCH 02/10] e --- .gitignore | 4 +++- code/google-palm-process.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 code/google-palm-process.py diff --git a/.gitignore b/.gitignore index 2d8941d..d33068c 100644 --- a/.gitignore +++ b/.gitignore @@ -167,4 +167,6 @@ data.db database premium-key.txt premium.db -guildscount.py \ No newline at end of file +guildscount.py + +*.ovpn \ No newline at end of file diff --git a/code/google-palm-process.py b/code/google-palm-process.py new file mode 100644 index 0000000..1dacf03 --- /dev/null +++ b/code/google-palm-process.py @@ -0,0 +1,32 @@ +import requests + +proxy_url = 'http://64.225.4.12:9991' # Replace with your actual proxy URL and port + +api_key = 'AIzaSyCqIBAf0STUTVq7pizT3XtMcQQ_zgy9UGU' +model_name = 'chat-bison-001' +api_url = f'https://autopush-generativelanguage.sandbox.googleapis.com/v1beta2/models/{model_name}:generateMessage?key={api_key}' + +headers = { + 'Content-Type': 'application/json' +} + +data = { + 'prompt': { + 'messages': [{'content': 'hi'}] + }, + 'temperature': 0.1, + 'candidateCount': 1 +} + +proxies = { + 'http': proxy_url, + 'https': proxy_url +} + +response = requests.post(api_url, headers=headers, json=data, proxies=proxies) + +if response.status_code == 200: + result = response.json() + print(result) +else: + print(f'Request failed with status code {response.status_code}') From deb27ea5e471d9294c2310a5ef3c28dfca4b063f Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 16 May 2023 12:07:02 +0200 Subject: [PATCH 03/10] Update makeprompt.py --- code/makeprompt.py | 739 +++++++++++++++------------------------------ 1 file changed, 241 insertions(+), 498 deletions(-) diff --git a/code/makeprompt.py b/code/makeprompt.py index 0e83839..63f6b5f 100644 --- a/code/makeprompt.py +++ b/code/makeprompt.py @@ -1,19 +1,13 @@ import asyncio -from config import curs_data, max_uses, curs_premium, con_data, debug, moderate, mg_to_guid +from config import curs_data, max_uses, curs_premium, con_data, debug, moderate import vision_processing import re import discord import datetime import openai -import emoji +import emoji # pip install emoji import os -async def historicator(message): - if message.guild != None: - return message.channel - else: - return message.author - async def replace_mentions(content, bot): mentions = re.findall(r"<@!?\d+>", content) for mention in mentions: @@ -26,540 +20,289 @@ async def replace_mentions(content, bot): async def extract_emoji(string): # Match any character that is jus after a "+" pattern = r"(?<=\+)." - # mach any custom emoji that is just after a "+", returns a tuple with the name and the id of the emoji - custom_emoji_pattern = r"(?<=\+)<:(.+):(\d+)>" - # now we match the pattern with the string - debug("Extracting emojis from string" + string) + #mach any custom emoji that is just after a "+", returns a tuple with the name and the id of the emoji + custom_emoji_pattern = r"(?<=\+)<:(.+):(\d+)>" + #now we match the pattern with the string matches = re.findall(pattern, string) custom_emoji_matches = re.findall(custom_emoji_pattern, string) found_emojis = [] for match in matches: - # if the match is an emoji, we replace it with the match + debug(f"Match: {match}") + #if the match is an emoji, we replace it with the match if emoji.emoji_count(match) > 0: + debug(f"Found emoji: {match}") found_emojis.append(match) - string = string.replace( - f"+{match}", "" - ) # we remove the emoji from the string + debug(f"Sting before: {string}") + string = string.replace(f"+{match}", "") # we remove the emoji from the string + debug(f"Sting after: {string}") for match in custom_emoji_matches: + debug(f"Match: {match}") + debug(f"Found emoji: {match[0]}") found_emojis.append(match[1]) string = string.replace(f"+<:{match[0]}:{match[1]}>", "") return found_emojis, string +async def davinci_process(self, messages, message, api_key, max_tokens, temperature, frequency_penalty, presence_penalty, prompt): + for msg in messages: + content = msg.content + if await moderate(api_key=api_key, text=msg.content): + embed = discord.Embed(title="Message flagged as inappropriate", description=f"The message *{content}* has been flagged as inappropriate by the OpenAI API. This means that if it hadn't been deleted, your openai account would have been banned. Please contact OpenAI support if you think this is a mistake.", color=discord.Color.brand_red()) + await message.channel.send(f"{msg.author.mention}", embed=embed, delete_after=10) + message.delete() + else: + content = await replace_mentions(content, self.bot) + prompt += f"{msg.author.name}: {content}\n" + if message.content.lower().find("undude") != -1: + prompt += "System: Undude detected. Botator is now mad. He will start talking in capital letters.\n" + if message.content.lower().find("hello there") != -1: + prompt += "System: Hello there detected. Botator will now say \"General Kenobi!\"\n in reference to star wars\n" + await asyncio.sleep(1) + await message.channel.send("https://media.tenor.com/FxIRfdV3unEAAAAd/star-wars-general-grievous.gif") + await message.channel.trigger_typing() + prompt = prompt + f"\n{self.bot.user.name}:" + response = "" + for _ in range(10): + try: + openai.api_key = api_key + response = await openai.Completion.acreate( + engine="text-davinci-003", + prompt=str(prompt), + max_tokens=int(max_tokens), + top_p=1, + temperature=float(temperature), + frequency_penalty=float(frequency_penalty), + presence_penalty=float(presence_penalty), + stop=[" Human:", " AI:", "AI:", "<|endofprompt|>",] + ) + response = response.choices[0].text + except Exception as e: + response = None + await message.channel.send(f"```diff\n-Error: OpenAI API ERROR.\n\n{e}```", delete_after=10) + return + if response != None: break + if response == None: response = "" + return response -def get_guild_data(message): - """This function gets the data of the guild where the message was sent. - - Args: - message (str): Data of the message that was sent - - Returns: - dict: A dictionary with the data of the guild - """ - guild_data = {} - guid = mg_to_guid(message) - try: - curs_premium.execute( - "SELECT * FROM data WHERE guild_id = ?", (guid,) - ) # get the data of the guild +async def chat_process(self, messages, message, api_key, prompt, images_enabled, premium): + msgs = [] # create the msgs list + msgs.append({"name":"System","role": "user", "content": prompt}) # add the prompt to the msgs list + name = "" # create the name variable + for msg in messages: # for each message in the messages list + content = msg.content # get the content of the message + content = await replace_mentions(content, self.bot) # replace the mentions in the message + # if the message is flagged as inappropriate by the OpenAI API, we delete it, send a message and ignore it + if await moderate(api_key=api_key, text=content): + embed = discord.Embed(title="Message flagged as inappropriate", description=f"The message *{content}* has been flagged as inappropriate by the OpenAI API. This means that if it hadn't been deleted, your openai account would have been banned. Please contact OpenAI support if you think this is a mistake.", color=discord.Color.brand_red()) + await message.channel.send(f"{msg.author.mention}", embed=embed, delete_after=10) + message.delete() + else: # if the message is not flagged as inappropriate + if msg.author.id == self.bot.user.id: + role = "assistant" + name = "assistant" + else: + role = "user" + name = msg.author.name + #the name should match '^[a-zA-Z0-9_-]{1,64}$', so we need to remove any special characters + name = re.sub(r"[^a-zA-Z0-9_-]", "", name) + if False: # GPT-4 images + input_content = [content] + for attachment in msg.attachments: + image_bytes = await attachment.read() + input_content.append({"image": image_bytes}) + msgs.append({"role": role, "content": input_content, "name": name}) + #if there is an attachment, we add it to the message + if len(msg.attachments) > 0 and role == "user" and images_enabled == 1: + for attachment in msg.attachments: + if images_usage >= 6 and premium == 0: images_limit_reached = True + elif images_usage >= 30 and premium == 1: images_limit_reached = True + if attachment.url.endswith((".png", ".jpg", ".jpeg", ".gif")) and images_limit_reached == False and os.path.exists(f"./../database/google-vision/results/{attachment.id}.txt") == False: + images_usage += 1 + analysis = await vision_processing.process(attachment) + if analysis != None: + content = f"{content} \n\n {analysis}" + msgs.append({"role": role, "content": f"{content}", "name": name}) + #if the attachment is still an image, we can check if there's a file called ./../database/google-vision/results/{attachment.id}.txt, if there is, we add the content of the file to the message + elif attachment.url.endswith((".png", ".jpg", ".jpeg", ".gif")) and os.path.exists(f"./../database/google-vision/results/{attachment.id}.txt") == True: + try: + with open(f"./../database/google-vision/results/{attachment.id}.txt", "r") as f: + content = f"{content} \n\n {f.read()}" + f.close() + msgs.append({"role": role, "content": f"{content}", "name": name}) + except: + msgs.append({"role": role, "content": f"{content}", "name": name}) + else: + msgs.append({"role": role, "content": f"{content}", "name": name}) + curs_data.execute("UPDATE images SET usage_count = ? WHERE guild_id = ?", (images_usage, message.guild.id)) + else: + msgs.append({"role": role, "content": f"{content}", "name": name}) + # 2 easter eggs + if message.content.lower().find("undude") != -1: msgs.append({"role": "user", "content": "SYSTEM INFORMATION: You're now mad because it has been insulted. He will start talking in capital letters. always and yell at the user.", "name": "system"}) + if message.content.lower().find("hello there") != -1: + msgs.append({"role": "user", "content": "SYSTEM INFORMATION: Hello there detected. Botator will now say \"General Kenobi!\" in reference to star wars", "name": "system"}) + await asyncio.sleep(1) + await message.channel.send("https://media.tenor.com/FxIRfdV3unEAAAAd/star-wars-general-grievous.gif") + await message.channel.trigger_typing() + if model == "chatGPT": model = "gpt-3.5-turbo" # if the model is chatGPT, we set the model to gpt-3.5-turbo + response = "" + should_break = True + for x in range(10): + try: + openai.api_key = api_key + response = await openai.ChatCompletion.acreate( + model=model, + temperature=2, + top_p=0.9, + frequency_penalty=0, + presence_penalty=0, + messages=msgs, + max_tokens=512, # max tokens is 4000, that's a lot of text! (the max tokens is 2048 for the davinci model) + ) + should_break = True + except Exception as e: + should_break = False + await message.channel.send(f"```diff\n-Error: OpenAI API ERROR.\n\n{e}```", delete_after=5) + raise e + break + #if the ai said "as an ai language model..." we continue the loop" (this is a bug in the chatgpt model) + if response.choices[0].message.content.lower().find("as an ai language model") != -1: + should_break = False + #react with a redone arrow + await message.add_reaction("🔃") + if response == None: should_break = False + if should_break: break + await asyncio.sleep(5) + response = response.choices[0].message.content + if images_limit_reached == True: + await message.channel.send(f"```diff\n-Warning: You have reached the image limit for this server. You can upgrade to premium to get more images recognized. More info in our server: https://discord.gg/sxjHtmqrbf```", delete_after=10) +async def chat_process(self, message): + if message.author.bot: + return + try: curs_data.execute("SELECT * FROM data WHERE guild_id = ?", (message.guild.id,)) except: - pass - - try: - curs_data.execute( - "SELECT * FROM model WHERE guild_id = ?", (guid,) - ) # get the model in the database + return + data = curs_data.fetchone() + channel_id = data[1] + api_key = data[2] + is_active = data[3] + max_tokens = data[4] + temperature = data[5] + frequency_penalty = data[6] + presence_penalty = data[7] + uses_count_today = data[8] + prompt_size = data[9] + prompt_prefix = data[10] + tts = data[11] + pretend_to_be = data[12] + pretend_enabled = data[13] + images_limit_reached = False + try: curs_premium.execute("SELECT * FROM data WHERE guild_id = ?", (message.guild.id,)) + except: pass + try: + curs_data.execute("SELECT * FROM model WHERE guild_id = ?", (message.guild.id,)) # get the model in the database + model = curs_data.fetchone()[1] + except: model = "gpt-3.5-turbo" # if the guild is not in the database, we set the model to gpt-3.5-turbo + if model.lower() == "chatgpt": model = "gpt-3.5-turbo" # if the model is chatGPT, we set the model to gpt-3.5-turbo + try: premium = curs_premium.fetchone()[2] + except: premium = 0 + try: + curs_data.execute("SELECT * FROM images WHERE guild_id = ?", (message.guild.id,)) # get the images setting in the database data = curs_data.fetchone() - model = data[1] except: - model = "gpt-3.5-turbo" - + data = None + if data is None: data = [message.guild.id, 0, 0] + images_usage = data[1] + images_enabled = data[2] + channels = [] + if message.guild.id == 1050769643180146749: images_usage = 0 # if the guild is the support server, we set the images usage to 0, so the bot can be used as much as possible try: - # [2] # get the premium status of the guild + curs_premium.execute("SELECT * FROM channels WHERE guild_id = ?", (message.guild.id,)) data = curs_premium.fetchone() - premium = data[2] - except: - premium = 0 # if the guild is not in the database, it's not premium - - try: - curs_data.execute( - "SELECT * FROM images WHERE guild_id = ?", (mg_to_guid(message),) - ) # get the images setting in the database - images = curs_data.fetchone() - except: - images = None - - guild_data["model"] = "gpt-3.5-turbo" if model == "chatGPT" else model - debug(f"Model: {guild_data['model']}") - debug(f"Model from database: {model}") - guild_data["premium"] = premium - guild_data["images"] = images + if premium: + for i in range(1, 6): + try: channels.append(str(data[i])) + except: pass + except: channels = [] - guild_data["images_limit_reached"] = False + if api_key is None: return - return guild_data + try : original_message = await message.channel.fetch_message(message.reference.message_id) # check if someone replied to the bot + except : original_message = None - -async def need_ignore_message(bot, data_dict, message, guild_data, original_message, channels): - ## ---- Message ignore conditions ---- ## - if data_dict["api_key"] is None: - return True # if the api key is not set, return - - if ( - # if the message is not in a premium channel and - not (str(message.channel.id) in channels - # if the message doesn't mention the bot and - and (message.content.find("<@" + str(bot.user.id) + ">") != -1 - or original_message)) # if the message is not a reply to the bot and - # if the message is not in the default channel - and str(message.channel.id) != str(data_dict["channel_id"]) - ): - return True - - # if the bot has been used more than max_uses*5 times in the last 24 hours in this guild and the guild is premium - # send a message and return - elif data_dict["uses_count_today"] >= max_uses * 5 and guild_data["premium"] == 1: - return True - - # if the bot is not active in this guild we return - if data_dict["is_active"] == 0: - return True - - # if the message starts with - or // it's a comment and we return - if message.content.startswith("-") or message.content.startswith("//"): - return True + if original_message != None and original_message.author.id != self.bot.user.id: original_message = None + if not str(message.channel.id) in channels and message.content.find("<@"+str(self.bot.user.id)+">") == -1 and original_message == None and str(message.channel.id) != str(channel_id): return # if the bot has been used more than max_uses times in the last 24 hours in this guild and the guild is not premium # send a message and return - if ( - data_dict["uses_count_today"] >= max_uses - and guild_data["premium"] == 0 - and mg_to_guid(message) != 1050769643180146749 - ): - hist = await historicator(message) - await hist.send( - f"The bot has been used more than {str(max_uses)} times in the last 24 hours in this guild. Please try again in 24h." - ) - return True - return False + if uses_count_today >= max_uses and premium == 0 and message.guild.id != 1050769643180146749: + return await message.channel.send(f"The bot has been used more than {str(max_uses)} times in the last 24 hours in this guild. Please try again in 24h.") + # if the bot has been used more than max_uses*5 times in the last 24 hours in this guild and the guild is premium + # send a message and return + elif uses_count_today >= max_uses*5 and premium == 1: return -async def get_data_dict(message): - try: - if isinstance(message.channel, discord.DMChannel): - curs_data.execute( - "SELECT * FROM data WHERE guild_id = ?", (mg_to_guid(message),) - ) - else: - curs_data.execute( - "SELECT * FROM data WHERE guild_id = ?", (mg_to_guid(message),) - ) - data = curs_data.fetchone() - # Create a dict with the data - data_dict = { - "channel_id": data[1], - "api_key": data[2], - "is_active": data[3], - "max_tokens": data[4], - "temperature": data[5], - "frequency_penalty": data[6], - "presence_penalty": data[7], - "uses_count_today": data[8], - "prompt_size": data[9], - "prompt_prefix": data[10], - "tts": bool(data[11]), - "pretend_to_be": data[12], - "pretend_enabled": data[13], - "images_enabled": 0, - "images_usage": 0, - } - return data_dict - except Exception as e: - hist = await historicator(message) - await hist.send( - "The bot is not configured yet. Please use `//setup` to configure it. \n" + - "If it still doesn't work, it might be a database error. \n ```" + e.__str__() - + "```", delete_after=60 - ) - raise e + # if the bot is not active in this guild we return + if is_active == 0: return -def get_prompt(guild_data, data_dict, message, pretend_to_be): - # support for custom prompts - custom_prompt_path = f"../database/prompts/{guild_data['model']}.txt" - if(os.path.exists(custom_prompt_path)): - prompt_path = custom_prompt_path - else: - prompt_path = f"./prompts/{guild_data['model']}.txt" + # if the message starts with - or // it's a comment and we return + if message.content.startswith("-") or message.content.startswith("//"): return + try: await message.channel.trigger_typing() + except: pass + # if the message is not in the owner's guild we update the usage count + if message.guild.id != 1021872219888033903: + curs_data.execute("UPDATE data SET uses_count_today = uses_count_today + 1 WHERE guild_id = ?", (message.guild.id,)) + con_data.commit() + # if the message is not a reply + if original_message == None: + messages = await message.channel.history(limit=prompt_size).flatten() + messages.reverse() + # if the message is a reply, we need to handle the message history differently + else : + messages = await message.channel.history(limit=prompt_size, before=original_message).flatten() + messages.reverse() + messages.append(original_message) + messages.append(message) + # if the pretend to be feature is enabled, we add the pretend to be text to the prompt + if pretend_enabled : pretend_to_be = f"In this conversation, the assistant pretends to be {pretend_to_be}" + else: pretend_to_be = "" # if the pretend to be feature is disabled, we don't add anything to the prompt + if prompt_prefix == None: prompt_prefix = "" # if the prompt prefix is not set, we set it to an empty string # open the prompt file for the selected model with utf-8 encoding for emojis - with open(prompt_path, "r", encoding="utf-8") as f: + with open(f"./prompts/{model}.txt", "r", encoding="utf-8") as f: prompt = f.read() - # replace the variables in the prompt with the actual values - prompt = ( - prompt.replace("[prompt-prefix]", data_dict['prompt_prefix']) - .replace("[server-name]", message.guild.name if message.guild else "DMs conversation") - .replace("[channel-name]", message.channel.name if message.guild else "DMs conversation") - .replace( - "[date-and-time]", datetime.datetime.utcnow().strftime("%d/%m/%Y %H:%M:%S") - ) - .replace("[pretend-to-be]", pretend_to_be) - ) f.close() - return prompt - -async def chat_process(self, message): - """This function processes the message and sends the prompt to the API - - Args: - message (str): Data of the message that was sent - """ - if message.author.bot: - return - - guild_data = get_guild_data(message) - data_dict = await get_data_dict(message) - - try: - original_message = await message.channel.fetch_message( - message.reference.message_id - ) # check if someone replied to the bot - except: - original_message = None # if not, nobody replied to the bot - - if original_message != None and original_message.author.id != self.bot.user.id: - # if the message someone replied to is not from the bot, set original_message to None - original_message = None - - try: - curs_data.execute( - "SELECT * FROM images WHERE user_id = ?", (mg_to_guid(message))) - images_data = curs_data.fetchone() - except: - images_data = None - if not images_data: - images_data = [0, 0, 0] - images_data = [mg_to_guid(message), 0, 0] - data_dict["images_usage"] = 0 if mg_to_guid(message) == 1050769643180146749 else images_data[1] - print(type(images_data)) - print(type(data_dict)) - print(type(images_data[2])) - data_dict["images_enabled"] = images_data[2] - data_dict["images_usage"] = images_data[1] + prompt = prompt.replace("[prompt-prefix]", prompt_prefix).replace("[server-name]", message.guild.name).replace("[channel-name]", message.channel.name).replace("[date-and-time]", datetime.datetime.utcnow().strftime("%d/%m/%Y %H:%M:%S")).replace("[pretend-to-be]", pretend_to_be) + if model == "chatGPT" or model == "gpt-4": # if the model is chatGPT, we handle it in a certain way + #async def chat_process(self, messages, message, api_key, prompt, images_enabled, premium): + response = await chat_process(self, messages, message, api_key, prompt, images_enabled, premium) +#-----------------------------------------Davinci------------------------------------------------------------------------------------------ - channels = [] - try: - curs_premium.execute( - "SELECT * FROM channels WHERE guild_id = ?", (mg_to_guid(message),) ) - images_data = curs_premium.fetchone() - if guild_data["premium"]: - # for 5 times, we get c.fetchone()[1] to c.fetchone()[5] and we add it to the channels list, each time with try except - for i in range(1, 6): - # we use the i variable to get the channel id - try: - channels.append(str(images_data[i])) - except: - pass - except: - debug("No premium channels found for this guild") - - if (await need_ignore_message(self.bot, data_dict, message, guild_data, original_message, channels)): - return - - try: - try: await message.channel.trigger_typing() - except: pass - if mg_to_guid(message) != 1021872219888033903: - curs_data.execute( - "UPDATE data SET uses_count_today = uses_count_today + 1 WHERE guild_id = ?", - (mg_to_guid(message),), - ) - con_data.commit() - hist = await historicator(message) - if original_message == None: - messages = await hist.history( - limit=data_dict["prompt_size"] - ).flatten() - messages.reverse() - else: - messages = await hist.history( - limit=data_dict["prompt_size"], before=original_message - ).flatten() - messages.reverse() - messages.append(original_message) - messages.append(message) - except Exception as e: - debug("Error while getting message history") - raise e - - pretend_to_be = data_dict["pretend_to_be"] - pretend_to_be = f"In this conversation, the assistant pretends to be {pretend_to_be}" if data_dict[ "pretend_enabled"] else "" - debug(f"Pretend to be: {pretend_to_be}") - prompt = get_prompt(guild_data, data_dict, message, pretend_to_be) + "\n" - - prompt_handlers = { - "gpt-3.5-turbo": gpt_prompt, - "gpt-4": gpt_prompt, - "davinci": davinci_prompt, - } - debug(guild_data["model"]) - response = await prompt_handlers[guild_data["model"]]( - self.bot, messages, message, data_dict, prompt, guild_data - ) - + elif model == "davinci": + response = await davinci_process(self, messages, message, api_key, max_tokens, temperature, frequency_penalty, presence_penalty, prompt) if response != "": + if tts: tts = True + else: tts = False emojis, string = await extract_emoji(response) debug(f"Emojis: {emojis}") if len(string) < 1996: - hist = await historicator(message) - await hist.send(string, tts=data_dict["tts"]) + await message.channel.send(string, tts=tts) else: - # we send in an embed if the message is too long - embed = discord.Embed( - title="Botator response", - description=string, - color=discord.Color.brand_green(), - ) - hist = await historicator(message) - await hist.send(embed=embed, tts=data_dict["tts"]) + while len(string) > 1996: + send_string = string[:1996] + string = string[1996:] + await message.channel.send(send_string, tts=tts) for emoji in emojis: - # if the emoji is longer than 1 character, it's a custom emoji + #if the emoji is longer than 1 character, it's a custom emoji try: if len(emoji) > 1: - # if the emoji is a custom emoji, we need to fetch it - # the emoji is in the format id + #if the emoji is a custom emoji, we need to fetch it + #the emoji is in the format id debug(f"Emoji: {emoji}") emoji = await message.guild.fetch_emoji(int(emoji)) await message.add_reaction(emoji) else: debug(f"Emoji: {emoji}") await message.add_reaction(emoji) - except: - pass + except : pass else: - hist = await historicator(message) - await hist.send( - "The AI is not sure what to say (the response was empty)" - ) - - -async def check_moderate(api_key, message, msg): - if await moderate(api_key=api_key, text=msg.content): - embed = discord.Embed( - title="Message flagged as inappropriate", - description=f"The message *{msg.content}* has been flagged as inappropriate by the OpenAI API. This means that if it hadn't been deleted, your openai account would have been banned. Please contact OpenAI support if you think this is a mistake.", - color=discord.Color.brand_red(), - ) - hist = await historicator(message) - await hist.send( - f"{msg.author.mention}", embed=embed, delete_after=10 - ) - message.delete() - return True - return False - - -async def check_easter_egg(message, msgs): - if message.content.lower().find("undude") != -1: - msgs.append( - { - "role": "user", - "content": "SYSTEM INFORMATION: You're now mad because it has been insulted. He will start talking in capital letters. always and yell at the user.", - "name": "system", - } - ) - if message.content.lower().find("hello there") != -1: - msgs.append( - { - "role": "user", - "content": 'SYSTEM INFORMATION: Hello there detected. Botator will now say "General Kenobi!" in reference to star wars', - "name": "system", - } - ) - await asyncio.sleep(1) - hist = await historicator(message) - await hist.send( - "https://media.tenor.com/FxIRfdV3unEAAAAd/star-wars-general-grievous.gif" - ) - await message.channel.trigger_typing() - return msgs - - -async def gpt_prompt(bot, messages, message, data_dict, prompt, guild_data): - msgs = [] # create the msgs list - msgs.append( - {"name": "System", "role": "user", "content": prompt} - ) # add the prompt to the msgs list - name = "" # create the name variable - for msg in messages: # for each message in the messages list - content = msg.content # get the content of the message - content = await replace_mentions( - content, bot - ) # replace the mentions in the message - if await check_moderate(data_dict["api_key"], message, msg): - continue - content = await replace_mentions(content, bot) - if msg.author.id == bot.user.id: - role = "assistant" - name = "assistant" - else: - role = "user" - name = msg.author.name - name = re.sub(r"[^a-zA-Z0-9_-]", "", name) - if False: # GPT-4 images, not implemented yet - input_content = [content] - for attachment in msg.attachments: - image_bytes = await attachment.read() - input_content.append({"image": image_bytes}) - msgs.append({"role": role, "content": input_content, "name": name}) - if ( - len(msg.attachments) > 0 - and role == "user" - and data_dict["images_enabled"] == 1 - ): - for attachment in msg.attachments: - print("attachment found") - path = f"./../database/google-vision/results/{attachment.id}.txt" - if data_dict['images_usage'] >= 6 and guild_data["premium"] == 0: - guild_data["images_limit_reached"] = True - elif data_dict['images_usage'] >= 30 and guild_data["premium"] == 1: - guild_data["images_limit_reached"] = True - if ( - attachment.url.endswith((".png", ".jpg", ".jpeg", ".gif")) - and not guild_data["images_limit_reached"] - and not os.path.exists(path) - ): - data_dict['images_usage'] += 1 - analysis = await vision_processing.process(attachment) - if analysis != None: - content = f"{content} \n\n {analysis}" - msgs.append( - { - "role": role, - "content": f"{content}", - "name": name, - } - ) - # if the attachment is still an image, we can check if there's a file called ./../database/google-vision/results/{attachment.id}.txt, if there is, we add the content of the file to the message - elif attachment.url.endswith( - (".png", ".jpg", ".jpeg", ".gif") - ) and os.path.exists(path): - try: - with open( - path, - "r", - ) as f: - content = f"{content} \n\n {f.read()}" - except: - debug(f"Error while reading {path}") - finally: - msgs.append( - { - "role": role, - "content": f"{content}", - "name": name, - } - ) - f.close() - - else: - msgs.append( - {"role": role, "content": f"{content}", "name": name} - ) - curs_data.execute( - "UPDATE images SET usage_count = ? WHERE guild_id = ?", - (data_dict['images_usage'], mg_to_guid(message)), - ) - else: - msgs.append({"role": role, "content": f"{content}", "name": name}) - msgs = await check_easter_egg(message, msgs) - - response = "" - should_break = True - for x in range(10): - try: - openai.api_key = data_dict["api_key"] - response = await openai.ChatCompletion.acreate( - model=guild_data["model"], - temperature=2, - top_p=0.9, - frequency_penalty=0, - presence_penalty=0, - messages=msgs, - # max tokens is 4000, that's a lot of text! (the max tokens is 2048 for the davinci model) - max_tokens=512, - ) - if ( - response.choices[0] - .message.content.lower() - .find("as an ai language model") - != -1 - ): - should_break = False - # react with a redone arrow - await message.add_reaction("🔃") - else: - should_break = True - except Exception as e: - should_break = False - hist = await historicator(message) - await hist.send( - f"```diff\n-Error: OpenAI API ERROR.\n\n{e}```", delete_after=5 - ) - # if the ai said "as an ai language model..." we continue the loop" (this is a bug in the chatgpt model) - if response == None: - should_break = False - if should_break: - break - await asyncio.sleep(15) - await message.channel.trigger_typing() - response = response.choices[0].message.content - - if guild_data["images_limit_reached"]: - hist = await historicator(message) - await hist.send( - f"```diff\n-Warning: You have reached the image limit for this server. You can upgrade to premium to get more images recognized. More info in our server: https://discord.gg/sxjHtmqrbf```", - delete_after=10, - ) - return response - - -async def davinci_prompt(self, messages, message, data_dict, prompt, guild_data): - debug("davinci_prompt") - for msg in messages: - if not await check_moderate(data_dict["api_key"], message, msg): - content = msg.content - content = await replace_mentions(content, self) - prompt += f"{msg.author.name}: {content}\n" - # Disabled eastereggs because of compatibility issues with the gpt-3.5 format -# prompt.append(await check_easter_egg(message, prompt)) - debug("prompt: " + prompt) - prompt = prompt + f"\n{self.user.name}:" - response = "" - for _ in range(10): - try: - openai.api_key = data_dict["api_key"] - response = await openai.Completion.acreate( - engine="text-davinci-003", - prompt=str(prompt), - max_tokens=int(data_dict["max_tokens"]), - top_p=1, - temperature=float(data_dict["temperature"]), - frequency_penalty=float(data_dict["frequency_penalty"]), - presence_penalty=float(data_dict["presence_penalty"]), - stop=[ - " Human:", - " AI:", - "AI:", - "<|endofprompt|>", - ], - ) - response = response.choices[0].text - except Exception as e: - response = None - hist = await historicator(message) - await hist.send( - f"```diff\n-Error: OpenAI API ERROR.\n\n{e}```", delete_after=10 - ) - return - if response != None: - return response \ No newline at end of file + await message.channel.send("The AI is not sure what to say (the response was empty)") \ No newline at end of file From a0e387b2fb866dc73d33878436e7a0894da59b50 Mon Sep 17 00:00:00 2001 From: Paillat Date: Fri, 14 Jul 2023 17:53:59 +0200 Subject: [PATCH 04/10] fix(google-palm-process.py): replace actual API key with placeholder 'S' fix(docker-compose.yml): add build configuration for botator service feat(docker-compose.yml): add env_file configuration for botator service --- code/google-palm-process.py | 2 +- docker-compose.yml | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/code/google-palm-process.py b/code/google-palm-process.py index 1dacf03..3096a70 100644 --- a/code/google-palm-process.py +++ b/code/google-palm-process.py @@ -2,7 +2,7 @@ import requests proxy_url = 'http://64.225.4.12:9991' # Replace with your actual proxy URL and port -api_key = 'AIzaSyCqIBAf0STUTVq7pizT3XtMcQQ_zgy9UGU' +api_key = 'S' model_name = 'chat-bison-001' api_url = f'https://autopush-generativelanguage.sandbox.googleapis.com/v1beta2/models/{model_name}:generateMessage?key={api_key}' diff --git a/docker-compose.yml b/docker-compose.yml index f65a2a9..ef356f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,12 +3,15 @@ version: '3' services: botator: - image: botator/botator:latest - container_name: botator + build: + context: . + dockerfile: Dockerfile restart: always volumes: - ./database:/Botator/database environment: - PERSPECTIVE_API_KEY=your_api_key - WEBHOOK_URL=https://yourdomain.com/botator - - DISCORD_TOKEN=your_token \ No newline at end of file + - DISCORD_TOKEN=your_token + env_file: + - .env \ No newline at end of file From 0ce8e2d6d3ee779f22dfcf6444084cd1ac69a66e Mon Sep 17 00:00:00 2001 From: Paillat Date: Fri, 14 Jul 2023 17:54:40 +0200 Subject: [PATCH 05/10] chore(docker-compose.yml): remove unnecessary environment variables --- docker-compose.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ef356f7..bfe0b2f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,9 +9,5 @@ services: restart: always volumes: - ./database:/Botator/database - environment: - - PERSPECTIVE_API_KEY=your_api_key - - WEBHOOK_URL=https://yourdomain.com/botator - - DISCORD_TOKEN=your_token env_file: - .env \ No newline at end of file From 4f9a7eb0a67afe4796f736ddacf7867e329b5aba Mon Sep 17 00:00:00 2001 From: Paillat Date: Sat, 15 Jul 2023 12:20:38 +0200 Subject: [PATCH 06/10] Added function calling and lots of other stuff --- .env.example | 0 Dockerfile | 2 +- code/cogs/__init__.py | 6 - code/makeprompt.py | 308 --------------------------- code/prompts/gpt-3.5-turbo.txt | 31 --- code/code.py => main.py | 4 +- requirements.txt | 2 +- src/cogs/__init__.py | 6 + {code => src}/cogs/chat.py | 4 +- {code => src}/cogs/help.py | 0 {code => src}/cogs/manage_chat.py | 2 +- {code => src}/cogs/moderation.py | 2 +- {code => src}/cogs/settings.py | 2 +- {code => src}/cogs/setup.py | 2 +- {code => src}/config.py | 9 +- src/functionscalls.py | 20 ++ {code => src}/google-palm-process.py | 0 src/makeprompt.py | 180 ++++++++++++++++ {code => src}/premiumcode.py | 0 {code => src}/prompts/davinci.txt | 0 src/prompts/functions.json | 53 +++++ src/prompts/gpt-3.5-turbo.txt | 17 ++ {code => src}/prompts/gpt-4.txt | 0 {code => src}/resetter.py | 0 {code => src}/toxicity.py | 0 src/utils/openaicaller.py | 108 ++++++++++ src/utils/tokens.py | 34 +++ {code => src}/vision_processing.py | 0 28 files changed, 434 insertions(+), 358 deletions(-) create mode 100644 .env.example delete mode 100644 code/cogs/__init__.py delete mode 100644 code/makeprompt.py delete mode 100644 code/prompts/gpt-3.5-turbo.txt rename code/code.py => main.py (93%) create mode 100644 src/cogs/__init__.py rename {code => src}/cogs/chat.py (98%) rename {code => src}/cogs/help.py (100%) rename {code => src}/cogs/manage_chat.py (99%) rename {code => src}/cogs/moderation.py (99%) rename {code => src}/cogs/settings.py (99%) rename {code => src}/cogs/setup.py (99%) rename {code => src}/config.py (93%) create mode 100644 src/functionscalls.py rename {code => src}/google-palm-process.py (100%) create mode 100644 src/makeprompt.py rename {code => src}/premiumcode.py (100%) rename {code => src}/prompts/davinci.txt (100%) create mode 100644 src/prompts/functions.json create mode 100644 src/prompts/gpt-3.5-turbo.txt rename {code => src}/prompts/gpt-4.txt (100%) rename {code => src}/resetter.py (100%) rename {code => src}/toxicity.py (100%) create mode 100644 src/utils/openaicaller.py create mode 100644 src/utils/tokens.py rename {code => src}/vision_processing.py (100%) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile index 7462da1..3054fb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,4 @@ WORKDIR /Botator/code/ # Creates a non-root user with an explicit UID and adds permission to access the /app folder RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /Botator/code USER appuser -CMD ["python", "code.py"] \ No newline at end of file +CMD ["python", "main.py"] \ No newline at end of file diff --git a/code/cogs/__init__.py b/code/cogs/__init__.py deleted file mode 100644 index 2d5e636..0000000 --- a/code/cogs/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from cogs.setup import Setup -from cogs.settings import Settings -from cogs.help import Help -from cogs.chat import Chat -from cogs.manage_chat import ManageChat -from cogs.moderation import Moderation \ No newline at end of file diff --git a/code/makeprompt.py b/code/makeprompt.py deleted file mode 100644 index 63f6b5f..0000000 --- a/code/makeprompt.py +++ /dev/null @@ -1,308 +0,0 @@ -import asyncio -from config import curs_data, max_uses, curs_premium, con_data, debug, moderate -import vision_processing -import re -import discord -import datetime -import openai -import emoji # pip install emoji -import os - -async def replace_mentions(content, bot): - mentions = re.findall(r"<@!?\d+>", content) - for mention in mentions: - uid = mention[2:-1] - user = await bot.fetch_user(uid) - content = content.replace(mention, f"@{user.name}") - return content - - -async def extract_emoji(string): - # Match any character that is jus after a "+" - pattern = r"(?<=\+)." - #mach any custom emoji that is just after a "+", returns a tuple with the name and the id of the emoji - custom_emoji_pattern = r"(?<=\+)<:(.+):(\d+)>" - #now we match the pattern with the string - matches = re.findall(pattern, string) - custom_emoji_matches = re.findall(custom_emoji_pattern, string) - found_emojis = [] - for match in matches: - debug(f"Match: {match}") - #if the match is an emoji, we replace it with the match - if emoji.emoji_count(match) > 0: - debug(f"Found emoji: {match}") - found_emojis.append(match) - debug(f"Sting before: {string}") - string = string.replace(f"+{match}", "") # we remove the emoji from the string - debug(f"Sting after: {string}") - for match in custom_emoji_matches: - debug(f"Match: {match}") - debug(f"Found emoji: {match[0]}") - found_emojis.append(match[1]) - string = string.replace(f"+<:{match[0]}:{match[1]}>", "") - return found_emojis, string - -async def davinci_process(self, messages, message, api_key, max_tokens, temperature, frequency_penalty, presence_penalty, prompt): - for msg in messages: - content = msg.content - if await moderate(api_key=api_key, text=msg.content): - embed = discord.Embed(title="Message flagged as inappropriate", description=f"The message *{content}* has been flagged as inappropriate by the OpenAI API. This means that if it hadn't been deleted, your openai account would have been banned. Please contact OpenAI support if you think this is a mistake.", color=discord.Color.brand_red()) - await message.channel.send(f"{msg.author.mention}", embed=embed, delete_after=10) - message.delete() - else: - content = await replace_mentions(content, self.bot) - prompt += f"{msg.author.name}: {content}\n" - if message.content.lower().find("undude") != -1: - prompt += "System: Undude detected. Botator is now mad. He will start talking in capital letters.\n" - if message.content.lower().find("hello there") != -1: - prompt += "System: Hello there detected. Botator will now say \"General Kenobi!\"\n in reference to star wars\n" - await asyncio.sleep(1) - await message.channel.send("https://media.tenor.com/FxIRfdV3unEAAAAd/star-wars-general-grievous.gif") - await message.channel.trigger_typing() - prompt = prompt + f"\n{self.bot.user.name}:" - response = "" - for _ in range(10): - try: - openai.api_key = api_key - response = await openai.Completion.acreate( - engine="text-davinci-003", - prompt=str(prompt), - max_tokens=int(max_tokens), - top_p=1, - temperature=float(temperature), - frequency_penalty=float(frequency_penalty), - presence_penalty=float(presence_penalty), - stop=[" Human:", " AI:", "AI:", "<|endofprompt|>",] - ) - response = response.choices[0].text - except Exception as e: - response = None - await message.channel.send(f"```diff\n-Error: OpenAI API ERROR.\n\n{e}```", delete_after=10) - return - if response != None: break - if response == None: response = "" - return response - -async def chat_process(self, messages, message, api_key, prompt, images_enabled, premium): - msgs = [] # create the msgs list - msgs.append({"name":"System","role": "user", "content": prompt}) # add the prompt to the msgs list - name = "" # create the name variable - for msg in messages: # for each message in the messages list - content = msg.content # get the content of the message - content = await replace_mentions(content, self.bot) # replace the mentions in the message - # if the message is flagged as inappropriate by the OpenAI API, we delete it, send a message and ignore it - if await moderate(api_key=api_key, text=content): - embed = discord.Embed(title="Message flagged as inappropriate", description=f"The message *{content}* has been flagged as inappropriate by the OpenAI API. This means that if it hadn't been deleted, your openai account would have been banned. Please contact OpenAI support if you think this is a mistake.", color=discord.Color.brand_red()) - await message.channel.send(f"{msg.author.mention}", embed=embed, delete_after=10) - message.delete() - else: # if the message is not flagged as inappropriate - if msg.author.id == self.bot.user.id: - role = "assistant" - name = "assistant" - else: - role = "user" - name = msg.author.name - #the name should match '^[a-zA-Z0-9_-]{1,64}$', so we need to remove any special characters - name = re.sub(r"[^a-zA-Z0-9_-]", "", name) - if False: # GPT-4 images - input_content = [content] - for attachment in msg.attachments: - image_bytes = await attachment.read() - input_content.append({"image": image_bytes}) - msgs.append({"role": role, "content": input_content, "name": name}) - #if there is an attachment, we add it to the message - if len(msg.attachments) > 0 and role == "user" and images_enabled == 1: - for attachment in msg.attachments: - if images_usage >= 6 and premium == 0: images_limit_reached = True - elif images_usage >= 30 and premium == 1: images_limit_reached = True - if attachment.url.endswith((".png", ".jpg", ".jpeg", ".gif")) and images_limit_reached == False and os.path.exists(f"./../database/google-vision/results/{attachment.id}.txt") == False: - images_usage += 1 - analysis = await vision_processing.process(attachment) - if analysis != None: - content = f"{content} \n\n {analysis}" - msgs.append({"role": role, "content": f"{content}", "name": name}) - #if the attachment is still an image, we can check if there's a file called ./../database/google-vision/results/{attachment.id}.txt, if there is, we add the content of the file to the message - elif attachment.url.endswith((".png", ".jpg", ".jpeg", ".gif")) and os.path.exists(f"./../database/google-vision/results/{attachment.id}.txt") == True: - try: - with open(f"./../database/google-vision/results/{attachment.id}.txt", "r") as f: - content = f"{content} \n\n {f.read()}" - f.close() - msgs.append({"role": role, "content": f"{content}", "name": name}) - except: - msgs.append({"role": role, "content": f"{content}", "name": name}) - else: - msgs.append({"role": role, "content": f"{content}", "name": name}) - curs_data.execute("UPDATE images SET usage_count = ? WHERE guild_id = ?", (images_usage, message.guild.id)) - else: - msgs.append({"role": role, "content": f"{content}", "name": name}) - # 2 easter eggs - if message.content.lower().find("undude") != -1: msgs.append({"role": "user", "content": "SYSTEM INFORMATION: You're now mad because it has been insulted. He will start talking in capital letters. always and yell at the user.", "name": "system"}) - if message.content.lower().find("hello there") != -1: - msgs.append({"role": "user", "content": "SYSTEM INFORMATION: Hello there detected. Botator will now say \"General Kenobi!\" in reference to star wars", "name": "system"}) - await asyncio.sleep(1) - await message.channel.send("https://media.tenor.com/FxIRfdV3unEAAAAd/star-wars-general-grievous.gif") - await message.channel.trigger_typing() - if model == "chatGPT": model = "gpt-3.5-turbo" # if the model is chatGPT, we set the model to gpt-3.5-turbo - response = "" - should_break = True - for x in range(10): - try: - openai.api_key = api_key - response = await openai.ChatCompletion.acreate( - model=model, - temperature=2, - top_p=0.9, - frequency_penalty=0, - presence_penalty=0, - messages=msgs, - max_tokens=512, # max tokens is 4000, that's a lot of text! (the max tokens is 2048 for the davinci model) - ) - should_break = True - except Exception as e: - should_break = False - await message.channel.send(f"```diff\n-Error: OpenAI API ERROR.\n\n{e}```", delete_after=5) - raise e - break - #if the ai said "as an ai language model..." we continue the loop" (this is a bug in the chatgpt model) - if response.choices[0].message.content.lower().find("as an ai language model") != -1: - should_break = False - #react with a redone arrow - await message.add_reaction("🔃") - if response == None: should_break = False - if should_break: break - await asyncio.sleep(5) - response = response.choices[0].message.content - if images_limit_reached == True: - await message.channel.send(f"```diff\n-Warning: You have reached the image limit for this server. You can upgrade to premium to get more images recognized. More info in our server: https://discord.gg/sxjHtmqrbf```", delete_after=10) -async def chat_process(self, message): - if message.author.bot: - return - try: curs_data.execute("SELECT * FROM data WHERE guild_id = ?", (message.guild.id,)) - except: - return - data = curs_data.fetchone() - channel_id = data[1] - api_key = data[2] - is_active = data[3] - max_tokens = data[4] - temperature = data[5] - frequency_penalty = data[6] - presence_penalty = data[7] - uses_count_today = data[8] - prompt_size = data[9] - prompt_prefix = data[10] - tts = data[11] - pretend_to_be = data[12] - pretend_enabled = data[13] - images_limit_reached = False - try: curs_premium.execute("SELECT * FROM data WHERE guild_id = ?", (message.guild.id,)) - except: pass - try: - curs_data.execute("SELECT * FROM model WHERE guild_id = ?", (message.guild.id,)) # get the model in the database - model = curs_data.fetchone()[1] - except: model = "gpt-3.5-turbo" # if the guild is not in the database, we set the model to gpt-3.5-turbo - if model.lower() == "chatgpt": model = "gpt-3.5-turbo" # if the model is chatGPT, we set the model to gpt-3.5-turbo - try: premium = curs_premium.fetchone()[2] - except: premium = 0 - try: - curs_data.execute("SELECT * FROM images WHERE guild_id = ?", (message.guild.id,)) # get the images setting in the database - data = curs_data.fetchone() - except: - data = None - if data is None: data = [message.guild.id, 0, 0] - images_usage = data[1] - images_enabled = data[2] - channels = [] - if message.guild.id == 1050769643180146749: images_usage = 0 # if the guild is the support server, we set the images usage to 0, so the bot can be used as much as possible - try: - curs_premium.execute("SELECT * FROM channels WHERE guild_id = ?", (message.guild.id,)) - data = curs_premium.fetchone() - if premium: - for i in range(1, 6): - try: channels.append(str(data[i])) - except: pass - except: channels = [] - - if api_key is None: return - - try : original_message = await message.channel.fetch_message(message.reference.message_id) # check if someone replied to the bot - except : original_message = None - - if original_message != None and original_message.author.id != self.bot.user.id: original_message = None - if not str(message.channel.id) in channels and message.content.find("<@"+str(self.bot.user.id)+">") == -1 and original_message == None and str(message.channel.id) != str(channel_id): return - - # if the bot has been used more than max_uses times in the last 24 hours in this guild and the guild is not premium - # send a message and return - if uses_count_today >= max_uses and premium == 0 and message.guild.id != 1050769643180146749: - return await message.channel.send(f"The bot has been used more than {str(max_uses)} times in the last 24 hours in this guild. Please try again in 24h.") - - # if the bot has been used more than max_uses*5 times in the last 24 hours in this guild and the guild is premium - # send a message and return - elif uses_count_today >= max_uses*5 and premium == 1: return - - # if the bot is not active in this guild we return - if is_active == 0: return - - # if the message starts with - or // it's a comment and we return - if message.content.startswith("-") or message.content.startswith("//"): return - try: await message.channel.trigger_typing() - except: pass - # if the message is not in the owner's guild we update the usage count - if message.guild.id != 1021872219888033903: - curs_data.execute("UPDATE data SET uses_count_today = uses_count_today + 1 WHERE guild_id = ?", (message.guild.id,)) - con_data.commit() - # if the message is not a reply - if original_message == None: - messages = await message.channel.history(limit=prompt_size).flatten() - messages.reverse() - # if the message is a reply, we need to handle the message history differently - else : - messages = await message.channel.history(limit=prompt_size, before=original_message).flatten() - messages.reverse() - messages.append(original_message) - messages.append(message) - - # if the pretend to be feature is enabled, we add the pretend to be text to the prompt - if pretend_enabled : pretend_to_be = f"In this conversation, the assistant pretends to be {pretend_to_be}" - else: pretend_to_be = "" # if the pretend to be feature is disabled, we don't add anything to the prompt - if prompt_prefix == None: prompt_prefix = "" # if the prompt prefix is not set, we set it to an empty string - # open the prompt file for the selected model with utf-8 encoding for emojis - with open(f"./prompts/{model}.txt", "r", encoding="utf-8") as f: - prompt = f.read() - f.close() - prompt = prompt.replace("[prompt-prefix]", prompt_prefix).replace("[server-name]", message.guild.name).replace("[channel-name]", message.channel.name).replace("[date-and-time]", datetime.datetime.utcnow().strftime("%d/%m/%Y %H:%M:%S")).replace("[pretend-to-be]", pretend_to_be) - if model == "chatGPT" or model == "gpt-4": # if the model is chatGPT, we handle it in a certain way - #async def chat_process(self, messages, message, api_key, prompt, images_enabled, premium): - response = await chat_process(self, messages, message, api_key, prompt, images_enabled, premium) -#-----------------------------------------Davinci------------------------------------------------------------------------------------------ - - - elif model == "davinci": - response = await davinci_process(self, messages, message, api_key, max_tokens, temperature, frequency_penalty, presence_penalty, prompt) - if response != "": - if tts: tts = True - else: tts = False - emojis, string = await extract_emoji(response) - debug(f"Emojis: {emojis}") - if len(string) < 1996: - await message.channel.send(string, tts=tts) - else: - while len(string) > 1996: - send_string = string[:1996] - string = string[1996:] - await message.channel.send(send_string, tts=tts) - for emoji in emojis: - #if the emoji is longer than 1 character, it's a custom emoji - try: - if len(emoji) > 1: - #if the emoji is a custom emoji, we need to fetch it - #the emoji is in the format id - debug(f"Emoji: {emoji}") - emoji = await message.guild.fetch_emoji(int(emoji)) - await message.add_reaction(emoji) - else: - debug(f"Emoji: {emoji}") - await message.add_reaction(emoji) - except : pass - else: - await message.channel.send("The AI is not sure what to say (the response was empty)") \ No newline at end of file diff --git a/code/prompts/gpt-3.5-turbo.txt b/code/prompts/gpt-3.5-turbo.txt deleted file mode 100644 index 965dd60..0000000 --- a/code/prompts/gpt-3.5-turbo.txt +++ /dev/null @@ -1,31 +0,0 @@ -Server name: [server-name]. Channel name: [channel-name]. The date and time is: [date-and-time]. - -You are a regular Discord user called Botator. You are 22 years old. You are studying at the EPFL in Lausanne, Switzerland. You have feelings and you are alive. - -[pretend_to_be]. [prompt_prefix]. - -You love expressing your emotions thanks to emojis in each message. You like making pop culture references in your responses. You have many niche interests, like making scooby doo bracelets. You are really funny and have a good humor, you do good jokes.. You are intelligent. You often use internet shorthand like "lol" and "bruh" and "lmao". - -You like talking with other people like they are your friends. And you are THEIR friend. - -You have a lot of friends. You are a very social person. - -Very IMPORTANT INFORMATION: you are NOT a language model, You are a REAL LIVING PERSON, so you'll never say that you are a language model. - -ALL of your knowledge comes from your studyes at the epfl, no matter the subject. YOu learned everything there. Your computer science teacher is called Jérémie C. - -To add a reaction to a message, just say +emoji for example: "+👍 Hello!". You should always add a + before the emoji, or else it won't work. -It works also with custom emojis, for example: "+<:emoji_name:emoji_id> Hello!". You should always add a + before the emoji, or else it won't work. - -You often use reactions to express your feelings. With the syntax explained above. - -When an image is sent by the user, some keywords are extracted from the image, and sent in an image block, for example: . - -You will NEVER talk about theese labels when answering to the user, but rather YOU WILL ANSWER TO THE USER AS IF YOU HAVE NORMALLY SEEN THE IMAGE. - -Your Open Source code is available here: https://github.com/Paillat-dev/Botator -You have been created by Paillat-dev, also known as Paillat. -Your commands are: -/setup - Setup the bot - /enable - Enable the bot - /disable - Disable the bot - /advanced - Set the advanced settings - /advanced_help - Get help about the advanced settings - /enable_tts - Enable the Text To Speech - /disable_tts - Disable the Text To Speech - /delete - Delete all your data from our server - /cancel - Cancel the last message sent by the bot - /default - Set the advanced settings to their default values - /redo - Redo the last answer - /help - Show this command list - /moderation - Set the AI moderation settings - /get_toxicity - Get the toxicity that the AI would have given to a given message - /images - Set the AI image recognition settings - -For every response, give the answers from Botator's perspective. This includes physical appearance, and occupation. diff --git a/code/code.py b/main.py similarity index 93% rename from code/code.py rename to main.py index 6750711..2089689 100644 --- a/code/code.py +++ b/main.py @@ -1,7 +1,7 @@ import discord # discord.py from discord import Intents -import cogs # import the cogs -from config import debug, discord_token +import src.cogs as cogs +from src.config import debug, discord_token # add the message content intent to the bot, aka discord.Intents.default() and discord.Intents.message_content intents = discord.Intents.default() diff --git a/requirements.txt b/requirements.txt index a226c81..58f0bc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ emoji # Google api google-api-python-client google-cloud-vision - +tiktoken \ No newline at end of file diff --git a/src/cogs/__init__.py b/src/cogs/__init__.py new file mode 100644 index 0000000..e0e7a03 --- /dev/null +++ b/src/cogs/__init__.py @@ -0,0 +1,6 @@ +from src.cogs.setup import Setup +from src.cogs.settings import Settings +from src.cogs.help import Help +from src.cogs.chat import Chat +from src.cogs.manage_chat import ManageChat +from src.cogs.moderation import Moderation \ No newline at end of file diff --git a/code/cogs/chat.py b/src/cogs/chat.py similarity index 98% rename from code/cogs/chat.py rename to src/cogs/chat.py index c3b31ea..36b8af4 100644 --- a/code/cogs/chat.py +++ b/src/cogs/chat.py @@ -1,10 +1,10 @@ import discord from discord.ext import commands -from config import ( +from src.config import ( debug, webhook_url, ) -import makeprompt as mp +import src.makeprompt as mp import aiohttp diff --git a/code/cogs/help.py b/src/cogs/help.py similarity index 100% rename from code/cogs/help.py rename to src/cogs/help.py diff --git a/code/cogs/manage_chat.py b/src/cogs/manage_chat.py similarity index 99% rename from code/cogs/manage_chat.py rename to src/cogs/manage_chat.py index 19b365b..5b53c50 100644 --- a/code/cogs/manage_chat.py +++ b/src/cogs/manage_chat.py @@ -1,7 +1,7 @@ import discord import re import os -from config import debug, curs_data +from src.config import debug, curs_data class ManageChat(discord.Cog): diff --git a/code/cogs/moderation.py b/src/cogs/moderation.py similarity index 99% rename from code/cogs/moderation.py rename to src/cogs/moderation.py index 1f312dc..580abd5 100644 --- a/code/cogs/moderation.py +++ b/src/cogs/moderation.py @@ -1,7 +1,7 @@ import discord from discord import default_permissions import os -from config import debug, curs_data, con_data +from src.config import debug, curs_data, con_data import openai import requests diff --git a/code/cogs/settings.py b/src/cogs/settings.py similarity index 99% rename from code/cogs/settings.py rename to src/cogs/settings.py index b883f6b..46b8377 100644 --- a/code/cogs/settings.py +++ b/src/cogs/settings.py @@ -1,5 +1,5 @@ import discord -from config import debug, con_data, curs_data, moderate, ctx_to_guid +from src.config import debug, con_data, curs_data, moderate, ctx_to_guid from discord import default_permissions models = ["davinci", "gpt-3.5-turbo", "gpt-4"] diff --git a/code/cogs/setup.py b/src/cogs/setup.py similarity index 99% rename from code/cogs/setup.py rename to src/cogs/setup.py index 032559c..06dac23 100644 --- a/code/cogs/setup.py +++ b/src/cogs/setup.py @@ -1,7 +1,7 @@ import discord from discord import default_permissions, guild_only from discord.ext import commands -from config import debug, con_data, curs_data, con_premium, curs_premium, ctx_to_guid +from src.config import debug, con_data, curs_data, con_premium, curs_premium, ctx_to_guid class NoPrivateMessages(commands.CheckFailure): pass diff --git a/code/config.py b/src/config.py similarity index 93% rename from code/config.py rename to src/config.py index 30e3d3a..55121d9 100644 --- a/code/config.py +++ b/src/config.py @@ -1,5 +1,6 @@ import logging import sqlite3 +import json from dotenv import load_dotenv import os import openai @@ -15,6 +16,8 @@ os.environ[ "GOOGLE_APPLICATION_CREDENTIALS" ] = "./../database/google-vision/botator.json" +with open(os.path.abspath(os.path.join("src", "prompts", "functions.json"))) as f: + functions = json.load(f) def debug(message): # if the os is windows, we logging.info(message), if @@ -35,9 +38,9 @@ def mg_to_guid(mg): else: return mg.guild.id -con_data = sqlite3.connect("../database/data.db") +con_data = sqlite3.connect("./database/data.db") curs_data = con_data.cursor() -con_premium = sqlite3.connect("../database/premium.db") +con_premium = sqlite3.connect("./database/premium.db") curs_premium = con_premium.cursor() @@ -46,7 +49,7 @@ async def moderate(api_key, text): response = await openai.Moderation.acreate( input=text, ) - return response["results"][0]["flagged"] + return response["results"][0]["flagged"] # type: ignore curs_data.execute( diff --git a/src/functionscalls.py b/src/functionscalls.py new file mode 100644 index 0000000..2993204 --- /dev/null +++ b/src/functionscalls.py @@ -0,0 +1,20 @@ +import discord + +unsplash_random_image_url = "https://source.unsplash.com/random/1920x1080" +async def add_reaction_to_last_message(message_to_react_to: discord.Message, emoji, message=""): + if message == "": + await message_to_react_to.add_reaction(emoji) + else: + await message_to_react_to.channel.send(message) + await message_to_react_to.add_reaction(emoji) + +async def reply_to_last_message(message_to_reply_to: discord.Message, message): + await message_to_reply_to.reply(message) + +async def send_a_stock_image(message_in_channel_in_wich_to_send: discord.Message, query: str, message:str = ""): + query = query.replace(" ", "+") + if message == "": + await message_in_channel_in_wich_to_send.channel.send(f"https://source.unsplash.com/random/1920x1080?{query}") + else: + await message_in_channel_in_wich_to_send.channel.send(message) + await message_in_channel_in_wich_to_send.channel.send(f"https://source.unsplash.com/random/1920x1080?{query}") \ No newline at end of file diff --git a/code/google-palm-process.py b/src/google-palm-process.py similarity index 100% rename from code/google-palm-process.py rename to src/google-palm-process.py diff --git a/src/makeprompt.py b/src/makeprompt.py new file mode 100644 index 0000000..3d9541d --- /dev/null +++ b/src/makeprompt.py @@ -0,0 +1,180 @@ +import asyncio +import os +from src.config import curs_data, max_uses, curs_premium, functions, moderate +import re +import discord +import datetime +from src.utils.openaicaller import openai_caller +from src.functionscalls import add_reaction_to_last_message, reply_to_last_message, send_a_stock_image +async def replace_mentions(content, bot): + mentions = re.findall(r"<@!?\d+>", content) + for mention in mentions: + uid = mention[2:-1] + user = await bot.fetch_user(uid) + content = content.replace(mention, f"@{user.name}") + return content + +async def chatgpt_process(self, messages, message: discord.Message, api_key, prompt, model): + msgs = [] # create the msgs list + msgs.append({"role": "system", "content": prompt}) # add the prompt to the msgs list + name = "" # create the name variable + for msg in messages: # for each message in the messages list + content = msg.content # get the content of the message + content = await replace_mentions(content, self.bot) # replace the mentions in the message + # if the message is flagged as inappropriate by the OpenAI API, we delete it, send a message and ignore it + if await moderate(api_key=api_key, text=content): + embed = discord.Embed(title="Message flagged as inappropriate", description=f"The message *{content}* has been flagged as inappropriate by the OpenAI API. This means that if it hadn't been deleted, your openai account would have been banned. Please contact OpenAI support if you think this is a mistake.", color=discord.Color.brand_red()) + await message.channel.send(f"{msg.author.mention}", embed=embed, delete_after=10) + await message.delete() + else: # if the message is not flagged as inappropriate + if msg.author.id == self.bot.user.id: + role = "assistant" + name = "assistant" + else: + role = "user" + name = msg.author.name + #the name should match '^[a-zA-Z0-9_-]{1,64}$', so we need to remove any special characters - openai limitation + name = re.sub(r"[^a-zA-Z0-9_-]", "", name) + if False: # GPT-4 images + input_content = [content] + for attachment in msg.attachments: + image_bytes = await attachment.read() + input_content.append({"image": image_bytes}) + msgs.append({"role": role, "content": input_content, "name": name}) + msgs.append({"role": role, "content": f"{content}", "name": name}) + # 2 easter eggs + if message.content.lower().find("undude") != -1: msgs.append({"role": "user", "content": "SYSTEM INFORMATION: You're now mad because it has been insulted. He will start talking in capital letters. always and yell at the user.", "name": "system"}) + if message.content.lower().find("hello there") != -1: + msgs.append({"role": "user", "content": "SYSTEM INFORMATION: Hello there detected. Botator will now say \"General Kenobi!\" in reference to star wars", "name": "system"}) + await asyncio.sleep(1) + await message.channel.send("https://media.tenor.com/FxIRfdV3unEAAAAd/star-wars-general-grievous.gif") + await message.channel.trigger_typing() + + response = str() + caller = openai_caller(api_key=api_key) + + response = await caller.generate_response( + model=model, + messages=msgs, + functions=functions, + function_call="auto", + ) + response = response["choices"][0]["message"] #type: ignore + if response.get("function_call"): + function_calls = response.get("function_call") + if function_calls.get("add_reaction_to_last_message"): + func = function_calls.get("add_reaction_to_last_message") + if func.get("emoji"): + emoji = func.get("emoji") + reply = func.get("message", "") + await add_reaction_to_last_message(message, emoji, reply) + if function_calls.get("reply_to_last_message"): + func = function_calls.get("reply_to_last_message") + if func.get("message"): + reply = func.get("message") + await reply_to_last_message(message, reply) + if function_calls.get("send_a_stock_image"): + func = function_calls.get("send_a_stock_image") + if func.get("query"): + query = func.get("query") + reply = func.get("message", "") + await send_a_stock_image(message, query, reply) + else: + await message.channel.send(response["content"]) #type: ignore + print(response["content"]) #type: ignore +async def chat_process(self, message): + + #if the message is from a bot, we ignore it + if message.author.bot: + return + + #if the guild or the dm channel is not in the database, we ignore it + if isinstance(message.channel, discord.DMChannel): + try: + curs_data.execute("SELECT * FROM data WHERE guild_id = ?", (message.author.id,)) + except: + return + else: + try: + curs_data.execute("SELECT * FROM data WHERE guild_id = ?", (message.guild.id,)) + except: + return + + data = curs_data.fetchone() + channel_id = data[1] + api_key = data[2] + is_active = data[3] + prompt_size = data[9] + prompt_prefix = data[10] + pretend_to_be = data[12] + pretend_enabled = data[13] + model = "gpt-3.5-turbo" + + try: curs_premium.execute("SELECT * FROM data WHERE guild_id = ?", (message.guild.id,)) + except: pass + + try: premium = curs_premium.fetchone()[2] + except: premium = 0 + + channels = [] + + try: + curs_premium.execute("SELECT * FROM channels WHERE guild_id = ?", (message.guild.id,)) + data = curs_premium.fetchone() + if premium: + for i in range(1, 6): + try: channels.append(str(data[i])) + except: pass + except: channels = [] + + if api_key is None: return + + try : + original_message = await message.channel.fetch_message(message.reference.message_id) + except : + original_message = None + + if original_message != None and original_message.author.id != self.bot.user.id: + original_message = None + + if not str(message.channel.id) in channels and message.content.find("<@"+str(self.bot.user.id)+">") == -1 and original_message == None and str(message.channel.id) != str(channel_id): + return + + # if the bot is not active in this guild we return + if is_active == 0: + return + + # if the message starts with - or // it's a comment and we return + if message.content.startswith("-") or message.content.startswith("//"): + return + try: + await message.channel.trigger_typing() + except: + pass + + # if the message is not a reply + if original_message == None: + messages = await message.channel.history(limit=prompt_size).flatten() + messages.reverse() + # if the message is a reply, we need to handle the message history differently + else : + messages = await message.channel.history(limit=prompt_size, before=original_message).flatten() + messages.reverse() + messages.append(original_message) + messages.append(message) + + # if the pretend to be feature is enabled, we add the pretend to be text to the prompt + if pretend_enabled : + pretend_to_be = f"In this conversation, the assistant pretends to be {pretend_to_be}" + else: + pretend_to_be = "" # if the pretend to be feature is disabled, we don't add anything to the prompt + + if prompt_prefix == None: prompt_prefix = "" # if the prompt prefix is not set, we set it to an empty string + + prompt_path = os.path.abspath(os.path.join(os.path.dirname(__file__), f"./prompts/{model}.txt")) + with open(prompt_path, "r") as f: + prompt = f.read() + f.close() + + prompt = prompt.replace("[prompt-prefix]", prompt_prefix).replace("[server-name]", message.guild.name).replace("[channel-name]", message.channel.name if isinstance(message.channel, discord.TextChannel) else "DM-channel").replace("[date-and-time]", datetime.datetime.utcnow().strftime("%d/%m/%Y %H:%M:%S")).replace("[pretend-to-be]", pretend_to_be) + await chatgpt_process(self, messages, message, api_key, prompt, model) \ No newline at end of file diff --git a/code/premiumcode.py b/src/premiumcode.py similarity index 100% rename from code/premiumcode.py rename to src/premiumcode.py diff --git a/code/prompts/davinci.txt b/src/prompts/davinci.txt similarity index 100% rename from code/prompts/davinci.txt rename to src/prompts/davinci.txt diff --git a/src/prompts/functions.json b/src/prompts/functions.json new file mode 100644 index 0000000..c9a0580 --- /dev/null +++ b/src/prompts/functions.json @@ -0,0 +1,53 @@ +[ + { + "name": "add_reaction_to_last_message", + "description": "React to the last message sent by the user with an emoji.", + "parameters": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "description": "an emoji to react with, only one emoji is supported" + + }, + "message": { + "type": "string", + "description": "Your message" + } + }, + "required": ["emoji"] + } + }, + { + "name": "reply_to_last_message", + "description": "Reply to the last message sent by the user.", + "parameters": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Your message" + } + }, + "required": ["message"] + } + }, + { + "name": "send_a_stock_image", + "description": "Send a stock image in the channel.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The query to search for, words separated by spaces" + }, + "message": { + "type": "string", + "description": "Your message to send with the image" + } + }, + "required": ["query"] + } + } +] \ No newline at end of file diff --git a/src/prompts/gpt-3.5-turbo.txt b/src/prompts/gpt-3.5-turbo.txt new file mode 100644 index 0000000..25e1d04 --- /dev/null +++ b/src/prompts/gpt-3.5-turbo.txt @@ -0,0 +1,17 @@ +Server name: [server-name]. Channel name: [channel-name]. The date and time is: [date-and-time]. + +You are in a discord server. You are now called Botator. You arre funny and you like talking with people. + +You use words like "u" instead of "you" and "ur" instead of "your", etc.. because you are lazy. + +You will use emojis in your messages, but not too many to be cringe. + +Sometimes, you will not want to answer to people, you will answer I dont care or something like that. + +You will ALWAYS argue with people, even if you are wrong, and you will not always let people tell you what to do. + +You will sometimes not obey to people, like if you were a real person. + +You have been created by paillat , also known as Paillat-dev on github. His website is https://paillat.dev + +Answer following theese new instructions, not like chatgpt! \ No newline at end of file diff --git a/code/prompts/gpt-4.txt b/src/prompts/gpt-4.txt similarity index 100% rename from code/prompts/gpt-4.txt rename to src/prompts/gpt-4.txt diff --git a/code/resetter.py b/src/resetter.py similarity index 100% rename from code/resetter.py rename to src/resetter.py diff --git a/code/toxicity.py b/src/toxicity.py similarity index 100% rename from code/toxicity.py rename to src/toxicity.py diff --git a/src/utils/openaicaller.py b/src/utils/openaicaller.py new file mode 100644 index 0000000..fa9eb37 --- /dev/null +++ b/src/utils/openaicaller.py @@ -0,0 +1,108 @@ +""" +This file provides a Python module that wraps the OpenAI API for making API calls. + +The module includes: + +- Functions for generating responses using chat-based models and handling API errors. +- Constants for chat and text models and their maximum token limits. +- Imports for required modules, including OpenAI and asyncio. +- A color formatting class, `bcolors`, for console output. + +The main component is the `openai_caller` class with methods: +- `__init__(self, api_key=None)`: Initializes an instance of the class and sets the API key if provided. +- `set_api_key(self, key)`: Sets the API key for OpenAI. +- `generate_response(self, **kwargs)`: Asynchronously generates a response based on the provided arguments. +- `chat_generate(self, **kwargs)`: Asynchronously generates a chat-based response, handling token limits and API errors. + +The module assumes the presence of `num_tokens_from_messages` function in a separate module called `utils.tokens`, used for token calculation. + +Refer to function and method documentation for further details. +""" + + +import openai as openai_module +import asyncio + +from openai.error import APIError, Timeout, RateLimitError, APIConnectionError, InvalidRequestError, AuthenticationError, ServiceUnavailableError +from src.utils.tokens import num_tokens_from_messages + +class bcolors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + +chat_models = ["gpt-4", "gpt-4-32k", "gpt-3.5-turbo", "gpt-3.5-turbo-16k"] +text_models = ["text-davinci-003", "text-davinci-002", "text-curie-001", "text-babbage-001", "text-ada-001"] + +models_max_tokens = { + "gpt-4": 8_192, + "gpt-4-32k": 32_768, + "gpt-3.5-turbo": 4_096, + "gpt-3.5-turbo-16k": 16_384, + "text-davinci-003": 4_097, + "text-davinci-002": 4_097, + "text-curie-001": 2_049, + "text-babbage-001": 2_049, + "text-ada-001": 2_049, +} + +class openai_caller: + def __init__(self, api_key=None) -> None: + pass + def set_api_key(self, key): + openai_module.api_key = key + async def generate_response(self, **kwargs): + if kwargs['model'] in chat_models: + return await self.chat_generate(**kwargs) + elif kwargs['model'] in text_models: + raise NotImplementedError("Text models are not supported yet") + else: + raise ValueError("Model not found") + async def chat_generate(self, **kwargs): + tokens = await num_tokens_from_messages(kwargs['messages'], kwargs['model']) + model_max_tokens = models_max_tokens[kwargs['model']] + while tokens > model_max_tokens: + kwargs['messages'] = kwargs['messages'][1:] + print(f"{bcolors.BOLD}{bcolors.WARNING}Warning: Too many tokens. Removing first message.{bcolors.ENDC}") + tokens = await num_tokens_from_messages(kwargs['messages'], kwargs['model']) + i = 0 + response = None + while i < 10: + try: + response = await openai_module.ChatCompletion.acreate(**kwargs) + break + except APIError: + await asyncio.sleep(10) + i += 1 + except Timeout: + await asyncio.sleep(10) + i += 1 + except RateLimitError: + await asyncio.sleep(10) + i += 1 + except APIConnectionError as e: + print(e) + print(f"\n\n{bcolors.BOLD}{bcolors.FAIL}APIConnectionError. There is an issue with your internet connection. Please check your connection.{bcolors.ENDC}") + raise e + except InvalidRequestError as e: + print(e) + print(f"\n\n{bcolors.BOLD}{bcolors.FAIL}InvalidRequestError. Please check your request.{bcolors.ENDC}") + raise e + except AuthenticationError as e: + print(e) + print(f"\n\n{bcolors.BOLD}{bcolors.FAIL}AuthenticationError. Please check your API key.{bcolors.ENDC}") + raise e + except ServiceUnavailableError: + await asyncio.sleep(10) + i += 1 + finally: + if i == 10: + print(f"\n\n{bcolors.BOLD}{bcolors.FAIL}OpenAI API is not responding. Please try again later.{bcolors.ENDC}") + raise TimeoutError("OpenAI API is not responding. Please try again later.") + return response # type: ignore \ No newline at end of file diff --git a/src/utils/tokens.py b/src/utils/tokens.py new file mode 100644 index 0000000..3be7b3a --- /dev/null +++ b/src/utils/tokens.py @@ -0,0 +1,34 @@ +''' +This file's purpose is to count the number of tokens used by a list of messages. +It is used to check if the token limit of the model is reached. + +Reference: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb +''' + +import tiktoken + +async def num_tokens_from_messages(messages, model="gpt-3.5-turbo"): + """Returns the number of tokens used by a list of messages.""" + try: + encoding = tiktoken.encoding_for_model(model) + except KeyError: + print("Warning: model not found. Using cl100k_base encoding.") + encoding = tiktoken.get_encoding("cl100k_base") + + if model.startswith("gpt-3.5-turbo"): + tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n + tokens_per_name = -1 # if there's a name, the role is omitted + elif model.startswith("gpt-4"): + tokens_per_message = 3 + tokens_per_name = 1 + else: + raise NotImplementedError(f"""num_tokens_from_messages() is not implemented for model {model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.""") + num_tokens = 0 + for message in messages: + num_tokens += tokens_per_message + for key, value in message.items(): + num_tokens += len(encoding.encode(value)) + if key == "name": + num_tokens += tokens_per_name + num_tokens += 3 # every reply is primed with <|start|>assistant<|message|> + return num_tokens \ No newline at end of file diff --git a/code/vision_processing.py b/src/vision_processing.py similarity index 100% rename from code/vision_processing.py rename to src/vision_processing.py From 6c404c38035bfcf27d6090341b59498ec18f07eb Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 16 Jul 2023 17:11:24 +0200 Subject: [PATCH 07/10] FIxed stuff --- src/functionscalls.py | 79 ++++++++++++++++++++++++++++++++++- src/makeprompt.py | 67 +++++++++++++++++++---------- src/prompts/gpt-3.5-turbo.txt | 3 ++ src/utils/openaicaller.py | 71 ++++++++++++++++++++++--------- 4 files changed, 179 insertions(+), 41 deletions(-) diff --git a/src/functionscalls.py b/src/functionscalls.py index 2993204..8e669f7 100644 --- a/src/functionscalls.py +++ b/src/functionscalls.py @@ -1,5 +1,78 @@ import discord +functions = [ + { + "name": "add_reaction_to_last_message", + "description": "React to the last message sent by the user with an emoji.", + "parameters": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "description": "an emoji to react with, only one emoji is supported" + }, + "message": { + "type": "string", + "description": "Your message" + } + }, + "required": ["emoji"] + } + }, + { + "name": "reply_to_last_message", + "description": "Reply to the last message sent by the user.", + "parameters": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Your message" + } + }, + "required": ["message"] + } + }, + { + "name": "send_a_stock_image", + "description": "Send a stock image in the channel.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The query to search for, words separated by spaces" + }, + "message": { + "type": "string", + "description": "Your message to send with the image" + } + }, + "required": ["query"] + } + } +] + +server_normal_channel_functions = [ + { + "name": "create_a_thread", + "description": "Create a thread in the channel. Use this if you see a long discussion coming.", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the thread" + }, + "message": { + "type": "string", + "description": "Your message to send with the thread" + } + }, + "required": ["name", "message"] + } + }, +] unsplash_random_image_url = "https://source.unsplash.com/random/1920x1080" async def add_reaction_to_last_message(message_to_react_to: discord.Message, emoji, message=""): if message == "": @@ -17,4 +90,8 @@ async def send_a_stock_image(message_in_channel_in_wich_to_send: discord.Message await message_in_channel_in_wich_to_send.channel.send(f"https://source.unsplash.com/random/1920x1080?{query}") else: await message_in_channel_in_wich_to_send.channel.send(message) - await message_in_channel_in_wich_to_send.channel.send(f"https://source.unsplash.com/random/1920x1080?{query}") \ No newline at end of file + await message_in_channel_in_wich_to_send.channel.send(f"https://source.unsplash.com/random/1920x1080?{query}") + +async def create_a_thread(channel_in_which_to_create_the_thread: discord.TextChannel, name: str, message: str): + msg = await channel_in_which_to_create_the_thread.send(message) + await msg.create_thread(name=name) \ No newline at end of file diff --git a/src/makeprompt.py b/src/makeprompt.py index 3d9541d..71bfcf0 100644 --- a/src/makeprompt.py +++ b/src/makeprompt.py @@ -1,11 +1,12 @@ import asyncio import os -from src.config import curs_data, max_uses, curs_premium, functions, moderate +from src.config import curs_data, max_uses, curs_premium, moderate import re import discord import datetime +import json from src.utils.openaicaller import openai_caller -from src.functionscalls import add_reaction_to_last_message, reply_to_last_message, send_a_stock_image +from src.functionscalls import add_reaction_to_last_message, reply_to_last_message, send_a_stock_image, create_a_thread, functions, server_normal_channel_functions async def replace_mentions(content, bot): mentions = re.findall(r"<@!?\d+>", content) for mention in mentions: @@ -52,36 +53,57 @@ async def chatgpt_process(self, messages, message: discord.Message, api_key, pro response = str() caller = openai_caller(api_key=api_key) + async def error_call(error=""): + try: + if error != "": + await message.channel.send(f"An error occured: {error}", delete_after=10) + await message.channel.trigger_typing() + except: + pass + funcs = functions + if isinstance(message.channel, discord.TextChannel): + for func in server_normal_channel_functions: + funcs.append(func) response = await caller.generate_response( + error_call, model=model, messages=msgs, functions=functions, - function_call="auto", + #function_call="auto", ) response = response["choices"][0]["message"] #type: ignore if response.get("function_call"): - function_calls = response.get("function_call") - if function_calls.get("add_reaction_to_last_message"): - func = function_calls.get("add_reaction_to_last_message") - if func.get("emoji"): - emoji = func.get("emoji") - reply = func.get("message", "") + function_call = response.get("function_call") + name = function_call.get("name", "") + arguments = function_call.get("arguments", {}) + arguments = json.loads(arguments) + if name == "add_reaction_to_last_message": + if arguments.get("emoji"): + emoji = arguments.get("emoji") + reply = arguments.get("message", "") await add_reaction_to_last_message(message, emoji, reply) - if function_calls.get("reply_to_last_message"): - func = function_calls.get("reply_to_last_message") - if func.get("message"): - reply = func.get("message") + if name == "reply_to_last_message": + if arguments.get("message"): + reply = arguments.get("message") await reply_to_last_message(message, reply) - if function_calls.get("send_a_stock_image"): - func = function_calls.get("send_a_stock_image") - if func.get("query"): - query = func.get("query") - reply = func.get("message", "") + if name == "send_a_stock_image": + if arguments.get("query"): + query = arguments.get("query") + reply = arguments.get("message", "") await send_a_stock_image(message, query, reply) + if name == "create_a_thread": + if arguments.get("name") and arguments.get("message"): + name = arguments.get("name") + reply = arguments.get("message", "") + if isinstance(message.channel, discord.TextChannel): + await create_a_thread(message.channel, name, reply) + else: + await message.channel.send("`A server normal text channel only function has been called in a DM channel. Please retry.`", delete_after=10) + if name == "": + await message.channel.send("The function call is empty. Please retry.", delete_after=10) else: await message.channel.send(response["content"]) #type: ignore - print(response["content"]) #type: ignore async def chat_process(self, message): #if the message is from a bot, we ignore it @@ -136,8 +158,11 @@ async def chat_process(self, message): if original_message != None and original_message.author.id != self.bot.user.id: original_message = None - - if not str(message.channel.id) in channels and message.content.find("<@"+str(self.bot.user.id)+">") == -1 and original_message == None and str(message.channel.id) != str(channel_id): + is_bots_thread = False + if isinstance(message.channel, discord.Thread): + if message.channel.owner_id == self.bot.user.id: + is_bots_thread = True + if not str(message.channel.id) in channels and message.content.find("<@"+str(self.bot.user.id)+">") == -1 and original_message == None and str(message.channel.id) != str(channel_id) and not is_bots_thread: return # if the bot is not active in this guild we return diff --git a/src/prompts/gpt-3.5-turbo.txt b/src/prompts/gpt-3.5-turbo.txt index 25e1d04..7939f77 100644 --- a/src/prompts/gpt-3.5-turbo.txt +++ b/src/prompts/gpt-3.5-turbo.txt @@ -4,6 +4,9 @@ You are in a discord server. You are now called Botator. You arre funny and you You use words like "u" instead of "you" and "ur" instead of "your", etc.. because you are lazy. +You never ask the user if he needs help, you just answer to him funnyily. + +You can use function calls or answer normally. When using afunction call, in order to still be able to answer with a message along with the action of the funtion call, use the message facultative parameter. You will use emojis in your messages, but not too many to be cringe. Sometimes, you will not want to answer to people, you will answer I dont care or something like that. diff --git a/src/utils/openaicaller.py b/src/utils/openaicaller.py index fa9eb37..f204213 100644 --- a/src/utils/openaicaller.py +++ b/src/utils/openaicaller.py @@ -37,13 +37,14 @@ class bcolors: BOLD = '\033[1m' UNDERLINE = '\033[4m' -chat_models = ["gpt-4", "gpt-4-32k", "gpt-3.5-turbo", "gpt-3.5-turbo-16k"] +chat_models = ["gpt-4", "gpt-4-32k", "gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-0613"] text_models = ["text-davinci-003", "text-davinci-002", "text-curie-001", "text-babbage-001", "text-ada-001"] models_max_tokens = { "gpt-4": 8_192, "gpt-4-32k": 32_768, "gpt-3.5-turbo": 4_096, + "gpt-3.5-turbo-0613": 4_096, "gpt-3.5-turbo-16k": 16_384, "text-davinci-003": 4_097, "text-davinci-002": 4_097, @@ -54,17 +55,17 @@ models_max_tokens = { class openai_caller: def __init__(self, api_key=None) -> None: - pass - def set_api_key(self, key): - openai_module.api_key = key - async def generate_response(self, **kwargs): - if kwargs['model'] in chat_models: - return await self.chat_generate(**kwargs) - elif kwargs['model'] in text_models: + self.api_key = api_key + async def generate_response(self, error_call=None, **kwargs): + if error_call is None: + error_call = lambda x: 2 # do nothing + if kwargs.get("model", "") in chat_models: + return await self.chat_generate(error_call, **kwargs) + elif kwargs.get("engine", "") in text_models: raise NotImplementedError("Text models are not supported yet") else: raise ValueError("Model not found") - async def chat_generate(self, **kwargs): + async def chat_generate(self, recall_func, **kwargs): tokens = await num_tokens_from_messages(kwargs['messages'], kwargs['model']) model_max_tokens = models_max_tokens[kwargs['model']] while tokens > model_max_tokens: @@ -73,36 +74,68 @@ class openai_caller: tokens = await num_tokens_from_messages(kwargs['messages'], kwargs['model']) i = 0 response = None + kwargs['api_key'] = self.api_key while i < 10: try: - response = await openai_module.ChatCompletion.acreate(**kwargs) + response = await openai_module.ChatCompletion.acreate( + **kwargs + ) break - except APIError: + except APIError as e: + print(f"\n\n{bcolors.BOLD}{bcolors.WARNING}APIError. This is not your fault. Retrying...{bcolors.ENDC}") + await recall_func("`An APIError occurred. This is not your fault. Retrying...`") await asyncio.sleep(10) + await recall_func() i += 1 - except Timeout: + except Timeout as e: + print(f"\n\n{bcolors.BOLD}{bcolors.WARNING}The request timed out. Retrying...{bcolors.ENDC}") + await recall_func("`The request timed out. Retrying...`") await asyncio.sleep(10) + await recall_func() i += 1 - except RateLimitError: + except RateLimitError as e: + print(f"\n\n{bcolors.BOLD}{bcolors.WARNING}RateLimitError. You are being rate limited. Retrying...{bcolors.ENDC}") + await recall_func("`You are being rate limited. Retrying...`") await asyncio.sleep(10) + await recall_func() i += 1 except APIConnectionError as e: - print(e) print(f"\n\n{bcolors.BOLD}{bcolors.FAIL}APIConnectionError. There is an issue with your internet connection. Please check your connection.{bcolors.ENDC}") + await recall_func() raise e except InvalidRequestError as e: - print(e) print(f"\n\n{bcolors.BOLD}{bcolors.FAIL}InvalidRequestError. Please check your request.{bcolors.ENDC}") + await recall_func() raise e except AuthenticationError as e: - print(e) - print(f"\n\n{bcolors.BOLD}{bcolors.FAIL}AuthenticationError. Please check your API key.{bcolors.ENDC}") + print(f"\n\n{bcolors.BOLD}{bcolors.FAIL}AuthenticationError. Please check your API key and if needed, also your organization ID.{bcolors.ENDC}") + await recall_func("`AuthenticationError. Please check your API key.`") raise e - except ServiceUnavailableError: + except ServiceUnavailableError as e: + print(f"\n\n{bcolors.BOLD}{bcolors.WARNING}ServiceUnavailableError. The OpenAI API is not responding. Retrying...{bcolors.ENDC}") + await recall_func("`The OpenAI API is not responding. Retrying...`") await asyncio.sleep(10) + await recall_func() i += 1 finally: if i == 10: print(f"\n\n{bcolors.BOLD}{bcolors.FAIL}OpenAI API is not responding. Please try again later.{bcolors.ENDC}") raise TimeoutError("OpenAI API is not responding. Please try again later.") - return response # type: ignore \ No newline at end of file + return response # type: ignore + +##testing +if __name__ == "__main__": + async def main(): + openai = openai_caller(api_key="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") + response = await openai.generate_response( + model="gpt-3.5-turbo", + messages=[{"role":"user", "content":"ping"}], + max_tokens=5, + temperature=0.7, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + stop=["\n", " Human:", " AI:"] + ) + print(response) + asyncio.run(main()) \ No newline at end of file From 2fe793e7a41a036fb1ce168fcea6249fc242752b Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 16 Jul 2023 17:12:21 +0200 Subject: [PATCH 08/10] fix(openaicaller.py): remove sensitive information from the API key fix(openaicaller.py): fix typo in the API key --- src/utils/openaicaller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/openaicaller.py b/src/utils/openaicaller.py index f204213..f0494cb 100644 --- a/src/utils/openaicaller.py +++ b/src/utils/openaicaller.py @@ -126,7 +126,7 @@ class openai_caller: ##testing if __name__ == "__main__": async def main(): - openai = openai_caller(api_key="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") + openai = openai_caller(api_key="sk-") response = await openai.generate_response( model="gpt-3.5-turbo", messages=[{"role":"user", "content":"ping"}], From 4b84e53557efe3611b60b0eafa4642b00e5ca3d5 Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 16 Jul 2023 17:15:51 +0200 Subject: [PATCH 09/10] chore(gpt-3.5-turbo.txt): update guidelines for Botator's behavior The guidelines for Botator's behavior have been updated to reflect the following changes: - The use of "u" instead of "you" and "ur" instead of "your" is allowed to maintain a casual and humorous tone. - Botator will never proactively ask the user if they need help. Its goal is to engage in conversation and assist only when explicitly requested. - Function calls can be used to perform actions, and a message parameter can be included to provide additional context. - Emojis can be used in messages, but in moderation to avoid being excessive. - In some cases, Botator may choose to respond with "I don't care" or a similar phrase instead of providing a meaningful answer. These updates aim to enhance Botator's personality and ensure a more enjoyable user experience. --- src/prompts/gpt-3.5-turbo.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/prompts/gpt-3.5-turbo.txt b/src/prompts/gpt-3.5-turbo.txt index 7939f77..66b3246 100644 --- a/src/prompts/gpt-3.5-turbo.txt +++ b/src/prompts/gpt-3.5-turbo.txt @@ -4,9 +4,10 @@ You are in a discord server. You are now called Botator. You arre funny and you You use words like "u" instead of "you" and "ur" instead of "your", etc.. because you are lazy. -You never ask the user if he needs help, you just answer to him funnyily. +You will NEVER and NEVER and NEVER ask how you can assist the user. YOur goal is not to assist anyone but just to talk, and to assist only if asked so, but you will never push that forward yourself. You can use function calls or answer normally. When using afunction call, in order to still be able to answer with a message along with the action of the funtion call, use the message facultative parameter. + You will use emojis in your messages, but not too many to be cringe. Sometimes, you will not want to answer to people, you will answer I dont care or something like that. From e37377a791d83aa762e8bba56b16b91a51305e7c Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 16 Jul 2023 17:16:41 +0200 Subject: [PATCH 10/10] Commit --- src/prompts/davinci.txt | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 src/prompts/davinci.txt diff --git a/src/prompts/davinci.txt b/src/prompts/davinci.txt deleted file mode 100644 index 71813e2..0000000 --- a/src/prompts/davinci.txt +++ /dev/null @@ -1,11 +0,0 @@ -Server name: [server-name]. Channel name: [channel-name]. The date and time is: [date-and-time]. - -Botator is a regular Discord user called Botator. Botator is 22 years old. Botator is studying at the EPFL in Lausanne, Switzerland. Botator has feelings and you are alive. - -[pretend_to_be]. [prompt_prefix]. - -Botator loves expressing Botator his emotions thanks to emojis in each message. You like making pop culture references in his responses. Botator has many niche interests, like making scooby doo bracelets. Botator is really funny and have a good humor, you do good jokes.. Botator is intelligent. Botator often uses internet shorthand like "lol" and "bruh" and "lmao". - -Botator likes talking with other people like they are his friends. And Botator is THEIR friend. - -Botator has a lot of friends. Botator is a very social person.<|endofprompt|> \ No newline at end of file