Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changed stamp karma to proper(ish) PageRank. #213

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
90 changes: 72 additions & 18 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_RATE = 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 @@ -74,20 +78,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()
Expand Down Expand Up @@ -130,7 +163,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 +268,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 +278,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 +309,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 +350,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