From 709382b8dac9562269f7ae71a1ef4fc96b4b6e1f Mon Sep 17 00:00:00 2001 From: Kaveen Kumarasinghe Date: Fri, 13 Jan 2023 01:30:59 -0500 Subject: [PATCH] api backoff --- README.md | 11 +++++--- cogs/draw_image_generation.py | 22 +++++++++------ cogs/gpt_3_commands_and_converser.py | 42 ++++++++++++++++++++-------- gpt3discord.py | 2 +- models/openai_model.py | 28 +++++++++++++------ 5 files changed, 72 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 2803848..cd9c260 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@ [![GitHub license](https://img.shields.io/github/license/Kav-K/GPT3Discord)](https://github.com/Kav-K/GPT3Discord/blob/master/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) +# Overview +A robust, all-in-one GPT3 interface for Discord. Chat just like ChatGPT right inside Discord! Generate beautiful AI art using DALL-E 2! Automatically moderate your server using AI! A thorough integration with permanent conversation memory, automatic request retry, fault tolerance and reliability for servers of any scale, and much more. SUPPORT SERVER FOR BOT SETUP: https://discord.gg/WvAHXDMS7Q (You can NOT use the bot here, it is for setup support ONLY) - # Screenshots

@@ -22,6 +23,9 @@ SUPPORT SERVER FOR BOT SETUP: https://discord.gg/WvAHXDMS7Q (You can NOT use the

# Recent Notable Updates +- **Automatic retry on API errors** - The bot will automatically retry API requests if they fail due to some issue with OpenAI's APIs, this is becoming increasingly important now as their APIs become under heavy load. + + - **Allow each individual user to enter their own API Key!** - Each request that a user makes will be made using their own API key! Check out the User-Input API Key section in this README for more details. @@ -34,9 +38,6 @@ SUPPORT SERVER FOR BOT SETUP: https://discord.gg/WvAHXDMS7Q (You can NOT use the - **AI-BASED SERVER MODERATION** - GPT3Discord now has a built-in AI-based moderation system that can automatically detect and remove toxic messages from your server. This is a great way to keep your server safe and clean, and it's completely automatic and **free**! Check out the commands section to learn how to enable it! -- **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! - - # Features - **Directly prompt GPT3 with `/gpt ask `** @@ -50,6 +51,8 @@ SUPPORT SERVER FOR BOT SETUP: https://discord.gg/WvAHXDMS7Q (You can NOT use the - **Automatic AI-Based Server Moderation** - Moderate your server automatically with AI! +- **Auto-retry on API errors** - Automatically resend failed requests to OpenAI's APIs! + - Automatically re-send your prompt and update the response in place if you edit your original prompt! - Async and fault tolerant, **can handle hundreds of users at once**, if the upstream API permits! diff --git a/cogs/draw_image_generation.py b/cogs/draw_image_generation.py index 19bf0a2..bb39aa5 100644 --- a/cogs/draw_image_generation.py +++ b/cogs/draw_image_generation.py @@ -4,6 +4,7 @@ import tempfile import traceback from io import BytesIO +import aiohttp import discord from PIL import Image from pycord.multicog import add_to_group @@ -60,18 +61,21 @@ class DrawDallEService(discord.Cog, name="DrawDallEService"): vary=vary if not draw_from_optimizer else None, custom_api_key=custom_api_key, ) - except ValueError as e: - ( - await ctx.channel.send( - f"Error: {e}. Please try again with a different prompt." - ) - if not from_context - else await ctx.respond( - f"Error: {e}. Please try again with a different prompt." - ) + + # Error catching for API errors + except aiohttp.ClientResponseError as e: + message = f"The API returned an invalid response: **{e.status}: {e.message}**" + await ctx.channel.send(message) if not from_context else await ctx.respond( + message ) return + except ValueError as e: + message = f"Error: {e}. Please try again with a different prompt." + await ctx.channel.send( message )if not from_context else await ctx.respond( message ) + + return + # Start building an embed to send to the user with the results of the image generation embed = discord.Embed( title="Image Generation Results" diff --git a/cogs/gpt_3_commands_and_converser.py b/cogs/gpt_3_commands_and_converser.py index 04f26b5..238c474 100644 --- a/cogs/gpt_3_commands_and_converser.py +++ b/cogs/gpt_3_commands_and_converser.py @@ -9,6 +9,8 @@ from pathlib import Path import aiofiles import json + +import aiohttp import discord from pycord.multicog import add_to_group @@ -826,6 +828,13 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): response_text = response_text.replace("<|endofstatement|>", "") return response_text + def remove_awaiting(self, author_id, channel_id, from_g_command): + if author_id in self.awaiting_responses: + self.awaiting_responses.remove(author_id) + if not from_g_command: + if channel_id in self.awaiting_thread_responses: + self.awaiting_thread_responses.remove(channel_id) + async def mention_to_username(self, ctx, message): if not discord.utils.raw_mentions(message): return message @@ -1148,31 +1157,33 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): if ctx.channel.id in self.awaiting_thread_responses: self.awaiting_thread_responses.remove(ctx.channel.id) + # Error catching for AIOHTTP Errors + except aiohttp.ClientResponseError as e: + message = f"The API returned an invalid response: **{e.status}: {e.message}**" + if from_context: + await ctx.send_followup(message) + else: + await ctx.reply(message) + self.remove_awaiting(ctx.author.id, ctx.channel.id, from_g_command) + + # Error catching for OpenAI model value errors except ValueError as e: if from_context: await ctx.send_followup(e) else: await ctx.reply(e) - if ctx.author.id in self.awaiting_responses: - self.awaiting_responses.remove(ctx.author.id) - if not from_g_command: - if ctx.channel.id in self.awaiting_thread_responses: - self.awaiting_thread_responses.remove(ctx.channel.id) + self.remove_awaiting(ctx.author.id, ctx.channel.id, from_g_command) + # General catch case for everything except Exception: message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting." - await ctx.send_followup(message) if from_context else await ctx.reply( message ) - if ctx.author.id in self.awaiting_responses: - self.awaiting_responses.remove(ctx.author.id) - if not from_g_command: - if ctx.channel.id in self.awaiting_thread_responses: - self.awaiting_thread_responses.remove(ctx.channel.id) + self.remove_awaiting(ctx.author.id, ctx.channel.id, from_g_command) traceback.print_exc() try: @@ -1786,6 +1797,15 @@ class SetupModal(discord.ui.Modal): ephemeral=True, delete_after=10, ) + + except aiohttp.ClientResponseError as e: + await interaction.response.send_message( + f"The API returned an invalid response: **{e.status}: {e.message}**", + ephemeral=True, + delete_after=30, + ) + return + except Exception as e: await interaction.response.send_message( f"Your API key looks invalid, the API returned: {e}. Please check that your API key is correct before proceeding", diff --git a/gpt3discord.py b/gpt3discord.py index 8085302..0e29667 100644 --- a/gpt3discord.py +++ b/gpt3discord.py @@ -24,7 +24,7 @@ from models.openai_model import Model from models.usage_service_model import UsageService from models.env_service_model import EnvService -__version__ = "5.3.2" +__version__ = "5.4" """ The pinecone service is used to store and retrieve conversation embeddings. diff --git a/models/openai_model.py b/models/openai_model.py index 2738129..46f6f90 100644 --- a/models/openai_model.py +++ b/models/openai_model.py @@ -8,11 +8,13 @@ import uuid from typing import Tuple, List, Any import aiohttp +import backoff import discord # An enum of two modes, TOP_P or TEMPERATURE import requests from PIL import Image +from aiohttp import RequestInfo from discord import File @@ -341,6 +343,10 @@ class Model: ) self._prompt_min_length = value + def backoff_handler(details): + print (f"Backing off {details['wait']:0.1f} seconds after {details['tries']} tries calling function {details['target']} | " + f"{details['exception'].status}: {details['exception'].message}") + async def valid_text_request(self, response): try: tokens_used = int(response["usage"]["total_tokens"]) @@ -351,8 +357,9 @@ class Model: + str(response["error"]["message"]) ) + @backoff.on_exception(backoff.expo, aiohttp.ClientResponseError, factor=3, base=5, max_tries=4, on_backoff=backoff_handler) async def send_embedding_request(self, text, custom_api_key=None): - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(raise_for_status=True) as session: payload = { "model": Models.EMBEDDINGS, "input": text, @@ -368,14 +375,15 @@ class Model: try: return response["data"][0]["embedding"] - except Exception as e: + except Exception: print(response) traceback.print_exc() return + @backoff.on_exception(backoff.expo, aiohttp.ClientResponseError, factor=3, base=5, max_tries=6, on_backoff=backoff_handler) async def send_moderations_request(self, text): # Use aiohttp to send the above request: - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(raise_for_status=True) as session: headers = { "Content-Type": "application/json", "Authorization": f"Bearer {self.openai_key}", @@ -388,6 +396,7 @@ class Model: ) as response: return await response.json() + @backoff.on_exception(backoff.expo, aiohttp.ClientResponseError, factor=3, base=5, max_tries=4, on_backoff=backoff_handler) async def send_summary_request(self, prompt, custom_api_key=None): """ Sends a summary request to the OpenAI API @@ -403,7 +412,7 @@ class Model: tokens = self.usage_service.count_tokens(summary_request_text) - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(raise_for_status=True) as session: payload = { "model": Models.DAVINCI, "prompt": summary_request_text, @@ -429,6 +438,7 @@ class Model: return response + @backoff.on_exception(backoff.expo, aiohttp.ClientResponseError, factor=3, base=5, max_tries=4, on_backoff=backoff_handler) async def send_request( self, prompt, @@ -457,7 +467,7 @@ class Model: f"Overrides -> temp:{temp_override}, top_p:{top_p_override} frequency:{frequency_penalty_override}, presence:{presence_penalty_override}" ) - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(raise_for_status=True) as session: payload = { "model": self.model, "prompt": prompt, @@ -511,6 +521,7 @@ class Model: return response + @backoff.on_exception(backoff.expo, aiohttp.ClientResponseError, factor=3, base=5, max_tries=4, on_backoff=backoff_handler) async def send_image_request( self, ctx, prompt, vary=None, custom_api_key=None ) -> tuple[File, list[Any]]: @@ -533,15 +544,16 @@ class Model: "Content-Type": "application/json", "Authorization": f"Bearer {self.openai_key if not custom_api_key else custom_api_key}", } - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(raise_for_status=True) as session: async with session.post( "https://api.openai.com/v1/images/generations", json=payload, headers=headers, ) as resp: response = await resp.json() + else: - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(raise_for_status=True) as session: data = aiohttp.FormData() data.add_field("n", str(self.num_images)) data.add_field("size", self.image_size) @@ -559,7 +571,7 @@ class Model: ) as resp: response = await resp.json() - # print(response) + print(response) image_urls = [] for result in response["data"]: