diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..beb6faf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ + +# Contributing Guidelines + +Thank you for considering contributing to our project! Please read the following guidelines before making your contribution. + +### New Feature Pull Requests + +- To contribute a new feature, please make sure to send your pull request to the `dev` branch. +- Your pull request should include a detailed description of the new feature you are adding and any relevant documentation. +- Please also make sure your code follows PEP8 standards. + +### Bugfix Pull Requests + +- If you are fixing a bug or issue, please send your pull request to the `main` branch. +- Describe the bug or issue that you are fixing in your pull request and include any relevant information. +- Make sure to test your fix thoroughly and include any necessary documentation. + +### General Guidelines + +- Please make sure to be a good person in all interactions and contributions. +- Be respectful and polite when communicating with other contributors and maintain a positive and constructive attitude. +- If you have any questions or need help with your contribution, feel free to reach out to our team. + +We appreciate your interest in improving our project and look forward to reviewing your contributions! diff --git a/cogs/chatgpt.py b/cogs/chatgpt.py index 62fa891..01ab713 100644 --- a/cogs/chatgpt.py +++ b/cogs/chatgpt.py @@ -8,6 +8,7 @@ import aiofiles import aiohttp import discord from discord.ext import commands, tasks +import matplotlib.pyplot as plt class ChatGPT(commands.Cog): @@ -16,7 +17,6 @@ class ChatGPT(commands.Cog): self.API_KEY = os.getenv("openai.api_key") self.working_dir = "tmp/chatgpt/" self.data_dir = "data/chatgpt/" - self.premium_role = 1200943915579228170 self.folder_setup() self.remind_me_loop.start() self.http_session = self.create_aiohttp_session() @@ -36,7 +36,8 @@ class ChatGPT(commands.Cog): self.data_dir, self.data_dir + "config", self.data_dir + "logs", - self.data_dir + "dalle" + self.data_dir + "dalle", + self.data_dir + "costs" ] for folder in folders: @@ -46,6 +47,116 @@ class ChatGPT(commands.Cog): except Exception as e: self.logger.exception(f"ChatGPT failed to make directories: {e}") + def text_cost_calc(self, model, input_tokens, output_tokens): + cost_table = {"gpt-3.5-turbo":{"input_tokens":0.0000005,"output_tokens":0.0000015}, + "gpt-4-turbo-preview":{"input_tokens":0.00001,"output_tokens":0.00003}, + "gpt-4-vision-preview":{"input_tokens":0.00001,"output_tokens":0.00003} + } + input_cost = cost_table[model]["input_tokens"] * input_tokens + output_cost = cost_table[model]["output_tokens"] * output_tokens + cost = input_cost + output_cost + return cost + + def add_cost(self, category: str, cost: float): + day = time.strftime("%d") + month = time.strftime("%B") + year = time.strftime("%Y") + filepath = f"{self.data_dir}costs/{month}_{day}_{year}.json" + if not os.path.exists(filepath): + with open(filepath, "w") as f: + json.dump({},f) + with open(filepath, "r") as f: + costs_dict = json.loads(f.readline()) + if category not in costs_dict: + costs_dict[category] = cost + else: + costs_dict[category] += cost + with open(filepath, "w") as f: + json.dump(costs_dict,f) + + async def graph_cost(self, graph_title, costs_per_day): + plt.plot(costs_per_day.values()) + plt.title(f"{graph_title} Costs") + plt.xlabel("Day") + plt.ylabel("Cost") + plt.savefig(f"{self.data_dir}costs/{graph_title}_costs.png") + plt.close() + return f"{self.data_dir}costs/{graph_title}_costs.png" + + async def graph_category(self, graph_title, cost_per_category): + bar_container = plt.barh(list(cost_per_category.keys()), cost_per_category.values()) + plt.title(f"{graph_title} Costs") + plt.xlabel("Category") + plt.ylabel("Cost") + plt.bar_label(bar_container) + plt.savefig(f"{self.data_dir}costs/{graph_title}_categories.png") + plt.close() + return f"{self.data_dir}costs/{graph_title}_categories.png" + + @commands.command( + name="costs", + brief="Get the costs for a given month and year.", + help="Get the costs for a given month and year. If no month or year is given, it will default to the current month and year.", + aliases=["cost"], + usage="costs [month] [year]", + ) + async def costs(self, ctx, month=time.strftime("%B"), year=time.strftime("%Y")): + print('working') + total_cost = 0 + cost_per_day = {} + for x in range(1,32): + daily_cost = 0 + if x < 10: + x = f"0{x}" + filepath = f"{self.data_dir}costs/{month}_{x}_{year}.json" + if os.path.exists(filepath): + with open(filepath, "r") as f: + costs_dict = json.loads(f.readline()) + for category, cost in costs_dict.items(): + total_cost += cost + daily_cost += cost + cost_per_day[x] = daily_cost + rounded_total = round(total_cost, 2) + if rounded_total == 0: + await ctx.send(f"No data for {month} {year}") + else: + graph_title = f"{month} {year}" + await ctx.send(f"Total cost for {month} {year}: ${rounded_total}", file=discord.File(await self.graph_cost(graph_title, cost_per_day))) + + @commands.command( + name="category_costs", + brief="Get the costs per category for a given month and year.", + help="Get the costs per category for a given month and year. If no month or year is given, it will default to the current month and year.", + aliases=["category_cost"], + usage="category_costs [month] [year] [day]", + ) + async def category_costs(self, ctx, month=time.strftime("%B"), year=time.strftime("%Y"), day=None): + total_cost = 0 + cost_per_category = {} + if day == None: + day_range = range(1,32) + else: + day_range = [int(day)] + for x in day_range: + daily_cost = 0 + if x < 10: + x = f"0{x}" + filepath = f"{self.data_dir}costs/{month}_{x}_{year}.json" + if os.path.exists(filepath): + with open(filepath, "r") as f: + costs_dict = json.loads(f.readline()) + for category, cost in costs_dict.items(): + if category not in cost_per_category: + cost_per_category[category] = cost + else: + cost_per_category[category] += cost + total_cost += cost + graph_title = f"{month} {year}" + rounded_total = round(total_cost, 2) + if rounded_total == 0: + await ctx.send(f"No data for {month} {year}") + else: + await ctx.send(f"Total cost for {month} {year}: ${rounded_total}", file=discord.File(await self.graph_category(graph_title, cost_per_category))) def create_channel_config(self, filepath): config_dict = { @@ -102,6 +213,10 @@ class ChatGPT(commands.Cog): async with self.http_session.post(url, json=data, headers=self.headers) as resp: response_data = await resp.json() response = response_data['choices'][0]['message']['content'] + input_tokens = response_data['usage']['prompt_tokens'] + output_tokens = response_data['usage']['completion_tokens'] + cost = self.text_cost_calc(model, input_tokens, output_tokens) + self.add_cost(model, cost) return response except Exception as error: @@ -190,15 +305,12 @@ class ChatGPT(commands.Cog): brief="Get an answer" ) async def question_gpt4(self, ctx): - if ctx.author.get_role(self.premium_role): - await ctx.send("One moment, let me think...") - question = ctx.message.content.split(" ", maxsplit=1)[1] - answer = await self.answer_question(question, "gpt-4-turbo-preview") - chunks = [answer[i:i+1999] for i in range(0, len(answer), 1999)] - for chunk in chunks: - await ctx.send(chunk) - else: - await ctx.send("Sorry you must be a premium member to use this command. (!donate)") + await ctx.send("One moment, let me think...") + question = ctx.message.content.split(" ", maxsplit=1)[1] + answer = await self.answer_question(question, "gpt-4-turbo-preview") + chunks = [answer[i:i+1999] for i in range(0, len(answer), 1999)] + for chunk in chunks: + await ctx.send(chunk) async def dalle_api_call(self, prompt: str, model: str="dall-e-2", quality: str="standard", size: str="1024x1024") -> tuple: data = { @@ -228,25 +340,22 @@ class ChatGPT(commands.Cog): async def generate_dalle_image(self, ctx, model, quality="standard", size="1024x1024") -> None: - if ctx.author.get_role(self.premium_role): - prompt = ctx.message.content.split(" ", maxsplit=1)[1] - await ctx.send(f"Please be patient this may take some time! Generating: {prompt}.") - resp_status, resp = await self.dalle_api_call(prompt, model=model, quality=quality, size=size) - if resp_status != 200: - await ctx.send(f"Error generating image: {resp_status}: {resp}") - return - my_filename = str(time.time_ns()) + ".png" - image_filepath = f"{self.data_dir}dalle/{my_filename}" - await self.download_image(resp, image_filepath) - with open(image_filepath, "rb") as fh: - f = discord.File(fh, filename=image_filepath) - prompt = prompt.replace('\n',' ') - log_data = f'Author: {ctx.author.name}, Prompt: {prompt}, Filename: {my_filename}\n' - with open(f"{self.data_dir}logs/dalle3.log", 'a') as log_filepath: - log_filepath.writelines(log_data) - await ctx.send(f'Generated by: {ctx.author.name}\nPrompt: {prompt}', file=f) - else: - await ctx.send("Sorry you must be a premium member to use this command. (!donate)") + prompt = ctx.message.content.split(" ", maxsplit=1)[1] + await ctx.send(f"Please be patient this may take some time! Generating: {prompt}.") + resp_status, resp = await self.dalle_api_call(prompt, model=model, quality=quality, size=size) + if resp_status != 200: + await ctx.send(f"Error generating image: {resp_status}: {resp}") + return + my_filename = str(time.time_ns()) + ".png" + image_filepath = f"{self.data_dir}dalle/{my_filename}" + await self.download_image(resp, image_filepath) + with open(image_filepath, "rb") as fh: + f = discord.File(fh, filename=image_filepath) + prompt = prompt.replace('\n',' ') + log_data = f'Author: {ctx.author.name}, Prompt: {prompt}, Filename: {my_filename}\n' + with open(f"{self.data_dir}logs/dalle3.log", 'a') as log_filepath: + log_filepath.writelines(log_data) + await ctx.send(f'Generated by: {ctx.author.name}\nPrompt: {prompt}', file=f) @commands.command( @@ -255,6 +364,7 @@ class ChatGPT(commands.Cog): brief="Generate Image" ) async def dalle2(self, ctx): + self.add_cost("dalle2", 0.02) await self.generate_dalle_image(ctx, model="dall-e-2") @@ -265,6 +375,7 @@ class ChatGPT(commands.Cog): aliases = ['dalle'] ) async def dalle3(self, ctx): + self.add_cost("dalle3", 0.04) await self.generate_dalle_image(ctx, model="dall-e-3") @@ -274,6 +385,7 @@ class ChatGPT(commands.Cog): brief="Generate HD Image", ) async def dalle3hd(self, ctx): + self.add_cost("dalle3hd", 0.08) await self.generate_dalle_image(ctx, model="dall-e-3", quality="hd", size="1792x1024") @commands.command( @@ -415,30 +527,17 @@ class ChatGPT(commands.Cog): async with ctx.channel.typing(): await asyncio.sleep(1) prompt = f"You are a {channel_vars['personality']} chat bot named Sparkytron 3000 created by @phixxy.com. Your personality should be {channel_vars['personality']}. You are currently in a {channel_vars['channel_topic']} chatroom. The message history is: {chat_history_string}" - - data = { - "model": "gpt-3.5-turbo", - "messages": [{"role": "user", "content": prompt}] - } + response = await self.answer_question(prompt) + if "Sparkytron 3000:" in response[0:17]: + response = response.replace("Sparkytron 3000:", "") + max_len = 1999 + if len(response) > max_len: + messages=[response[y-max_len:y] for y in range(max_len, len(response)+max_len,max_len)] + else: + messages=[response] + for message in messages: + await ctx.channel.send(message) - url = "https://api.openai.com/v1/chat/completions" - - try: - async with self.http_session.post(url, json=data, headers=self.headers) as resp: - response_data = await resp.json() - response = response_data['choices'][0]['message']['content'] - if "Sparkytron 3000:" in response[0:17]: - response = response.replace("Sparkytron 3000:", "") - max_len = 1999 - if len(response) > max_len: - messages=[response[y-max_len:y] for y in range(max_len, len(response)+max_len,max_len)] - else: - messages=[response] - for message in messages: - await ctx.channel.send(message) - - except Exception as error: - self.logger.exception("Problem with chat_response in chatgpt") @commands.Cog.listener() async def on_reaction_add(self, reaction, user): diff --git a/cogs/donate.py b/cogs/donate.py index dc3e195..8bb60c1 100644 --- a/cogs/donate.py +++ b/cogs/donate.py @@ -10,7 +10,7 @@ class Donate(commands.Cog): def __init__(self, bot): self.bot = bot - self.donation_link = "https://patreon.com/soullesssurvival" + self.donation_link = "https://patreon.com/phixxy" self.data_dir = "data/donate/" self.donor_file = "data/donate/supporters.txt" self.folder_setup()