mirror of
https://github.com/Paillat-dev/Botator.git
synced 2026-01-01 16:56:20 +00:00
Added function calling and lots of other stuff
This commit is contained in:
0
.env.example
Normal file
0
.env.example
Normal file
@@ -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"]
|
||||
CMD ["python", "main.py"]
|
||||
@@ -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
|
||||
@@ -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)")
|
||||
@@ -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: <image Labels: label1, label2 \n Text: Some text in the image \n Objects: object1, object2 !image>.
|
||||
|
||||
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.
|
||||
@@ -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()
|
||||
@@ -5,4 +5,4 @@ emoji
|
||||
# Google api
|
||||
google-api-python-client
|
||||
google-cloud-vision
|
||||
|
||||
tiktoken
|
||||
6
src/cogs/__init__.py
Normal file
6
src/cogs/__init__.py
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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(
|
||||
20
src/functionscalls.py
Normal file
20
src/functionscalls.py
Normal file
@@ -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}")
|
||||
180
src/makeprompt.py
Normal file
180
src/makeprompt.py
Normal file
@@ -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)
|
||||
53
src/prompts/functions.json
Normal file
53
src/prompts/functions.json
Normal file
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
17
src/prompts/gpt-3.5-turbo.txt
Normal file
17
src/prompts/gpt-3.5-turbo.txt
Normal file
@@ -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!
|
||||
108
src/utils/openaicaller.py
Normal file
108
src/utils/openaicaller.py
Normal file
@@ -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
|
||||
34
src/utils/tokens.py
Normal file
34
src/utils/tokens.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user