From fbdc03b8ddcf81903f30349bd05a4c72bcafc3a0 Mon Sep 17 00:00:00 2001 From: Rene Teigen Date: Wed, 11 Jan 2023 18:37:13 +0000 Subject: [PATCH 1/9] Enable json openers with overrides --- README.md | 2 + cogs/gpt_3_commands_and_converser.py | 86 ++++++++++++++++++---------- models/user_model.py | 18 ++++++ openers/english_translator.json | 8 +++ 4 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 openers/english_translator.json diff --git a/README.md b/README.md index d65116c..18a358e 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ These commands are grouped, so each group has a prefix but you can easily tab co - Custom openers need to be placed as a .txt file in the `openers` directory, in the same directory as `gpt3discord.py` +- Can use files in the `{"text": your prompt, "temp":0, "top_p":0,"frequency_penalty":0,"presence_penalty":0}` format to include permanent overrides + `/gpt converse minimal:yes` - Start a conversation with the bot, like ChatGPT, with minimal context (saves tokens) - Note that the above options for `/gpt converse` can be combined (you can combine minimal, private, and opener!) diff --git a/cogs/gpt_3_commands_and_converser.py b/cogs/gpt_3_commands_and_converser.py index cd12ecf..afd051c 100644 --- a/cogs/gpt_3_commands_and_converser.py +++ b/cogs/gpt_3_commands_and_converser.py @@ -8,6 +8,7 @@ from pathlib import Path import aiofiles +import json import discord from pycord.multicog import add_to_group @@ -636,11 +637,17 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): ) self.conversation_threads[after.channel.id].count += 1 + overrides = self.conversation_threads[after.channel.id].get_overrides() + await self.encapsulated_send( id=after.channel.id, prompt=edited_content, ctx=ctx, response_message=response_message, + temp_override=overrides["temperature"], + top_p_override=overrides["top_p"], + frequency_penalty_override=overrides["frequency_penalty"], + presence_penalty_override=overrides["presence_penalty"], ) self.redo_users[after.author.id].prompt = after.content @@ -774,10 +781,17 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): self.conversation_threads[message.channel.id].history ) + #set conversation overrides + overrides = self.conversation_threads[message.channel.id].get_overrides() + await self.encapsulated_send( message.channel.id, primary_prompt, message, + temp_override=overrides["temperature"], + top_p_override=overrides["top_p"], + frequency_penalty_override=overrides["frequency_penalty"], + presence_penalty_override=overrides["presence_penalty"], custom_api_key=user_api_key, ) @@ -1236,30 +1250,6 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): ) return - if not opener and not opener_file: - user_id_normalized = user.id - else: - user_id_normalized = ctx.author.id - if ( - opener_file - ): # only load in files if it's included in the command, if not pass on as normal - if opener_file.endswith(".txt"): - # Load the file and read it into opener - opener_file = EnvService.find_shared_file( - f"openers{separator}{opener_file}" - ) - opener_file = await self.load_file(opener_file, ctx) - if ( - not opener - ): # if we only use opener_file then only pass on opener_file for the opening prompt - opener = opener_file - else: - opener = opener_file + opener - if not opener_file: - return - else: - pass - if private: await ctx.respond(user.name + "'s private conversation with GPT3") thread = await ctx.channel.create_thread( @@ -1287,10 +1277,46 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): self.CONVERSATION_STARTER_TEXT ) + if not opener and not opener_file: + user_id_normalized = user.id + else: + user_id_normalized = ctx.author.id + if not opener_file: + pass + else: + if opener_file.endswith((".txt", ".json")): + # Load the file and read it into opener + opener_file = EnvService.find_shared_file( + f"openers{separator}{opener_file}" + ) + opener_file = await self.load_file(opener_file, ctx) + try: + opener_file = json.loads(opener_file) # Try opening as json, if it fails it'll do it as a str + temperature=None if not opener_file["temperature"] else opener_file["temperature"] + top_p=None if not opener_file["top_p"] else opener_file["top_p"] + frequency_penalty=None if not opener_file["frequency_penalty"] else opener_file["frequency_penalty"] + presence_penalty=None if not opener_file["presence_penalty"] else opener_file["presence_penalty"] + self.conversation_threads[thread.id].set_overrides(temperature, top_p, frequency_penalty, presence_penalty) # set overrides + if not opener: # if we only use opener_file then only pass on opener_file for the opening prompt + opener = opener_file["text"] + else: + opener = opener_file["text"] + opener + except: + if not opener: # if we only use opener_file then only pass on opener_file for the opening prompt + opener = opener_file + else: + opener = opener_file + opener + + # Set user as owner before sending anything that can error and leave the thread unowned + self.conversation_thread_owners[user_id_normalized] = thread.id + overrides = self.conversation_threads[thread.id].get_overrides() + await thread.send( - "<@" - + str(user_id_normalized) - + "> You are now conversing with GPT3. *Say hi to start!*\n End the conversation by saying `end`.\n\n If you want GPT3 to ignore your messages, start your messages with `~`\n\nYour conversation will remain active even if you leave this thread and talk in other GPT supported channels, unless you end the conversation!" + f"<@{str(user_id_normalized)}> You are now conversing with GPT3. *Say hi to start!*\n" + f"Overrides for this thread is **temp={overrides['temperature']}**, **top_p={overrides['top_p']}**, **frequency penalty={overrides['frequency_penalty']}**, **presence penalty={overrides['presence_penalty']}**\n" + f"End the conversation by saying `end`.\n\n" + f"If you want GPT3 to ignore your messages, start your messages with `~`\n\n" + f"Your conversation will remain active even if you leave this thread and talk in other GPT supported channels, unless you end the conversation!" ) # send opening @@ -1313,14 +1339,16 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): if thread.id not in self.conversation_threads or self.pinecone_service else "".join(self.conversation_threads[thread.id].history), thread_message, + temp_override=overrides["temperature"], + top_p_override=overrides["top_p"], + frequency_penalty_override=overrides["frequency_penalty"], + presence_penalty_override=overrides["presence_penalty"], custom_api_key=user_api_key, ) self.awaiting_responses.remove(user_id_normalized) if thread.id in self.awaiting_thread_responses: self.awaiting_thread_responses.remove(thread.id) - self.conversation_thread_owners[user_id_normalized] = thread.id - @add_to_group("system") @discord.slash_command( name="moderations-test", diff --git a/models/user_model.py b/models/user_model.py index 358ed43..adcd1fa 100644 --- a/models/user_model.py +++ b/models/user_model.py @@ -57,6 +57,24 @@ class Thread: self.id = id self.history = [] self.count = 0 + self.temperature = None + self.top_p = None + self.frequency_penalty = None + self.presence_penalty = None + + def set_overrides(self, temperature=None,top_p=None,frequency_penalty=None,presence_penalty=None): + self.temperature = temperature + self.top_p = top_p + self.frequency_penalty = frequency_penalty + self.presence_penalty = presence_penalty + + def get_overrides(self): + return { + "temperature": self.temperature, + "top_p": self.temperature, + "frequency_penalty": self.frequency_penalty, + "presence_penalty": self.presence_penalty, + } # These user objects should be accessible by ID, for example if we had a bunch of user # objects in a list, and we did `if 1203910293001 in user_list`, it would return True diff --git a/openers/english_translator.json b/openers/english_translator.json new file mode 100644 index 0000000..c2dbf3f --- /dev/null +++ b/openers/english_translator.json @@ -0,0 +1,8 @@ +{ + "text":"I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. ", + "temperature":0.77, + "top_p":0.9, + "frequency_penalty":0.95, + "presence_penalty":0.95 +} + From cab2c184b6159de7b54e1c6f8a0998112ce46842 Mon Sep 17 00:00:00 2001 From: Rene Teigen Date: Wed, 11 Jan 2023 19:39:47 +0000 Subject: [PATCH 2/9] Fix some directory traversal and smooth out behavior if file is not found --- cogs/gpt_3_commands_and_converser.py | 65 +++++++++++++++------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/cogs/gpt_3_commands_and_converser.py b/cogs/gpt_3_commands_and_converser.py index afd051c..5e85028 100644 --- a/cogs/gpt_3_commands_and_converser.py +++ b/cogs/gpt_3_commands_and_converser.py @@ -1267,16 +1267,6 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): self.conversation_threads[thread.id] = Thread(thread.id) - # Append the starter text for gpt3 to the user's history so it gets concatenated with the prompt later - if minimal or opener_file: - self.conversation_threads[thread.id].history.append( - self.CONVERSATION_STARTER_TEXT_MINIMAL - ) - elif not minimal: - self.conversation_threads[thread.id].history.append( - self.CONVERSATION_STARTER_TEXT - ) - if not opener and not opener_file: user_id_normalized = user.id else: @@ -1284,28 +1274,45 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): if not opener_file: pass else: - if opener_file.endswith((".txt", ".json")): + if not opener_file.endswith((".txt", ".json")): + opener_file = None + else: # Load the file and read it into opener - opener_file = EnvService.find_shared_file( - f"openers{separator}{opener_file}" - ) - opener_file = await self.load_file(opener_file, ctx) try: - opener_file = json.loads(opener_file) # Try opening as json, if it fails it'll do it as a str - temperature=None if not opener_file["temperature"] else opener_file["temperature"] - top_p=None if not opener_file["top_p"] else opener_file["top_p"] - frequency_penalty=None if not opener_file["frequency_penalty"] else opener_file["frequency_penalty"] - presence_penalty=None if not opener_file["presence_penalty"] else opener_file["presence_penalty"] - self.conversation_threads[thread.id].set_overrides(temperature, top_p, frequency_penalty, presence_penalty) # set overrides - if not opener: # if we only use opener_file then only pass on opener_file for the opening prompt - opener = opener_file["text"] - else: - opener = opener_file["text"] + opener + opener_file = re.sub(".+(?=[\\//])", "", opener_file) # remove paths from the opener file + opener_file = EnvService.find_shared_file( + f"openers{separator}{opener_file}" + ) + opener_file = await self.load_file(opener_file, ctx) + try: # Try opening as json, if it fails it'll do it as a str + opener_file = json.loads(opener_file) + temperature=None if not opener_file["temperature"] else opener_file["temperature"] + top_p=None if not opener_file["top_p"] else opener_file["top_p"] + frequency_penalty=None if not opener_file["frequency_penalty"] else opener_file["frequency_penalty"] + presence_penalty=None if not opener_file["presence_penalty"] else opener_file["presence_penalty"] + self.conversation_threads[thread.id].set_overrides(temperature, top_p, frequency_penalty, presence_penalty) # set overrides + if not opener: # if we only use opener_file then only pass on opener_file for the opening prompt + opener = opener_file["text"] + else: + opener = opener_file["text"] + opener + except: # Parse as just regular text + if not opener: + opener = opener_file + else: + opener = opener_file + opener except: - if not opener: # if we only use opener_file then only pass on opener_file for the opening prompt - opener = opener_file - else: - opener = opener_file + opener + opener_file = None + + + # Append the starter text for gpt3 to the user's history so it gets concatenated with the prompt later + if minimal or opener_file: + self.conversation_threads[thread.id].history.append( + self.CONVERSATION_STARTER_TEXT_MINIMAL + ) + elif not minimal: + self.conversation_threads[thread.id].history.append( + self.CONVERSATION_STARTER_TEXT + ) # Set user as owner before sending anything that can error and leave the thread unowned self.conversation_thread_owners[user_id_normalized] = thread.id From 7ecc56f0aebdac7877446a0dc91c0ec380dcc37a Mon Sep 17 00:00:00 2001 From: Rene Teigen Date: Wed, 11 Jan 2023 20:10:47 +0000 Subject: [PATCH 3/9] Made the json parsin more robust and fixed bugs --- README.md | 2 +- cogs/gpt_3_commands_and_converser.py | 14 +++++++------- models/user_model.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 18a358e..3458955 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ These commands are grouped, so each group has a prefix but you can easily tab co - Custom openers need to be placed as a .txt file in the `openers` directory, in the same directory as `gpt3discord.py` -- Can use files in the `{"text": your prompt, "temp":0, "top_p":0,"frequency_penalty":0,"presence_penalty":0}` format to include permanent overrides +- Can use .json files in the `{"text": your prompt, "temp":0, "top_p":0,"frequency_penalty":0,"presence_penalty":0}` format to include permanent overrides `/gpt converse minimal:yes` - Start a conversation with the bot, like ChatGPT, with minimal context (saves tokens) diff --git a/cogs/gpt_3_commands_and_converser.py b/cogs/gpt_3_commands_and_converser.py index 5e85028..b0c0b26 100644 --- a/cogs/gpt_3_commands_and_converser.py +++ b/cogs/gpt_3_commands_and_converser.py @@ -1286,15 +1286,15 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): opener_file = await self.load_file(opener_file, ctx) try: # Try opening as json, if it fails it'll do it as a str opener_file = json.loads(opener_file) - temperature=None if not opener_file["temperature"] else opener_file["temperature"] - top_p=None if not opener_file["top_p"] else opener_file["top_p"] - frequency_penalty=None if not opener_file["frequency_penalty"] else opener_file["frequency_penalty"] - presence_penalty=None if not opener_file["presence_penalty"] else opener_file["presence_penalty"] + temperature=opener_file.get("temperature", None) + top_p=opener_file.get("top_p", None) + frequency_penalty=opener_file.get("frequency_penalty", None) + presence_penalty=opener_file.get("presence_penalty", None) self.conversation_threads[thread.id].set_overrides(temperature, top_p, frequency_penalty, presence_penalty) # set overrides if not opener: # if we only use opener_file then only pass on opener_file for the opening prompt - opener = opener_file["text"] + opener = opener_file.get('text', "error getting text") else: - opener = opener_file["text"] + opener + opener = opener_file.get('text', "error getting text") + opener except: # Parse as just regular text if not opener: opener = opener_file @@ -1328,7 +1328,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): # send opening if opener: - thread_message = await thread.send("***Opening prompt*** \n" + opener) + thread_message = await thread.send("***Opening prompt*** \n" + str(opener)) if thread.id in self.conversation_threads: self.awaiting_responses.append(user_id_normalized) self.awaiting_thread_responses.append(thread.id) diff --git a/models/user_model.py b/models/user_model.py index adcd1fa..91c5ad4 100644 --- a/models/user_model.py +++ b/models/user_model.py @@ -71,7 +71,7 @@ class Thread: def get_overrides(self): return { "temperature": self.temperature, - "top_p": self.temperature, + "top_p": self.top_p, "frequency_penalty": self.frequency_penalty, "presence_penalty": self.presence_penalty, } From b74e0bf80eea0317274cbeebf68740bf76fdceb2 Mon Sep 17 00:00:00 2001 From: Rene Teigen Date: Wed, 11 Jan 2023 20:19:29 +0000 Subject: [PATCH 4/9] Comment changes --- cogs/gpt_3_commands_and_converser.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cogs/gpt_3_commands_and_converser.py b/cogs/gpt_3_commands_and_converser.py index b0c0b26..bc0b806 100644 --- a/cogs/gpt_3_commands_and_converser.py +++ b/cogs/gpt_3_commands_and_converser.py @@ -1275,7 +1275,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): pass else: if not opener_file.endswith((".txt", ".json")): - opener_file = None + opener_file = None # Just start a regular thread if the file fails to load else: # Load the file and read it into opener try: @@ -1284,13 +1284,13 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): f"openers{separator}{opener_file}" ) opener_file = await self.load_file(opener_file, ctx) - try: # Try opening as json, if it fails it'll do it as a str + try: # Try opening as json, if it fails it'll just pass the whole txt or json to the opener opener_file = json.loads(opener_file) temperature=opener_file.get("temperature", None) top_p=opener_file.get("top_p", None) frequency_penalty=opener_file.get("frequency_penalty", None) presence_penalty=opener_file.get("presence_penalty", None) - self.conversation_threads[thread.id].set_overrides(temperature, top_p, frequency_penalty, presence_penalty) # set overrides + self.conversation_threads[thread.id].set_overrides(temperature, top_p, frequency_penalty, presence_penalty) if not opener: # if we only use opener_file then only pass on opener_file for the opening prompt opener = opener_file.get('text', "error getting text") else: @@ -1301,7 +1301,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): else: opener = opener_file + opener except: - opener_file = None + opener_file = None # Just start a regular thread if the file fails to load # Append the starter text for gpt3 to the user's history so it gets concatenated with the prompt later @@ -1314,7 +1314,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): self.CONVERSATION_STARTER_TEXT ) - # Set user as owner before sending anything that can error and leave the thread unowned + # Set user as thread owner before sending anything that can error and leave the thread unowned self.conversation_thread_owners[user_id_normalized] = thread.id overrides = self.conversation_threads[thread.id].get_overrides() From c8f4878caa543ebe2cd661497633c802b438693d Mon Sep 17 00:00:00 2001 From: Rene Teigen Date: Wed, 11 Jan 2023 23:58:56 +0000 Subject: [PATCH 5/9] Added some extra info about json openers to the README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 3458955..ab64c92 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,18 @@ Moreover, an important thing to keep in mind is: pinecone indexes are currently Permanent memory using pinecone is still in alpha, I will be working on cleaning up this work, adding auto-clearing, and optimizing for stability and reliability, any help and feedback is appreciated (**add me on Discord Kaveen#0001 for pinecone help**)! If at any time you're having too many issues with pinecone, simply remove the `PINECONE_TOKEN` line in your `.env` file and the bot will revert to using conversation summarizations. +# Permanent overrides in threads +This bot now supports having overrides be permanent in an entire conversation if you use an opener file which includes them. The new opener files should be .json files formatted like this. `text` corresponds to what you want the conversational opener to be and the rest map 1:1 to the appropriate model settings. An example .json file is included by the name of `english_translator.json` in the `openers` folder +```json +{ + "text": your prompt, + "temp":0, + "top_p":0, + "frequency_penalty":0, + "presence_penalty":0 +} +``` + # User-Input API Keys (Multi-key tenancy) This bot supports multi-user tenancy in regards to API keys. This means that, if you wanted, you could make it such that each user needs to enter their own API key in order to use commands that use GPT3 and DALLE. From 1dd4f5df3ad3b429d74f9c3c2303aa2688582050 Mon Sep 17 00:00:00 2001 From: Rene Teigen Date: Wed, 11 Jan 2023 23:59:55 +0000 Subject: [PATCH 6/9] Small readme fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab64c92..aac8289 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ Permanent memory using pinecone is still in alpha, I will be working on cleaning This bot now supports having overrides be permanent in an entire conversation if you use an opener file which includes them. The new opener files should be .json files formatted like this. `text` corresponds to what you want the conversational opener to be and the rest map 1:1 to the appropriate model settings. An example .json file is included by the name of `english_translator.json` in the `openers` folder ```json { - "text": your prompt, + "text": "your prompt", "temp":0, "top_p":0, "frequency_penalty":0, From 997cd84f27f4b7313e8be316ccee0b8c8a947823 Mon Sep 17 00:00:00 2001 From: Kaveen Kumarasinghe Date: Wed, 11 Jan 2023 19:01:52 -0500 Subject: [PATCH 7/9] Catch the ValueError on no file --- cogs/gpt_3_commands_and_converser.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cogs/gpt_3_commands_and_converser.py b/cogs/gpt_3_commands_and_converser.py index 223f725..557b99c 100644 --- a/cogs/gpt_3_commands_and_converser.py +++ b/cogs/gpt_3_commands_and_converser.py @@ -1326,10 +1326,14 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): ): # only load in files if it's included in the command, if not pass on as normal if opener_file.endswith(".txt"): # Load the file and read it into opener - opener_file = EnvService.find_shared_file( - f"openers{separator}{opener_file}" - ) - opener_file = await self.load_file(opener_file, ctx) + try: + opener_file = EnvService.find_shared_file( + f"openers{separator}{opener_file}" + ) + opener_file = await self.load_file(opener_file, ctx) + except ValueError as e: + await ctx.respond("Error loading the file, "+ str(e),ephemeral=True, delete_after=180) + return if ( not opener ): # if we only use opener_file then only pass on opener_file for the opening prompt From d34017bfb16688e35db1ec5018203030570ff78d Mon Sep 17 00:00:00 2001 From: Kaveen Kumarasinghe Date: Wed, 11 Jan 2023 19:04:42 -0500 Subject: [PATCH 8/9] bump version --- gpt3discord.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpt3discord.py b/gpt3discord.py index a43146a..33ed9bf 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.2" +__version__ = "5.3" """ The pinecone service is used to store and retrieve conversation embeddings. From 30390ee5173b4b545a32c724a5b44f3fcabf6367 Mon Sep 17 00:00:00 2001 From: Rene Teigen Date: Thu, 12 Jan 2023 00:11:38 +0000 Subject: [PATCH 9/9] Nasty stuff from merging --- cogs/gpt_3_commands_and_converser.py | 34 +++------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/cogs/gpt_3_commands_and_converser.py b/cogs/gpt_3_commands_and_converser.py index 557b99c..dfc85ea 100644 --- a/cogs/gpt_3_commands_and_converser.py +++ b/cogs/gpt_3_commands_and_converser.py @@ -1314,37 +1314,6 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): ) return - if opener: - opener = await self.mention_to_username(ctx, opener) - - if not opener and not opener_file: - user_id_normalized = user.id - else: - user_id_normalized = ctx.author.id - if ( - opener_file - ): # only load in files if it's included in the command, if not pass on as normal - if opener_file.endswith(".txt"): - # Load the file and read it into opener - try: - opener_file = EnvService.find_shared_file( - f"openers{separator}{opener_file}" - ) - opener_file = await self.load_file(opener_file, ctx) - except ValueError as e: - await ctx.respond("Error loading the file, "+ str(e),ephemeral=True, delete_after=180) - return - if ( - not opener - ): # if we only use opener_file then only pass on opener_file for the opening prompt - opener = opener_file - else: - opener = opener_file + opener - if not opener_file: - return - else: - pass - if private: await ctx.respond(user.name + "'s private conversation with GPT3") thread = await ctx.channel.create_thread( @@ -1362,6 +1331,9 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"): self.conversation_threads[thread.id] = Thread(thread.id) + if opener: + opener = await self.mention_to_username(ctx, opener) + if not opener and not opener_file: user_id_normalized = user.id else: