From 193a967d4160513bdaf9eced8b3ce8971e4f086c Mon Sep 17 00:00:00 2001 From: Kaveen Kumarasinghe Date: Thu, 29 Dec 2022 02:31:36 -0500 Subject: [PATCH] REVAMP ASYNC, fully working now, massively improve immage prompt optimizer --- README.md | 7 +- cogs/draw_image_generation.py | 7 +- cogs/gpt_3_commands_and_converser.py | 33 ++++++- models/deletion_service.py | 8 +- models/openai_model.py | 135 ++++++++++++++++----------- requirements.txt | 4 +- 6 files changed, 124 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index ae14039..7455e03 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,16 @@ - **AUTOMATIC CHAT SUMMARIZATION!** - When the context limit of a conversation is reached, the bot will use GPT3 itself to summarize the conversation to reduce the tokens, and continue conversing with you, this allows you to chat for a long time! +- **PERMANENT MEMORY FOR CONVERSATIONS COMING SOON USING EMBEDDINGS!** + - **DALL-E Image Generation** - **REDO ON EDIT** - When you edit a prompt, it will automatically be resent to GPT3 and the response updated! -- **Fully async!** - The bot will never be blocked when processing someone else's request, allowing for use in large servers with multiple messages per second! +- **Fully async and fault tolerant - REVAMPED** - The bot will never be blocked when processing someone else's request, allowing for use in large servers with multiple messages per second! + +- No need for the OpenAI and Asgiref libraries anymore! + # Features - **Directly prompt GPT3 with `!g `** diff --git a/cogs/draw_image_generation.py b/cogs/draw_image_generation.py index b25170e..98d6bbd 100644 --- a/cogs/draw_image_generation.py +++ b/cogs/draw_image_generation.py @@ -96,16 +96,17 @@ class DrawDallEService(commands.Cog, name="DrawDallEService"): message = await response_message.edit( embed=embed, file=file, - view=SaveView(image_urls, self, self.converser_cog), ) + await message.edit(view=SaveView(image_urls, self, self.converser_cog, message)) else: # Varying case if not draw_from_optimizer: result_message = await response_message.edit_original_response( content="Image variation completed!", embed=embed, file=file, - view=SaveView(image_urls, self, self.converser_cog, True), ) + await result_message.edit(view=SaveView(image_urls, self, self.converser_cog,result_message, True)) + redo_users[message.author.id] = RedoUser( prompt, message, result_message ) @@ -214,7 +215,7 @@ class SaveView(discord.ui.View): self, image_urls, cog, converser_cog, message, no_retry=False, only_save=None ): super().__init__( - timeout=10 if not only_save else None + timeout=3600 if not only_save else None ) # 10 minute timeout for Retry, Save self.image_urls = image_urls self.cog = cog diff --git a/cogs/gpt_3_commands_and_converser.py b/cogs/gpt_3_commands_and_converser.py index 9d92412..5116e51 100644 --- a/cogs/gpt_3_commands_and_converser.py +++ b/cogs/gpt_3_commands_and_converser.py @@ -48,13 +48,14 @@ class GPT3ComCon(commands.Cog, name="GPT3ComCon"): "that'll be all", ] self.last_used = {} - self.GLOBAL_COOLDOWN_TIME = 1 + self.GLOBAL_COOLDOWN_TIME = 0.25 self.usage_service = usage_service self.model = model self.summarize = self.model.summarize_conversations self.deletion_queue = deletion_queue self.users_to_interactions = defaultdict(list) self.redo_users = {} + self.awaiting_responses = [] try: # Attempt to read a conversation starter text string from the file. @@ -438,6 +439,9 @@ class GPT3ComCon(commands.Cog, name="GPT3ComCon"): ) self.check_conversing(message) + # We got a response, we can allow the user to type again + self.awaiting_responses.remove(message.author.id) + # If the response text is > 3500 characters, paginate and send debug_message = self.generate_debug_message(prompt, response) @@ -458,9 +462,6 @@ class GPT3ComCon(commands.Cog, name="GPT3ComCon"): self.redo_users[message.author.id].add_interaction( response_message.id ) - print( - f"Added the interaction {response_message.id} to the redo user {message.author.id}" - ) original_message[message.author.id] = message.id else: # We have response_text available, this is the original message that we want to edit @@ -602,13 +603,15 @@ class GPT3ComCon(commands.Cog, name="GPT3ComCon"): if prompt == "converse" or prompt == "converse nothread": # If the user is already conversating, don't let them start another conversation if message.author.id in self.conversating_users: - await message.reply( + message = await message.reply( "You are already conversating with GPT3. End the conversation with !g end or just say 'end' in a supported channel" ) + await self.deletion_queue(message) return # If the user is not already conversating, start a conversation with GPT3 self.conversating_users[message.author.id] = User(message.author.id) + # Append the starter text for gpt3 to the user's history so it gets concatenated with the prompt later self.conversating_users[message.author.id].history.append( self.CONVERSATION_STARTER_TEXT @@ -653,6 +656,26 @@ class GPT3ComCon(commands.Cog, name="GPT3ComCon"): # history to the prompt. We can do this by checking if the user is in the conversating_users dictionary, and if they are, # we can append their history to the prompt. if message.author.id in self.conversating_users: + + # Since this is async, we don't want to allow the user to send another prompt while a conversation + # prompt is processing, that'll mess up the conversation history! + if message.author.id in self.awaiting_responses: + message = await message.reply( + "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message." + ) + + # get the current date, add 10 seconds to it, and then turn it into a timestamp. + # we need to use our deletion service because this isn't an interaction, it's a regular message. + deletion_time = datetime.datetime.now() + datetime.timedelta(seconds=10) + deletion_time = deletion_time.timestamp() + + deletion_message = Deletion(message, deletion_time) + await self.deletion_queue.put(deletion_message) + + return + + self.awaiting_responses.append(message.author.id) + self.conversating_users[message.author.id].history.append( "\nHuman: " + prompt + "<|endofstatement|>\n" ) diff --git a/models/deletion_service.py b/models/deletion_service.py index 3970d52..d1a5710 100644 --- a/models/deletion_service.py +++ b/models/deletion_service.py @@ -2,6 +2,8 @@ import asyncio import traceback from datetime import datetime +import discord + class Deletion: def __init__(self, message, timestamp): @@ -26,7 +28,11 @@ class Deletion: # Check if the current timestamp is greater than the deletion timestamp if datetime.now().timestamp() > deletion.timestamp: # If the deletion timestamp has passed, delete the message - await deletion.message.delete_original_response() + # check if deletion.message is of type discord.Message + if isinstance(deletion.message, discord.Message): + await deletion.message.delete() + else: + await deletion.message.delete_original_response() else: await deletion_queue.put(deletion) diff --git a/models/openai_model.py b/models/openai_model.py index d79fb17..84cd6c0 100644 --- a/models/openai_model.py +++ b/models/openai_model.py @@ -4,14 +4,13 @@ import tempfile import uuid from typing import Tuple, List, Any +import aiohttp import discord -import openai # An enum of two modes, TOP_P or TEMPERATURE import requests from PIL import Image from discord import File -from asgiref.sync import sync_to_async class Mode: @@ -72,8 +71,7 @@ class Model: "model_max_tokens", ] - openai.api_key = os.getenv("OPENAI_TOKEN") - + self.openai_key = os.getenv("OPENAI_TOKEN") # Use the @property and @setter decorators for all the self fields to provide value checking @property def summarize_threshold(self): @@ -305,22 +303,29 @@ class Model: tokens = self.usage_service.count_tokens(summary_request_text) - response = await sync_to_async(openai.Completion.create)( - model=Models.DAVINCI, - prompt=summary_request_text, - temperature=0.5, - top_p=1, - max_tokens=self.max_tokens - tokens, - presence_penalty=self.presence_penalty, - frequency_penalty=self.frequency_penalty, - best_of=self.best_of, - ) - - print(response["choices"][0]["text"]) - - tokens_used = int(response["usage"]["total_tokens"]) - self.usage_service.update_usage(tokens_used) - return response + async with aiohttp.ClientSession() as session: + payload = { + "model": Models.DAVINCI, + "prompt": summary_request_text, + "temperature": 0.5, + "top_p": 1, + "max_tokens": self.max_tokens - tokens, + "presence_penalty": self.presence_penalty, + "frequency_penalty": self.frequency_penalty, + "best_of": self.best_of, + } + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.openai_key}" + } + async with session.post("https://api.openai.com/v1/completions", json=payload, headers=headers) as resp: + response = await resp.json() + + print(response["choices"][0]["text"]) + + tokens_used = int(response["usage"]["total_tokens"]) + self.usage_service.update_usage(tokens_used) + return response async def send_request( self, @@ -347,31 +352,28 @@ class Model: print("The prompt about to be sent is " + prompt) - response = await sync_to_async(openai.Completion.create)( - model=Models.DAVINCI - if any(role.name in self.DAVINCI_ROLES for role in message.author.roles) - else self.model, # Davinci override for admin users - prompt=prompt, - temperature=self.temp if not temp_override else temp_override, - top_p=self.top_p if not top_p_override else top_p_override, - max_tokens=self.max_tokens - tokens - if not max_tokens_override - else max_tokens_override, - presence_penalty=self.presence_penalty - if not presence_penalty_override - else presence_penalty_override, - frequency_penalty=self.frequency_penalty - if not frequency_penalty_override - else frequency_penalty_override, - best_of=self.best_of if not best_of_override else best_of_override, - ) - # print(response.__dict__) - - # Parse the total tokens used for this request and response pair from the response - tokens_used = int(response["usage"]["total_tokens"]) - self.usage_service.update_usage(tokens_used) - - return response + async with aiohttp.ClientSession() as session: + payload = { + "model": Models.DAVINCI if any( + role.name in self.DAVINCI_ROLES for role in message.author.roles) else self.model, + "prompt": prompt, + "temperature": self.temp if not temp_override else temp_override, + "top_p": self.top_p if not top_p_override else top_p_override, + "max_tokens": self.max_tokens - tokens if not max_tokens_override else max_tokens_override, + "presence_penalty": self.presence_penalty if not presence_penalty_override else presence_penalty_override, + "frequency_penalty": self.frequency_penalty if not frequency_penalty_override else frequency_penalty_override, + "best_of": self.best_of if not best_of_override else best_of_override, + } + headers = { + "Authorization": f"Bearer {self.openai_key}" + } + async with session.post("https://api.openai.com/v1/completions", json=payload, headers=headers) as resp: + response = await resp.json() + # Parse the total tokens used for this request and response pair from the response + tokens_used = int(response["usage"]["total_tokens"]) + self.usage_service.update_usage(tokens_used) + + return response async def send_image_request(self, prompt, vary=None) -> tuple[File, list[Any]]: # Validate that all the parameters are in a good state before we send the request @@ -385,20 +387,39 @@ class Model: # print("The prompt about to be sent is " + prompt) self.usage_service.update_usage_image(self.image_size) + response = None + if not vary: - response = await sync_to_async(openai.Image.create)( - prompt=prompt, - n=self.num_images, - size=self.image_size, - ) + payload = { + "prompt": prompt, + "n": self.num_images, + "size": self.image_size + } + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.openai_key}" + } + async with aiohttp.ClientSession() as session: + async with session.post("https://api.openai.com/v1/images/generations", json=payload, headers=headers) as resp: + response = await resp.json() else: - response = await sync_to_async(openai.Image.create_variation)( - image=open(vary, "rb"), - n=self.num_images, - size=self.image_size, - ) - - print(response.__dict__) + async with aiohttp.ClientSession() as session: + data = aiohttp.FormData() + data.add_field("n", str(self.num_images)) + data.add_field("size", self.image_size) + with open(vary, "rb") as f: + data.add_field("image", f, filename="file.png", content_type="image/png") + + async with session.post( + "https://api.openai.com/v1/images/variations", + headers={ + "Authorization": "Bearer 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", + }, + data=data + ) as resp: + response = await resp.json() + + print(response) image_urls = [] for result in response["data"]: diff --git a/requirements.txt b/requirements.txt index 6f338bf..0ddfb98 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,5 @@ py-cord==2.3.2 -openai==0.25.0 Pillow==9.3.0 python-dotenv==0.21.0 requests==2.28.1 -transformers==4.25.1 -asgiref==3.6.0 \ No newline at end of file +transformers==4.25.1 \ No newline at end of file