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 55b1f99a..bb035b6f 100644 --- a/modules/stampcollection.py +++ b/modules/stampcollection.py @@ -18,10 +18,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_RATE = 0.25 # Decay of votes + PRECISION = 8 # Decimal points of precision with stamp solving def __str__(self): return "Stamps Module" @@ -45,9 +49,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 stamps do nothing + or emoji not in vote_strengths_per_emoji): # votes with emojis other than stamps do nothing return - + vote_strength = vote_strengths_per_emoji[emoji] if negative: vote_strength *= -1 @@ -77,20 +81,49 @@ 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: - score = (self.gamma * 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 - - 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)) + if total_votes_by_user != 0 and toi != fromi: + score = votes_for_user / total_votes_by_user + users_matrix[toi, fromi] = score + + # self.log.debug(self.class_name, matrix=users_matrix) + + user_raw_stamps_vector = np.zeros(user_count) + user_raw_stamps_vector[0] = 1.0 # God has 1 karma + + scores = user_raw_stamps_vector + drains = self.utils.get_total_drains() + 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_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) + + # Check if solved + solved = np.all(old_scores.round(self.PRECISION) == scores.round(self.PRECISION)) + if solved: + # Double check work. + 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 # 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) + self.utils.log_error(alert) self.export_scores_csv() # self.print_all_scores() @@ -133,7 +166,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) @@ -238,7 +271,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", @@ -248,10 +281,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( @@ -279,6 +312,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() @@ -309,6 +353,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 5bdf2318..529e2c14 100644 --- a/servicemodules/discord.py +++ b/servicemodules/discord.py @@ -107,7 +107,7 @@ async def on_message(message: discord.message.Message) -> None: except Exception as e: why_traceback.append(f"There was a(n) {e} asking the {module} module!") log.error(class_name, error=f"Caught error in {module} module!") - 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 @@ -146,6 +146,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") + why_traceback.append(f"That response was a callback, so I called it.") # Callbacks can take a while to run, so we tell discord to say "Stampy is typing..." @@ -199,9 +200,10 @@ async def on_message(message: discord.message.Message) -> None: sys.stdout.flush() return except Exception as e: + log.error(e) why_traceback.append(f"There was a(n) {e} trying to send or callback the top response!") log.error(class_name, error=f"Caught 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 @@ -295,7 +297,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..20ceb83b 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 @@ -491,6 +492,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) @@ -546,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) + await self.log_error_async(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}```")