From b44a7000cf478d39956750855e91ef3d54df496b Mon Sep 17 00:00:00 2001 From: Birger Moell Date: Tue, 13 Dec 2022 23:50:25 +0100 Subject: [PATCH 01/12] Added buttons and commands to bot --- bot/bot.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/bot/bot.py b/bot/bot.py index 03da1483..1515273b 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -52,6 +52,39 @@ intents = discord.Intents.default() intents.message_content = True client = OpenChatGPTClient(intents=intents) +class LikeButton(discord.ui.Button): + def __init__(self, label, channel, username, prompt): + super().__init__(label=label, style=discord.ButtonStyle.green, emoji="👍") + self.channel = channel + self.username = username + self.prompt = prompt + async def callback(self, interaction): + # interaction holds the interaction object + #await interaction.response.defer() + await interaction.response.send_message("Thanks for your feedback. You liked this 👍 ") + +class NeutralButton(discord.ui.Button): + def __init__(self, label, channel, username, prompt): + super().__init__(label=label, style=discord.ButtonStyle.green, emoji="😐") + self.channel = channel + self.username = username + self.prompt = prompt + async def callback(self, interaction): + # interaction holds the interaction object + #await interaction.response.defer() + await interaction.response.send_message("Thanks for your feedback. You thought this was neutral 😐 ") + +class DislikeButton(discord.ui.Button): + def __init__(self, label, channel, username, prompt): + super().__init__(label=label, style=discord.ButtonStyle.green, emoji="👎") + self.channel = channel + self.username = username + self.prompt = prompt + async def callback(self, interaction): + # interaction holds the interaction object + #await interaction.response.defer() + # send the feedback to the backend # + await interaction.response.send_message("Thanks for your feedback. You disliked this 👎 ") @client.tree.command() async def register(interaction: discord.Interaction): @@ -80,6 +113,37 @@ async def list_participants(interaction: discord.Interaction): await interaction.response.send_message("Failed to fetch participants") +async def send_propmt_with_response_and_buttons(channel, username, prompt, response): + await channel.send(f'What do you think about the following interaction: \nprompt: {prompt} \nresponse: {response}') + #await channel.send(f'Please click on the button that best describes your reaction to the response:') + + # add buttons + view = discord.ui.View() + like=LikeButton(label="Like", channel=channel, username=username, prompt=prompt) + neutral=NeutralButton(label="Neutral", channel=channel, username=username, prompt=prompt) + dislike=DislikeButton(label="Dislike", channel=channel, username=username, prompt=prompt) + + view.add_item(item=like) + view.add_item(item=neutral) + view.add_item(item=dislike) + await channel.send(view=view) + + +@client.tree.command() +async def review_prompts(interaction: discord.Interaction, number_of_prompts: int): + # get the prompt from the db + url = f"{prompts_url}?begin_id=0&limit={number_of_prompts}" + response = requests.get(url, headers=headers) + if response.status_code == 200: + prompts = response.json() + print("the responses are", prompts) + for prompt in prompts: + await send_propmt_with_response_and_buttons(interaction.channel, interaction.user.name, prompt['prompt'], prompt["response"]) + else: + await interaction.response.send_message("Failed to get prompts for review") + + + @client.tree.command() async def add_prompt(interaction: discord.Interaction, prompt: str, response: str, language: str = "en"): """Uploads a single prompt to the server.""" @@ -92,7 +156,9 @@ async def add_prompt(interaction: discord.Interaction, prompt: str, response: st } response = requests.post(prompts_url, headers=headers, json=prompt) if response.status_code == 200: - await interaction.response.send_message("Added your prompt") + await send_propmt_with_response_and_buttons(interaction.channel, interaction.user.name, prompt['prompt'], prompt["response"]) + # send the prompt back with buttons for the user to click on + #await interaction.response.send_message("Added your prompt") else: await interaction.response.send_message("Failed to add the prompt") From a10c337b3de9426ca5389c85edd7a27f37c1ad53 Mon Sep 17 00:00:00 2001 From: Birger Moell Date: Tue, 13 Dec 2022 23:51:14 +0100 Subject: [PATCH 02/12] Lint fixes --- bot/bot.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 1515273b..f3cd1acf 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -52,27 +52,32 @@ intents = discord.Intents.default() intents.message_content = True client = OpenChatGPTClient(intents=intents) + class LikeButton(discord.ui.Button): def __init__(self, label, channel, username, prompt): super().__init__(label=label, style=discord.ButtonStyle.green, emoji="👍") self.channel = channel self.username = username self.prompt = prompt + async def callback(self, interaction): # interaction holds the interaction object - #await interaction.response.defer() + # await interaction.response.defer() await interaction.response.send_message("Thanks for your feedback. You liked this 👍 ") + class NeutralButton(discord.ui.Button): def __init__(self, label, channel, username, prompt): super().__init__(label=label, style=discord.ButtonStyle.green, emoji="😐") self.channel = channel self.username = username self.prompt = prompt + async def callback(self, interaction): # interaction holds the interaction object - #await interaction.response.defer() - await interaction.response.send_message("Thanks for your feedback. You thought this was neutral 😐 ") + # await interaction.response.defer() + await interaction.response.send_message("Thanks for your feedback. You thought this was neutral 😐 ") + class DislikeButton(discord.ui.Button): def __init__(self, label, channel, username, prompt): @@ -80,12 +85,14 @@ class DislikeButton(discord.ui.Button): self.channel = channel self.username = username self.prompt = prompt + async def callback(self, interaction): # interaction holds the interaction object - #await interaction.response.defer() + # await interaction.response.defer() # send the feedback to the backend # await interaction.response.send_message("Thanks for your feedback. You disliked this 👎 ") + @client.tree.command() async def register(interaction: discord.Interaction): """Registers the user for submissions.""" @@ -114,14 +121,14 @@ async def list_participants(interaction: discord.Interaction): async def send_propmt_with_response_and_buttons(channel, username, prompt, response): - await channel.send(f'What do you think about the following interaction: \nprompt: {prompt} \nresponse: {response}') - #await channel.send(f'Please click on the button that best describes your reaction to the response:') + await channel.send(f"What do you think about the following interaction: \nprompt: {prompt} \nresponse: {response}") + # await channel.send(f'Please click on the button that best describes your reaction to the response:') # add buttons view = discord.ui.View() - like=LikeButton(label="Like", channel=channel, username=username, prompt=prompt) - neutral=NeutralButton(label="Neutral", channel=channel, username=username, prompt=prompt) - dislike=DislikeButton(label="Dislike", channel=channel, username=username, prompt=prompt) + like = LikeButton(label="Like", channel=channel, username=username, prompt=prompt) + neutral = NeutralButton(label="Neutral", channel=channel, username=username, prompt=prompt) + dislike = DislikeButton(label="Dislike", channel=channel, username=username, prompt=prompt) view.add_item(item=like) view.add_item(item=neutral) @@ -138,12 +145,13 @@ async def review_prompts(interaction: discord.Interaction, number_of_prompts: in prompts = response.json() print("the responses are", prompts) for prompt in prompts: - await send_propmt_with_response_and_buttons(interaction.channel, interaction.user.name, prompt['prompt'], prompt["response"]) + await send_propmt_with_response_and_buttons( + interaction.channel, interaction.user.name, prompt["prompt"], prompt["response"] + ) else: await interaction.response.send_message("Failed to get prompts for review") - @client.tree.command() async def add_prompt(interaction: discord.Interaction, prompt: str, response: str, language: str = "en"): """Uploads a single prompt to the server.""" @@ -156,9 +164,11 @@ async def add_prompt(interaction: discord.Interaction, prompt: str, response: st } response = requests.post(prompts_url, headers=headers, json=prompt) if response.status_code == 200: - await send_propmt_with_response_and_buttons(interaction.channel, interaction.user.name, prompt['prompt'], prompt["response"]) + await send_propmt_with_response_and_buttons( + interaction.channel, interaction.user.name, prompt["prompt"], prompt["response"] + ) # send the prompt back with buttons for the user to click on - #await interaction.response.send_message("Added your prompt") + # await interaction.response.send_message("Added your prompt") else: await interaction.response.send_message("Failed to add the prompt") From c807737ab42f7c89fecc75a97814cacaacd83be9 Mon Sep 17 00:00:00 2001 From: Birger Moell Date: Wed, 14 Dec 2022 01:15:20 +0100 Subject: [PATCH 03/12] Fix typo and added logger --- bot/bot.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index f3cd1acf..a29993b3 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -6,8 +6,10 @@ import os import discord import requests from discord import app_commands +from loguru import logger from dotenv import load_dotenv + bot_url = "https://discord.com/api/oauth2/authorize?client_id=1051614245940375683&permissions=8&scope=bot" # Load up all the important environment variables. @@ -44,7 +46,7 @@ class OpenChatGPTClient(discord.Client): else: # This can take up to an hour for the commands to be registered. await self.tree.sync() - print("Ready!") + logger.debug("Ready!") # List the set of intents needed for commands to operate properly. @@ -105,7 +107,7 @@ async def register(interaction: discord.Interaction): if response.status_code == 200: await interaction.response.send_message(f"Added you {interaction.user.name}") else: - print(response) + logger.debug(response) await interaction.response.send_message("Failed to add you") @@ -120,7 +122,7 @@ async def list_participants(interaction: discord.Interaction): await interaction.response.send_message("Failed to fetch participants") -async def send_propmt_with_response_and_buttons(channel, username, prompt, response): +async def send_prompt_with_response_and_button(channel, username, prompt, response): await channel.send(f"What do you think about the following interaction: \nprompt: {prompt} \nresponse: {response}") # await channel.send(f'Please click on the button that best describes your reaction to the response:') @@ -143,9 +145,9 @@ async def review_prompts(interaction: discord.Interaction, number_of_prompts: in response = requests.get(url, headers=headers) if response.status_code == 200: prompts = response.json() - print("the responses are", prompts) + logger.debug("the responses are:", prompts) for prompt in prompts: - await send_propmt_with_response_and_buttons( + await send_prompt_with_response_and_button( interaction.channel, interaction.user.name, prompt["prompt"], prompt["response"] ) else: @@ -164,7 +166,7 @@ async def add_prompt(interaction: discord.Interaction, prompt: str, response: st } response = requests.post(prompts_url, headers=headers, json=prompt) if response.status_code == 200: - await send_propmt_with_response_and_buttons( + await send_prompt_with_response_and_button( interaction.channel, interaction.user.name, prompt["prompt"], prompt["response"] ) # send the prompt back with buttons for the user to click on From 0ff400c947673b0fb3a7f8c32d7c95de636198c1 Mon Sep 17 00:00:00 2001 From: Birger Moell Date: Wed, 14 Dec 2022 01:16:03 +0100 Subject: [PATCH 04/12] lint fix --- bot/bot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index a29993b3..2509138a 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -6,9 +6,8 @@ import os import discord import requests from discord import app_commands -from loguru import logger from dotenv import load_dotenv - +from loguru import logger bot_url = "https://discord.com/api/oauth2/authorize?client_id=1051614245940375683&permissions=8&scope=bot" From 55a2b892b0bd3f01329d3cea0099591030d81d75 Mon Sep 17 00:00:00 2001 From: Alexander Mattick Date: Wed, 14 Dec 2022 19:57:45 +0100 Subject: [PATCH 05/12] implemented "Ranked Pairs" algorithm for merging rankings from different users --- backend/postprocessing/rankings.py | 135 +++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 backend/postprocessing/rankings.py diff --git a/backend/postprocessing/rankings.py b/backend/postprocessing/rankings.py new file mode 100644 index 00000000..929c9d04 --- /dev/null +++ b/backend/postprocessing/rankings.py @@ -0,0 +1,135 @@ +import numpy as np +from typing import List + + +def head_to_head_votes(ranks:List[List[int]]): + tallies = np.zeros((len(ranks[0]), len(ranks[0]))) + names = sorted(ranks[0]) + ranks = np.array(ranks) + # we want the sorted indices + ranks = np.argsort(ranks, axis=1) + for i in range(ranks.shape[1]): + for j in range(i+1, ranks.shape[1]): + ## now count the cases someone voted for i over j + over_j = np.sum(ranks[:,i]b or b>a for all a,b. + + + Returns + ------- + out : False if the pairs do not contain a cycle, True if the pairs contain a cycle + + + """ + # get all condorcet losers (pairs that loose to all other pairs) + # idea: filter all losers that are never winners + #print("pairs", pairs) + if len(pairs) <= 1: + return False + losers= [c_lose for c_lose in np.unique(pairs[:,1]) if c_lose not in pairs[:,0]] + if len(losers)==0: + # if we recursively removed pairs, and at some point we did not have + # a condorcet loser, that means everything is both a winner and loser, + # yielding at least one (winner,loser), (loser,winner) pair + return True + + new = [] + for p in pairs: + if p[1] not in losers: + new.append(p) + return cycle_detect(np.array(new)) + +def get_winner(pairs): + """ + This returns _one_ concordant winner. + It could be that there are multiple concordant winners, but in our case + since we are interested in a ranking, we have to choose one at random. + """ + losers = np.unique(pairs[:,1]).astype(int) + winners = np.unique(pairs[:,0]).astype(int) + for w in winners: + if w not in losers: + return w +def get_ranking(pairs): + """ + Abuses concordance property to get a (not necessarily unqiue) ranking. + The lack of uniqueness is due to the potential existance of multiple + equally ranked winners. We have to pick one, which is where + the non-uniqueness comes from + """ + if len(pairs) ==1: + return list(pairs[0]) + w = get_winner(pairs) + # now remove the winner from the list of pairs + p_new = np.array([(a,b) for a,b in pairs if a != w]) + return [w]+get_ranking(p_new) + +def ranked_pairs(ranks: List[List[int]]): + """ + Expects a list of rankings for an item like: + [("w","x","z","y") for _ in range(3)] + + [("w","y","x","z") for _ in range(2)] + + [("x","y","z","w") for _ in range(4)] + + [("x","z","w","y") for _ in range(5)] + + [("y","w","x","z") for _ in range(1)] + This code is quite brain melting, but the idea is the following: + 1. create a head-to-head matrix that tallies up all win-lose combinations of preferences + 2. take all combinations that win more than they loose and sort those by how often they win + 3. use that to create an (implicit) directed graph + 4. recursively extract nodes from the graph that do not have incoming edges + 5. said recursive list is the ranking + """ + tallies,names = head_to_head_votes(ranks) + tallies = tallies - tallies.T + #print(tallies) + ## note: the resulting tally matrix should be skew-symmetric + ## order by strenght of victory (using tideman's original method, don't think it would make a difference for us) + sorted_majorities = [] + for i in range(len(ranks[0])): + for j in range(len(ranks[i])): + if tallies[i, j] > 0: + sorted_majorities.append((i, j, tallies[i, j])) + ## we don't explicitly deal with tied majorities here + sorted_majorities = np.array(sorted(sorted_majorities, key=lambda x: x[2], reverse=True)) + ## now do lock ins + lock_ins = [] + for (x, y, _) in sorted_majorities: + # invariant: lock_ins has no cycles here + lock_ins.append((x,y)) + #print("lock ins are now",np.array(lock_ins)) + if cycle_detect(np.array(lock_ins)): + #print("backup: cycle detected") + # if there's a cycle, delete the new addition and continue + lock_ins = lock_ins[:-1] + # now simply return all winners in order, and attach the losers + # to the back. This is because the overall loser might not be unique + # and (by concordance property) may never exist in any winning set to begin with. + # (otherwise he would either not be the loser, or cycles exist!) + # Since there could be multiple overall losers, we just return them in any order + # as we are unable to find a closer ranking + numerical_ranks = np.array(get_ranking(np.array(lock_ins))).astype(int) + conversion = [names[n] for n in numerical_ranks] + return conversion + + + +if __name__ == "__main__": + ranks = ( + [("w","x","z","y") for _ in range(1)] + + [("w","y","x","z") for _ in range(2)] + #+ [("x","y","z","w") for _ in range(4)] + + [("x","z","w","y") for _ in range(5)] + + [("y","w","x","z") for _ in range(1)] + #[("y","z","w","x") for _ in range(1000)] + ) + rp=ranked_pairs(ranks) + print(rp) \ No newline at end of file From 38ca08446d560797522b7828720032799584d32a Mon Sep 17 00:00:00 2001 From: Alexander Mattick Date: Wed, 14 Dec 2022 20:32:23 +0100 Subject: [PATCH 06/12] ran pre-commit hook --- backend/postprocessing/rankings.py | 92 ++++++++++++++++-------------- backend/requirements.txt | 1 + 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/backend/postprocessing/rankings.py b/backend/postprocessing/rankings.py index 929c9d04..38686f67 100644 --- a/backend/postprocessing/rankings.py +++ b/backend/postprocessing/rankings.py @@ -1,42 +1,45 @@ -import numpy as np +# -*- coding: utf-8 -*- from typing import List +import numpy as np -def head_to_head_votes(ranks:List[List[int]]): + +def head_to_head_votes(ranks: List[List[int]]): tallies = np.zeros((len(ranks[0]), len(ranks[0]))) names = sorted(ranks[0]) ranks = np.array(ranks) # we want the sorted indices ranks = np.argsort(ranks, axis=1) for i in range(ranks.shape[1]): - for j in range(i+1, ranks.shape[1]): - ## now count the cases someone voted for i over j - over_j = np.sum(ranks[:,i]b or b>a for all a,b. - - + + Returns ------- out : False if the pairs do not contain a cycle, True if the pairs contain a cycle - + """ # get all condorcet losers (pairs that loose to all other pairs) # idea: filter all losers that are never winners - #print("pairs", pairs) + # print("pairs", pairs) if len(pairs) <= 1: return False - losers= [c_lose for c_lose in np.unique(pairs[:,1]) if c_lose not in pairs[:,0]] - if len(losers)==0: + losers = [c_lose for c_lose in np.unique(pairs[:, 1]) if c_lose not in pairs[:, 0]] + if len(losers) == 0: # if we recursively removed pairs, and at some point we did not have # a condorcet loser, that means everything is both a winner and loser, # yielding at least one (winner,loser), (loser,winner) pair @@ -48,30 +51,34 @@ def cycle_detect(pairs): new.append(p) return cycle_detect(np.array(new)) + def get_winner(pairs): """ This returns _one_ concordant winner. It could be that there are multiple concordant winners, but in our case since we are interested in a ranking, we have to choose one at random. """ - losers = np.unique(pairs[:,1]).astype(int) - winners = np.unique(pairs[:,0]).astype(int) + losers = np.unique(pairs[:, 1]).astype(int) + winners = np.unique(pairs[:, 0]).astype(int) for w in winners: if w not in losers: return w + + def get_ranking(pairs): """ Abuses concordance property to get a (not necessarily unqiue) ranking. The lack of uniqueness is due to the potential existance of multiple - equally ranked winners. We have to pick one, which is where + equally ranked winners. We have to pick one, which is where the non-uniqueness comes from """ - if len(pairs) ==1: + if len(pairs) == 1: return list(pairs[0]) w = get_winner(pairs) # now remove the winner from the list of pairs - p_new = np.array([(a,b) for a,b in pairs if a != w]) - return [w]+get_ranking(p_new) + p_new = np.array([(a, b) for a, b in pairs if a != w]) + return [w] + get_ranking(p_new) + def ranked_pairs(ranks: List[List[int]]): """ @@ -86,28 +93,28 @@ def ranked_pairs(ranks: List[List[int]]): 2. take all combinations that win more than they loose and sort those by how often they win 3. use that to create an (implicit) directed graph 4. recursively extract nodes from the graph that do not have incoming edges - 5. said recursive list is the ranking + 5. said recursive list is the ranking """ - tallies,names = head_to_head_votes(ranks) + tallies, names = head_to_head_votes(ranks) tallies = tallies - tallies.T - #print(tallies) - ## note: the resulting tally matrix should be skew-symmetric - ## order by strenght of victory (using tideman's original method, don't think it would make a difference for us) + # print(tallies) + # note: the resulting tally matrix should be skew-symmetric + # order by strenght of victory (using tideman's original method, don't think it would make a difference for us) sorted_majorities = [] for i in range(len(ranks[0])): for j in range(len(ranks[i])): if tallies[i, j] > 0: sorted_majorities.append((i, j, tallies[i, j])) - ## we don't explicitly deal with tied majorities here + # we don't explicitly deal with tied majorities here sorted_majorities = np.array(sorted(sorted_majorities, key=lambda x: x[2], reverse=True)) - ## now do lock ins + # now do lock ins lock_ins = [] for (x, y, _) in sorted_majorities: # invariant: lock_ins has no cycles here - lock_ins.append((x,y)) - #print("lock ins are now",np.array(lock_ins)) + lock_ins.append((x, y)) + # print("lock ins are now",np.array(lock_ins)) if cycle_detect(np.array(lock_ins)): - #print("backup: cycle detected") + # print("backup: cycle detected") # if there's a cycle, delete the new addition and continue lock_ins = lock_ins[:-1] # now simply return all winners in order, and attach the losers @@ -116,20 +123,19 @@ def ranked_pairs(ranks: List[List[int]]): # (otherwise he would either not be the loser, or cycles exist!) # Since there could be multiple overall losers, we just return them in any order # as we are unable to find a closer ranking - numerical_ranks = np.array(get_ranking(np.array(lock_ins))).astype(int) + numerical_ranks = np.array(get_ranking(np.array(lock_ins))).astype(int) conversion = [names[n] for n in numerical_ranks] return conversion - if __name__ == "__main__": ranks = ( - [("w","x","z","y") for _ in range(1)] - + [("w","y","x","z") for _ in range(2)] - #+ [("x","y","z","w") for _ in range(4)] - + [("x","z","w","y") for _ in range(5)] - + [("y","w","x","z") for _ in range(1)] - #[("y","z","w","x") for _ in range(1000)] + [("w", "x", "z", "y") for _ in range(1)] + + [("w", "y", "x", "z") for _ in range(2)] + # + [("x","y","z","w") for _ in range(4)] + + [("x", "z", "w", "y") for _ in range(5)] + + [("y", "w", "x", "z") for _ in range(1)] + # [("y","z","w","x") for _ in range(1000)] ) - rp=ranked_pairs(ranks) - print(rp) \ No newline at end of file + rp = ranked_pairs(ranks) + print(rp) diff --git a/backend/requirements.txt b/backend/requirements.txt index 92668609..b882d594 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,7 @@ alembic==1.8.1 fastapi==0.88.0 loguru==0.6.0 +numpy==1.22.4 psycopg2-binary==2.9.5 pydantic==1.9.1 python-dotenv==0.21.0 From f04e0efd4934441b4e6bbe6b73ac8f2f763a050e Mon Sep 17 00:00:00 2001 From: Birger Moell Date: Wed, 14 Dec 2022 21:55:33 +0100 Subject: [PATCH 07/12] Updated code to work on LAION server --- bot/bot.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 2509138a..6f7ede73 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -27,7 +27,9 @@ headers = {"X-API-Key": API_SERVER_KEY} # For testing only. TEST_GUILD = os.getenv("TEST_GUILD") - +TEST_GUILD_LAION = os.getenv("TEST_GUILD_LAION") +# TEST_GUILD = False +guild_ids = [TEST_GUILD, TEST_GUILD_LAION] # Initiate the client and command tree to create slash commands. class OpenChatGPTClient(discord.Client): @@ -39,10 +41,16 @@ class OpenChatGPTClient(discord.Client): if TEST_GUILD: # When testing the bot it's handy to run in a single server (called a # Guide in the API). This is relatively fast. - guild = discord.Object(id=TEST_GUILD) - self.tree.copy_global_to(guild=guild) - await self.tree.sync(guild=guild) - else: + for guild_id in guild_ids: + guild = discord.Object(id=guild_id) + self.tree.copy_global_to(guild=guild) + await self.tree.sync(guild=guild) + + + # guild = discord.Object(id=TEST_GUILD) + # self.tree.copy_global_to(guild=guild) + # await self.tree.sync(guild=guild) + else: # This can take up to an hour for the commands to be registered. await self.tree.sync() logger.debug("Ready!") From 7dc714fc30acbb02a30fb174f9315ae9fe218628 Mon Sep 17 00:00:00 2001 From: Birger Moell Date: Wed, 14 Dec 2022 21:56:32 +0100 Subject: [PATCH 08/12] Lint fix --- bot/bot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 6f7ede73..aff74b26 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -31,6 +31,7 @@ TEST_GUILD_LAION = os.getenv("TEST_GUILD_LAION") # TEST_GUILD = False guild_ids = [TEST_GUILD, TEST_GUILD_LAION] + # Initiate the client and command tree to create slash commands. class OpenChatGPTClient(discord.Client): def __init__(self, *, intents: discord.Intents): @@ -45,12 +46,11 @@ class OpenChatGPTClient(discord.Client): guild = discord.Object(id=guild_id) self.tree.copy_global_to(guild=guild) await self.tree.sync(guild=guild) - - + # guild = discord.Object(id=TEST_GUILD) # self.tree.copy_global_to(guild=guild) # await self.tree.sync(guild=guild) - else: + else: # This can take up to an hour for the commands to be registered. await self.tree.sync() logger.debug("Ready!") From 96d668fa4b08724ed8b7d5c1f925e1c8bd542c44 Mon Sep 17 00:00:00 2001 From: Yannic Kilcher Date: Wed, 14 Dec 2022 23:08:08 +0100 Subject: [PATCH 09/12] Rough draft of story summarization interaction --- backend/app/api/v1/api.py | 3 +- backend/app/api/v1/tasks.py | 106 ++++++++++++++++++++++++++++++++ backend/app/schemas/protocol.py | 63 +++++++++++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 backend/app/api/v1/tasks.py create mode 100644 backend/app/schemas/protocol.py diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py index 7c97c9cb..5a704c2d 100644 --- a/backend/app/api/v1/api.py +++ b/backend/app/api/v1/api.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -from app.api.v1 import labelers, prompts +from app.api.v1 import labelers, prompts, tasks from fastapi import APIRouter api_router = APIRouter() api_router.include_router(labelers.router, prefix="/labelers", tags=["labelers"]) api_router.include_router(prompts.router, prefix="/prompts", tags=["prompts"]) +api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"]) diff --git a/backend/app/api/v1/tasks.py b/backend/app/api/v1/tasks.py new file mode 100644 index 00000000..494281d0 --- /dev/null +++ b/backend/app/api/v1/tasks.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +from typing import Any, List +from uuid import UUID + +from app.api import deps +from app.schemas import protocol as protocol_schema +from fastapi import APIRouter, Depends, HTTPException +from fastapi.security.api_key import APIKey +from loguru import logger +from sqlmodel import Session +from starlette.status import HTTP_400_BAD_REQUEST + +router = APIRouter() + + +@router.post("/", response_model=List[protocol_schema.SummarizeStoryTask]) # work with Union once more types are added +def request_task( + *, + db: Session = Depends(deps.get_db), + api_key: APIKey = Depends(deps.get_api_key), + request: protocol_schema.GenericTaskRequest, # work with Union once more types are added +) -> Any: + """ + Create new task. + """ + deps.api_auth(api_key, db, create=True) + + # TODO: Create a task and store it in the database. + + match (request.type): + case "generic": + # here we create a task at random (and store it in the database) + logger.info("Frontend requested a generic task.") + task = protocol_schema.SummarizeStoryTask( + story="This is a story. A very long story. So long, it needs to be summarized.", + ) + + case _: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail="Invalid request type.", + ) + if request.user_id is not None: + task.addressed_users = [request.user_id] + + return [task] + + +@router.post("/{task_id}/ack") +def acknowledge_task( + *, + db: Session = Depends(deps.get_db), + api_key: APIKey = Depends(deps.get_api_key), + task_id: UUID, + response: protocol_schema.PostCreatedTaskResponse, +) -> Any: + """ + The frontend acknowledges a task. + """ + deps.api_auth(api_key, db, create=True) + + match (response.type): + case "post_created": + logger.info(f"Frontend acknowledged {task_id=} and created {response.post_id=}.") + # here we would store the post id in the database for the task + case _: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail="Invalid response type.", + ) + + return {} + + +@router.post("/interaction") +def post_interaction( + *, + db: Session = Depends(deps.get_db), + api_key: APIKey = Depends(deps.get_api_key), + interaction: protocol_schema.TextReplyToPost, +) -> Any: + """ + The frontend acknowledges a task. + """ + deps.api_auth(api_key, db, create=True) + + response = [] + match (interaction.type): + case "text_reply_to_post": + logger.info( + f"Frontend reports text reply to {interaction.post_id=} with {interaction.text=} by {interaction.user_id=}." + ) + # here we would store the text reply in the database + response.append( + protocol_schema.TaskDone( + reply_to_post_id=interaction.user_post_id, + addressed_users=[interaction.user_id], + ) + ) + case _: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail="Invalid response type.", + ) + + return response diff --git a/backend/app/schemas/protocol.py b/backend/app/schemas/protocol.py new file mode 100644 index 00000000..a63b9c11 --- /dev/null +++ b/backend/app/schemas/protocol.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +from typing import Literal, Optional +from uuid import UUID + +import pydantic +from pydantic import BaseModel + + +class TaskRequest(BaseModel): + """The frontend asks the backend for a task.""" + + type: Literal + user_id: Optional[str] = None + + +class GenericTaskRequest(TaskRequest): + type: Literal["generic"] = "generic" + + +class Task(BaseModel): + """A task is a unit of work that the backend gives to the frontend.""" + + id: UUID = pydantic.Field(default_factory=UUID) + type: Literal + addressed_users: Optional[list[str]] = None + + +class TaskResponse(BaseModel): + """A task response is a message from the frontend to acknowledge the given task.""" + + type: Literal + status: Literal["success", "failure"] + + +class PostCreatedTaskResponse(TaskResponse): + type: Literal["post_created"] = "post_created" + post_id: UUID + + +class SummarizeStoryTask(Task): + type: Literal["summarize_story"] = "summarize_story" + story: str + + +class TaskDone(Task): + type: Literal["task_done"] = "task_done" + reply_to_post_id: UUID + + +class Interaction(BaseModel): + """An interaction is a message from the frontend to the backend.""" + + type: Literal + user_id: str + + +class TextReplyToPost(Interaction): + """A user has replied to a post with text.""" + + type: Literal["text_reply_to_post"] = "text_reply_to_post" + post_id: UUID + user_post_id: UUID + text: str From 2b02ef2ddcab57c431035edd316051f2a15f87e6 Mon Sep 17 00:00:00 2001 From: Yannic Kilcher Date: Wed, 14 Dec 2022 23:51:42 +0100 Subject: [PATCH 10/12] docstring fix --- backend/app/api/v1/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/api/v1/tasks.py b/backend/app/api/v1/tasks.py index 494281d0..2a5397e5 100644 --- a/backend/app/api/v1/tasks.py +++ b/backend/app/api/v1/tasks.py @@ -80,7 +80,7 @@ def post_interaction( interaction: protocol_schema.TextReplyToPost, ) -> Any: """ - The frontend acknowledges a task. + The frontend reports an interaction. """ deps.api_auth(api_key, db, create=True) From 1f286196f97b6c940c204ee827c8eefe5d004c02 Mon Sep 17 00:00:00 2001 From: Yannic Kilcher Date: Wed, 14 Dec 2022 23:52:36 +0100 Subject: [PATCH 11/12] added gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0d20b648 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc From c0e9f037c673d0db8f0877bd4885cf016b36fc3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Thu, 15 Dec 2022 10:39:36 +0100 Subject: [PATCH 12/12] add config defaults for postgres dev docker image, support setting POSTGRES_PASSWORD --- .gitignore | 1 + backend/app/config.py | 20 ++++++++++++++++++-- backend/app/schemas/protocol.py | 8 ++++---- backend/scripts/run-local.sh | 2 -- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 0d20b648..eb3a37f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +.venv *.pyc diff --git a/backend/app/config.py b/backend/app/config.py index 85cd8631..f0218bee 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# touch -from typing import List, Optional, Union +from typing import Any, Dict, List, Optional, Union from pydantic import AnyHttpUrl, BaseSettings, PostgresDsn, validator @@ -8,8 +7,25 @@ from pydantic import AnyHttpUrl, BaseSettings, PostgresDsn, validator class Settings(BaseSettings): PROJECT_NAME: str = "open-chatGPT backend" API_V1_STR: str = "/api/v1" + + POSTGRES_SERVER: str = "localhost:5432" + POSTGRES_USER: str = "postgres" + POSTGRES_PASSWORD: str = "postgres" + POSTGRES_DB: str = "postgres" DATABASE_URI: Optional[PostgresDsn] = None + @validator("DATABASE_URI", pre=True) + def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: + if isinstance(v, str): + return v + return PostgresDsn.build( + scheme="postgresql", + user=values.get("POSTGRES_USER"), + password=values.get("POSTGRES_PASSWORD"), + host=values.get("POSTGRES_SERVER"), + path=f"/{values.get('POSTGRES_DB') or ''}", + ) + BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] UPDATE_ALEMBIC: bool = True diff --git a/backend/app/schemas/protocol.py b/backend/app/schemas/protocol.py index a63b9c11..554c49e1 100644 --- a/backend/app/schemas/protocol.py +++ b/backend/app/schemas/protocol.py @@ -9,7 +9,7 @@ from pydantic import BaseModel class TaskRequest(BaseModel): """The frontend asks the backend for a task.""" - type: Literal + type: str user_id: Optional[str] = None @@ -21,14 +21,14 @@ class Task(BaseModel): """A task is a unit of work that the backend gives to the frontend.""" id: UUID = pydantic.Field(default_factory=UUID) - type: Literal + type: str addressed_users: Optional[list[str]] = None class TaskResponse(BaseModel): """A task response is a message from the frontend to acknowledge the given task.""" - type: Literal + type: str status: Literal["success", "failure"] @@ -50,7 +50,7 @@ class TaskDone(Task): class Interaction(BaseModel): """An interaction is a message from the frontend to the backend.""" - type: Literal + type: str user_id: str diff --git a/backend/scripts/run-local.sh b/backend/scripts/run-local.sh index 7c6efabc..d8bd86c6 100755 --- a/backend/scripts/run-local.sh +++ b/backend/scripts/run-local.sh @@ -1,5 +1,3 @@ #!/usr/bin/env bash -export DATABASE_URI=postgresql://postgres:postgres@localhost:5432/postgres - uvicorn app.main:app --reload