Skip to content

Commit

Permalink
Changed stamp karma to proper(ish) PageRank.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tayler6000 committed Feb 19, 2023
1 parent 652954f commit a99b72b
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 33 deletions.
9 changes: 3 additions & 6 deletions api/gooseai.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from utilities import Utilities
from structlog import get_logger
from typing import Any
import asyncio
import json
import requests

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"]:
Expand Down
20 changes: 8 additions & 12 deletions api/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"]:
Expand Down
85 changes: 74 additions & 11 deletions modules/stampcollection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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 [
Expand Down
8 changes: 4 additions & 4 deletions servicemodules/discord.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions utilities/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit a99b72b

Please sign in to comment.