From a99b72b8dce85e3e845adf59c74e6d16ce7b2d97 Mon Sep 17 00:00:00 2001 From: TJ Porter Date: Sat, 18 Feb 2023 19:11:37 -0600 Subject: [PATCH] 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)