Changed conversations to be keyed by thread

The one starting it is the thread owner
Can only own one thread but can converse in multiple

Added a check dmchannel for the message edit moderation
Gave an error when opening a conversation then ending it
Rene Teigen 2 years ago
parent 602742eb59
commit c0e0d8ba8b

@ -14,7 +14,7 @@ from models.deletion_service_model import Deletion
from models.env_service_model import EnvService from models.env_service_model import EnvService
from models.message_model import Message from models.message_model import Message
from models.moderations_service_model import Moderation from models.moderations_service_model import Moderation
from models.user_model import User, RedoUser from models.user_model import RedoUser, Thread
from models.check_model import Check from models.check_model import Check
from models.autocomplete_model import Settings_autocompleter, File_autocompleter from models.autocomplete_model import Settings_autocompleter, File_autocompleter
from collections import defaultdict from collections import defaultdict
@ -44,7 +44,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
self.debug_channel = None self.debug_channel = None
self.bot = bot self.bot = bot
self._last_member_ = None self._last_member_ = None
self.conversating_users = {} self.conversation_threads = {}
self.DAVINCI_ROLES = ["admin", "Admin", "GPT", "gpt"] self.DAVINCI_ROLES = ["admin", "Admin", "GPT", "gpt"]
self.END_PROMPTS = [ self.END_PROMPTS = [
"end", "end",
@ -103,7 +103,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
) )
self.TEXT_CUTOFF = 1900 self.TEXT_CUTOFF = 1900
self.message_queue = message_queue self.message_queue = message_queue
self.conversation_threads = {} self.conversation_thread_owners = {}
# Create slash command groups # Create slash command groups
dalle = discord.SlashCommandGroup( dalle = discord.SlashCommandGroup(
@ -220,11 +220,12 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
await thread.delete() await thread.delete()
await ctx.respond("All conversation threads have been deleted.") await ctx.respond("All conversation threads have been deleted.")
def check_conversing(self, user_id, channel_id, message_content): #TODO: add extra condition to check if multi is enabled for the thread, stated in conversation_threads
def check_conversing(self, user_id, channel_id, message_content, multi=None):
cond1 = ( cond1 = (
user_id in self.conversating_users channel_id in self.conversation_threads
and user_id in self.conversation_threads #and user_id in self.conversation_thread_owners
and channel_id == self.conversation_threads[user_id] #and channel_id == self.conversation_thread_owners[user_id]
) )
# If the trimmed message starts with a Tilde, then we want to not contribute this to the conversation # If the trimmed message starts with a Tilde, then we want to not contribute this to the conversation
try: try:
@ -235,23 +236,33 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
return (cond1) and cond2 return (cond1) and cond2
async def end_conversation(self, message, opener_user_id=None): async def end_conversation(self, ctx, opener_user_id=None):
normalized_user_id = opener_user_id if opener_user_id else message.author.id normalized_user_id = opener_user_id if opener_user_id else ctx.author.id
self.conversating_users.pop(normalized_user_id) try:
channel_id = self.conversation_thread_owners[normalized_user_id]
except:
await ctx.delete(delay=5)
await ctx.reply("Only the conversation starter can end this.", delete_after=5)
return
self.conversation_threads.pop(channel_id)
if isinstance(message, discord.ApplicationContext): if isinstance(ctx, discord.ApplicationContext):
await message.respond( await ctx.respond(
"Your conversation has ended!", ephemeral=True, delete_after=10 "You have ended the conversation with GPT3. Start a conversation with /gpt converse", ephemeral=True, delete_after=10
)
elif isinstance(ctx, discord.Interaction):
await ctx.response.send_message(
"You have ended the conversation with GPT3. Start a conversation with /gpt converse", ephemeral=True, delete_after=10
) )
else: else:
await message.reply( await ctx.reply(
"You have ended the conversation with GPT3. Start a conversation with /gpt converse" "You have ended the conversation with GPT3. Start a conversation with /gpt converse"
) )
# Close all conversation threads for the user # Close all conversation threads for the user
if normalized_user_id in self.conversation_threads: if normalized_user_id in self.conversation_thread_owners:
thread_id = self.conversation_threads[normalized_user_id] thread_id = self.conversation_thread_owners[normalized_user_id]
self.conversation_threads.pop(normalized_user_id) self.conversation_thread_owners.pop(normalized_user_id)
# Attempt to close and lock the thread. # Attempt to close and lock the thread.
try: try:
@ -449,10 +460,10 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
async def check_conversation_limit(self, message): async def check_conversation_limit(self, message):
# After each response, check if the user has reached the conversation limit in terms of messages or time. # After each response, check if the user has reached the conversation limit in terms of messages or time.
if message.author.id in self.conversating_users: if message.channel.id in self.conversation_threads:
# If the user has reached the max conversation length, end the conversation # If the user has reached the max conversation length, end the conversation
if ( if (
self.conversating_users[message.author.id].count self.conversation_threads[message.channel.id].count
>= self.model.max_conversation_length >= self.model.max_conversation_length
): ):
await message.reply( await message.reply(
@ -475,26 +486,27 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
) )
# Get the last entry from the user's conversation history # Get the last entry from the user's conversation history
new_conversation_history.append( new_conversation_history.append(
self.conversating_users[message.author.id].history[-1] + "\n" self.conversation_threads[message.channel.id].history[-1] + "\n"
) )
self.conversating_users[message.author.id].history = new_conversation_history self.conversation_threads[message.channel.id].history = new_conversation_history
# A listener for message edits to redo prompts if they are edited # A listener for message edits to redo prompts if they are edited
@discord.Cog.listener() @discord.Cog.listener()
async def on_message_edit(self, before, after): async def on_message_edit(self, before, after):
# Moderation # Moderation
if ( if not isinstance(after.channel, discord.DMChannel):
after.guild.id in self.moderation_queues if (
and self.moderation_queues[after.guild.id] is not None after.guild.id in self.moderation_queues
): and self.moderation_queues[after.guild.id] is not None
# Create a timestamp that is 0.5 seconds from now ):
timestamp = ( # Create a timestamp that is 0.5 seconds from now
datetime.datetime.now() + datetime.timedelta(seconds=0.5) timestamp = (
).timestamp() datetime.datetime.now() + datetime.timedelta(seconds=0.5)
await self.moderation_queues[after.guild.id].put( ).timestamp()
Moderation(after, timestamp) await self.moderation_queues[after.guild.id].put(
) Moderation(after, timestamp)
)
if after.author.id in self.redo_users: if after.author.id in self.redo_users:
if after.id == original_message[after.author.id]: if after.id == original_message[after.author.id]:
@ -506,22 +518,22 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
# If the user is conversing, we need to get their conversation history, delete the last # If the user is conversing, we need to get their conversation history, delete the last
# "Human:" message, create a new Human: section with the new prompt, and then set the prompt to # "Human:" message, create a new Human: section with the new prompt, and then set the prompt to
# the new prompt, then send that new prompt as the new prompt. # the new prompt, then send that new prompt as the new prompt.
if after.author.id in self.conversating_users: if after.channel.id in self.conversation_threads:
# Remove the last two elements from the history array and add the new Human: prompt # Remove the last two elements from the history array and add the new Human: prompt
self.conversating_users[ self.conversation_threads[
after.author.id after.channel.id
].history = self.conversating_users[after.author.id].history[:-2] ].history = self.conversation_threads[after.channel.id].history[:-2]
self.conversating_users[after.author.id].history.append( self.conversation_threads[after.channel.id].history.append(
f"\nHuman: {after.content}<|endofstatement|>\n" f"\nHuman: {after.content}<|endofstatement|>\n"
) )
edited_content = "".join( edited_content = "".join(
self.conversating_users[after.author.id].history self.conversation_threads[after.channel.id].history
) )
self.conversating_users[after.author.id].count += 1 self.conversation_threads[after.channel.id].count += 1
print("Doing the encapsulated send") print("Doing the encapsulated send")
await self.encapsulated_send( await self.encapsulated_send(
user_id=after.author.id, id=after.channel.id,
prompt=edited_content, prompt=edited_content,
ctx=ctx, ctx=ctx,
response_message=response_message, response_message=response_message,
@ -570,7 +582,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
# We want to have conversationality functionality. To have gpt3 remember context, we need to append the conversation/prompt # We want to have conversationality functionality. To have gpt3 remember context, we need to append the conversation/prompt
# history to the prompt. We can do this by checking if the user is in the conversating_users dictionary, and if they are, # 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. # we can append their history to the prompt.
if message.author.id in self.conversating_users: if message.channel.id in self.conversation_threads:
# Since this is async, we don't want to allow the user to send another prompt while a conversation # 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! # prompt is processing, that'll mess up the conversation history!
@ -595,28 +607,28 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
original_message[message.author.id] = message.id original_message[message.author.id] = message.id
self.conversating_users[message.author.id].history.append( self.conversation_threads[message.channel.id].history.append(
"\nHuman: " + prompt + "<|endofstatement|>\n" "\nHuman: " + prompt + "<|endofstatement|>\n"
) )
# increment the conversation counter for the user # increment the conversation counter for the user
self.conversating_users[message.author.id].count += 1 self.conversation_threads[message.channel.id].count += 1
# Send the request to the model # Send the request to the model
# If conversing, the prompt to send is the history, otherwise, it's just the prompt # If conversing, the prompt to send is the history, otherwise, it's just the prompt
await self.encapsulated_send( await self.encapsulated_send(
message.author.id, message.channel.id,
prompt prompt
if message.author.id not in self.conversating_users if message.channel.id not in self.conversation_threads
else "".join(self.conversating_users[message.author.id].history), else "".join(self.conversation_threads[message.channel.id].history),
message, message,
) )
# ctx can be of type AppContext(interaction) or Message # ctx can be of type AppContext(interaction) or Message
async def encapsulated_send( async def encapsulated_send(
self, self,
user_id, id,
prompt, prompt,
ctx, ctx,
temp_override=None, temp_override=None,
@ -646,7 +658,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
# Check if the prompt is about to go past the token limit # Check if the prompt is about to go past the token limit
if ( if (
user_id in self.conversating_users id in self.conversation_threads
and tokens > self.model.summarize_threshold and tokens > self.model.summarize_threshold
and not from_g_command and not from_g_command
): ):
@ -663,7 +675,7 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
# Check again if the prompt is about to go past the token limit # Check again if the prompt is about to go past the token limit
new_prompt = ( new_prompt = (
"".join(self.conversating_users[user_id].history) + "\nGPTie: " "".join(self.conversation_threads[id].history) + "\nGPTie: "
) )
tokens = self.usage_service.count_tokens(new_prompt) tokens = self.usage_service.count_tokens(new_prompt)
@ -713,8 +725,8 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
) )
# If the user is conversing, add the GPT response to their conversation history. # If the user is conversing, add the GPT response to their conversation history.
if user_id in self.conversating_users and not from_g_command: if id in self.conversation_threads and not from_g_command:
self.conversating_users[user_id].history.append( self.conversation_threads[id].history.append(
"\nGPTie: " + str(response_text) + "<|endofstatement|>\n" "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
) )
@ -726,12 +738,12 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
response_message = ( response_message = (
await ctx.respond( await ctx.respond(
response_text, response_text,
view=RedoView(ctx, self, user_id), view=ConversationView(ctx, self, ctx.channel.id),
) )
if from_context if from_context
else await ctx.reply( else await ctx.reply(
response_text, response_text,
view=RedoView(ctx, self, user_id), view=ConversationView(ctx, self, ctx.channel.id),
) )
) )
@ -742,10 +754,10 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
else await ctx.fetch_message(response_message.id) else await ctx.fetch_message(response_message.id)
) )
self.redo_users[user_id] = RedoUser( self.redo_users[ctx.author.id] = RedoUser(
prompt, ctx, ctx, actual_response_message prompt, ctx, ctx, actual_response_message
) )
self.redo_users[user_id].add_interaction(actual_response_message.id) self.redo_users[ctx.author.id].add_interaction(actual_response_message.id)
# We are doing a redo, edit the message. # We are doing a redo, edit the message.
else: else:
@ -755,8 +767,8 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
self.generate_debug_message(prompt, response), self.debug_channel self.generate_debug_message(prompt, response), self.debug_channel
) )
if user_id in self.awaiting_responses: if ctx.author.id in self.awaiting_responses:
self.awaiting_responses.remove(user_id) self.awaiting_responses.remove(ctx.author.id)
# Error catching for OpenAI model value errors # Error catching for OpenAI model value errors
except ValueError as e: except ValueError as e:
@ -775,8 +787,8 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
await ctx.send_followup(message) if from_context else await ctx.reply( await ctx.send_followup(message) if from_context else await ctx.reply(
message message
) )
if user_id in self.awaiting_responses: if ctx.author.id in self.awaiting_responses:
self.awaiting_responses.remove(user_id) self.awaiting_responses.remove(ctx.author.id)
traceback.print_exc() traceback.print_exc()
try: try:
@ -901,11 +913,11 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
user = ctx.user user = ctx.user
if user.id in self.conversating_users: if user.id in self.conversation_thread_owners:
message = await ctx.respond( message = await ctx.respond(
"You are already conversating with GPT3. End the conversation with !g end or just say 'end' in a supported channel" "You've already created a thread, end it before creating a new one",
delete_after=5
) )
await self.deletion_queue(message)
return return
if not opener and not opener_file: if not opener and not opener_file:
@ -930,18 +942,6 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
else: else:
pass pass
self.conversating_users[user_id_normalized] = User(user_id_normalized)
# 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.conversating_users[user_id_normalized].history.append(
self.CONVERSATION_STARTER_TEXT_MINIMAL
)
elif not minimal:
self.conversating_users[user_id_normalized].history.append(
self.CONVERSATION_STARTER_TEXT
)
if private: if private:
await ctx.respond(user.name + "'s private conversation with GPT3") await ctx.respond(user.name + "'s private conversation with GPT3")
thread = await ctx.channel.create_thread( thread = await ctx.channel.create_thread(
@ -957,6 +957,18 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
auto_archive_duration=60, auto_archive_duration=60,
) )
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
)
await thread.send( await thread.send(
"<@" "<@"
+ str(user_id_normalized) + str(user_id_normalized)
@ -966,24 +978,26 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
# send opening # send opening
if opener: if opener:
thread_message = await thread.send("***Opening prompt*** \n" + opener) thread_message = await thread.send("***Opening prompt*** \n" + opener)
if user_id_normalized in self.conversating_users: if thread.id in self.conversation_threads:
self.awaiting_responses.append(user_id_normalized) self.awaiting_responses.append(user_id_normalized)
self.conversating_users[user_id_normalized].history.append( self.conversation_threads[thread.id].history.append(
"\nHuman: " + opener + "<|endofstatement|>\n" "\nHuman: " + opener + "<|endofstatement|>\n"
) )
self.conversating_users[user_id_normalized].count += 1 self.conversation_threads[thread.id].count += 1
await self.encapsulated_send( await self.encapsulated_send(
user_id_normalized, thread.id,
opener opener
if user_id_normalized not in self.conversating_users if thread.id not in self.conversation_threads
else "".join(self.conversating_users[user_id_normalized].history), else "".join(self.conversation_threads[thread.id].history),
thread_message, thread_message,
) )
self.awaiting_responses.remove(user_id_normalized)
self.conversation_thread_owners[user_id_normalized] = thread.id
self.conversation_threads[user_id_normalized] = thread.id
@add_to_group("system") @add_to_group("system")
@discord.slash_command( @discord.slash_command(
@ -1063,10 +1077,15 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
guild_ids=ALLOWED_GUILDS, guild_ids=ALLOWED_GUILDS,
) )
@discord.guild_only() @discord.guild_only()
async def end_chat(self, ctx: discord.ApplicationContext): async def end(self, ctx: discord.ApplicationContext):
await ctx.defer(ephemeral=True) await ctx.defer(ephemeral=True)
user_id = ctx.user.id user_id = ctx.user.id
if user_id in self.conversating_users: try:
thread_id = self.conversation_thread_owners[user_id]
except:
await ctx.respond("You haven't started any conversations", ephemeral=True, delete_after=10)
return
if thread_id in self.conversation_threads:
try: try:
await self.end_conversation(ctx) await self.end_conversation(ctx)
except Exception as e: except Exception as e:
@ -1140,14 +1159,14 @@ class GPT3ComCon(discord.Cog, name="GPT3ComCon"):
await self.process_settings_command(ctx, parameter, value) await self.process_settings_command(ctx, parameter, value)
class RedoView(discord.ui.View): class ConversationView(discord.ui.View):
def __init__(self, ctx, converser_cog, user_id): def __init__(self, ctx, converser_cog, id):
super().__init__(timeout=3600) # 1 hour interval to redo. super().__init__(timeout=3600) # 1 hour interval to redo.
self.converser_cog = converser_cog self.converser_cog = converser_cog
self.ctx = ctx self.ctx = ctx
self.add_item(RedoButton(self.converser_cog)) self.add_item(RedoButton(self.converser_cog))
if user_id in self.converser_cog.conversating_users: if id in self.converser_cog.conversation_threads:
self.add_item(EndConvoButton(self.converser_cog)) self.add_item(EndConvoButton(self.converser_cog))
async def on_timeout(self): async def on_timeout(self):
@ -1164,7 +1183,7 @@ class RedoView(discord.ui.View):
) )
class EndConvoButton(discord.ui.Button["RedoView"]): class EndConvoButton(discord.ui.Button["ConversationView"]):
def __init__(self, converser_cog): def __init__(self, converser_cog):
super().__init__(style=discord.ButtonStyle.danger, label="End Conversation") super().__init__(style=discord.ButtonStyle.danger, label="End Conversation")
self.converser_cog = converser_cog self.converser_cog = converser_cog
@ -1173,16 +1192,10 @@ class EndConvoButton(discord.ui.Button["RedoView"]):
# Get the user # Get the user
user_id = interaction.user.id user_id = interaction.user.id
if user_id in self.converser_cog.redo_users and self.converser_cog.redo_users[ if user_id in self.converser_cog.conversation_thread_owners and self.converser_cog.conversation_thread_owners[user_id] == interaction.channel.id:
user_id
].in_interaction(interaction.message.id):
try: try:
await self.converser_cog.end_conversation( await self.converser_cog.end_conversation(
self.converser_cog.redo_users[user_id].message, interaction, opener_user_id=interaction.user.id
opener_user_id=user_id,
)
await interaction.response.send_message(
"Your conversation has ended!", ephemeral=True, delete_after=10
) )
except Exception as e: except Exception as e:
print(e) print(e)
@ -1197,7 +1210,7 @@ class EndConvoButton(discord.ui.Button["RedoView"]):
) )
class RedoButton(discord.ui.Button["RedoView"]): class RedoButton(discord.ui.Button["ConversationView"]):
def __init__(self, converser_cog): def __init__(self, converser_cog):
super().__init__(style=discord.ButtonStyle.danger, label="Retry") super().__init__(style=discord.ButtonStyle.danger, label="Retry")
self.converser_cog = converser_cog self.converser_cog = converser_cog
@ -1219,7 +1232,7 @@ class RedoButton(discord.ui.Button["RedoView"]):
) )
await self.converser_cog.encapsulated_send( await self.converser_cog.encapsulated_send(
user_id, prompt, ctx, response_message id=user_id, prompt=prompt, ctx=ctx, response_message=response_message
) )
else: else:
await interaction.response.send_message( await interaction.response.send_message(

@ -50,3 +50,24 @@ class User:
def __str__(self): def __str__(self):
return self.__repr__() return self.__repr__()
class Thread:
def __init__(self, id):
self.id = id
self.history = []
self.count = 0
# 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
# if the user with that ID was in the list
def __eq__(self, other):
return self.id == other.id
def __hash__(self):
return hash(self.id)
def __repr__(self):
return f"Thread(id={self.id}, history={self.history})"
def __str__(self):
return self.__repr__()
Loading…
Cancel
Save