Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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 @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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()

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

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

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}```")

Expand Down