From a99b72b8dce85e3e845adf59c74e6d16ce7b2d97 Mon Sep 17 00:00:00 2001 From: TJ Porter Date: Sat, 18 Feb 2023 19:11:37 -0600 Subject: [PATCH 1/6] Changed stamp karma to proper(ish) PageRank. Changed log_error and log_exception to not be async, made log_error_async and log_exception_async instead. Old names now get the event loop for you and await the async versions for you. Changed some logging messages. Added a recalculatestamps command which can be used for debugging, otherwise it's pretty much useless. Added a get_total_drains utility function to see how many drains there are in the PageRank calculation. --- api/gooseai.py | 9 ++-- api/openai.py | 20 ++++----- modules/stampcollection.py | 85 +++++++++++++++++++++++++++++++++----- servicemodules/discord.py | 8 ++-- utilities/utilities.py | 9 ++++ 5 files changed, 98 insertions(+), 33 deletions(-) diff --git a/api/gooseai.py b/api/gooseai.py index e1c19369..bcc8c946 100644 --- a/api/gooseai.py +++ b/api/gooseai.py @@ -3,7 +3,6 @@ from utilities import Utilities from structlog import get_logger from typing import Any -import asyncio import json import requests @@ -48,9 +47,8 @@ def get_engine(self) -> GooseAIEngines: return engine except Exception as e: log.error(self.class_name, _msg=f"Got error checking if {engine.name} is online.", e=e) - loop = asyncio.get_running_loop() - loop.create_task(utils.log_error(f"Got error checking if {engine.name} is online.")) - loop.create_task(utils.log_exception(e)) + utils.log_error(f"Got error checking if {engine.name} is online.") + utils.log_exception(e) log.critical(self.class_name, error="No engines for GooseAI are online!") def completion( @@ -85,8 +83,7 @@ def get_response(self, engine: GooseAIEngines, prompt: str, logit_bias: dict[int if "error" in response: error = response["error"] log.error(self.class_name, code=error["code"], error=error["message"], info=error["type"]) - loop = asyncio.get_running_loop() - loop.create_task(utils.log_error(f'GooseAI Error {error["code"]} ({error["type"]}): {error["message"]}')) + utils.log_error(f'GooseAI Error {error["code"]} ({error["type"]}): {error["message"]}') return "" if response["choices"]: diff --git a/api/openai.py b/api/openai.py index 94dd327d..e0d54bce 100644 --- a/api/openai.py +++ b/api/openai.py @@ -45,15 +45,13 @@ def cf_risk_level(self, prompt): ) except openai.error.AuthenticationError as e: self.log.error(self.class_name, error="OpenAI Authentication Failed") - loop = asyncio.get_running_loop() - loop.create_task(utils.log_error(f"OpenAI Authenication Failed")) - loop.create_task(utils.log_exception(e)) + utils.log_error(f"OpenAI Authenication Failed") + utils.log_exception(e) return 2 except openai.error.RateLimitError as e: self.log.warning(self.class_name, error="OpenAI Rate Limit Exceeded") - loop = asyncio.get_running_loop() - loop.create_task(utils.log_error(f"OpenAI Rate Limit Exceeded")) - loop.create_task(utils.log_exception(e)) + utils.log_error(f"OpenAI Rate Limit Exceeded") + utils.log_exception(e) return 2 output_label = response["choices"][0]["text"] @@ -134,15 +132,13 @@ def get_response(self, engine: OpenAIEngines, prompt: str, logit_bias: dict[int, ) except openai.error.AuthenticationError as e: self.log.error(self.class_name, error="OpenAI Authentication Failed") - loop = asyncio.get_running_loop() - loop.create_task(utils.log_error(f"OpenAI Authenication Failed")) - loop.create_task(utils.log_exception(e)) + utils.log_error(f"OpenAI Authenication Failed") + utils.log_exception(e) return "" except openai.error.RateLimitError as e: self.log.warning(self.class_name, error="OpenAI Rate Limit Exceeded") - loop = asyncio.get_running_loop() - loop.create_task(utils.log_error(f"OpenAI Rate Limit Exceeded")) - loop.create_task(utils.log_exception(e)) + utils.log_error(f"OpenAI Rate Limit Exceeded") + utils.log_exception(e) return "" if response["choices"]: diff --git a/modules/stampcollection.py b/modules/stampcollection.py index 9e720696..6b6925a7 100644 --- a/modules/stampcollection.py +++ b/modules/stampcollection.py @@ -15,10 +15,14 @@ "goldstamp": 5 } + class StampsModule(Module): STAMPS_RESET_MESSAGE = "full stamp history reset complete" UNAUTHORIZED_MESSAGE = "You can't do that!" + MAX_ROUNDS = 1000 # If we don't converge stamps after 1,000 rounds give up. + DECAY = 0.25 # Decay of votes + PRECISION = 8 # Decimal points of precision with stamp solving def __str__(self): return "Stamps Module" @@ -42,9 +46,9 @@ def update_vote(self, emoji: str, from_id: int, to_id: int, if (to_id == stampy_id # votes for stampy do nothing or to_id == from_id # votes for yourself do nothing - or emoji not in vote_strengths_per_emoji): # votes with emojis other than stamp and goldstamp do nothing + or emoji not in vote_strengths_per_emoji): # votes with emojis other than stamp and goldstamp do nothing return - + vote_strength = vote_strengths_per_emoji[emoji] if negative: vote_strength *= -1 @@ -78,16 +82,54 @@ def calculate_stamps(self): toi = self.utils.index[to_id] total_votes_by_user = self.utils.get_votes_by_user(from_id) if total_votes_by_user != 0: - score = (self.gamma * votes_for_user) / total_votes_by_user + score = votes_for_user / total_votes_by_user users_matrix[toi, from_id_index] = score - for i in range(1, user_count): - users_matrix[i, i] = -1.0 - users_matrix[0, 0] = 1.0 + for i in range(user_count): + users_matrix[i, i] = 0 + + # self.log.debug(self.class_name, matrix=users_matrix) user_count_matrix = np.zeros(user_count) user_count_matrix[0] = 1.0 # God has 1 karma - self.utils.scores = list(np.linalg.solve(users_matrix, user_count_matrix)) + scores = user_count_matrix + drains = self.utils.get_total_drains() + decay = 1 - self.DECAY + # self.log.debug(self.class_name, msg="There is" + (" not" if not drains else "") + " a drain!") + for i in range(self.MAX_ROUNDS): + old_scores = scores + scores = np.dot(users_matrix, scores) * decay + if drains: # If there are drains, we need to make sure stampy always has 1 trust. + scores[0] = 1 + # self.log.debug(self.class_name, step=scores) + + # Check if solved + solved = True + for a, b in zip(old_scores, scores): + if not round(a, self.PRECISION) == round(b, self.PRECISION): + solved = False + break + if solved: + # Double check work. + solved = False + for a in scores: + if round(a, self.PRECISION) != 0: + solved = True + break + if not solved and drains == 0: + self.log.warning( + self.class_name, + msg=f"After double checking (at {i+1} round(s)), turns out we have a stamp loop.", + ) + drains = 1 + continue + self.utils.scores = list(scores) + self.log.info(self.class_name, msg=f"Solved stamps in {i+1} round(s).") + break + if not solved: + alert = f"Took over {self.MAX_ROUNDS} rounds to solve for stamps!" + self.log.warning(self.class_name, alert=alert) + self.utils.log_error(alert) self.export_scores_csv() # self.print_all_scores() @@ -130,7 +172,7 @@ def print_all_scores(self): name = "<@" + str(user_id) + ">" stamps = self.get_user_stamps(user_id) total_stamps += stamps - self.log.info(self.class_name, name=name, stamps=stamps) + self.log.info(self.class_name, name=name, stamps=stamps, raw_stamps=stamps / self.total_votes) self.log.info(self.class_name, total_votes=self.total_votes) self.log.info(self.class_name, total_stamps=total_stamps) @@ -235,7 +277,7 @@ async def process_raw_reaction_event(self, event): ms_gid = event.message_id from_id = event.user_id to_id = author_id_int - + self.log.info( self.class_name, update="STAMP AWARDED", @@ -245,10 +287,10 @@ async def process_raw_reaction_event(self, event): reaction_message_author_id=to_id, reaction_message_author_name=message.author.name, ) - + # I believe this call was a duplicate and it should not be called twice # self.update_vote(emoji, from_id, to_id, False, False) - + stamps_before_update = self.get_user_stamps(to_id) self.update_vote(emoji, from_id, to_id, negative=(event_type == "REACTION_REMOVE")) self.log.info( @@ -276,6 +318,17 @@ def process_message(self, message): return Response(confidence=10, callback=self.reloadallstamps, args=[message]) else: return Response(confidence=10, text=self.UNAUTHORIZED_MESSAGE, args=[message]) + elif text == "recalculatestamps": + if message.service == Services.DISCORD: + asked_by_admin = discord.utils.get(message.author.roles, name="bot admin") + if asked_by_admin: + return Response( + confidence=10, + callback=self.recalculate_stamps, + args=[message], + ) + else: + return Response(confidence=10, text=self.UNAUTHORIZED_MESSAGE, args=[message]) return Response() @@ -306,6 +359,16 @@ async def reloadallstamps(self, message): confidence=10, text=self.STAMPS_RESET_MESSAGE, why="robertskmiles reset the stamp history", ) + async def recalculate_stamps(self, message): + self.log.info(self.class_name, ALERT="Recalculating Stamps") + await message.channel.send("Recalculating stamps...") + self.calculate_stamps() + return Response( + confidence=10, + text="Done!", + why="I was asked to recalculate stamps", + ) + @property def test_cases(self): return [ diff --git a/servicemodules/discord.py b/servicemodules/discord.py index f27a58cd..cdd62481 100644 --- a/servicemodules/discord.py +++ b/servicemodules/discord.py @@ -102,7 +102,7 @@ async def on_message(message: discord.message.Message) -> None: try: response = module.process_message(message) except Exception as e: - await self.utils.log_exception(e) + await self.utils.log_exception_async(e) if response: response.module = module # tag it with the module it came from, for future reference @@ -139,7 +139,7 @@ async def on_message(message: discord.message.Message) -> None: try: if top_response.callback: log.info(class_name, msg="Top response is a callback. Calling it") - + # Callbacks can take a while to run, so we tell discord to say "Stampy is typing..." # Note that sometimes a callback will run but not send a message, in which case he'll seem to be typing but not say anything. I think this will be rare though. async with message.channel._channel.typing(): @@ -180,7 +180,7 @@ async def on_message(message: discord.message.Message) -> None: return except Exception as e: log.error(e) - await self.utils.log_exception(e) + await self.utils.log_exception_async(e) # if we ever get here, we've gone maximum_recursion_depth layers deep without the top response being text # so that's likely an infinite regress @@ -271,7 +271,7 @@ async def on_raw_reaction_add(payload: discord.raw_models.RawReactionActionEvent try: await module.process_raw_reaction_event(payload) except Exception as e: - await self.utils.log_exception(e) + await self.utils.log_exception_async(e) @self.utils.client.event async def on_raw_reaction_remove(payload: discord.raw_models.RawReactionActionEvent) -> None: diff --git a/utilities/utilities.py b/utilities/utilities.py index 46b5bc17..3801c28e 100644 --- a/utilities/utilities.py +++ b/utilities/utilities.py @@ -491,6 +491,15 @@ def get_all_user_votes(self): query = "SELECT user,votedFor,votecount from uservotes;" return self.db.query(query) + def get_total_drains(self) -> int: + query = ( + "SELECT count(*) as drains " + "FROM (SELECT `votedFor` as `user`, sum(`votecount`) as votes_received FROM `uservotes`" + "GROUP BY `votedFor`) AS B LEFT JOIN (SELECT `user`, sum(`votecount`) as votes_made FROM " + "`uservotes` GROUP BY `user`) AS A USING(`user`) WHERE `votes_made` is NULL;" + ) + return self.db.query(query)[0][0] + def get_users(self): query = "SELECT user from (SELECT user FROM uservotes UNION SELECT votedFor as user FROM uservotes)" result = self.db.query(query) From 4b08bb62ebb15703df57fe3b0c8033475f8ecab2 Mon Sep 17 00:00:00 2001 From: TJ Porter Date: Sun, 19 Feb 2023 08:00:58 -0600 Subject: [PATCH 2/6] Update modules/stampcollection.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Peter Hozák --- modules/stampcollection.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/stampcollection.py b/modules/stampcollection.py index 6b6925a7..0bec53e7 100644 --- a/modules/stampcollection.py +++ b/modules/stampcollection.py @@ -104,11 +104,7 @@ def calculate_stamps(self): # self.log.debug(self.class_name, step=scores) # Check if solved - solved = True - for a, b in zip(old_scores, scores): - if not round(a, self.PRECISION) == round(b, self.PRECISION): - solved = False - break + solved = np.all(old_scores.round(self.PRECISION) == scores.round(self.PRECISION)) if solved: # Double check work. solved = False From 489f9245d67d4535047e3ab9aabdccd2e9a3c6ab Mon Sep 17 00:00:00 2001 From: TJ Porter Date: Sun, 19 Feb 2023 10:58:31 -0600 Subject: [PATCH 3/6] Simplified double-check on stamps calc. --- modules/stampcollection.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/modules/stampcollection.py b/modules/stampcollection.py index 0bec53e7..fba5066a 100644 --- a/modules/stampcollection.py +++ b/modules/stampcollection.py @@ -107,21 +107,18 @@ def calculate_stamps(self): solved = np.all(old_scores.round(self.PRECISION) == scores.round(self.PRECISION)) if solved: # Double check work. - solved = False - for a in scores: - if round(a, self.PRECISION) != 0: - solved = True - break + solved = np.any(scores.round(self.PRECISION) != 0) if not solved and drains == 0: self.log.warning( self.class_name, msg=f"After double checking (at {i+1} round(s)), turns out we have a stamp loop.", ) drains = 1 - continue + continue # Re-solve self.utils.scores = list(scores) self.log.info(self.class_name, msg=f"Solved stamps in {i+1} round(s).") break + if not solved: alert = f"Took over {self.MAX_ROUNDS} rounds to solve for stamps!" self.log.warning(self.class_name, alert=alert) From f130cf7d5b979fa65069db2423df8b84938edb33 Mon Sep 17 00:00:00 2001 From: TJ Porter Date: Sun, 19 Feb 2023 11:25:59 -0600 Subject: [PATCH 4/6] Did some refactoring. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Peter Hozák --- modules/stampcollection.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/modules/stampcollection.py b/modules/stampcollection.py index fba5066a..2fb8116b 100644 --- a/modules/stampcollection.py +++ b/modules/stampcollection.py @@ -21,7 +21,7 @@ class StampsModule(Module): STAMPS_RESET_MESSAGE = "full stamp history reset complete" UNAUTHORIZED_MESSAGE = "You can't do that!" MAX_ROUNDS = 1000 # If we don't converge stamps after 1,000 rounds give up. - DECAY = 0.25 # Decay of votes + DECAY_RATE = 0.25 # Decay of votes PRECISION = 8 # Decimal points of precision with stamp solving def __str__(self): @@ -78,27 +78,25 @@ def calculate_stamps(self): # self.log.debug(self.class_name, votes=votes) for from_id, to_id, votes_for_user in votes: - from_id_index = self.utils.index[from_id] + fromi = self.utils.index[from_id] toi = self.utils.index[to_id] total_votes_by_user = self.utils.get_votes_by_user(from_id) - if total_votes_by_user != 0: + if total_votes_by_user != 0 and toi != fromi: score = votes_for_user / total_votes_by_user - users_matrix[toi, from_id_index] = score - for i in range(user_count): - users_matrix[i, i] = 0 + users_matrix[toi, fromi] = score # self.log.debug(self.class_name, matrix=users_matrix) - user_count_matrix = np.zeros(user_count) - user_count_matrix[0] = 1.0 # God has 1 karma + user_raw_stamps_vector = np.zeros(user_count) + user_raw_stamps_vector[0] = 1.0 # God has 1 karma - scores = user_count_matrix + scores = user_raw_stamps_vector drains = self.utils.get_total_drains() - decay = 1 - self.DECAY + decay_factor = 1 - self.DECAY_RATE # self.log.debug(self.class_name, msg="There is" + (" not" if not drains else "") + " a drain!") for i in range(self.MAX_ROUNDS): old_scores = scores - scores = np.dot(users_matrix, scores) * decay + scores = np.dot(users_matrix, scores) * decay_factor if drains: # If there are drains, we need to make sure stampy always has 1 trust. scores[0] = 1 # self.log.debug(self.class_name, step=scores) From 9eb4ee9b55d91e220cba7a22024cc822265c4256 Mon Sep 17 00:00:00 2001 From: Tayler Porter Date: Sun, 26 Feb 2023 14:33:25 -0600 Subject: [PATCH 5/6] Adding the log_error and log_exception changes that somehow got lost. --- utilities/utilities.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/utilities/utilities.py b/utilities/utilities.py index 3801c28e..7b2c1ac3 100644 --- a/utilities/utilities.py +++ b/utilities/utilities.py @@ -23,6 +23,7 @@ from utilities.discordutils import DiscordMessage, DiscordUser from utilities.serviceutils import ServiceMessage from typing import List +import asyncio import discord import json import os @@ -555,16 +556,41 @@ def get_time_running(self): message += " and " + str(time_running.second) + " seconds." return message - async def log_exception(self, e: Exception) -> None: + def log_exception(self, e: Exception) -> None: + loop = None + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.get_event_loop() + if not loop: + log.error(self.class_name, error="Cannot get event loop to send this exception!", e=e) + return + loop.create_task(self.log_exception_async(e)) + + def log_error(self, error_message: str) -> None: + loop = None + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.get_event_loop() + if not loop: + log.error(self.class_name, error="Cannot get event loop to send this error!", e=error_message) + return + loop.create_task(self.log_error_async(error_message)) + + async def log_exception_async(self, e: Exception) -> None: parts = ["Traceback (most recent call last):\n"] parts.extend(traceback.format_stack(limit=25)[:-2]) parts.extend(traceback.format_exception(*sys.exc_info())[1:]) error_message = "".join(parts) await self.log_error(error_message) - async def log_error(self, error_message: str) -> None: + async def log_error_async(self, error_message: str) -> None: if self.error_channel is None: self.error_channel = self.client.get_channel(int(stampy_error_log_channel_id)) + if self.error_channel is None: + log.warning(self.class_name, warning="Cannot send this error as stampy cannot find the error channel!", e=error_message) + return for msg_chunk in Utilities.split_message_for_discord(error_message, max_length=discord_message_length_limit-6): await self.error_channel.send(f"```{msg_chunk}```") From 8cb079ea7c53f9676b7fc5d66fe778263a43af12 Mon Sep 17 00:00:00 2001 From: Tayler Porter Date: Sun, 26 Feb 2023 17:10:45 -0600 Subject: [PATCH 6/6] Fixing error logging. --- utilities/utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilities/utilities.py b/utilities/utilities.py index 7b2c1ac3..20ceb83b 100644 --- a/utilities/utilities.py +++ b/utilities/utilities.py @@ -583,7 +583,7 @@ async def log_exception_async(self, e: Exception) -> None: parts.extend(traceback.format_stack(limit=25)[:-2]) parts.extend(traceback.format_exception(*sys.exc_info())[1:]) error_message = "".join(parts) - await self.log_error(error_message) + await self.log_error_async(error_message) async def log_error_async(self, error_message: str) -> None: if self.error_channel is None: