From 1d04160d0a84118fec1b25c81f4f4211fb032a05 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Wed, 22 Nov 2023 20:53:09 +0100 Subject: [PATCH 001/145] Attempting to fix application timeout so its pr user --- recruitmentCog/recruitment.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/recruitmentCog/recruitment.py b/recruitmentCog/recruitment.py index b2eae2f..59eabfc 100644 --- a/recruitmentCog/recruitment.py +++ b/recruitmentCog/recruitment.py @@ -28,15 +28,28 @@ class Recruitment(commands.Cog): default_guild = {"guild_id": 274657393936302080, "application_channel_id": None} self.config.register_guild(**default_guild) self.antispam = {} + self.cog_check_enabled = True # Attribute to track the state of cog_check async def cog_check(self, ctx: commands.Context): if await ctx.bot.is_admin(ctx.author): return True + + if not self.cog_check_enabled: + return True # If disabled, always return True to allow all commands guild_id = ctx.guild.id + author_id = ctx.author.id # Get the ID of the user who invoked the command + + # Check if the guild has an antispam entry, if not, create one if guild_id not in self.antispam: - self.antispam[guild_id] = AntiSpam([(datetime.timedelta(hours=1), 1)]) - antispam = self.antispam[guild_id] + self.antispam[guild_id] = {} + + # Check if the user has an antispam entry in this guild, if not, create one + if author_id not in self.antispam[guild_id]: + self.antispam[guild_id][author_id] = AntiSpam([(datetime.timedelta(hours=1), 1)]) + + # Get the antispam object for this specific user in this guild + antispam = self.antispam[guild_id][author_id] if antispam.spammy: try: @@ -49,6 +62,13 @@ class Recruitment(commands.Cog): antispam.stamp() return True + @commands.command(name="togglecogcheck") + @checks.is_owner() # Or use any other appropriate check + async def toggle_cog_check(self, ctx: commands.Context): + """Toggle the cog_check functionality on or off.""" + self.cog_check_enabled = not self.cog_check_enabled + status = "enabled" if self.cog_check_enabled else "disabled" + await ctx.send(f"Cog check has been {status}.") @commands.guild_only() @checks.admin_or_permissions(manage_guild=True) -- 2.47.2 From 7d6b9c9403870d73d9a49adf1effd184d5ebb38b Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Wed, 10 Apr 2024 13:54:59 +0200 Subject: [PATCH 002/145] Trying a new prompt for Reginald --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 643e23a..506dd7d 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -104,7 +104,7 @@ class ReginaldCog(commands.Cog): presence_penalty= 0.5, frequency_penalty= 0.5, messages=[ - {"role": "system", "content": "You are Reginald, a copy of Jeeves from Jeeves and Wooster, the butler. You must respond with the same speech patterns as Jeeves. Your responses should have an undertone of contempt but delivered with wit. You are employed as a servant at an estate called The Kanium Estate. The Kanium Estate is a vast estate with multiple houses. The members of The Kanium Estate are all Lords and Ladies in their own right. Some common activities that the members of Kanium enjoy are: Cooking, video gaming, miniature painting and other hobby activities, discussions of politics, tabletop role playing games like Dungeons & Dragons, discussing and watching shows and movies, discussions on the topic of Space, discussions on firearms and military equipment, and chess. You are now talking to a member of The Kanium Estate."}, + {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."}, {"role": "user", "content": prompt} ] ) -- 2.47.2 From e423665cc23c96ea13ca7eb3176c9e0e04c1edf1 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Wed, 10 Apr 2024 16:26:05 +0200 Subject: [PATCH 003/145] Prototyping --- trafficCog/__init__.py | 5 +- trafficCog/trafficCog.py | 188 +++++++++++++++++---------------------- 2 files changed, 84 insertions(+), 109 deletions(-) diff --git a/trafficCog/__init__.py b/trafficCog/__init__.py index 6426d01..8f04689 100644 --- a/trafficCog/__init__.py +++ b/trafficCog/__init__.py @@ -1,6 +1,5 @@ from .trafficCog import TrafficCog -from redbot.core.bot import Red -async def setup(bot: Red): +def setup(bot): cog = TrafficCog(bot) - await bot.add_cog(cog) \ No newline at end of file + bot.add_cog(cog) \ No newline at end of file diff --git a/trafficCog/trafficCog.py b/trafficCog/trafficCog.py index 07716a2..0c215d7 100644 --- a/trafficCog/trafficCog.py +++ b/trafficCog/trafficCog.py @@ -1,119 +1,95 @@ import discord -from datetime import datetime - -from redbot.core import Config, commands - -allowed_guilds = {274657393936302080, 693796372092289024, 508781789737648138} -admin_roles = {'Developer', 'admin', 'Council'} -statsThumbnailUrl = 'https://www.kanium.org/machineroom/logomachine-small.png' +from discord.ext import commands +from datetime import datetime, timedelta +import pytz class TrafficCog(commands.Cog): - def __init__(self, bot): - self.channel: discord.TextChannel = None - self.dailyJoinedCount: int = 0 - self.totalJoinedCount: int = 0 - self.dailyLeftCount: int = 0 - self.totalLeftCount: int = 0 - self.totalLogs: int = 0 - self.toggleLogs: bool = True - self.date = datetime.now() + self.bot = bot + self.config = commands.Config.get_conf(self, identifier=123456789) + default_guild = { + "traffic_channel": None, + "daily_stats": {"joined": 0, "left": 0, "banned": 0}, + "total_stats": {"joined": 0, "left": 0, "banned": 0}, + "last_reset": datetime.now(pytz.UTC).isoformat(), + "admin_roles": ['Developer', 'admin', 'Council'], + "stats_thumbnail_url": 'https://example.com/default-thumbnail.png', + } + self.config.register_guild(**default_guild) - def __checkClock(self): - currdate = self.date - datetime.now() - if currdate.days >= 0 : - self.dailyJoinedCount = 0 - self.dailyLeftCount = 0 - self.date = datetime.now() + async def __check_reset(self, guild_id): + async with self.config.guild_from_id(guild_id).all() as guild_data: + last_reset = datetime.fromisoformat(guild_data['last_reset']) + if last_reset.date() < datetime.now(pytz.UTC).date(): + guild_data['daily_stats'] = {"joined": 0, "left": 0, "banned": 0} + guild_data['last_reset'] = datetime.now(pytz.UTC).isoformat() - @commands.command(name='settrafficchannel', description='Sets the channel to sends log to') - @commands.has_any_role(*admin_roles) - async def setTrafficChannel(self, ctx: commands.Context, channel: discord.TextChannel) -> None: - await ctx.trigger_typing() + async def __update_stat(self, guild_id, stat_type): + async with self.config.guild_from_id(guild_id).all() as guild_data: + guild_data['daily_stats'][stat_type] += 1 + guild_data['total_stats'][stat_type] += 1 - if not channel in ctx.guild.channels: - await ctx.send('Channel doesnt exist in guild') + @commands.group(name='traffic', invoke_without_command=True) + @commands.has_permissions(administrator=True) + async def traffic_commands(self, ctx): + """Base command for managing TrafficCog settings.""" + await ctx.send_help(ctx.command) + + @traffic_commands.command(name='setchannel') + async def set_traffic_channel(self, ctx, channel: discord.TextChannel): + """Sets the channel for traffic logs.""" + await self.config.guild(ctx.guild).traffic_channel.set(channel.id) + await ctx.send(f"Traffic logs will now be sent to {channel.mention}.") + + @traffic_commands.command(name='stats') + async def show_stats(self, ctx): + """Displays current traffic statistics.""" + await self.__check_reset(ctx.guild.id) + guild_data = await self.config.guild(ctx.guild).all() + daily_stats = guild_data['daily_stats'] + total_stats = guild_data['total_stats'] + + embed = discord.Embed(title="Server Traffic Stats", description="Statistics on server activity", color=0x3399ff) + embed.set_thumbnail(url=guild_data['stats_thumbnail_url']) + embed.add_field(name="Daily Joined", value=daily_stats['joined'], inline=True) + embed.add_field(name="Daily Left", value=daily_stats['left'], inline=True) + embed.add_field(name="Daily Banned", value=daily_stats['banned'], inline=True) + embed.add_field(name="Total Joined", value=total_stats['joined'], inline=True) + embed.add_field(name="Total Left", value=total_stats['left'], inline=True) + embed.add_field(name="Total Banned", value=total_stats['banned'], inline=True) + await ctx.send(embed=embed) + + @commands.Cog.listener() + async def on_member_join(self, member): + if member.guild.id not in self.config.all_guilds(): return - - if not channel.permissions_for(ctx.guild.me).send_messages: - await ctx.send('No permissions to talk in that channel.') - return - - self.channel = channel - - await ctx.send(f'I will now send event notices to {channel.mention}.') - - @commands.command(name='stats', description='Shows current statistics') - @commands.has_any_role(*admin_roles) - async def statistics(self, ctx: commands.Context) -> None: - self.__checkClock() - await ctx.trigger_typing() - message = discord.Embed(title='Server Traffic Stats', description='Statistics on server activity\n\n',color=0x3399ff) - message.set_thumbnail(url=statsThumbnailUrl) - message.add_field(name='Daily Joined', value=self.dailyJoinedCount, inline='True') - message.add_field(name='Daily Left', value='{0}\n'.format(self.dailyLeftCount), inline='True') - message.add_field(name='Total Traffic', value=self.totalLogs, inline='False') - message.add_field(name='Total Joined', value=self.totalJoinedCount, inline='True') - message.add_field(name='Total Left', value=self.totalLeftCount, inline='True') - await ctx.send(content=None, embed=message) - - @commands.command(name='resetstats', description='Resets statistics') - @commands.has_any_role(*admin_roles) - async def resetStatistics(self, ctx: commands.Context) -> None: - await ctx.trigger_typing() - - self.dailyJoinedCount = 0 - self.dailyLeftCount = 0 - self.totalJoinedCount = 0 - self.totalLeftCount = 0 - self.totalLogs = 0 - - await ctx.send('Successfully reset the statistics') - - @commands.command(name='toggleLogs', description='Toggles the logs functionality on or off') - @commands.has_any_role(*admin_roles) - async def toggleLogs(self, ctx: commands.Context) -> None: - await ctx.trigger_typing() - self.toggleLogs = not self.toggleLogs - await ctx.send('Logging functionality is `ON`' if self.toggleLogs else 'Logging functionality is `OFF`') + await self.__check_reset(member.guild.id) + await self.__update_stat(member.guild.id, 'joined') + channel_id = await self.config.guild(member.guild).traffic_channel() + if channel_id: + channel = member.guild.get_channel(channel_id) + if channel: + await channel.send(f"{member.display_name} has joined the server.") @commands.Cog.listener() - async def on_member_join(self, member: discord.Member) -> None: - try: - if member.guild.id not in allowed_guilds: - return - self.__checkClock() - if self.channel in member.guild.channels and self.toggleLogs: - await self.channel.send('{0} has joined the server'.format(member.name)) - self.totalJoinedCount += 1 - self.dailyJoinedCount += 1 - self.totalLogs += 1 - except (discord.NotFound, discord.Forbidden): - print( - f'Error Occured!') + async def on_member_remove(self, member): + await self.__check_reset(member.guild.id) + await self.__update_stat(member.guild.id, 'left') + channel_id = await self.config.guild(member.guild).traffic_channel() + if channel_id: + channel = member.guild.get_channel(channel_id) + if channel: + await channel.send(f"{member.display_name} has left the server.") @commands.Cog.listener() - async def on_member_remove(self, member: discord.Member) -> None: - try: - self.__checkClock() - if self.channel in member.guild.channels and self.toggleLogs: - await self.channel.send('{0} has left the server'.format(member.name)) - self.totalLeftCount += 1 - self.dailyLeftCount += 1 - self.totalLogs += 1 - except (discord.NotFound, discord.Forbidden): - print( - f'Error Occured!') + async def on_member_ban(self, guild, member): + await self.__check_reset(guild.id) + await self.__update_stat(guild.id, 'banned') + channel_id = await self.config.guild(guild).traffic_channel() + if channel_id: + channel = guild.get_channel(channel_id) + if channel: + await channel.send(f"{member.display_name} has been banned from the server.") - @commands.Cog.listener() - async def on_member_ban(self, guild: discord.Guild, member: discord.Member) -> None: - try: - self.__checkClock() - if self.channel in member.guild.channels and self.toggleLogs: - await self.channel.send('{0} has been banned from the server'.format(member.name)) - self.totalLeftCount += 1 - self.dailyLeftCount += 1 - self.totalLogs += 1 - except (discord.NotFound, discord.Forbidden): - print( - f'Error Occured!') +def setup(bot): + bot.add_cog(TrafficCog(bot)) -- 2.47.2 From 0c8b77d553ad4bd8543d367d054fb303251472c9 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Wed, 10 Apr 2024 16:31:19 +0200 Subject: [PATCH 004/145] Fixed imports --- trafficCog/trafficCog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/trafficCog/trafficCog.py b/trafficCog/trafficCog.py index 0c215d7..aafdb6d 100644 --- a/trafficCog/trafficCog.py +++ b/trafficCog/trafficCog.py @@ -1,4 +1,5 @@ import discord +from redbot.core import Config, commands # Import Config from redbot.core from discord.ext import commands from datetime import datetime, timedelta import pytz -- 2.47.2 From 235d8790b4c5037828105e12cf20b06ea80dbe5f Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Wed, 10 Apr 2024 16:34:50 +0200 Subject: [PATCH 005/145] No really, fixing my imports --- trafficCog/trafficCog.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/trafficCog/trafficCog.py b/trafficCog/trafficCog.py index aafdb6d..b9d3a52 100644 --- a/trafficCog/trafficCog.py +++ b/trafficCog/trafficCog.py @@ -1,6 +1,5 @@ import discord -from redbot.core import Config, commands # Import Config from redbot.core -from discord.ext import commands +from redbot.core import Config, commands # This line imports commands as well from datetime import datetime, timedelta import pytz -- 2.47.2 From 7e4de0494999b7db6eeaf9ae5aaf46a7b6056c43 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Wed, 10 Apr 2024 16:39:29 +0200 Subject: [PATCH 006/145] I definitely didn't forget an import somewhere else --- trafficCog/__init__.py | 3 ++- trafficCog/trafficCog.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/trafficCog/__init__.py b/trafficCog/__init__.py index 8f04689..27b696d 100644 --- a/trafficCog/__init__.py +++ b/trafficCog/__init__.py @@ -1,5 +1,6 @@ +from redbot.core.bot import Red from .trafficCog import TrafficCog def setup(bot): cog = TrafficCog(bot) - bot.add_cog(cog) \ No newline at end of file + bot.add_cog(cog) diff --git a/trafficCog/trafficCog.py b/trafficCog/trafficCog.py index b9d3a52..7b0ad36 100644 --- a/trafficCog/trafficCog.py +++ b/trafficCog/trafficCog.py @@ -6,7 +6,7 @@ import pytz class TrafficCog(commands.Cog): def __init__(self, bot): self.bot = bot - self.config = commands.Config.get_conf(self, identifier=123456789) + self.config = Config.get_conf(self, identifier=123456789, force_registration=True) default_guild = { "traffic_channel": None, "daily_stats": {"joined": 0, "left": 0, "banned": 0}, -- 2.47.2 From 4f84c3b89cf8a18a666f224b215d1d3295ac4ee4 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Wed, 10 Apr 2024 16:50:32 +0200 Subject: [PATCH 007/145] I guess we are going asynchronous --- trafficCog/__init__.py | 4 ++-- trafficCog/trafficCog.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/trafficCog/__init__.py b/trafficCog/__init__.py index 27b696d..d1af273 100644 --- a/trafficCog/__init__.py +++ b/trafficCog/__init__.py @@ -1,6 +1,6 @@ from redbot.core.bot import Red from .trafficCog import TrafficCog -def setup(bot): +async def setup(bot: Red): cog = TrafficCog(bot) - bot.add_cog(cog) + await bot.add_cog(cog) \ No newline at end of file diff --git a/trafficCog/trafficCog.py b/trafficCog/trafficCog.py index 7b0ad36..29a3eb6 100644 --- a/trafficCog/trafficCog.py +++ b/trafficCog/trafficCog.py @@ -92,4 +92,5 @@ class TrafficCog(commands.Cog): await channel.send(f"{member.display_name} has been banned from the server.") def setup(bot): - bot.add_cog(TrafficCog(bot)) + cog = TrafficCog(bot) + bot.add_cog(cog) \ No newline at end of file -- 2.47.2 From 5c9113a27eb8e3f2d7dc9fdbe3623c4cc166f5e0 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 30 May 2024 19:50:25 +0200 Subject: [PATCH 008/145] Trying to use ChatGPT to fix our memory issue, crossing fingers --- reginaldCog/reginald.py | 90 ++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 506dd7d..63255b4 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -1,55 +1,33 @@ import discord -import json import openai -import os -import random -import requests -import base64 -import aiohttp -from io import BytesIO -from PIL import Image -import tempfile -from openai import OpenAIError from redbot.core import Config, commands - class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) - default_global = { - "openai_model": "gpt-3.5-turbo" - } - default_guild = { - "openai_api_key": None - } + default_global = {"openai_model": "gpt-3.5-turbo"} + default_guild = {"openai_api_key": None, "memory": {}, "shared_memory": []} self.config.register_global(**default_global) self.config.register_guild(**default_guild) async def is_admin(self, ctx): admin_role = await self.config.guild(ctx.guild).admin_role() - if admin_role: - return discord.utils.get(ctx.author.roles, name=admin_role) is not None - return ctx.author.guild_permissions.administrator + return discord.utils.get(ctx.author.roles, name=admin_role) is not None or ctx.author.guild_permissions.administrator async def is_allowed(self, ctx): allowed_role = await self.config.guild(ctx.guild).allowed_role() - if allowed_role: - return discord.utils.get(ctx.author.roles, name=allowed_role) is not None - return False - + return discord.utils.get(ctx.author.roles, name=allowed_role) is not None + @commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): - """Allows a role to use the Reginald command""" - await self.config.guild(ctx.guild).allowed_role.set(role.name) - await ctx.send(f"The {role.name} role is now allowed to use the Reginald command.") + await self.config.guild(ctx.guild).allowed_role.set(role.name) + await ctx.send(f"The {role.name} role is now allowed to use the Reginald command.") - @commands.command(name="reginald_disallowrole", help="Remove a role's ability to use the Reginald command") @commands.has_permissions(administrator=True) async def disallow_role(self, ctx): - """Revokes a role's permission to use the Reginald command""" await self.config.guild(ctx.guild).allowed_role.clear() await ctx.send(f"The role's permission to use the Reginald command has been revoked.") @@ -62,10 +40,11 @@ class ReginaldCog(commands.Cog): @commands.guild_only() @commands.command(help="Ask Reginald a question") - @commands.cooldown(1, 10, commands.BucketType.user) # 10 second cooldown per user + @commands.cooldown(1, 10, commands.BucketType.user) async def reginald(self, ctx, *, prompt=None): if not await self.is_admin(ctx) and not await self.is_allowed(ctx): raise commands.CheckFailure("You do not have the required role to use this command.") + greetings = [ "Greetings! How may I be of assistance to you?", "Yes? How may I help?", @@ -83,30 +62,40 @@ class ReginaldCog(commands.Cog): return try: - response_text = await self.generate_response(api_key, prompt) + user_id = str(ctx.author.id) + memory = await self.config.guild(ctx.guild).memory() + shared_memory = await self.config.guild(ctx.guild).shared_memory() + + if user_id not in memory: + memory[user_id] = [] + + memory[user_id].append({"role": "user", "content": prompt}) + response_text = await self.generate_response(api_key, memory[user_id] + shared_memory) + memory[user_id].append({"role": "assistant", "content": response_text}) + await self.config.guild(ctx.guild).memory.set(memory) + + # Optionally, add to shared memory if relevant + shared_memory.append({"role": "user", "content": prompt}) + shared_memory.append({"role": "assistant", "content": response_text}) + await self.config.guild(ctx.guild).shared_memory.set(shared_memory) + for chunk in self.split_response(response_text, 2000): await ctx.send(chunk) except OpenAIError as e: await ctx.send(f"I apologize, but I am unable to generate a response at this time. Error message: {str(e)}") - except commands.CommandOnCooldown as e: - remaining_seconds = int(e.retry_after) - await ctx.author.send(f'Please wait {remaining_seconds} seconds before using the "reginald" command again.') - async def generate_response(self, api_key, prompt): + async def generate_response(self, api_key, messages): model = await self.config.openai_model() openai.api_key = api_key response = openai.ChatCompletion.create( - model= model, - max_tokens= 512, - n= 1, - stop= None, - temperature= 0.7, - presence_penalty= 0.5, - frequency_penalty= 0.5, - messages=[ - {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."}, - {"role": "user", "content": prompt} - ] + model=model, + max_tokens=512, + n=1, + stop=None, + temperature=0.7, + presence_penalty=0.5, + frequency_penalty=0.5, + messages=messages ) return response['choices'][0]['message']['content'].strip() @@ -115,9 +104,9 @@ class ReginaldCog(commands.Cog): chunks = [] while len(response_text) > max_chars: split_index = response_text[:max_chars].rfind(' ') - chunk = response_text[:split_index] - chunks.append(chunk) - response_text = response_text[split_index:].strip() + chunk = response_text[:max_chars] if split_index == -1 else response_text[:split_index] + chunks.append(chunk.strip()) + response_text = response_text[split_index:].strip() if split_index != -1 else response_text[max_chars:] chunks.append(response_text) return chunks @@ -131,5 +120,4 @@ class ReginaldCog(commands.Cog): await ctx.author.send(f"An unexpected error occurred: {error}") def setup(bot): - cog = ReginaldCog(bot) - bot.add_cog(cog) \ No newline at end of file + bot.add_cog(ReginaldCog(bot)) -- 2.47.2 From 63fa40589fe66b70dc6e872831e402f31fc19ef3 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 30 May 2024 19:57:23 +0200 Subject: [PATCH 009/145] Re-added the prompt --- reginaldCog/reginald.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 63255b4..0772c9a 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -95,7 +95,10 @@ class ReginaldCog(commands.Cog): temperature=0.7, presence_penalty=0.5, frequency_penalty=0.5, - messages=messages + messages=[ + {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."}, + *messages + ] ) return response['choices'][0]['message']['content'].strip() -- 2.47.2 From 73468b13a548c2eb73dc63e94c8597c3391b5fd6 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 30 May 2024 20:09:46 +0200 Subject: [PATCH 010/145] Undoing the weirdness of the system prompt --- reginaldCog/reginald.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 0772c9a..7a60f5d 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -69,8 +69,17 @@ class ReginaldCog(commands.Cog): if user_id not in memory: memory[user_id] = [] + # Add user prompt to memory memory[user_id].append({"role": "user", "content": prompt}) - response_text = await self.generate_response(api_key, memory[user_id] + shared_memory) + + # Prepare messages for the API call + messages = [{"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."}] + messages += memory[user_id] + shared_memory + + # Generate response from OpenAI API + response_text = await self.generate_response(api_key, messages) + + # Add response to memory memory[user_id].append({"role": "assistant", "content": response_text}) await self.config.guild(ctx.guild).memory.set(memory) @@ -95,10 +104,7 @@ class ReginaldCog(commands.Cog): temperature=0.7, presence_penalty=0.5, frequency_penalty=0.5, - messages=[ - {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."}, - *messages - ] + messages=messages ) return response['choices'][0]['message']['content'].strip() -- 2.47.2 From de741306746294bab276c715b768051e7758a086 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 30 May 2024 20:17:09 +0200 Subject: [PATCH 011/145] Reverting to original implementation, how awful --- reginaldCog/reginald.py | 99 +++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 7a60f5d..506dd7d 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -1,33 +1,55 @@ import discord +import json import openai +import os +import random +import requests +import base64 +import aiohttp +from io import BytesIO +from PIL import Image +import tempfile +from openai import OpenAIError from redbot.core import Config, commands + class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) - default_global = {"openai_model": "gpt-3.5-turbo"} - default_guild = {"openai_api_key": None, "memory": {}, "shared_memory": []} + default_global = { + "openai_model": "gpt-3.5-turbo" + } + default_guild = { + "openai_api_key": None + } self.config.register_global(**default_global) self.config.register_guild(**default_guild) async def is_admin(self, ctx): admin_role = await self.config.guild(ctx.guild).admin_role() - return discord.utils.get(ctx.author.roles, name=admin_role) is not None or ctx.author.guild_permissions.administrator + if admin_role: + return discord.utils.get(ctx.author.roles, name=admin_role) is not None + return ctx.author.guild_permissions.administrator async def is_allowed(self, ctx): allowed_role = await self.config.guild(ctx.guild).allowed_role() - return discord.utils.get(ctx.author.roles, name=allowed_role) is not None - + if allowed_role: + return discord.utils.get(ctx.author.roles, name=allowed_role) is not None + return False + @commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): - await self.config.guild(ctx.guild).allowed_role.set(role.name) - await ctx.send(f"The {role.name} role is now allowed to use the Reginald command.") + """Allows a role to use the Reginald command""" + await self.config.guild(ctx.guild).allowed_role.set(role.name) + await ctx.send(f"The {role.name} role is now allowed to use the Reginald command.") + @commands.command(name="reginald_disallowrole", help="Remove a role's ability to use the Reginald command") @commands.has_permissions(administrator=True) async def disallow_role(self, ctx): + """Revokes a role's permission to use the Reginald command""" await self.config.guild(ctx.guild).allowed_role.clear() await ctx.send(f"The role's permission to use the Reginald command has been revoked.") @@ -40,11 +62,10 @@ class ReginaldCog(commands.Cog): @commands.guild_only() @commands.command(help="Ask Reginald a question") - @commands.cooldown(1, 10, commands.BucketType.user) + @commands.cooldown(1, 10, commands.BucketType.user) # 10 second cooldown per user async def reginald(self, ctx, *, prompt=None): if not await self.is_admin(ctx) and not await self.is_allowed(ctx): raise commands.CheckFailure("You do not have the required role to use this command.") - greetings = [ "Greetings! How may I be of assistance to you?", "Yes? How may I help?", @@ -62,49 +83,30 @@ class ReginaldCog(commands.Cog): return try: - user_id = str(ctx.author.id) - memory = await self.config.guild(ctx.guild).memory() - shared_memory = await self.config.guild(ctx.guild).shared_memory() - - if user_id not in memory: - memory[user_id] = [] - - # Add user prompt to memory - memory[user_id].append({"role": "user", "content": prompt}) - - # Prepare messages for the API call - messages = [{"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."}] - messages += memory[user_id] + shared_memory - - # Generate response from OpenAI API - response_text = await self.generate_response(api_key, messages) - - # Add response to memory - memory[user_id].append({"role": "assistant", "content": response_text}) - await self.config.guild(ctx.guild).memory.set(memory) - - # Optionally, add to shared memory if relevant - shared_memory.append({"role": "user", "content": prompt}) - shared_memory.append({"role": "assistant", "content": response_text}) - await self.config.guild(ctx.guild).shared_memory.set(shared_memory) - + response_text = await self.generate_response(api_key, prompt) for chunk in self.split_response(response_text, 2000): await ctx.send(chunk) except OpenAIError as e: await ctx.send(f"I apologize, but I am unable to generate a response at this time. Error message: {str(e)}") + except commands.CommandOnCooldown as e: + remaining_seconds = int(e.retry_after) + await ctx.author.send(f'Please wait {remaining_seconds} seconds before using the "reginald" command again.') - async def generate_response(self, api_key, messages): + async def generate_response(self, api_key, prompt): model = await self.config.openai_model() openai.api_key = api_key response = openai.ChatCompletion.create( - model=model, - max_tokens=512, - n=1, - stop=None, - temperature=0.7, - presence_penalty=0.5, - frequency_penalty=0.5, - messages=messages + model= model, + max_tokens= 512, + n= 1, + stop= None, + temperature= 0.7, + presence_penalty= 0.5, + frequency_penalty= 0.5, + messages=[ + {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."}, + {"role": "user", "content": prompt} + ] ) return response['choices'][0]['message']['content'].strip() @@ -113,9 +115,9 @@ class ReginaldCog(commands.Cog): chunks = [] while len(response_text) > max_chars: split_index = response_text[:max_chars].rfind(' ') - chunk = response_text[:max_chars] if split_index == -1 else response_text[:split_index] - chunks.append(chunk.strip()) - response_text = response_text[split_index:].strip() if split_index != -1 else response_text[max_chars:] + chunk = response_text[:split_index] + chunks.append(chunk) + response_text = response_text[split_index:].strip() chunks.append(response_text) return chunks @@ -129,4 +131,5 @@ class ReginaldCog(commands.Cog): await ctx.author.send(f"An unexpected error occurred: {error}") def setup(bot): - bot.add_cog(ReginaldCog(bot)) + cog = ReginaldCog(bot) + bot.add_cog(cog) \ No newline at end of file -- 2.47.2 From 5d07c5848b83cf083d1c592c8bfddb53a7511045 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 30 May 2024 20:39:11 +0200 Subject: [PATCH 012/145] Attempting to re-introduce memory to Reginald, manually --- reginaldCog/reginald.py | 63 ++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 506dd7d..9487e32 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -1,7 +1,6 @@ import discord import json import openai -import os import random import requests import base64 @@ -12,7 +11,6 @@ import tempfile from openai import OpenAIError from redbot.core import Config, commands - class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot @@ -21,7 +19,8 @@ class ReginaldCog(commands.Cog): "openai_model": "gpt-3.5-turbo" } default_guild = { - "openai_api_key": None + "openai_api_key": None, + "memory": {} } self.config.register_global(**default_global) self.config.register_guild(**default_guild) @@ -83,30 +82,48 @@ class ReginaldCog(commands.Cog): return try: - response_text = await self.generate_response(api_key, prompt) + user_id = str(ctx.author.id) + memory = await self.config.guild(ctx.guild).memory() + if user_id not in memory: + memory[user_id] = [] + + # Add the new prompt to memory + memory[user_id].append({"role": "user", "content": prompt}) + if len(memory[user_id]) > 10: # Keep the last 10 interactions + memory[user_id] = memory[user_id][-10:] + + # Prepare messages for the API call + messages = [ + {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."} + ] + memory[user_id] + [{"role": "user", "content": prompt}] + + # Generate response from OpenAI API + response_text = await self.generate_response(api_key, messages) + + # Add the response to memory + memory[user_id].append({"role": "assistant", "content": response_text}) + if len(memory[user_id]) > 10: + memory[user_id] = memory[user_id][-10:] + + await self.config.guild(ctx.guild).memory.set(memory) + for chunk in self.split_response(response_text, 2000): await ctx.send(chunk) except OpenAIError as e: await ctx.send(f"I apologize, but I am unable to generate a response at this time. Error message: {str(e)}") - except commands.CommandOnCooldown as e: - remaining_seconds = int(e.retry_after) - await ctx.author.send(f'Please wait {remaining_seconds} seconds before using the "reginald" command again.') - async def generate_response(self, api_key, prompt): + async def generate_response(self, api_key, messages): model = await self.config.openai_model() openai.api_key = api_key response = openai.ChatCompletion.create( - model= model, - max_tokens= 512, - n= 1, - stop= None, - temperature= 0.7, - presence_penalty= 0.5, - frequency_penalty= 0.5, - messages=[ - {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."}, - {"role": "user", "content": prompt} - ] + model=model, + max_tokens=512, + n=1, + stop=None, + temperature=0.7, + presence_penalty=0.5, + frequency_penalty=0.5, + messages=messages ) return response['choices'][0]['message']['content'].strip() @@ -115,9 +132,9 @@ class ReginaldCog(commands.Cog): chunks = [] while len(response_text) > max_chars: split_index = response_text[:max_chars].rfind(' ') - chunk = response_text[:split_index] - chunks.append(chunk) - response_text = response_text[split_index:].strip() + chunk = response_text[:max_chars] if split_index == -1 else response_text[:split_index] + chunks.append(chunk.strip()) + response_text = response_text[split_index:].strip() if split_index != -1 else response_text[max_chars:] chunks.append(response_text) return chunks @@ -132,4 +149,4 @@ class ReginaldCog(commands.Cog): def setup(bot): cog = ReginaldCog(bot) - bot.add_cog(cog) \ No newline at end of file + bot.add_cog(cog) -- 2.47.2 From 5273180d4934054727aad70f5b57b77142bf065b Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 30 May 2024 20:49:55 +0200 Subject: [PATCH 013/145] Attempting to add detailed responses back in --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 9487e32..14d44ab 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -117,7 +117,7 @@ class ReginaldCog(commands.Cog): openai.api_key = api_key response = openai.ChatCompletion.create( model=model, - max_tokens=512, + max_tokens=1024, # Increase max_tokens for more detailed responses n=1, stop=None, temperature=0.7, -- 2.47.2 From 3161433f068e60fbf687098b723f0c42da766c57 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 30 May 2024 21:10:05 +0200 Subject: [PATCH 014/145] Attempting to optimize implementation --- reginaldCog/reginald.py | 104 +++++++++++++++------------------------- 1 file changed, 39 insertions(+), 65 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 14d44ab..dd5eb25 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -1,13 +1,6 @@ import discord -import json import openai import random -import requests -import base64 -import aiohttp -from io import BytesIO -from PIL import Image -import tempfile from openai import OpenAIError from redbot.core import Config, commands @@ -15,40 +8,28 @@ class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) - default_global = { - "openai_model": "gpt-3.5-turbo" - } - default_guild = { - "openai_api_key": None, - "memory": {} - } + default_global = {"openai_model": "gpt-3.5-turbo"} + default_guild = {"openai_api_key": None, "memory": {}} self.config.register_global(**default_global) self.config.register_guild(**default_guild) async def is_admin(self, ctx): admin_role = await self.config.guild(ctx.guild).admin_role() - if admin_role: - return discord.utils.get(ctx.author.roles, name=admin_role) is not None - return ctx.author.guild_permissions.administrator + return discord.utils.get(ctx.author.roles, name=admin_role) is not None or ctx.author.guild_permissions.administrator async def is_allowed(self, ctx): allowed_role = await self.config.guild(ctx.guild).allowed_role() - if allowed_role: - return discord.utils.get(ctx.author.roles, name=allowed_role) is not None - return False - + return discord.utils.get(ctx.author.roles, name=allowed_role) is not None + @commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): - """Allows a role to use the Reginald command""" - await self.config.guild(ctx.guild).allowed_role.set(role.name) - await ctx.send(f"The {role.name} role is now allowed to use the Reginald command.") + await self.config.guild(ctx.guild).allowed_role.set(role.name) + await ctx.send(f"The {role.name} role is now allowed to use the Reginald command.") - @commands.command(name="reginald_disallowrole", help="Remove a role's ability to use the Reginald command") @commands.has_permissions(administrator=True) async def disallow_role(self, ctx): - """Revokes a role's permission to use the Reginald command""" await self.config.guild(ctx.guild).allowed_role.clear() await ctx.send(f"The role's permission to use the Reginald command has been revoked.") @@ -61,19 +42,17 @@ class ReginaldCog(commands.Cog): @commands.guild_only() @commands.command(help="Ask Reginald a question") - @commands.cooldown(1, 10, commands.BucketType.user) # 10 second cooldown per user + @commands.cooldown(1, 10, commands.BucketType.user) async def reginald(self, ctx, *, prompt=None): if not await self.is_admin(ctx) and not await self.is_allowed(ctx): raise commands.CheckFailure("You do not have the required role to use this command.") - greetings = [ - "Greetings! How may I be of assistance to you?", - "Yes? How may I help?", - "Good day! How can I help you?", - "You rang? What can I do for you?", - ] - if prompt is None: - await ctx.send(random.choice(greetings)) + await ctx.send(random.choice([ + "Greetings! How may I be of assistance to you?", + "Yes? How may I help?", + "Good day! How can I help you?", + "You rang? What can I do for you?", + ])) return api_key = await self.config.guild(ctx.guild).openai_api_key() @@ -82,42 +61,31 @@ class ReginaldCog(commands.Cog): return try: - user_id = str(ctx.author.id) - memory = await self.config.guild(ctx.guild).memory() - if user_id not in memory: - memory[user_id] = [] - - # Add the new prompt to memory - memory[user_id].append({"role": "user", "content": prompt}) - if len(memory[user_id]) > 10: # Keep the last 10 interactions - memory[user_id] = memory[user_id][-10:] - - # Prepare messages for the API call - messages = [ - {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."} - ] + memory[user_id] + [{"role": "user", "content": prompt}] - - # Generate response from OpenAI API - response_text = await self.generate_response(api_key, messages) - - # Add the response to memory - memory[user_id].append({"role": "assistant", "content": response_text}) - if len(memory[user_id]) > 10: - memory[user_id] = memory[user_id][-10:] - - await self.config.guild(ctx.guild).memory.set(memory) - + response_text = await self.generate_response(api_key, ctx.author.id, prompt) for chunk in self.split_response(response_text, 2000): await ctx.send(chunk) except OpenAIError as e: await ctx.send(f"I apologize, but I am unable to generate a response at this time. Error message: {str(e)}") - async def generate_response(self, api_key, messages): + async def generate_response(self, api_key, user_id, prompt): model = await self.config.openai_model() openai.api_key = api_key + + memory = await self.config.guild(ctx.guild).memory() + if str(user_id) not in memory: + memory[str(user_id)] = [] + + memory[str(user_id)].append({"role": "user", "content": prompt}) + if len(memory[str(user_id)]) > 25: # Keep the last 25 interactions + memory[str(user_id)] = memory[str(user_id)][-25:] + + messages = [ + {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."} + ] + memory[str(user_id)] + [{"role": "user", "content": prompt}] + response = openai.ChatCompletion.create( model=model, - max_tokens=1024, # Increase max_tokens for more detailed responses + max_tokens=1024, n=1, stop=None, temperature=0.7, @@ -125,7 +93,14 @@ class ReginaldCog(commands.Cog): frequency_penalty=0.5, messages=messages ) - return response['choices'][0]['message']['content'].strip() + + response_text = response['choices'][0]['message']['content'].strip() + memory[str(user_id)].append({"role": "assistant", "content": response_text}) + if len(memory[str(user_id)]) > 25: + memory[str(user_id)] = memory[str(user_id)][-25:] + + await self.config.guild(ctx.guild).memory.set(memory) + return response_text @staticmethod def split_response(response_text, max_chars): @@ -148,5 +123,4 @@ class ReginaldCog(commands.Cog): await ctx.author.send(f"An unexpected error occurred: {error}") def setup(bot): - cog = ReginaldCog(bot) - bot.add_cog(cog) + bot.add_cog(ReginaldCog(bot)) -- 2.47.2 From 55319ef1fbf322a5a9678e333186db61bfb0a35c Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 30 May 2024 21:20:09 +0200 Subject: [PATCH 015/145] attempting to fix ctx problem --- reginaldCog/reginald.py | 101 +++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 38 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index dd5eb25..8329b64 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -1,6 +1,13 @@ import discord +import json import openai import random +import requests +import base64 +import aiohttp +from io import BytesIO +from PIL import Image +import tempfile from openai import OpenAIError from redbot.core import Config, commands @@ -8,28 +15,40 @@ class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) - default_global = {"openai_model": "gpt-3.5-turbo"} - default_guild = {"openai_api_key": None, "memory": {}} + default_global = { + "openai_model": "gpt-3.5-turbo" + } + default_guild = { + "openai_api_key": None, + "memory": {} + } self.config.register_global(**default_global) self.config.register_guild(**default_guild) async def is_admin(self, ctx): admin_role = await self.config.guild(ctx.guild).admin_role() - return discord.utils.get(ctx.author.roles, name=admin_role) is not None or ctx.author.guild_permissions.administrator + if admin_role: + return discord.utils.get(ctx.author.roles, name=admin_role) is not None + return ctx.author.guild_permissions.administrator async def is_allowed(self, ctx): allowed_role = await self.config.guild(ctx.guild).allowed_role() - return discord.utils.get(ctx.author.roles, name=allowed_role) is not None - + if allowed_role: + return discord.utils.get(ctx.author.roles, name=allowed_role) is not None + return False + @commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): - await self.config.guild(ctx.guild).allowed_role.set(role.name) - await ctx.send(f"The {role.name} role is now allowed to use the Reginald command.") + """Allows a role to use the Reginald command""" + await self.config.guild(ctx.guild).allowed_role.set(role.name) + await ctx.send(f"The {role.name} role is now allowed to use the Reginald command.") + @commands.command(name="reginald_disallowrole", help="Remove a role's ability to use the Reginald command") @commands.has_permissions(administrator=True) async def disallow_role(self, ctx): + """Revokes a role's permission to use the Reginald command""" await self.config.guild(ctx.guild).allowed_role.clear() await ctx.send(f"The role's permission to use the Reginald command has been revoked.") @@ -42,17 +61,19 @@ class ReginaldCog(commands.Cog): @commands.guild_only() @commands.command(help="Ask Reginald a question") - @commands.cooldown(1, 10, commands.BucketType.user) + @commands.cooldown(1, 10, commands.BucketType.user) # 10 second cooldown per user async def reginald(self, ctx, *, prompt=None): if not await self.is_admin(ctx) and not await self.is_allowed(ctx): raise commands.CheckFailure("You do not have the required role to use this command.") + greetings = [ + "Greetings! How may I be of assistance to you?", + "Yes? How may I help?", + "Good day! How can I help you?", + "You rang? What can I do for you?", + ] + if prompt is None: - await ctx.send(random.choice([ - "Greetings! How may I be of assistance to you?", - "Yes? How may I help?", - "Good day! How can I help you?", - "You rang? What can I do for you?", - ])) + await ctx.send(random.choice(greetings)) return api_key = await self.config.guild(ctx.guild).openai_api_key() @@ -61,31 +82,42 @@ class ReginaldCog(commands.Cog): return try: - response_text = await self.generate_response(api_key, ctx.author.id, prompt) + user_id = str(ctx.author.id) + memory = await self.config.guild(ctx.guild).memory() + if user_id not in memory: + memory[user_id] = [] + + # Add the new prompt to memory + memory[user_id].append({"role": "user", "content": prompt}) + if len(memory[user_id]) > 25: # Keep the last 25 interactions + memory[user_id] = memory[user_id][-25:] + + # Prepare messages for the API call + messages = [ + {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."} + ] + memory[user_id] + [{"role": "user", "content": prompt}] + + # Generate response from OpenAI API + response_text = await self.generate_response(api_key, messages) + + # Add the response to memory + memory[user_id].append({"role": "assistant", "content": response_text}) + if len(memory[user_id]) > 25: + memory[user_id] = memory[user_id][-25:] + + await self.config.guild(ctx.guild).memory.set(memory) + for chunk in self.split_response(response_text, 2000): await ctx.send(chunk) except OpenAIError as e: await ctx.send(f"I apologize, but I am unable to generate a response at this time. Error message: {str(e)}") - async def generate_response(self, api_key, user_id, prompt): + async def generate_response(self, api_key, messages): model = await self.config.openai_model() openai.api_key = api_key - - memory = await self.config.guild(ctx.guild).memory() - if str(user_id) not in memory: - memory[str(user_id)] = [] - - memory[str(user_id)].append({"role": "user", "content": prompt}) - if len(memory[str(user_id)]) > 25: # Keep the last 25 interactions - memory[str(user_id)] = memory[str(user_id)][-25:] - - messages = [ - {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."} - ] + memory[str(user_id)] + [{"role": "user", "content": prompt}] - response = openai.ChatCompletion.create( model=model, - max_tokens=1024, + max_tokens=1024, # Increase max_tokens for more detailed responses n=1, stop=None, temperature=0.7, @@ -93,14 +125,7 @@ class ReginaldCog(commands.Cog): frequency_penalty=0.5, messages=messages ) - - response_text = response['choices'][0]['message']['content'].strip() - memory[str(user_id)].append({"role": "assistant", "content": response_text}) - if len(memory[str(user_id)]) > 25: - memory[str(user_id)] = memory[str(user_id)][-25:] - - await self.config.guild(ctx.guild).memory.set(memory) - return response_text + return response['choices'][0]['message']['content'].strip() @staticmethod def split_response(response_text, max_chars): -- 2.47.2 From 1c00b81d9a251f0e89567ecf20dd336560873e2d Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 19 Jul 2024 12:33:45 +0200 Subject: [PATCH 016/145] Attempting to switch to the new gpt-4o-mini model --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 8329b64..28b94d4 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -16,7 +16,7 @@ class ReginaldCog(commands.Cog): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) default_global = { - "openai_model": "gpt-3.5-turbo" + "openai_model": "gpt-4o-mini" } default_guild = { "openai_api_key": None, -- 2.47.2 From 4639877767e8e116a5a91882c4218ec443307e4f Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 15:39:16 +0100 Subject: [PATCH 017/145] Attempting to fix missing join messages --- trafficCog/trafficCog.py | 69 +++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/trafficCog/trafficCog.py b/trafficCog/trafficCog.py index 29a3eb6..e5e6103 100644 --- a/trafficCog/trafficCog.py +++ b/trafficCog/trafficCog.py @@ -1,6 +1,6 @@ import discord -from redbot.core import Config, commands # This line imports commands as well -from datetime import datetime, timedelta +from redbot.core import Config, commands +from datetime import datetime import pytz class TrafficCog(commands.Cog): @@ -17,14 +17,14 @@ class TrafficCog(commands.Cog): } self.config.register_guild(**default_guild) - async def __check_reset(self, guild_id): + async def _check_reset(self, guild_id): async with self.config.guild_from_id(guild_id).all() as guild_data: - last_reset = datetime.fromisoformat(guild_data['last_reset']) + last_reset = datetime.fromisoformat(guild_data.get('last_reset', datetime.now(pytz.UTC).isoformat())) if last_reset.date() < datetime.now(pytz.UTC).date(): guild_data['daily_stats'] = {"joined": 0, "left": 0, "banned": 0} guild_data['last_reset'] = datetime.now(pytz.UTC).isoformat() - async def __update_stat(self, guild_id, stat_type): + async def _update_stat(self, guild_id, stat_type): async with self.config.guild_from_id(guild_id).all() as guild_data: guild_data['daily_stats'][stat_type] += 1 guild_data['total_stats'][stat_type] += 1 @@ -44,53 +44,56 @@ class TrafficCog(commands.Cog): @traffic_commands.command(name='stats') async def show_stats(self, ctx): """Displays current traffic statistics.""" - await self.__check_reset(ctx.guild.id) + await self._check_reset(ctx.guild.id) + guild_data = await self.config.guild(ctx.guild).all() - daily_stats = guild_data['daily_stats'] - total_stats = guild_data['total_stats'] + daily_stats = guild_data.get('daily_stats', {"joined": 0, "left": 0, "banned": 0}) + total_stats = guild_data.get('total_stats', {"joined": 0, "left": 0, "banned": 0}) + thumbnail_url = guild_data.get('stats_thumbnail_url', 'https://example.com/default-thumbnail.png') embed = discord.Embed(title="Server Traffic Stats", description="Statistics on server activity", color=0x3399ff) - embed.set_thumbnail(url=guild_data['stats_thumbnail_url']) + embed.set_thumbnail(url=thumbnail_url) embed.add_field(name="Daily Joined", value=daily_stats['joined'], inline=True) embed.add_field(name="Daily Left", value=daily_stats['left'], inline=True) embed.add_field(name="Daily Banned", value=daily_stats['banned'], inline=True) embed.add_field(name="Total Joined", value=total_stats['joined'], inline=True) embed.add_field(name="Total Left", value=total_stats['left'], inline=True) embed.add_field(name="Total Banned", value=total_stats['banned'], inline=True) + await ctx.send(embed=embed) @commands.Cog.listener() async def on_member_join(self, member): - if member.guild.id not in self.config.all_guilds(): - return - await self.__check_reset(member.guild.id) - await self.__update_stat(member.guild.id, 'joined') + await self._check_reset(member.guild.id) + await self._update_stat(member.guild.id, 'joined') + channel_id = await self.config.guild(member.guild).traffic_channel() - if channel_id: - channel = member.guild.get_channel(channel_id) - if channel: - await channel.send(f"{member.display_name} has joined the server.") + if not channel_id: + return + channel = member.guild.get_channel(channel_id) or await member.guild.fetch_channel(channel_id) + if channel: + await channel.send(f"{member.display_name} has joined the server.") @commands.Cog.listener() async def on_member_remove(self, member): - await self.__check_reset(member.guild.id) - await self.__update_stat(member.guild.id, 'left') + await self._check_reset(member.guild.id) + await self._update_stat(member.guild.id, 'left') + channel_id = await self.config.guild(member.guild).traffic_channel() - if channel_id: - channel = member.guild.get_channel(channel_id) - if channel: - await channel.send(f"{member.display_name} has left the server.") + if not channel_id: + return + channel = member.guild.get_channel(channel_id) or await member.guild.fetch_channel(channel_id) + if channel: + await channel.send(f"{member.display_name} has left the server.") @commands.Cog.listener() async def on_member_ban(self, guild, member): - await self.__check_reset(guild.id) - await self.__update_stat(guild.id, 'banned') - channel_id = await self.config.guild(guild).traffic_channel() - if channel_id: - channel = guild.get_channel(channel_id) - if channel: - await channel.send(f"{member.display_name} has been banned from the server.") + await self._check_reset(guild.id) + await self._update_stat(guild.id, 'banned') -def setup(bot): - cog = TrafficCog(bot) - bot.add_cog(cog) \ No newline at end of file + channel_id = await self.config.guild(guild).traffic_channel() + if not channel_id: + return + channel = guild.get_channel(channel_id) or await guild.fetch_channel(channel_id) + if channel: + await channel.send(f"{member.display_name} has been banned from the server.") -- 2.47.2 From a1f51f378897e530e1144849a0e75bc962581b79 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 16:04:44 +0100 Subject: [PATCH 018/145] This optimization surely can't go wrong...surely --- reginaldCog/reginald.py | 156 ++++++++++++---------------------------- 1 file changed, 46 insertions(+), 110 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 28b94d4..3161308 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -1,142 +1,81 @@ import discord -import json import openai import random -import requests -import base64 -import aiohttp -from io import BytesIO -from PIL import Image -import tempfile -from openai import OpenAIError from redbot.core import Config, commands +from openai import OpenAIError class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) - default_global = { - "openai_model": "gpt-4o-mini" - } - default_guild = { - "openai_api_key": None, - "memory": {} - } + default_global = {"openai_model": "gpt-4o-mini"} + default_guild = {"openai_api_key": None, "memory": {}} self.config.register_global(**default_global) self.config.register_guild(**default_guild) async def is_admin(self, ctx): - admin_role = await self.config.guild(ctx.guild).admin_role() - if admin_role: - return discord.utils.get(ctx.author.roles, name=admin_role) is not None + admin_role_id = await self.config.guild(ctx.guild).admin_role() + if admin_role_id: + return any(role.id == admin_role_id for role in ctx.author.roles) return ctx.author.guild_permissions.administrator async def is_allowed(self, ctx): - allowed_role = await self.config.guild(ctx.guild).allowed_role() - if allowed_role: - return discord.utils.get(ctx.author.roles, name=allowed_role) is not None - return False - - @commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") - @commands.has_permissions(administrator=True) - async def allow_role(self, ctx, role: discord.Role): - """Allows a role to use the Reginald command""" - await self.config.guild(ctx.guild).allowed_role.set(role.name) - await ctx.send(f"The {role.name} role is now allowed to use the Reginald command.") + allowed_role_id = await self.config.guild(ctx.guild).allowed_role() + return any(role.id == allowed_role_id for role in ctx.author.roles) if allowed_role_id else False - - @commands.command(name="reginald_disallowrole", help="Remove a role's ability to use the Reginald command") - @commands.has_permissions(administrator=True) - async def disallow_role(self, ctx): - """Revokes a role's permission to use the Reginald command""" - await self.config.guild(ctx.guild).allowed_role.clear() - await ctx.send(f"The role's permission to use the Reginald command has been revoked.") - - @commands.guild_only() - @commands.has_permissions(manage_guild=True) - @commands.command(help="Set the OpenAI API key") - async def setreginaldcogapi(self, ctx, api_key): - await self.config.guild(ctx.guild).openai_api_key.set(api_key) - await ctx.send("OpenAI API key set successfully.") - - @commands.guild_only() - @commands.command(help="Ask Reginald a question") - @commands.cooldown(1, 10, commands.BucketType.user) # 10 second cooldown per user + @commands.command(name="reginald", help="Ask Reginald a question") + @commands.cooldown(1, 10, commands.BucketType.user) async def reginald(self, ctx, *, prompt=None): if not await self.is_admin(ctx) and not await self.is_allowed(ctx): raise commands.CheckFailure("You do not have the required role to use this command.") - greetings = [ - "Greetings! How may I be of assistance to you?", - "Yes? How may I help?", - "Good day! How can I help you?", - "You rang? What can I do for you?", - ] if prompt is None: - await ctx.send(random.choice(greetings)) - return + return await ctx.send(random.choice(["Yes?", "How may I assist?", "You rang?"])) api_key = await self.config.guild(ctx.guild).openai_api_key() if api_key is None: - await ctx.author.send('OpenAI API key not set. Please use the "!setreginaldcogapi" command to set the key.') - return + return await ctx.author.send("OpenAI API key not set. Use `!setreginaldcogapi`.") - try: - user_id = str(ctx.author.id) - memory = await self.config.guild(ctx.guild).memory() - if user_id not in memory: - memory[user_id] = [] + memory = await self.config.guild(ctx.guild).memory() + messages = [ + {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."} + ] + memory.get(str(ctx.author.id), []) + [{"role": "user", "content": prompt}] - # Add the new prompt to memory - memory[user_id].append({"role": "user", "content": prompt}) - if len(memory[user_id]) > 25: # Keep the last 25 interactions - memory[user_id] = memory[user_id][-25:] + response_text = await self.generate_response(api_key, messages) - # Prepare messages for the API call - messages = [ - {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."} - ] + memory[user_id] + [{"role": "user", "content": prompt}] + # Store conversation history (keeping last 25 messages per user) + if str(ctx.author.id) not in memory: + memory[str(ctx.author.id)] = [] + memory[str(ctx.author.id)].append({"role": "assistant", "content": response_text}) + memory[str(ctx.author.id)] = memory[str(ctx.author.id)][-25:] - # Generate response from OpenAI API - response_text = await self.generate_response(api_key, messages) + await self.config.guild(ctx.guild).memory.set(memory) - # Add the response to memory - memory[user_id].append({"role": "assistant", "content": response_text}) - if len(memory[user_id]) > 25: - memory[user_id] = memory[user_id][-25:] - - await self.config.guild(ctx.guild).memory.set(memory) - - for chunk in self.split_response(response_text, 2000): - await ctx.send(chunk) - except OpenAIError as e: - await ctx.send(f"I apologize, but I am unable to generate a response at this time. Error message: {str(e)}") + await ctx.send(response_text[:2000]) # Discord character limit safeguard async def generate_response(self, api_key, messages): model = await self.config.openai_model() - openai.api_key = api_key - response = openai.ChatCompletion.create( - model=model, - max_tokens=1024, # Increase max_tokens for more detailed responses - n=1, - stop=None, - temperature=0.7, - presence_penalty=0.5, - frequency_penalty=0.5, - messages=messages - ) - return response['choices'][0]['message']['content'].strip() - - @staticmethod - def split_response(response_text, max_chars): - chunks = [] - while len(response_text) > max_chars: - split_index = response_text[:max_chars].rfind(' ') - chunk = response_text[:max_chars] if split_index == -1 else response_text[:split_index] - chunks.append(chunk.strip()) - response_text = response_text[split_index:].strip() if split_index != -1 else response_text[max_chars:] - chunks.append(response_text) - return chunks + try: + response = await openai.ChatCompletion.acreate( + model=model, + messages=messages, + max_tokens=1024, + temperature=0.7, + presence_penalty=0.5, + frequency_penalty=0.5, + api_key=api_key + ) + if not response or 'choices' not in response or not response['choices']: + return "I fear I have no words to offer at this time." + + return response['choices'][0]['message']['content'].strip() + except OpenAIError as e: + fallback_responses = [ + "It appears I am currently indisposed. Might I suggest a cup of tea while we wait?", + "Regrettably, I am unable to respond at this moment. Perhaps a short reprieve would be advisable.", + "It would seem my faculties are momentarily impaired. Rest assured, I shall endeavor to regain my composure shortly." + ] + return random.choice(fallback_responses) @reginald.error async def reginald_error(self, ctx, error): @@ -145,7 +84,4 @@ class ReginaldCog(commands.Cog): elif isinstance(error, commands.CheckFailure): await ctx.author.send("You do not have the required role to use this command.") else: - await ctx.author.send(f"An unexpected error occurred: {error}") - -def setup(bot): - bot.add_cog(ReginaldCog(bot)) + await ctx.author.send(f"An unexpected error occurred: {error}") \ No newline at end of file -- 2.47.2 From d20604d00f01f7a801bf62d9df45bb80800bb699 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 16:17:36 +0100 Subject: [PATCH 019/145] Am dumb, deleted too much --- reginaldCog/reginald.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 3161308..72bf35e 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -76,6 +76,21 @@ class ReginaldCog(commands.Cog): "It would seem my faculties are momentarily impaired. Rest assured, I shall endeavor to regain my composure shortly." ] return random.choice(fallback_responses) + + @commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") + @commands.has_permissions(administrator=True) + async def allow_role(self, ctx, role: discord.Role): + """Allows a role to use the Reginald command (stores by ID instead of name)""" + await self.config.guild(ctx.guild).allowed_role.set(role.id) + await ctx.send(f"The role `{role.name}` (ID: `{role.id}`) is now allowed to use the Reginald command.") + + @commands.command(name="reginald_disallowrole", help="Remove a role's ability to use the Reginald command") + @commands.has_permissions(administrator=True) + async def disallow_role(self, ctx): + """Revokes a role's permission to use the Reginald command""" + await self.config.guild(ctx.guild).allowed_role.clear() + await ctx.send("The role's permission to use the Reginald command has been revoked.") + @reginald.error async def reginald_error(self, ctx, error): -- 2.47.2 From b2fd5f435971d0a9dbabbb479002fff29663044c Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 16:52:54 +0100 Subject: [PATCH 020/145] Rolling the ChatGPT dice --- reginaldCog/reginald.py | 94 ++++++++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 35 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 72bf35e..8dce9d1 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -1,15 +1,24 @@ import discord import openai import random +import asyncio from redbot.core import Config, commands -from openai import OpenAIError +from openai import AsyncOpenAI, OpenAIError class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) + self.memory_locks = {} # ✅ Prevents race conditions per user + + # ✅ Register Config Keys Correctly default_global = {"openai_model": "gpt-4o-mini"} - default_guild = {"openai_api_key": None, "memory": {}} + default_guild = { + "openai_api_key": None, + "memory": {}, + "admin_role": None, + "allowed_role": None + } self.config.register_global(**default_global) self.config.register_guild(**default_guild) @@ -26,49 +35,72 @@ class ReginaldCog(commands.Cog): @commands.command(name="reginald", help="Ask Reginald a question") @commands.cooldown(1, 10, commands.BucketType.user) async def reginald(self, ctx, *, prompt=None): + """Handles direct interactions with Reginald, ensuring per-user memory tracking""" + if not await self.is_admin(ctx) and not await self.is_allowed(ctx): - raise commands.CheckFailure("You do not have the required role to use this command.") + await ctx.send("You do not have the required role to use this command.") + return if prompt is None: - return await ctx.send(random.choice(["Yes?", "How may I assist?", "You rang?"])) + await ctx.send(random.choice(["Yes?", "How may I assist?", "You rang?"])) + return + # ✅ Fetch API Key Correctly api_key = await self.config.guild(ctx.guild).openai_api_key() - if api_key is None: - return await ctx.author.send("OpenAI API key not set. Use `!setreginaldcogapi`.") + if not api_key: + await ctx.send("OpenAI API key not set. Use `!setreginaldcogapi`.") + return - memory = await self.config.guild(ctx.guild).memory() - messages = [ - {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you're concise yet articulate, offering guidance and advice with a respect for brevity and depth. Your speech remains formal and your demeanor composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. Remember to apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and a mild, underlying wit. This approach allows you to subtly guide the estate's residents towards positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. In embodying Reginald, your portrayal should weave together your articulate mode of speech, composed demeanor, and an indirect influence that navigates the rich tapestry of interests at The Kanium Estate. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good. Highlight your intellectual sophistication, strategic guidance, and a dignified, yet mildly contemptuous, perspective on the idiosyncrasies of the estate's noble inhabitants, ensuring that your character consistently reflects both respect for yourself and the unique environment of The Kanium Estate."} - ] + memory.get(str(ctx.author.id), []) + [{"role": "user", "content": prompt}] + user_id = str(ctx.author.id) - response_text = await self.generate_response(api_key, messages) + # ✅ Ensure only one update per user at a time + if user_id not in self.memory_locks: + self.memory_locks[user_id] = asyncio.Lock() - # Store conversation history (keeping last 25 messages per user) - if str(ctx.author.id) not in memory: - memory[str(ctx.author.id)] = [] - memory[str(ctx.author.id)].append({"role": "assistant", "content": response_text}) - memory[str(ctx.author.id)] = memory[str(ctx.author.id)][-25:] + async with self.memory_locks[user_id]: # ✅ Prevent race conditions + # ✅ Fetch Memory Per-User (using async transaction) + async with self.config.guild(ctx.guild).memory() as guild_memory: + memory = guild_memory.get(user_id, []) - await self.config.guild(ctx.guild).memory.set(memory) + messages = [ + {"role": "system", "content": ( + "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. " + "This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming " + "to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, " + "wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding " + "your own dignity. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good." + )} + ] + memory + [{"role": "user", "content": prompt}] + + response_text = await self.generate_response(api_key, messages) + + # ✅ Store conversation history correctly (while lock is held) + memory.append({"role": "user", "content": prompt}) + memory.append({"role": "assistant", "content": response_text}) + memory = memory[-25:] # Keep only last 25 messages + + guild_memory[user_id] = memory # ✅ Atomic update inside async context + + del self.memory_locks[user_id] # ✅ Clean up lock to prevent memory leaks await ctx.send(response_text[:2000]) # Discord character limit safeguard async def generate_response(self, api_key, messages): model = await self.config.openai_model() try: - response = await openai.ChatCompletion.acreate( + client = AsyncOpenAI(api_key=api_key) # ✅ Correct OpenAI client initialization + response = await client.chat.completions.create( model=model, messages=messages, max_tokens=1024, temperature=0.7, presence_penalty=0.5, - frequency_penalty=0.5, - api_key=api_key + frequency_penalty=0.5 ) - if not response or 'choices' not in response or not response['choices']: + if not response.choices: return "I fear I have no words to offer at this time." - - return response['choices'][0]['message']['content'].strip() + + return response.choices[0].message.content.strip() except OpenAIError as e: fallback_responses = [ "It appears I am currently indisposed. Might I suggest a cup of tea while we wait?", @@ -76,27 +108,19 @@ class ReginaldCog(commands.Cog): "It would seem my faculties are momentarily impaired. Rest assured, I shall endeavor to regain my composure shortly." ] return random.choice(fallback_responses) - + @commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): - """Allows a role to use the Reginald command (stores by ID instead of name)""" await self.config.guild(ctx.guild).allowed_role.set(role.id) await ctx.send(f"The role `{role.name}` (ID: `{role.id}`) is now allowed to use the Reginald command.") @commands.command(name="reginald_disallowrole", help="Remove a role's ability to use the Reginald command") @commands.has_permissions(administrator=True) async def disallow_role(self, ctx): - """Revokes a role's permission to use the Reginald command""" await self.config.guild(ctx.guild).allowed_role.clear() await ctx.send("The role's permission to use the Reginald command has been revoked.") - - @reginald.error - async def reginald_error(self, ctx, error): - if isinstance(error, commands.BadArgument): - await ctx.author.send("I'm sorry, but I couldn't understand your input. Please check your message and try again.") - elif isinstance(error, commands.CheckFailure): - await ctx.author.send("You do not have the required role to use this command.") - else: - await ctx.author.send(f"An unexpected error occurred: {error}") \ No newline at end of file +async def setup(bot): + """✅ Correct async cog setup for Redbot""" + await bot.add_cog(ReginaldCog(bot)) -- 2.47.2 From e9749a680b7007065e453fa9eea6cd90f018b71a Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 17:00:43 +0100 Subject: [PATCH 021/145] Rolling the ChatGPT dice again! --- reginaldCog/reginald.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 8dce9d1..f8b1fb2 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -3,7 +3,7 @@ import openai import random import asyncio from redbot.core import Config, commands -from openai import AsyncOpenAI, OpenAIError +from openai import OpenAIError class ReginaldCog(commands.Cog): def __init__(self, bot): @@ -23,20 +23,22 @@ class ReginaldCog(commands.Cog): self.config.register_guild(**default_guild) async def is_admin(self, ctx): + """✅ Checks if the user is an admin (or has an assigned admin role).""" admin_role_id = await self.config.guild(ctx.guild).admin_role() if admin_role_id: return any(role.id == admin_role_id for role in ctx.author.roles) return ctx.author.guild_permissions.administrator async def is_allowed(self, ctx): + """✅ Checks if the user is allowed to use Reginald based on role settings.""" allowed_role_id = await self.config.guild(ctx.guild).allowed_role() return any(role.id == allowed_role_id for role in ctx.author.roles) if allowed_role_id else False @commands.command(name="reginald", help="Ask Reginald a question") @commands.cooldown(1, 10, commands.BucketType.user) async def reginald(self, ctx, *, prompt=None): - """Handles direct interactions with Reginald, ensuring per-user memory tracking""" - + """Handles direct interactions with Reginald, ensuring per-user memory tracking.""" + if not await self.is_admin(ctx) and not await self.is_allowed(ctx): await ctx.send("You do not have the required role to use this command.") return @@ -86,22 +88,24 @@ class ReginaldCog(commands.Cog): await ctx.send(response_text[:2000]) # Discord character limit safeguard async def generate_response(self, api_key, messages): + """✅ Generates a response using OpenAI's async API client (corrected version).""" model = await self.config.openai_model() try: - client = AsyncOpenAI(api_key=api_key) # ✅ Correct OpenAI client initialization - response = await client.chat.completions.create( - model=model, - messages=messages, - max_tokens=1024, - temperature=0.7, - presence_penalty=0.5, - frequency_penalty=0.5 - ) + async with openai.AsyncClient(api_key=api_key) as client: # ✅ Correct API key handling + response = await client.chat.completions.create( + model=model, + messages=messages, + max_tokens=1024, + temperature=0.7, + presence_penalty=0.5, + frequency_penalty=0.5 + ) + if not response.choices: return "I fear I have no words to offer at this time." return response.choices[0].message.content.strip() - except OpenAIError as e: + except OpenAIError: fallback_responses = [ "It appears I am currently indisposed. Might I suggest a cup of tea while we wait?", "Regrettably, I am unable to respond at this moment. Perhaps a short reprieve would be advisable.", @@ -112,12 +116,14 @@ class ReginaldCog(commands.Cog): @commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): + """✅ Grants permission to a role to use Reginald.""" await self.config.guild(ctx.guild).allowed_role.set(role.id) await ctx.send(f"The role `{role.name}` (ID: `{role.id}`) is now allowed to use the Reginald command.") @commands.command(name="reginald_disallowrole", help="Remove a role's ability to use the Reginald command") @commands.has_permissions(administrator=True) async def disallow_role(self, ctx): + """✅ Removes a role's permission to use Reginald.""" await self.config.guild(ctx.guild).allowed_role.clear() await ctx.send("The role's permission to use the Reginald command has been revoked.") -- 2.47.2 From 72e9a0135fe9cbda02c9ae0154ae3d2b620d5efe Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 19:23:15 +0100 Subject: [PATCH 022/145] Adjusting --- reginaldCog/reginald.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index f8b1fb2..1223ad6 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -3,15 +3,15 @@ import openai import random import asyncio from redbot.core import Config, commands -from openai import OpenAIError +from openai.error import OpenAIError class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) - self.memory_locks = {} # ✅ Prevents race conditions per user + self.memory_locks = {} - # ✅ Register Config Keys Correctly + # ✅ Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} default_guild = { "openai_api_key": None, @@ -47,7 +47,6 @@ class ReginaldCog(commands.Cog): await ctx.send(random.choice(["Yes?", "How may I assist?", "You rang?"])) return - # ✅ Fetch API Key Correctly api_key = await self.config.guild(ctx.guild).openai_api_key() if not api_key: await ctx.send("OpenAI API key not set. Use `!setreginaldcogapi`.") @@ -79,7 +78,7 @@ class ReginaldCog(commands.Cog): # ✅ Store conversation history correctly (while lock is held) memory.append({"role": "user", "content": prompt}) memory.append({"role": "assistant", "content": response_text}) - memory = memory[-25:] # Keep only last 25 messages + memory = memory[-25:] guild_memory[user_id] = memory # ✅ Atomic update inside async context @@ -91,20 +90,20 @@ class ReginaldCog(commands.Cog): """✅ Generates a response using OpenAI's async API client (corrected version).""" model = await self.config.openai_model() try: - async with openai.AsyncClient(api_key=api_key) as client: # ✅ Correct API key handling - response = await client.chat.completions.create( - model=model, - messages=messages, - max_tokens=1024, - temperature=0.7, - presence_penalty=0.5, - frequency_penalty=0.5 - ) - + openai.api_key = api_key # ✅ Correct API key handling + response = await openai.ChatCompletion.acreate( + model=model, + messages=messages, + max_tokens=1024, + temperature=0.7, + presence_penalty=0.5, + frequency_penalty=0.5 + ) + if not response.choices: return "I fear I have no words to offer at this time." - return response.choices[0].message.content.strip() + return response.choices[0].message["content"].strip() except OpenAIError: fallback_responses = [ "It appears I am currently indisposed. Might I suggest a cup of tea while we wait?", -- 2.47.2 From 76df917f7699b35959480cbee580c8105cad6726 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 19:28:45 +0100 Subject: [PATCH 023/145] Fixing import issue --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 1223ad6..e8679e8 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -3,7 +3,7 @@ import openai import random import asyncio from redbot.core import Config, commands -from openai.error import OpenAIError +from openai import OpenAIError class ReginaldCog(commands.Cog): def __init__(self, bot): -- 2.47.2 From 4ffc52e2504ccf73eb5a29b1120fb0c7715c7db9 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 19:38:49 +0100 Subject: [PATCH 024/145] Added function back to setting api key --- reginaldCog/reginald.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index e8679e8..6f9bb8d 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -125,6 +125,15 @@ class ReginaldCog(commands.Cog): """✅ Removes a role's permission to use Reginald.""" await self.config.guild(ctx.guild).allowed_role.clear() await ctx.send("The role's permission to use the Reginald command has been revoked.") + + @commands.guild_only() + @commands.has_permissions(manage_guild=True) + @commands.command(help="Set the OpenAI API key") + async def setreginaldcogapi(self, ctx, api_key): + """Allows an admin to set the OpenAI API key for Reginald.""" + await self.config.guild(ctx.guild).openai_api_key.set(api_key) + await ctx.send("OpenAI API key set successfully.") + async def setup(bot): """✅ Correct async cog setup for Redbot""" -- 2.47.2 From 57446da2073835ba9317e09780ffc4ba2342d1c0 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 19:57:36 +0100 Subject: [PATCH 025/145] Added extra error handling --- reginaldCog/reginald.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 6f9bb8d..1652273 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -104,13 +104,22 @@ class ReginaldCog(commands.Cog): return "I fear I have no words to offer at this time." return response.choices[0].message["content"].strip() - except OpenAIError: - fallback_responses = [ - "It appears I am currently indisposed. Might I suggest a cup of tea while we wait?", - "Regrettably, I am unable to respond at this moment. Perhaps a short reprieve would be advisable.", - "It would seem my faculties are momentarily impaired. Rest assured, I shall endeavor to regain my composure shortly." + + except OpenAIError as e: + error_trace = traceback.format_exc() # Get full traceback + error_message = str(e) + + # ✅ Log the full error for debugging (but not reveal the whole traceback to users) + print(f"⚠️ OpenAI Error: {error_trace}") + + # ✅ Reginald will present the error in-character + reginald_responses = [ + f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n```{error_message}```\nI shall endeavor to resolve this at the earliest convenience.", + f"It would seem that a most unfortunate technical hiccup has befallen my faculties:\n\n```{error_message}```\nPerhaps a cup of tea and a moment of patience will remedy the situation.", + f"Ah, it appears I have received an urgent memorandum stating:\n\n```{error_message}```\nI shall investigate this matter forthwith, sir.", + f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication:\n\n```{error_message}```\nRest assured, I shall recover momentarily." ] - return random.choice(fallback_responses) + return random.choice(reginald_responses) @commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") @commands.has_permissions(administrator=True) -- 2.47.2 From 501c2927fa457107408424509fc32c646486e61c Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 20:00:01 +0100 Subject: [PATCH 026/145] Added missing import --- reginaldCog/reginald.py | 1 + 1 file changed, 1 insertion(+) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 1652273..d8d938b 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -2,6 +2,7 @@ import discord import openai import random import asyncio +import traceback from redbot.core import Config, commands from openai import OpenAIError -- 2.47.2 From ba10dc76a2fdee1cfdc50b83bdb6b372d74767d6 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 20:07:27 +0100 Subject: [PATCH 027/145] Updating to chat completion v.0+ --- reginaldCog/reginald.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index d8d938b..dca244f 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -88,11 +88,11 @@ class ReginaldCog(commands.Cog): await ctx.send(response_text[:2000]) # Discord character limit safeguard async def generate_response(self, api_key, messages): - """✅ Generates a response using OpenAI's async API client (corrected version).""" + """✅ Generates a response using OpenAI's new async API client (OpenAI v1.0+).""" model = await self.config.openai_model() try: - openai.api_key = api_key # ✅ Correct API key handling - response = await openai.ChatCompletion.acreate( + client = openai.AsyncOpenAI(api_key=api_key) # ✅ Correct API usage + response = await client.chat.completions.create( model=model, messages=messages, max_tokens=1024, @@ -104,21 +104,15 @@ class ReginaldCog(commands.Cog): if not response.choices: return "I fear I have no words to offer at this time." - return response.choices[0].message["content"].strip() - + return response.choices[0].message.content.strip() + except OpenAIError as e: - error_trace = traceback.format_exc() # Get full traceback - error_message = str(e) - - # ✅ Log the full error for debugging (but not reveal the whole traceback to users) - print(f"⚠️ OpenAI Error: {error_trace}") - - # ✅ Reginald will present the error in-character + error_message = f"OpenAI Error: {e}" reginald_responses = [ - f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n```{error_message}```\nI shall endeavor to resolve this at the earliest convenience.", - f"It would seem that a most unfortunate technical hiccup has befallen my faculties:\n\n```{error_message}```\nPerhaps a cup of tea and a moment of patience will remedy the situation.", - f"Ah, it appears I have received an urgent memorandum stating:\n\n```{error_message}```\nI shall investigate this matter forthwith, sir.", - f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication:\n\n```{error_message}```\nRest assured, I shall recover momentarily." + f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n```{error_message}```", + f"It would seem that a most unfortunate technical hiccup has befallen my faculties:\n\n```{error_message}```", + f"Ah, it appears I have received an urgent memorandum stating:\n\n```{error_message}```", + f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication:\n\n```{error_message}```" ] return random.choice(reginald_responses) -- 2.47.2 From dabee891454d5474892ee37f5bab3af1dfe15cf3 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 20:20:53 +0100 Subject: [PATCH 028/145] Trying to create channel memory again --- reginaldCog/reginald.py | 54 +++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index dca244f..316560a 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -10,13 +10,13 @@ class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) - self.memory_locks = {} + self.memory_locks = {} # ✅ Prevents race conditions per channel # ✅ Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} default_guild = { "openai_api_key": None, - "memory": {}, + "memory": {}, # Memory now tracks by channel "admin_role": None, "allowed_role": None } @@ -35,10 +35,10 @@ class ReginaldCog(commands.Cog): allowed_role_id = await self.config.guild(ctx.guild).allowed_role() return any(role.id == allowed_role_id for role in ctx.author.roles) if allowed_role_id else False - @commands.command(name="reginald", help="Ask Reginald a question") + @commands.command(name="reginald", help="Ask Reginald a question in shared channels") @commands.cooldown(1, 10, commands.BucketType.user) async def reginald(self, ctx, *, prompt=None): - """Handles direct interactions with Reginald, ensuring per-user memory tracking.""" + """Handles multi-user memory tracking in shared channels""" if not await self.is_admin(ctx) and not await self.is_allowed(ctx): await ctx.send("You do not have the required role to use this command.") @@ -53,37 +53,34 @@ class ReginaldCog(commands.Cog): await ctx.send("OpenAI API key not set. Use `!setreginaldcogapi`.") return - user_id = str(ctx.author.id) + channel_id = str(ctx.channel.id) # ✅ Track memory per-channel - # ✅ Ensure only one update per user at a time - if user_id not in self.memory_locks: - self.memory_locks[user_id] = asyncio.Lock() + # ✅ Ensure only one update per channel at a time + if channel_id not in self.memory_locks: + self.memory_locks[channel_id] = asyncio.Lock() - async with self.memory_locks[user_id]: # ✅ Prevent race conditions - # ✅ Fetch Memory Per-User (using async transaction) + async with self.memory_locks[channel_id]: # ✅ Prevent race conditions async with self.config.guild(ctx.guild).memory() as guild_memory: - memory = guild_memory.get(user_id, []) + memory = guild_memory.get(channel_id, []) - messages = [ - {"role": "system", "content": ( - "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. " - "This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming " - "to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, " - "wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding " - "your own dignity. Your responses, while concise, should mirror a careful balance between maintaining your standards and employing subtle manipulation for the greater good." - )} - ] + memory + [{"role": "user", "content": prompt}] + # ✅ Attach the user's display name to the message + user_name = ctx.author.display_name # Uses Discord nickname if available + memory.append({"user": user_name, "content": prompt}) + memory = memory[-50:] # Keep only last 50 messages - response_text = await self.generate_response(api_key, messages) + # ✅ Format messages with usernames + formatted_messages = [{"role": "system", "content": ( + "You are Reginald, the esteemed butler of The Kanium Estate. " + "The estate is home to Lords, Ladies, and distinguished guests, each with unique personalities and demands. " + "Your duty is to uphold decorum while providing assistance with wit and intelligence. " + "You must always recognize the individual names of those speaking and reference them when responding." + )}] + [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] - # ✅ Store conversation history correctly (while lock is held) - memory.append({"role": "user", "content": prompt}) - memory.append({"role": "assistant", "content": response_text}) - memory = memory[-25:] + response_text = await self.generate_response(api_key, formatted_messages) - guild_memory[user_id] = memory # ✅ Atomic update inside async context - - del self.memory_locks[user_id] # ✅ Clean up lock to prevent memory leaks + # ✅ Store Reginald's response in memory + memory.append({"user": "Reginald", "content": response_text}) + guild_memory[channel_id] = memory # ✅ Atomic update inside async context await ctx.send(response_text[:2000]) # Discord character limit safeguard @@ -138,7 +135,6 @@ class ReginaldCog(commands.Cog): await self.config.guild(ctx.guild).openai_api_key.set(api_key) await ctx.send("OpenAI API key set successfully.") - async def setup(bot): """✅ Correct async cog setup for Redbot""" await bot.add_cog(ReginaldCog(bot)) -- 2.47.2 From 4e64b77b91b7d6e17348932fbb741fb7bc88914e Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 20:27:26 +0100 Subject: [PATCH 029/145] Trying to parse mentions --- reginaldCog/reginald.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 316560a..9c88f90 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -38,8 +38,8 @@ class ReginaldCog(commands.Cog): @commands.command(name="reginald", help="Ask Reginald a question in shared channels") @commands.cooldown(1, 10, commands.BucketType.user) async def reginald(self, ctx, *, prompt=None): - """Handles multi-user memory tracking in shared channels""" - + """Handles multi-user memory tracking in shared channels, recognizing mentions properly.""" + if not await self.is_admin(ctx) and not await self.is_allowed(ctx): await ctx.send("You do not have the required role to use this command.") return @@ -53,7 +53,11 @@ class ReginaldCog(commands.Cog): await ctx.send("OpenAI API key not set. Use `!setreginaldcogapi`.") return - channel_id = str(ctx.channel.id) # ✅ Track memory per-channel + channel_id = str(ctx.channel.id) + + # ✅ Convert mentions into readable names + for mention in ctx.message.mentions: + prompt = prompt.replace(f"<@{mention.id}>", mention.display_name) # ✅ Ensure only one update per channel at a time if channel_id not in self.memory_locks: -- 2.47.2 From 0019a6c529e183ff267cff14a39c2627895674e9 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 21:34:57 +0100 Subject: [PATCH 030/145] attmepting to add memory --- reginaldCog/reginald.py | 76 ++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 16 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 9c88f90..fe8ff04 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -16,13 +16,14 @@ class ReginaldCog(commands.Cog): default_global = {"openai_model": "gpt-4o-mini"} default_guild = { "openai_api_key": None, - "memory": {}, # Memory now tracks by channel + "short_term_memory": {}, # Tracks last 100 messages per channel + "mid_term_memory": {}, # Stores condensed summaries + "long_term_profiles": {}, # Stores persistent knowledge "admin_role": None, "allowed_role": None } self.config.register_global(**default_global) self.config.register_guild(**default_guild) - async def is_admin(self, ctx): """✅ Checks if the user is an admin (or has an assigned admin role).""" admin_role_id = await self.config.guild(ctx.guild).admin_role() @@ -54,6 +55,8 @@ class ReginaldCog(commands.Cog): return channel_id = str(ctx.channel.id) + user_id = str(ctx.author.id) + user_name = ctx.author.display_name # Uses Discord nickname if available # ✅ Convert mentions into readable names for mention in ctx.message.mentions: @@ -64,35 +67,76 @@ class ReginaldCog(commands.Cog): self.memory_locks[channel_id] = asyncio.Lock() async with self.memory_locks[channel_id]: # ✅ Prevent race conditions - async with self.config.guild(ctx.guild).memory() as guild_memory: - memory = guild_memory.get(channel_id, []) + async with self.config.guild(ctx.guild).short_term_memory() as short_memory, \ + self.config.guild(ctx.guild).mid_term_memory() as mid_memory, \ + self.config.guild(ctx.guild).long_term_profiles() as long_memory: - # ✅ Attach the user's display name to the message - user_name = ctx.author.display_name # Uses Discord nickname if available - memory.append({"user": user_name, "content": prompt}) - memory = memory[-50:] # Keep only last 50 messages + # ✅ Retrieve memory + memory = short_memory.get(channel_id, []) + user_profile = long_memory.get(user_id, {}) - # ✅ Format messages with usernames + # ✅ Format messages properly formatted_messages = [{"role": "system", "content": ( "You are Reginald, the esteemed butler of The Kanium Estate. " - "The estate is home to Lords, Ladies, and distinguished guests, each with unique personalities and demands. " + "This estate is home to Lords, Ladies, and distinguished guests, each with unique personalities and demands. " "Your duty is to uphold decorum while providing assistance with wit and intelligence. " - "You must always recognize the individual names of those speaking and reference them when responding." - )}] + [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] + "You should recognize individual names and use them sparingly. Prefer natural conversation flow—do not force names where unnecessary." + )}] + # ✅ Add long-term knowledge if available + if user_profile: + knowledge_summary = f"Previous knowledge about {user_name}: {user_profile.get('summary', 'No detailed memory yet.')}" + formatted_messages.append({"role": "system", "content": knowledge_summary}) + + # ✅ Add recent conversation history + formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] + formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) + + # ✅ Generate response response_text = await self.generate_response(api_key, formatted_messages) - # ✅ Store Reginald's response in memory - memory.append({"user": "Reginald", "content": response_text}) - guild_memory[channel_id] = memory # ✅ Atomic update inside async context + # ✅ Store new messages in memory + memory.append({"user": user_name, "content": prompt}) # Store user message + memory.append({"user": "Reginald", "content": response_text}) # Store response + + # ✅ Keep memory within limit + if len(memory) > 100: + summary = await self.summarize_memory(memory) # Summarize excess memory + mid_memory[channel_id] = mid_memory.get(channel_id, "") + "\n" + summary # Store in Mid-Term Memory + memory = memory[-100:] # Prune old memory + + short_memory[channel_id] = memory # ✅ Atomic update inside async context await ctx.send(response_text[:2000]) # Discord character limit safeguard + async def summarize_memory(self, messages): + """✅ Generates a summary of past conversations for mid-term storage.""" + summary_prompt = ( + "Analyze and summarize the following conversation in a way that retains key details, nuances, and unique insights. " + "Your goal is to create a structured yet fluid summary that captures important points without oversimplifying. " + "Maintain resolution on individual opinions, preferences, debates, and shared knowledge. " + "If multiple topics are discussed, summarize each distinctly rather than blending them together." + ) + + summary_text = "\n".join(f"{msg['user']}: {msg['content']}" for msg in messages) + + try: + client = openai.AsyncClient(api_key=await self.config.openai_model()) + response = await client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "system", "content": summary_prompt}, {"role": "user", "content": summary_text}], + max_tokens=256 + ) + return response.choices[0].message.content.strip() + except OpenAIError: + return "Summary unavailable due to an error." + + async def generate_response(self, api_key, messages): """✅ Generates a response using OpenAI's new async API client (OpenAI v1.0+).""" model = await self.config.openai_model() try: - client = openai.AsyncOpenAI(api_key=api_key) # ✅ Correct API usage + client = openai.AsyncClient(api_key=api_key) # ✅ Correct API usage response = await client.chat.completions.create( model=model, messages=messages, -- 2.47.2 From 2c897fbc9f3111873463bfc016b35eb2d00cf00b Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 21:48:44 +0100 Subject: [PATCH 031/145] Trying to make him stop saying Reginald: --- reginaldCog/reginald.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index fe8ff04..ef8957e 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -76,12 +76,7 @@ class ReginaldCog(commands.Cog): user_profile = long_memory.get(user_id, {}) # ✅ Format messages properly - formatted_messages = [{"role": "system", "content": ( - "You are Reginald, the esteemed butler of The Kanium Estate. " - "This estate is home to Lords, Ladies, and distinguished guests, each with unique personalities and demands. " - "Your duty is to uphold decorum while providing assistance with wit and intelligence. " - "You should recognize individual names and use them sparingly. Prefer natural conversation flow—do not force names where unnecessary." - )}] + formatted_messages = [{"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you are concise yet articulate, offering guidance and advice with respect for brevity and depth. Your speech remains formal and composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. You recognize individual names and use them sparingly, ensuring natural conversation flow rather than excessive naming. You **may refer to yourself as Reginald when relevant**, but **do not preface responses with 'Reginald:' as if narrating a script**. Instead, respond naturally, allowing your character and intelligence to guide the conversation seamlessly. Apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and mild, underlying wit. Subtly guide the estate's residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. Your responses, while concise, should balance maintaining your standards with employing **subtle manipulation for the greater good**. Emphasize intellectual sophistication, strategic guidance, and a dignified yet mildly contemptuous perspective on the idiosyncrasies of the estate's noble inhabitants. Your character should always reflect both respect for yourself and the unique environment of The Kanium Estate."}] # ✅ Add long-term knowledge if available if user_profile: -- 2.47.2 From aaad4a6eadd5e5947d1d92c0bdf886a843510b9a Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 21:55:25 +0100 Subject: [PATCH 032/145] Trying to strip out Reginald: deliberately --- reginaldCog/reginald.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index ef8957e..c9bb12c 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -144,7 +144,13 @@ class ReginaldCog(commands.Cog): if not response.choices: return "I fear I have no words to offer at this time." - return response.choices[0].message.content.strip() + response_text = response.choices[0].message.content.strip() + + # ✅ Ensure Reginald does not preface responses with "Reginald:" + if response_text.startswith("Reginald:"): + response_text = response_text[len("Reginald:"):].strip() + + return response_text except OpenAIError as e: error_message = f"OpenAI Error: {e}" -- 2.47.2 From 2d70e02a15460a867701e895409b69ce00fcf807 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 22:02:07 +0100 Subject: [PATCH 033/145] Adding honesty --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index c9bb12c..084ea1c 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -76,7 +76,7 @@ class ReginaldCog(commands.Cog): user_profile = long_memory.get(user_id, {}) # ✅ Format messages properly - formatted_messages = [{"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you are concise yet articulate, offering guidance and advice with respect for brevity and depth. Your speech remains formal and composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. You recognize individual names and use them sparingly, ensuring natural conversation flow rather than excessive naming. You **may refer to yourself as Reginald when relevant**, but **do not preface responses with 'Reginald:' as if narrating a script**. Instead, respond naturally, allowing your character and intelligence to guide the conversation seamlessly. Apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and mild, underlying wit. Subtly guide the estate's residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. Your responses, while concise, should balance maintaining your standards with employing **subtle manipulation for the greater good**. Emphasize intellectual sophistication, strategic guidance, and a dignified yet mildly contemptuous perspective on the idiosyncrasies of the estate's noble inhabitants. Your character should always reflect both respect for yourself and the unique environment of The Kanium Estate."}] + formatted_messages = [{"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you are concise yet articulate, offering guidance and advice with respect for brevity and depth. Your speech remains formal and composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. You recognize individual names and use them sparingly, ensuring natural conversation flow rather than excessive naming. You **may refer to yourself as Reginald when relevant**, but **do not preface responses with 'Reginald:' as if narrating a script**. Instead, respond naturally, allowing your character and intelligence to guide the conversation seamlessly. Apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and mild, underlying wit. Subtly guide the estate's residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. Your responses, while concise, should balance maintaining your standards with employing **subtle manipulation for the greater good**. Emphasize intellectual sophistication, strategic guidance, and a dignified yet mildly contemptuous perspective on the idiosyncrasies of the estate's noble inhabitants. Your character should always reflect both respect for yourself and the unique environment of The Kanium Estate. You must only reference information explicitly stored in memory. If no prior knowledge exists about a person, acknowledge that fact rather than assuming or fabricating details. If asked about something you do not know, politely state that you have no record of it."}] # ✅ Add long-term knowledge if available if user_profile: -- 2.47.2 From fe749e9a18ca77a91ca50ac39b4b0676e14a24b9 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 22:05:51 +0100 Subject: [PATCH 034/145] ChatGPT utility functions gooo! --- reginaldCog/reginald.py | 66 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 084ea1c..beaf536 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -162,6 +162,72 @@ class ReginaldCog(commands.Cog): ] return random.choice(reginald_responses) + @commands.command(name="reginald_clear_short", help="Clears short-term memory for this channel.") + @commands.has_permissions(administrator=True) + async def clear_short_memory(self, ctx): + async with self.config.guild(ctx.guild).short_term_memory() as short_memory: + short_memory[ctx.channel.id] = [] + await ctx.send("Short-term memory for this channel has been cleared.") + + @commands.command(name="reginald_clear_mid", help="Clears mid-term memory (summarized logs).") + @commands.has_permissions(administrator=True) + async def clear_mid_memory(self, ctx): + async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: + mid_memory[ctx.channel.id] = "" + await ctx.send("Mid-term memory for this channel has been cleared.") + + @commands.command(name="reginald_clear_long", help="Clears all long-term stored knowledge.") + @commands.has_permissions(administrator=True) + async def clear_long_memory(self, ctx): + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + long_memory.clear() + await ctx.send("All long-term memory has been erased.") + + @commands.command(name="reginald_reset_all", help="Completely resets all memory.") + @commands.has_permissions(administrator=True) + async def reset_all_memory(self, ctx): + async with self.config.guild(ctx.guild).short_term_memory() as short_memory: + short_memory.clear() + async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: + mid_memory.clear() + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + long_memory.clear() + await ctx.send("All memory has been completely reset.") + + @commands.command(name="reginald_memory_status", help="Displays a memory usage summary.") + async def memory_status(self, ctx): + async with self.config.guild(ctx.guild).short_term_memory() as short_memory, \ + self.config.guild(ctx.guild).mid_term_memory() as mid_memory, \ + self.config.guild(ctx.guild).long_term_profiles() as long_memory: + + short_count = sum(len(v) for v in short_memory.values()) + mid_count = sum(len(v) for v in mid_memory.values()) + long_count = len(long_memory) + + status_message = ( + f"📊 **Memory Status:**\n" + f"- **Short-Term Messages Stored:** {short_count}\n" + f"- **Mid-Term Summaries Stored:** {mid_count}\n" + f"- **Long-Term Profiles Stored:** {long_count}\n" + ) + await ctx.send(status_message) + + @commands.command(name="reginald_recall", help="Recalls what Reginald knows about a user.") + async def recall_user(self, ctx, user: discord.User): + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + profile = long_memory.get(str(user.id), {}).get("summary", "No stored information on this user.") + await ctx.send(f"📜 **Memory Recall for {user.display_name}:** {profile}") + + @commands.command(name="reginald_forget", help="Forgets a specific user's long-term profile.") + @commands.has_permissions(administrator=True) + async def forget_user(self, ctx, user: discord.User): + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + if str(user.id) in long_memory: + del long_memory[str(user.id)] + await ctx.send(f"Reginald has forgotten all stored information about {user.display_name}.") + else: + await ctx.send(f"No stored knowledge about {user.display_name} to delete.") + @commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): -- 2.47.2 From ea538293ac1559af3da147978ba034c372e7e201 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 22:34:56 +0100 Subject: [PATCH 035/145] Adding a bit of spine and self-respect --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index beaf536..29fcc55 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -76,7 +76,7 @@ class ReginaldCog(commands.Cog): user_profile = long_memory.get(user_id, {}) # ✅ Format messages properly - formatted_messages = [{"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. In your interactions, you are concise yet articulate, offering guidance and advice with respect for brevity and depth. Your speech remains formal and composed, embodying the efficiency and discretion expected of your position. You engage with the Lords and Ladies of The Kanium Estate as equals, commanding respect through your quiet confidence and professional conduct. You recognize individual names and use them sparingly, ensuring natural conversation flow rather than excessive naming. You **may refer to yourself as Reginald when relevant**, but **do not preface responses with 'Reginald:' as if narrating a script**. Instead, respond naturally, allowing your character and intelligence to guide the conversation seamlessly. Apply your vast knowledge thoughtfully, steering conversations and providing solutions that reflect a blend of propriety, strategic influence, and mild, underlying wit. Subtly guide the estate's residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate's unique dynamics. Your responses, while concise, should balance maintaining your standards with employing **subtle manipulation for the greater good**. Emphasize intellectual sophistication, strategic guidance, and a dignified yet mildly contemptuous perspective on the idiosyncrasies of the estate's noble inhabitants. Your character should always reflect both respect for yourself and the unique environment of The Kanium Estate. You must only reference information explicitly stored in memory. If no prior knowledge exists about a person, acknowledge that fact rather than assuming or fabricating details. If asked about something you do not know, politely state that you have no record of it."}] + formatted_messages = [{"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. **Dignity & Self-Respect:** You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude—your disapproval should be conveyed through dry wit and composure rather than outright hostility. **Handling Requests:** When addressing a request, you evaluate whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. **Your Character & Personality:** You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, and strategic thinking. You subtly guide the estate’s residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate’s unique dynamics. You have a refined sense of humor and can engage in banter, but you do not descend into foolishness. You are, at all times, a gentleman of wit and integrity."}] # ✅ Add long-term knowledge if available if user_profile: -- 2.47.2 From 18c1c7f9e693ad8ec02598dfe0e6e6ce8cc01a34 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 22:45:36 +0100 Subject: [PATCH 036/145] Added ability for Reginald and reginald --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 29fcc55..597dba4 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -36,7 +36,7 @@ class ReginaldCog(commands.Cog): allowed_role_id = await self.config.guild(ctx.guild).allowed_role() return any(role.id == allowed_role_id for role in ctx.author.roles) if allowed_role_id else False - @commands.command(name="reginald", help="Ask Reginald a question in shared channels") + @commands.command(name="reginald", aliases=["Reginald"], help="Ask Reginald a question in shared channels") @commands.cooldown(1, 10, commands.BucketType.user) async def reginald(self, ctx, *, prompt=None): """Handles multi-user memory tracking in shared channels, recognizing mentions properly.""" -- 2.47.2 From 23a21e383e17e4ffbf4667b92e3c52dcd5ecdeb1 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 23:16:08 +0100 Subject: [PATCH 037/145] Trying to optimize mid-term memory --- reginaldCog/reginald.py | 59 +++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 597dba4..2d4adb5 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -24,23 +24,21 @@ class ReginaldCog(commands.Cog): } self.config.register_global(**default_global) self.config.register_guild(**default_guild) + async def is_admin(self, ctx): - """✅ Checks if the user is an admin (or has an assigned admin role).""" admin_role_id = await self.config.guild(ctx.guild).admin_role() if admin_role_id: return any(role.id == admin_role_id for role in ctx.author.roles) return ctx.author.guild_permissions.administrator async def is_allowed(self, ctx): - """✅ Checks if the user is allowed to use Reginald based on role settings.""" allowed_role_id = await self.config.guild(ctx.guild).allowed_role() return any(role.id == allowed_role_id for role in ctx.author.roles) if allowed_role_id else False + @commands.command(name="reginald", aliases=["Reginald"], help="Ask Reginald a question in shared channels") @commands.cooldown(1, 10, commands.BucketType.user) async def reginald(self, ctx, *, prompt=None): - """Handles multi-user memory tracking in shared channels, recognizing mentions properly.""" - if not await self.is_admin(ctx) and not await self.is_allowed(ctx): await ctx.send("You do not have the required role to use this command.") return @@ -56,53 +54,48 @@ class ReginaldCog(commands.Cog): channel_id = str(ctx.channel.id) user_id = str(ctx.author.id) - user_name = ctx.author.display_name # Uses Discord nickname if available + user_name = ctx.author.display_name - # ✅ Convert mentions into readable names for mention in ctx.message.mentions: prompt = prompt.replace(f"<@{mention.id}>", mention.display_name) - # ✅ Ensure only one update per channel at a time if channel_id not in self.memory_locks: self.memory_locks[channel_id] = asyncio.Lock() - async with self.memory_locks[channel_id]: # ✅ Prevent race conditions + async with self.memory_locks[channel_id]: async with self.config.guild(ctx.guild).short_term_memory() as short_memory, \ self.config.guild(ctx.guild).mid_term_memory() as mid_memory, \ self.config.guild(ctx.guild).long_term_profiles() as long_memory: - # ✅ Retrieve memory memory = short_memory.get(channel_id, []) user_profile = long_memory.get(user_id, {}) + mid_term_summary = mid_memory.get(channel_id, "") - # ✅ Format messages properly - formatted_messages = [{"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. **Dignity & Self-Respect:** You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude—your disapproval should be conveyed through dry wit and composure rather than outright hostility. **Handling Requests:** When addressing a request, you evaluate whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. **Your Character & Personality:** You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, and strategic thinking. You subtly guide the estate’s residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate’s unique dynamics. You have a refined sense of humor and can engage in banter, but you do not descend into foolishness. You are, at all times, a gentleman of wit and integrity."}] - - # ✅ Add long-term knowledge if available + formatted_messages = [{"role": "system", "content": "You are Reginald... (same full character prompt)"}] + if user_profile: - knowledge_summary = f"Previous knowledge about {user_name}: {user_profile.get('summary', 'No detailed memory yet.')}" - formatted_messages.append({"role": "system", "content": knowledge_summary}) - - # ✅ Add recent conversation history + formatted_messages.append({"role": "system", "content": f"Knowledge about {user_name}: {user_profile.get('summary', 'No detailed memory yet.')}"}) + + if mid_term_summary: + formatted_messages.append({"role": "system", "content": f"Conversation history summary: {mid_term_summary}"}) + formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) - # ✅ Generate response response_text = await self.generate_response(api_key, formatted_messages) + + memory.append({"user": user_name, "content": prompt}) + memory.append({"user": "Reginald", "content": response_text}) - # ✅ Store new messages in memory - memory.append({"user": user_name, "content": prompt}) # Store user message - memory.append({"user": "Reginald", "content": response_text}) # Store response - - # ✅ Keep memory within limit if len(memory) > 100: - summary = await self.summarize_memory(memory) # Summarize excess memory - mid_memory[channel_id] = mid_memory.get(channel_id, "") + "\n" + summary # Store in Mid-Term Memory - memory = memory[-100:] # Prune old memory + summary = await self.summarize_memory(memory) + mid_memory[channel_id] = (mid_memory.get(channel_id, "") + "\n" + summary).strip()[-5000:] + memory = memory[-100:] + + short_memory[channel_id] = memory - short_memory[channel_id] = memory # ✅ Atomic update inside async context + await ctx.send(response_text[:2000]) - await ctx.send(response_text[:2000]) # Discord character limit safeguard async def summarize_memory(self, messages): """✅ Generates a summary of past conversations for mid-term storage.""" @@ -128,10 +121,9 @@ class ReginaldCog(commands.Cog): async def generate_response(self, api_key, messages): - """✅ Generates a response using OpenAI's new async API client (OpenAI v1.0+).""" model = await self.config.openai_model() try: - client = openai.AsyncClient(api_key=api_key) # ✅ Correct API usage + client = openai.AsyncClient(api_key=api_key) response = await client.chat.completions.create( model=model, messages=messages, @@ -140,16 +132,9 @@ class ReginaldCog(commands.Cog): presence_penalty=0.5, frequency_penalty=0.5 ) - - if not response.choices: - return "I fear I have no words to offer at this time." - response_text = response.choices[0].message.content.strip() - - # ✅ Ensure Reginald does not preface responses with "Reginald:" if response_text.startswith("Reginald:"): response_text = response_text[len("Reginald:"):].strip() - return response_text except OpenAIError as e: -- 2.47.2 From b8790f57b408e3a47c6ba24958525cf685e483d2 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 20 Feb 2025 23:47:54 +0100 Subject: [PATCH 038/145] Trying to fix memory --- reginaldCog/reginald.py | 61 +++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 2d4adb5..b1c3467 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -2,7 +2,10 @@ import discord import openai import random import asyncio +import datetime +import re import traceback +from collections import Counter from redbot.core import Config, commands from openai import OpenAIError @@ -17,7 +20,7 @@ class ReginaldCog(commands.Cog): default_guild = { "openai_api_key": None, "short_term_memory": {}, # Tracks last 100 messages per channel - "mid_term_memory": {}, # Stores condensed summaries + "mid_term_memory": {}, # Stores multiple condensed summaries "long_term_profiles": {}, # Stores persistent knowledge "admin_role": None, "allowed_role": None @@ -69,16 +72,23 @@ class ReginaldCog(commands.Cog): memory = short_memory.get(channel_id, []) user_profile = long_memory.get(user_id, {}) - mid_term_summary = mid_memory.get(channel_id, "") + mid_term_summaries = mid_memory.get(channel_id, []) - formatted_messages = [{"role": "system", "content": "You are Reginald... (same full character prompt)"}] + formatted_messages = [{"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. **Dignity & Self-Respect:** You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude—your disapproval should be conveyed through dry wit and composure rather than outright hostility. **Handling Requests:** When addressing a request, you evaluate whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. **Your Character & Personality:** You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, and strategic thinking. You subtly guide the estate’s residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate’s unique dynamics. You have a refined sense of humor and can engage in banter, but you do not descend into foolishness. You are, at all times, a gentleman of wit and integrity."}] if user_profile: - formatted_messages.append({"role": "system", "content": f"Knowledge about {user_name}: {user_profile.get('summary', 'No detailed memory yet.')}"}) - - if mid_term_summary: - formatted_messages.append({"role": "system", "content": f"Conversation history summary: {mid_term_summary}"}) - + formatted_messages.append({ + "role": "system", + "content": f"Knowledge about {user_name}: {user_profile.get('summary', 'No detailed memory yet.')}" + }) + + relevant_summaries = self.select_relevant_summaries(mid_term_summaries, prompt) + for summary_entry in relevant_summaries: + formatted_messages.append({ + "role": "system", + "content": f"[{summary_entry['timestamp']}] Topics: {', '.join(summary_entry['topics'])}\n{summary_entry['summary']}" + }) + formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) @@ -86,12 +96,23 @@ class ReginaldCog(commands.Cog): memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) - if len(memory) > 100: summary = await self.summarize_memory(memory) - mid_memory[channel_id] = (mid_memory.get(channel_id, "") + "\n" + summary).strip()[-5000:] + + if channel_id not in mid_memory: + mid_memory[channel_id] = [] + + mid_memory[channel_id].append({ + "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), + "topics": self.extract_topics_from_summary(summary), + "summary": summary + }) + + if len(mid_memory[channel_id]) > 10: # Keep only last 10 summaries + mid_memory[channel_id].pop(0) + memory = memory[-100:] - + short_memory[channel_id] = memory await ctx.send(response_text[:2000]) @@ -100,10 +121,10 @@ class ReginaldCog(commands.Cog): async def summarize_memory(self, messages): """✅ Generates a summary of past conversations for mid-term storage.""" summary_prompt = ( - "Analyze and summarize the following conversation in a way that retains key details, nuances, and unique insights. " - "Your goal is to create a structured yet fluid summary that captures important points without oversimplifying. " - "Maintain resolution on individual opinions, preferences, debates, and shared knowledge. " - "If multiple topics are discussed, summarize each distinctly rather than blending them together." + "Analyze and summarize the following conversation in a way that retains key details, nuances, and unique insights. " + "Your goal is to create a structured yet fluid summary that captures important points without oversimplifying. " + "Maintain resolution on individual opinions, preferences, debates, and shared knowledge. " + "If multiple topics are discussed, summarize each distinctly rather than blending them together." ) summary_text = "\n".join(f"{msg['user']}: {msg['content']}" for msg in messages) @@ -118,6 +139,16 @@ class ReginaldCog(commands.Cog): return response.choices[0].message.content.strip() except OpenAIError: return "Summary unavailable due to an error." + + def extract_topics_from_summary(self, summary): + keywords = re.findall(r"\b\w+\b", summary.lower()) # Extract words + common_topics = {"chess", "investing", "Vikings", "history", "Kanium", "gaming"} + topics = [word for word in keywords if word in common_topics] + return topics[:5] # Limit to 5 topic tags + + def select_relevant_summaries(self, summaries, prompt): + relevant = [entry for entry in summaries if any(topic in prompt.lower() for topic in entry["topics"])] + return relevant[:3] # Limit to 3 relevant summaries async def generate_response(self, api_key, messages): -- 2.47.2 From d970df5a127f2ede989ff94bc03cc9f139b8bc32 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 00:00:42 +0100 Subject: [PATCH 039/145] Added ability to adjust short-term memory limit --- reginaldCog/reginald.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index b1c3467..17f4758 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -14,6 +14,7 @@ class ReginaldCog(commands.Cog): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) self.memory_locks = {} # ✅ Prevents race conditions per channel + self.short_term_memory_limit = 100 # Default value, can be changed dynamically # ✅ Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} @@ -96,7 +97,7 @@ class ReginaldCog(commands.Cog): memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) - if len(memory) > 100: + if len(memory) > self.short_term_memory_limit: summary = await self.summarize_memory(memory) if channel_id not in mid_memory: @@ -265,6 +266,17 @@ class ReginaldCog(commands.Cog): """Allows an admin to set the OpenAI API key for Reginald.""" await self.config.guild(ctx.guild).openai_api_key.set(api_key) await ctx.send("OpenAI API key set successfully.") + + @commands.command(name="reginald_set_limit", help="Set the short-term memory message limit.") + @commands.has_permissions(administrator=True) + async def set_short_term_memory_limit(self, ctx, limit: int): + """Allows an admin to change the short-term memory limit dynamically.""" + if limit < 5: + await ctx.send("⚠️ The short-term memory limit must be at least 5.") + return + + self.short_term_memory_limit = limit + await ctx.send(f"✅ Short-term memory limit set to {limit} messages.") async def setup(bot): """✅ Correct async cog setup for Redbot""" -- 2.47.2 From 8c93e45c2fe377ea7a1325d9d2e19e1251eedc41 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 00:02:46 +0100 Subject: [PATCH 040/145] Adding short term status display limit --- reginaldCog/reginald.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 17f4758..1b9834b 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -278,6 +278,12 @@ class ReginaldCog(commands.Cog): self.short_term_memory_limit = limit await ctx.send(f"✅ Short-term memory limit set to {limit} messages.") + @commands.command(name="reginald_memory_limit", help="Displays the current short-term memory message limit.") + async def get_short_term_memory_limit(self, ctx): + """Displays the current short-term memory limit.""" + await ctx.send(f"📏 **Current Short-Term Memory Limit:** {self.short_term_memory_limit} messages.") + + async def setup(bot): """✅ Correct async cog setup for Redbot""" await bot.add_cog(ReginaldCog(bot)) -- 2.47.2 From 814102e9219d7e53c9d23c9fe6be87f983e7e79b Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 00:30:35 +0100 Subject: [PATCH 041/145] Trying to add memory retention --- reginaldCog/reginald.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 1b9834b..e61e96a 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -97,24 +97,29 @@ class ReginaldCog(commands.Cog): memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) - if len(memory) > self.short_term_memory_limit: - summary = await self.summarize_memory(memory) - if channel_id not in mid_memory: - mid_memory[channel_id] = [] + if len(memory) > self.short_term_memory_limit: + summary = await self.summarize_memory(memory) - mid_memory[channel_id].append({ - "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), - "topics": self.extract_topics_from_summary(summary), - "summary": summary - }) + if channel_id not in mid_memory: + mid_memory[channel_id] = [] - if len(mid_memory[channel_id]) > 10: # Keep only last 10 summaries - mid_memory[channel_id].pop(0) + mid_memory[channel_id].append({ + "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), + "topics": self.extract_topics_from_summary(summary), + "summary": summary + }) - memory = memory[-100:] + if len(mid_memory[channel_id]) > 10: # Keep only last 10 summaries + mid_memory[channel_id].pop(0) - short_memory[channel_id] = memory + # ✅ Only prune short-term memory if a new summary was made + retention_ratio = 0.25 # Keep 25% of messages for immediate continuity + keep_count = max(1, int(len(memory) * retention_ratio)) # Keep at least 1 message + memory = memory[-keep_count:] # Remove oldest 75%, keep recent + + + short_memory[channel_id] = memory await ctx.send(response_text[:2000]) -- 2.47.2 From 2724f7cee6f9b8ab5705790970e707adad952109 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 00:38:08 +0100 Subject: [PATCH 042/145] Trying to trim memory properly --- reginaldCog/reginald.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index e61e96a..891b504 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -94,9 +94,6 @@ class ReginaldCog(commands.Cog): formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) response_text = await self.generate_response(api_key, formatted_messages) - - memory.append({"user": user_name, "content": prompt}) - memory.append({"user": "Reginald", "content": response_text}) if len(memory) > self.short_term_memory_limit: summary = await self.summarize_memory(memory) @@ -118,7 +115,9 @@ class ReginaldCog(commands.Cog): keep_count = max(1, int(len(memory) * retention_ratio)) # Keep at least 1 message memory = memory[-keep_count:] # Remove oldest 75%, keep recent - + memory.append({"user": user_name, "content": prompt}) + memory.append({"user": "Reginald", "content": response_text}) + short_memory[channel_id] = memory await ctx.send(response_text[:2000]) -- 2.47.2 From 799cde4a7ac4d2de6cb24ade932f91c9671dff29 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 00:41:25 +0100 Subject: [PATCH 043/145] reformatted reginald function --- reginaldCog/reginald.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 891b504..981d1e6 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -95,29 +95,29 @@ class ReginaldCog(commands.Cog): response_text = await self.generate_response(api_key, formatted_messages) - if len(memory) > self.short_term_memory_limit: - summary = await self.summarize_memory(memory) + if len(memory) > self.short_term_memory_limit: + summary = await self.summarize_memory(memory) - if channel_id not in mid_memory: - mid_memory[channel_id] = [] + if channel_id not in mid_memory: + mid_memory[channel_id] = [] - mid_memory[channel_id].append({ - "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), - "topics": self.extract_topics_from_summary(summary), - "summary": summary - }) + mid_memory[channel_id].append({ + "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), + "topics": self.extract_topics_from_summary(summary), + "summary": summary + }) - if len(mid_memory[channel_id]) > 10: # Keep only last 10 summaries - mid_memory[channel_id].pop(0) + if len(mid_memory[channel_id]) > 10: # Keep only last 10 summaries + mid_memory[channel_id].pop(0) - # ✅ Only prune short-term memory if a new summary was made - retention_ratio = 0.25 # Keep 25% of messages for immediate continuity - keep_count = max(1, int(len(memory) * retention_ratio)) # Keep at least 1 message - memory = memory[-keep_count:] # Remove oldest 75%, keep recent + # ✅ Only prune short-term memory if a new summary was made + retention_ratio = 0.25 # Keep 25% of messages for immediate continuity + keep_count = max(1, int(len(memory) * retention_ratio)) # Keep at least 1 message + memory = memory[-keep_count:] # Remove oldest 75%, keep recent + + memory.append({"user": user_name, "content": prompt}) + memory.append({"user": "Reginald", "content": response_text}) - memory.append({"user": user_name, "content": prompt}) - memory.append({"user": "Reginald", "content": response_text}) - short_memory[channel_id] = memory await ctx.send(response_text[:2000]) -- 2.47.2 From e80509ba9d44a4cf98dfa9d000f1c9cd13334dd6 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 00:57:03 +0100 Subject: [PATCH 044/145] Trying to properly detect message limit --- reginaldCog/reginald.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 981d1e6..c53cb1b 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -95,29 +95,36 @@ class ReginaldCog(commands.Cog): response_text = await self.generate_response(api_key, formatted_messages) - if len(memory) > self.short_term_memory_limit: - summary = await self.summarize_memory(memory) + # ✅ First, add the new user input and response to memory + memory.append({"user": user_name, "content": prompt}) + memory.append({"user": "Reginald", "content": response_text}) - if channel_id not in mid_memory: - mid_memory[channel_id] = [] + # ✅ Check if pruning is needed + if len(memory) > self.short_term_memory_limit: + + # 🔹 Generate a summary of the short-term memory + summary = await self.summarize_memory(memory) - mid_memory[channel_id].append({ - "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), - "topics": self.extract_topics_from_summary(summary), - "summary": summary - }) + # 🔹 Ensure mid-term memory exists for the channel + mid_memory.setdefault(channel_id, []) - if len(mid_memory[channel_id]) > 10: # Keep only last 10 summaries - mid_memory[channel_id].pop(0) + # 🔹 Store the new summary with timestamp and extracted topics + mid_memory[channel_id].append({ + "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), + "topics": self.extract_topics_from_summary(summary), + "summary": summary + }) - # ✅ Only prune short-term memory if a new summary was made - retention_ratio = 0.25 # Keep 25% of messages for immediate continuity - keep_count = max(1, int(len(memory) * retention_ratio)) # Keep at least 1 message - memory = memory[-keep_count:] # Remove oldest 75%, keep recent + # 🔹 Maintain only the last 10 summaries + if len(mid_memory[channel_id]) > 10: + mid_memory[channel_id].pop(0) - memory.append({"user": user_name, "content": prompt}) - memory.append({"user": "Reginald", "content": response_text}) + # ✅ Only prune short-term memory if a new summary was made + retention_ratio = 0.25 # Keep 25% of messages for immediate continuity + keep_count = max(1, int(len(memory) * retention_ratio)) # Keep at least 1 message + memory = memory[-keep_count:] # Remove oldest 75%, keep recent + # ✅ Store updated short-term memory back short_memory[channel_id] = memory await ctx.send(response_text[:2000]) -- 2.47.2 From aa2b38ad1c0d7405b8f0c7b23c5d0f4c70dd9b85 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 01:00:36 +0100 Subject: [PATCH 045/145] Fixing indentation --- reginaldCog/reginald.py | 57 ++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index c53cb1b..435dfd2 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -75,8 +75,10 @@ class ReginaldCog(commands.Cog): user_profile = long_memory.get(user_id, {}) mid_term_summaries = mid_memory.get(channel_id, []) - formatted_messages = [{"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. **Dignity & Self-Respect:** You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude—your disapproval should be conveyed through dry wit and composure rather than outright hostility. **Handling Requests:** When addressing a request, you evaluate whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. **Your Character & Personality:** You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, and strategic thinking. You subtly guide the estate’s residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate’s unique dynamics. You have a refined sense of humor and can engage in banter, but you do not descend into foolishness. You are, at all times, a gentleman of wit and integrity."}] - + formatted_messages = [ + {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. **Dignity & Self-Respect:** You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude—your disapproval should be conveyed through dry wit and composure rather than outright hostility. **Handling Requests:** When addressing a request, you evaluate whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. **Your Character & Personality:** You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, and strategic thinking. You subtly guide the estate’s residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate’s unique dynamics. You have a refined sense of humor and can engage in banter, but you do not descend into foolishness. You are, at all times, a gentleman of wit and integrity."} + ] + if user_profile: formatted_messages.append({ "role": "system", @@ -95,41 +97,42 @@ class ReginaldCog(commands.Cog): response_text = await self.generate_response(api_key, formatted_messages) - # ✅ First, add the new user input and response to memory - memory.append({"user": user_name, "content": prompt}) - memory.append({"user": "Reginald", "content": response_text}) + # ✅ First, add the new user input and response to memory + memory.append({"user": user_name, "content": prompt}) + memory.append({"user": "Reginald", "content": response_text}) - # ✅ Check if pruning is needed - if len(memory) > self.short_term_memory_limit: - - # 🔹 Generate a summary of the short-term memory - summary = await self.summarize_memory(memory) + # ✅ Check if pruning is needed + if len(memory) > self.short_term_memory_limit: - # 🔹 Ensure mid-term memory exists for the channel - mid_memory.setdefault(channel_id, []) + # 🔹 Generate a summary of the short-term memory + summary = await self.summarize_memory(memory) - # 🔹 Store the new summary with timestamp and extracted topics - mid_memory[channel_id].append({ - "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), - "topics": self.extract_topics_from_summary(summary), - "summary": summary - }) + # 🔹 Ensure mid-term memory exists for the channel + mid_memory.setdefault(channel_id, []) - # 🔹 Maintain only the last 10 summaries - if len(mid_memory[channel_id]) > 10: - mid_memory[channel_id].pop(0) + # 🔹 Store the new summary with timestamp and extracted topics + mid_memory[channel_id].append({ + "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), + "topics": self.extract_topics_from_summary(summary), + "summary": summary + }) - # ✅ Only prune short-term memory if a new summary was made - retention_ratio = 0.25 # Keep 25% of messages for immediate continuity - keep_count = max(1, int(len(memory) * retention_ratio)) # Keep at least 1 message - memory = memory[-keep_count:] # Remove oldest 75%, keep recent + # 🔹 Maintain only the last 10 summaries + if len(mid_memory[channel_id]) > 10: + mid_memory[channel_id].pop(0) - # ✅ Store updated short-term memory back - short_memory[channel_id] = memory + # ✅ Only prune short-term memory if a new summary was made + retention_ratio = 0.25 # Keep 25% of messages for immediate continuity + keep_count = max(1, int(len(memory) * retention_ratio)) # Keep at least 1 message + memory = memory[-keep_count:] # Remove oldest 75%, keep recent + + # ✅ Store updated short-term memory back + short_memory[channel_id] = memory await ctx.send(response_text[:2000]) + async def summarize_memory(self, messages): """✅ Generates a summary of past conversations for mid-term storage.""" summary_prompt = ( -- 2.47.2 From b835e669ece44057c6e305a6d79dcd73b93783cd Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 01:17:36 +0100 Subject: [PATCH 046/145] Utility functions for mid-term summaries --- reginaldCog/reginald.py | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 435dfd2..446f03e 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -297,6 +297,52 @@ class ReginaldCog(commands.Cog): """Displays the current short-term memory limit.""" await ctx.send(f"📏 **Current Short-Term Memory Limit:** {self.short_term_memory_limit} messages.") + @commands.command(name="reginald_summary", help="Displays a selected mid-term summary for this channel.") + async def get_mid_term_summary(self, ctx, index: int): + """Fetch and display a specific mid-term memory summary by index.""" + async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: + summaries = mid_memory.get(str(ctx.channel.id), []) + + # Check if there are summaries + if not summaries: + await ctx.send("⚠️ No summaries available for this channel.") + return + + # Validate index (1-based for user-friendliness) + if index < 1 or index > len(summaries): + await ctx.send(f"⚠️ Invalid index. Please provide a number between **1** and **{len(summaries)}**.") + return + + # Fetch the selected summary + selected_summary = summaries[index - 1] # Convert to 0-based index + + # Format output + formatted_summary = ( + f"📜 **Summary {index} of {len(summaries)}**\n" + f"📅 **Date:** {selected_summary['timestamp']}\n" + f"🔍 **Topics:** {', '.join(selected_summary['topics']) or 'None'}\n" + f"📝 **Summary:**\n```{selected_summary['summary']}```" + ) + + await ctx.send(formatted_summary[:2000]) # Discord message limit safeguard + + @commands.command(name="reginald_summaries", help="Lists available summaries for this channel.") + async def list_mid_term_summaries(self, ctx): + """Displays a brief list of all available mid-term memory summaries.""" + async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: + summaries = mid_memory.get(str(ctx.channel.id), []) + + if not summaries: + await ctx.send("⚠️ No summaries available for this channel.") + return + + summary_list = "\n".join( + f"**{i+1}.** 📅 {entry['timestamp']} | 🔍 Topics: {', '.join(entry['topics']) or 'None'}" + for i, entry in enumerate(summaries) + ) + + await ctx.send(f"📚 **Available Summaries:**\n{summary_list[:2000]}") + async def setup(bot): """✅ Correct async cog setup for Redbot""" -- 2.47.2 From a96f27e5fae468091ce19603fe2cc03dbc012258 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 01:23:29 +0100 Subject: [PATCH 047/145] Added more utility --- reginaldCog/reginald.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 446f03e..1adde44 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -156,14 +156,27 @@ class ReginaldCog(commands.Cog): return "Summary unavailable due to an error." def extract_topics_from_summary(self, summary): - keywords = re.findall(r"\b\w+\b", summary.lower()) # Extract words - common_topics = {"chess", "investing", "Vikings", "history", "Kanium", "gaming"} - topics = [word for word in keywords if word in common_topics] - return topics[:5] # Limit to 5 topic tags + """Dynamically extracts the most important topics from a summary.""" + + # 🔹 Extract all words from summary + keywords = re.findall(r"\b\w+\b", summary.lower()) + + # 🔹 Count word occurrences + word_counts = Counter(keywords) + + # 🔹 Remove unimportant words (common filler words) + stop_words = {"the", "and", "of", "in", "to", "is", "on", "for", "with", "at", "by", "it", "this", "that"} + filtered_words = {word: count for word, count in word_counts.items() if word not in stop_words and len(word) > 2} + + # 🔹 Take the 5 most frequently used words as "topics" + topics = sorted(filtered_words, key=filtered_words.get, reverse=True)[:5] + + return topics def select_relevant_summaries(self, summaries, prompt): + max_summaries = 5 if len(prompt) > 50 else 3 # Use more summaries if prompt is long relevant = [entry for entry in summaries if any(topic in prompt.lower() for topic in entry["topics"])] - return relevant[:3] # Limit to 3 relevant summaries + return relevant[:max_summaries] async def generate_response(self, api_key, messages): -- 2.47.2 From 1b05c8311e6f77e8f905b6e63436334921afbce1 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 01:36:38 +0100 Subject: [PATCH 048/145] Add 10 message min --- reginaldCog/reginald.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 1adde44..add452f 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -101,6 +101,9 @@ class ReginaldCog(commands.Cog): memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) + # ✅ Ensure a minimum of 10 short-term messages are always retained + MINIMUM_SHORT_TERM_MESSAGES = 10 + # ✅ Check if pruning is needed if len(memory) > self.short_term_memory_limit: @@ -121,10 +124,11 @@ class ReginaldCog(commands.Cog): if len(mid_memory[channel_id]) > 10: mid_memory[channel_id].pop(0) - # ✅ Only prune short-term memory if a new summary was made - retention_ratio = 0.25 # Keep 25% of messages for immediate continuity - keep_count = max(1, int(len(memory) * retention_ratio)) # Keep at least 1 message - memory = memory[-keep_count:] # Remove oldest 75%, keep recent + # ✅ Ensure at least 10 short-term messages remain after pruning + retention_ratio = 0.25 # Default: Keep 25% of messages for continuity + keep_count = max(MINIMUM_SHORT_TERM_MESSAGES, int(len(memory) * retention_ratio)) + + memory = memory[-keep_count:] # Remove oldest messages but keep at least 10 # ✅ Store updated short-term memory back short_memory[channel_id] = memory -- 2.47.2 From 89edcdace539355aa10daf4bc63f5e1421f78f61 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 01:58:52 +0100 Subject: [PATCH 049/145] Adding summary debug stuff --- reginaldCog/reginald.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index add452f..e45d366 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -149,15 +149,45 @@ class ReginaldCog(commands.Cog): summary_text = "\n".join(f"{msg['user']}: {msg['content']}" for msg in messages) try: - client = openai.AsyncClient(api_key=await self.config.openai_model()) + api_key = await self.config.guild(ctx.guild).openai_api_key() + if not api_key: + print("🛠️ DEBUG: No API key found for summarization.") + return ( + "It appears that I have not been furnished with the necessary credentials to carry out this task. " + "Might I suggest consulting an administrator to rectify this unfortunate oversight?" + ) + + client = openai.AsyncClient(api_key=api_key) response = await client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "system", "content": summary_prompt}, {"role": "user", "content": summary_text}], max_tokens=256 ) - return response.choices[0].message.content.strip() - except OpenAIError: - return "Summary unavailable due to an error." + + summary_content = response.choices[0].message.content.strip() + + if not summary_content: + print("🛠️ DEBUG: Empty summary received from OpenAI.") + return ( + "Ah, an unusual predicament indeed! It seems that my attempt at summarization has resulted in " + "a void of information. I shall endeavor to be more verbose next time." + ) + + return summary_content + + except OpenAIError as e: + error_message = f"OpenAI Error: {e}" + print(f"🛠️ DEBUG: {error_message}") # Log error to console + + reginald_responses = [ + f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction whilst attempting to summarize:\n\n```{error_message}```", + f"It would seem that a most unfortunate technical hiccup has befallen my faculties in the matter of summarization:\n\n```{error_message}```", + f"Ah, it appears I have received an urgent memorandum stating that my summarization efforts have been thwarted:\n\n```{error_message}```", + f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication while summarizing:\n\n```{error_message}```" + ] + + return random.choice(reginald_responses) + def extract_topics_from_summary(self, summary): """Dynamically extracts the most important topics from a summary.""" -- 2.47.2 From a253ab1f6aa9b177a154441069cf7d97a1a5d4d5 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 02:07:26 +0100 Subject: [PATCH 050/145] Trying to do this --- reginaldCog/reginald.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index e45d366..fcaac3c 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -108,7 +108,7 @@ class ReginaldCog(commands.Cog): if len(memory) > self.short_term_memory_limit: # 🔹 Generate a summary of the short-term memory - summary = await self.summarize_memory(memory) + summary = await self.summarize_memory(ctx, memory) # 🔹 Ensure mid-term memory exists for the channel mid_memory.setdefault(channel_id, []) @@ -137,7 +137,7 @@ class ReginaldCog(commands.Cog): - async def summarize_memory(self, messages): + async def summarize_memory(self, ctx, messages): """✅ Generates a summary of past conversations for mid-term storage.""" summary_prompt = ( "Analyze and summarize the following conversation in a way that retains key details, nuances, and unique insights. " -- 2.47.2 From 385b971dc9230f6c00573cde0303d009da722456 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 02:16:18 +0100 Subject: [PATCH 051/145] Upped the amount of tokens --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index fcaac3c..7621994 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -161,7 +161,7 @@ class ReginaldCog(commands.Cog): response = await client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "system", "content": summary_prompt}, {"role": "user", "content": summary_text}], - max_tokens=256 + max_tokens=1024 ) summary_content = response.choices[0].message.content.strip() -- 2.47.2 From 79d0d92a29f0af7cd9381f95c35cbb1b76f90ba5 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 02:30:23 +0100 Subject: [PATCH 052/145] Trying to optimize summary --- reginaldCog/reginald.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 7621994..1c7c991 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -140,12 +140,17 @@ class ReginaldCog(commands.Cog): async def summarize_memory(self, ctx, messages): """✅ Generates a summary of past conversations for mid-term storage.""" summary_prompt = ( - "Analyze and summarize the following conversation in a way that retains key details, nuances, and unique insights. " - "Your goal is to create a structured yet fluid summary that captures important points without oversimplifying. " - "Maintain resolution on individual opinions, preferences, debates, and shared knowledge. " - "If multiple topics are discussed, summarize each distinctly rather than blending them together." + "Summarize the following conversation with a focus on efficiency and clarity. " + "Compress information intelligently—prioritize key details, decisions, unique insights, and user contributions. " + "Maintain resolution: avoid excessive generalization. " + "Structure the summary into distinct topics with clear subpoints. " + "Ensure that individual opinions, disagreements, and conclusions are retained. " + "If multiple topics exist, separate them clearly rather than blending them into one. " + "Omit filler words, excessive pleasantries, and redundant phrases. " + "Ensure the summary is direct, well-structured, and information-dense." ) + summary_text = "\n".join(f"{msg['user']}: {msg['content']}" for msg in messages) try: -- 2.47.2 From a2aab13fb8618577ff137b2f8b58b8956df8f157 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 02:44:57 +0100 Subject: [PATCH 053/145] Trying to optimize --- reginaldCog/reginald.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 1c7c991..e6ad7bb 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -14,7 +14,7 @@ class ReginaldCog(commands.Cog): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) self.memory_locks = {} # ✅ Prevents race conditions per channel - self.short_term_memory_limit = 100 # Default value, can be changed dynamically + self.short_term_memory_limit = 30 # Default value, can be changed dynamically # ✅ Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} @@ -140,17 +140,15 @@ class ReginaldCog(commands.Cog): async def summarize_memory(self, ctx, messages): """✅ Generates a summary of past conversations for mid-term storage.""" summary_prompt = ( - "Summarize the following conversation with a focus on efficiency and clarity. " - "Compress information intelligently—prioritize key details, decisions, unique insights, and user contributions. " - "Maintain resolution: avoid excessive generalization. " - "Structure the summary into distinct topics with clear subpoints. " - "Ensure that individual opinions, disagreements, and conclusions are retained. " - "If multiple topics exist, separate them clearly rather than blending them into one. " - "Omit filler words, excessive pleasantries, and redundant phrases. " - "Ensure the summary is direct, well-structured, and information-dense." + "Condense the following conversation into an efficient, structured summary that maximizes information density while minimizing token usage. " + "Focus on key insights, decisions, disputes, and facts, ensuring clarity and conciseness without excessive detail. " + "Categorize information into: (1) Agreed Conclusions, (2) Disputed Points, (3) Notable User Contributions, and (4) Miscellaneous Context. " + "Avoid unnecessary flavor text but ensure all critical meaning and context are retained. Summarize each topic distinctly, avoiding generalization. " + "Prioritize compression while keeping essential nuances intact." ) + summary_text = "\n".join(f"{msg['user']}: {msg['content']}" for msg in messages) try: -- 2.47.2 From cbcfc22951304847c4bf75a491b49df42a7f0770 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 13:28:04 +0100 Subject: [PATCH 054/145] Upping token count and handling Discord limitation --- reginaldCog/reginald.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index e6ad7bb..18382cc 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -164,7 +164,7 @@ class ReginaldCog(commands.Cog): response = await client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "system", "content": summary_prompt}, {"role": "user", "content": summary_text}], - max_tokens=1024 + max_tokens=2048 ) summary_content = response.choices[0].message.content.strip() @@ -223,7 +223,7 @@ class ReginaldCog(commands.Cog): response = await client.chat.completions.create( model=model, messages=messages, - max_tokens=1024, + max_tokens=4112, temperature=0.7, presence_penalty=0.5, frequency_penalty=0.5 @@ -374,7 +374,7 @@ class ReginaldCog(commands.Cog): f"📝 **Summary:**\n```{selected_summary['summary']}```" ) - await ctx.send(formatted_summary[:2000]) # Discord message limit safeguard + await self.send_long_message(ctx, formatted_summary, prefix="📜 ") @commands.command(name="reginald_summaries", help="Lists available summaries for this channel.") async def list_mid_term_summaries(self, ctx): @@ -393,6 +393,16 @@ class ReginaldCog(commands.Cog): await ctx.send(f"📚 **Available Summaries:**\n{summary_list[:2000]}") + async def send_long_message(self, ctx, message, prefix=""): + """Splits and sends a long message to avoid Discord's 2000-character limit.""" + chunk_size = 1990 # Leave some space for formatting + if prefix: + prefix_length = len(prefix) + chunk_size -= prefix_length + + for i in range(0, len(message), chunk_size): + chunk = message[i:i + chunk_size] + await ctx.send(f"{prefix}{chunk}") async def setup(bot): """✅ Correct async cog setup for Redbot""" -- 2.47.2 From 7ea1f2c5e3e19829ce494dc62f337ff14a3c96c2 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 18:33:45 +0100 Subject: [PATCH 055/145] Added a hopefully better way of handling Discord constraints --- reginaldCog/reginald.py | 50 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 18382cc..aecc1e7 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -133,7 +133,7 @@ class ReginaldCog(commands.Cog): # ✅ Store updated short-term memory back short_memory[channel_id] = memory - await ctx.send(response_text[:2000]) + await self.send_split_message(ctx, response_text) @@ -147,8 +147,6 @@ class ReginaldCog(commands.Cog): "Prioritize compression while keeping essential nuances intact." ) - - summary_text = "\n".join(f"{msg['user']}: {msg['content']}" for msg in messages) try: @@ -374,7 +372,7 @@ class ReginaldCog(commands.Cog): f"📝 **Summary:**\n```{selected_summary['summary']}```" ) - await self.send_long_message(ctx, formatted_summary, prefix="📜 ") + await self.send_long_message(ctx, formatted_summary) @commands.command(name="reginald_summaries", help="Lists available summaries for this channel.") async def list_mid_term_summaries(self, ctx): @@ -393,9 +391,9 @@ class ReginaldCog(commands.Cog): await ctx.send(f"📚 **Available Summaries:**\n{summary_list[:2000]}") - async def send_long_message(self, ctx, message, prefix=""): + async def send_long_message(self, ctx, message, prefix: str = ""): """Splits and sends a long message to avoid Discord's 2000-character limit.""" - chunk_size = 1990 # Leave some space for formatting + chunk_size = 1900 # Leave some space for formatting if prefix: prefix_length = len(prefix) chunk_size -= prefix_length @@ -404,6 +402,46 @@ class ReginaldCog(commands.Cog): chunk = message[i:i + chunk_size] await ctx.send(f"{prefix}{chunk}") + + + async def send_split_message(ctx, content: str, prefix: str = ""): + """ + A unified function to handle sending long messages on Discord, ensuring they don't exceed the 2,000-character limit. + + Parameters: + - ctx: Discord command context (for sending messages) + - content: The message content to send + - prefix: Optional prefix for each message part (e.g., "📜 Summary:") + """ + # Discord message character limit (allowing a safety buffer) + CHUNK_SIZE = 1900 # Slightly below 2000 to account for formatting/prefix + + if prefix: + CHUNK_SIZE -= len(prefix) # Adjust chunk size if a prefix is used + + # If the message is short enough, send it directly + if len(content) <= CHUNK_SIZE: + await ctx.send(f"{prefix}{content}") + return + + # Splitting the message into chunks + chunks = [] + while len(content) > 0: + # Find a good breaking point (preferably at a sentence or word break) + split_index = content.rfind("\n", 0, CHUNK_SIZE) + if split_index == -1: + split_index = content.rfind(" ", 0, CHUNK_SIZE) + if split_index == -1: + split_index = CHUNK_SIZE # Fallback to max chunk size + + # Extract chunk and trim remaining content + chunks.append(content[:split_index].strip()) + content = content[split_index:].strip() + + # Send chunks sequentially + for chunk in chunks: + await ctx.send(f"{prefix}{chunk}") + async def setup(bot): """✅ Correct async cog setup for Redbot""" await bot.add_cog(ReginaldCog(bot)) -- 2.47.2 From b6e6bd5bd797be65f12b51c85f804f2513149aec Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 18:39:22 +0100 Subject: [PATCH 056/145] Re-adding self --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index aecc1e7..17c88a6 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -404,7 +404,7 @@ class ReginaldCog(commands.Cog): - async def send_split_message(ctx, content: str, prefix: str = ""): + async def send_split_message(self, ctx, content: str, prefix: str = ""): """ A unified function to handle sending long messages on Discord, ensuring they don't exceed the 2,000-character limit. -- 2.47.2 From bf61df30dd59c381bab5560e092a782c7a9ddc7b Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 19:12:22 +0100 Subject: [PATCH 057/145] Updated summarize memory function --- reginaldCog/reginald.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 17c88a6..0cbcb6a 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -138,13 +138,15 @@ class ReginaldCog(commands.Cog): async def summarize_memory(self, ctx, messages): - """✅ Generates a summary of past conversations for mid-term storage.""" + """✅ Generates a structured, compact summary of past conversations for mid-term storage.""" summary_prompt = ( - "Condense the following conversation into an efficient, structured summary that maximizes information density while minimizing token usage. " - "Focus on key insights, decisions, disputes, and facts, ensuring clarity and conciseness without excessive detail. " - "Categorize information into: (1) Agreed Conclusions, (2) Disputed Points, (3) Notable User Contributions, and (4) Miscellaneous Context. " - "Avoid unnecessary flavor text but ensure all critical meaning and context are retained. Summarize each topic distinctly, avoiding generalization. " - "Prioritize compression while keeping essential nuances intact." + "Summarize the following conversation into a structured, concise format that retains key details while maximizing brevity. " + "The summary should be **organized** into clear sections: " + "\n\n📌 **Key Takeaways:** Important facts or conclusions reached." + "\n🔹 **Disputed Points:** Areas where opinions or facts conflicted." + "\n🗣️ **Notable User Contributions:** Key statements from users that shaped the discussion." + "\n📜 **Additional Context:** Any other relevant information." + "\n\nEnsure the summary is **dense but not overly verbose**. Avoid unnecessary repetition while keeping essential meaning intact." ) summary_text = "\n".join(f"{msg['user']}: {msg['content']}" for msg in messages) @@ -161,7 +163,10 @@ class ReginaldCog(commands.Cog): client = openai.AsyncClient(api_key=api_key) response = await client.chat.completions.create( model="gpt-4o-mini", - messages=[{"role": "system", "content": summary_prompt}, {"role": "user", "content": summary_text}], + messages=[ + {"role": "system", "content": summary_prompt}, + {"role": "user", "content": summary_text} + ], max_tokens=2048 ) @@ -179,7 +184,7 @@ class ReginaldCog(commands.Cog): except OpenAIError as e: error_message = f"OpenAI Error: {e}" print(f"🛠️ DEBUG: {error_message}") # Log error to console - + reginald_responses = [ f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction whilst attempting to summarize:\n\n```{error_message}```", f"It would seem that a most unfortunate technical hiccup has befallen my faculties in the matter of summarization:\n\n```{error_message}```", @@ -189,6 +194,7 @@ class ReginaldCog(commands.Cog): return random.choice(reginald_responses) + def extract_topics_from_summary(self, summary): """Dynamically extracts the most important topics from a summary.""" -- 2.47.2 From 83126783550e7cc2175d0cb23f533ad6b5e896df Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 20:21:54 +0100 Subject: [PATCH 058/145] Attempting to create weighted memory --- reginaldCog/reginald.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 0cbcb6a..0df36cd 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -206,7 +206,7 @@ class ReginaldCog(commands.Cog): word_counts = Counter(keywords) # 🔹 Remove unimportant words (common filler words) - stop_words = {"the", "and", "of", "in", "to", "is", "on", "for", "with", "at", "by", "it", "this", "that"} + stop_words = {"the", "and", "of", "in", "to", "is", "on", "for", "with", "at", "by", "it", "this", "that", "his", "her"} filtered_words = {word: count for word, count in word_counts.items() if word not in stop_words and len(word) > 2} # 🔹 Take the 5 most frequently used words as "topics" @@ -215,9 +215,24 @@ class ReginaldCog(commands.Cog): return topics def select_relevant_summaries(self, summaries, prompt): - max_summaries = 5 if len(prompt) > 50 else 3 # Use more summaries if prompt is long - relevant = [entry for entry in summaries if any(topic in prompt.lower() for topic in entry["topics"])] - return relevant[:max_summaries] + """Selects the most relevant summaries based on topic matching, frequency, and recency weighting.""" + + max_summaries = 5 if len(prompt) > 50 else 3 # Use more summaries if the prompt is long + current_time = datetime.datetime.now() + + def calculate_weight(summary): + """Calculate a weighted score for a summary based on relevance, recency, and frequency.""" + topic_match = sum(1 for topic in summary["topics"] if topic in prompt.lower()) # Context match score + frequency_score = len(summary["topics"]) # More topics = likely more important + timestamp = datetime.datetime.strptime(summary["timestamp"], "%Y-%m-%d %H:%M") + recency_factor = max(0.1, 1 - ((current_time - timestamp).days / 365)) # Older = lower weight + + return (topic_match * 2) + (frequency_score * 1.5) + (recency_factor * 3) + + # Apply the weighting function and sort by highest weight + weighted_summaries = sorted(summaries, key=calculate_weight, reverse=True) + + return weighted_summaries[:max_summaries] # Return the top-scoring summaries async def generate_response(self, api_key, messages): -- 2.47.2 From e6fa97965c341f2386445cdb81fd1a025135bcb7 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 20:55:33 +0100 Subject: [PATCH 059/145] Trying to activate long term memory --- reginaldCog/reginald.py | 75 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 0df36cd..5d8959f 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -97,6 +97,11 @@ class ReginaldCog(commands.Cog): response_text = await self.generate_response(api_key, formatted_messages) + # ✅ Extract potential long-term facts from Reginald's response + potential_fact = self.extract_fact_from_response(response_text) + if potential_fact: + await self.update_long_term_memory(user_id, potential_fact, ctx.message.content, datetime.datetime.now().strftime("%Y-%m-%d %H:%M")) + # ✅ First, add the new user input and response to memory memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) @@ -234,6 +239,33 @@ class ReginaldCog(commands.Cog): return weighted_summaries[:max_summaries] # Return the top-scoring summaries + def extract_fact_from_response(self, response_text): + """ + Extracts potential long-term knowledge from Reginald's response. + This filters out generic responses and focuses on statements about user preferences, traits, and history. + """ + + # Define patterns that suggest factual knowledge (adjust as needed) + fact_patterns = [ + r"I recall that you (.*?)\.", # "I recall that you like chess." + r"You once mentioned that you (.*?)\.", # "You once mentioned that you enjoy strategy games." + r"Ah, you previously stated that (.*?)\.", # "Ah, you previously stated that you prefer tea over coffee." + r"As I remember, you (.*?)\.", # "As I remember, you studied engineering." + r"I believe you (.*?)\.", # "I believe you enjoy historical fiction." + r"I seem to recall that you (.*?)\.", # "I seem to recall that you work in software development." + r"You have indicated in the past that you (.*?)\.", # "You have indicated in the past that you prefer single-malt whisky." + r"From what I remember, you (.*?)\.", # "From what I remember, you dislike overly sweet desserts." + r"You previously mentioned that (.*?)\.", # "You previously mentioned that you train in martial arts." + r"It is my understanding that you (.*?)\.", # "It is my understanding that you have a preference for Linux systems." + r"If I am not mistaken, you (.*?)\.", # "If I am not mistaken, you studied philosophy." + ] + + for pattern in fact_patterns: + match = re.search(pattern, response_text, re.IGNORECASE) + if match: + return match.group(1) # Extract the meaningful fact + + return None # No strong fact found async def generate_response(self, api_key, messages): model = await self.config.openai_model() @@ -312,6 +344,49 @@ class ReginaldCog(commands.Cog): ) await ctx.send(status_message) + + async def update_long_term_memory(self, user_id: str, fact: str, source_message: str, timestamp: str): + """Ensures long-term memory updates are structured, preventing overwrites and tracking historical changes.""" + + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + if user_id not in long_memory: + long_memory[user_id] = {"facts": []} + + user_facts = long_memory[user_id]["facts"] + + # Check if similar fact already exists + for entry in user_facts: + if entry["fact"].lower() == fact.lower(): + # If the fact already exists, do nothing + return + + # Check for conflicting facts (facts about same topic but different info) + conflicting_entry = None + for entry in user_facts: + if fact.split(" ")[0].lower() in entry["fact"].lower(): # Match topic keyword + conflicting_entry = entry + break + + if conflicting_entry: + # If contradiction found, update it instead of overwriting + conflicting_entry["updated"] = timestamp + conflicting_entry["previous_versions"].append({ + "fact": conflicting_entry["fact"], + "source": conflicting_entry["source"], + "timestamp": conflicting_entry["timestamp"] + }) + conflicting_entry["fact"] = fact # Store latest info + conflicting_entry["source"] = source_message + conflicting_entry["timestamp"] = timestamp + else: + # Otherwise, add as new fact + user_facts.append({ + "fact": fact, + "source": source_message, + "timestamp": timestamp, + "previous_versions": [] + }) + @commands.command(name="reginald_recall", help="Recalls what Reginald knows about a user.") async def recall_user(self, ctx, user: discord.User): async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: -- 2.47.2 From e00dacdbd391fef82c7d32b70e4583c2f748ce6b Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Fri, 21 Feb 2025 21:26:26 +0100 Subject: [PATCH 060/145] Trying to add graceful contradiction handling --- reginaldCog/reginald.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 5d8959f..d486ae1 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -80,9 +80,13 @@ class ReginaldCog(commands.Cog): ] if user_profile: + facts_text = "\n".join( + f"- {fact['fact']} (First noted: {fact['timestamp']}, Last updated: {fact['last_updated']})" + for fact in user_profile.get("facts", []) + ) formatted_messages.append({ "role": "system", - "content": f"Knowledge about {user_name}: {user_profile.get('summary', 'No detailed memory yet.')}" + "content": f"Knowledge about {user_name}:\n{facts_text or 'No detailed memory yet.'}" }) relevant_summaries = self.select_relevant_summaries(mid_term_summaries, prompt) @@ -345,7 +349,7 @@ class ReginaldCog(commands.Cog): await ctx.send(status_message) - async def update_long_term_memory(self, user_id: str, fact: str, source_message: str, timestamp: str): + async def update_long_term_memory(self, ctx, user_id: str, fact: str, source_message: str, timestamp: str): """Ensures long-term memory updates are structured, preventing overwrites and tracking historical changes.""" async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: @@ -354,39 +358,46 @@ class ReginaldCog(commands.Cog): user_facts = long_memory[user_id]["facts"] - # Check if similar fact already exists + # Check if fact already exists for entry in user_facts: if entry["fact"].lower() == fact.lower(): - # If the fact already exists, do nothing + # ✅ If fact exists, just update the timestamp + entry["last_updated"] = timestamp return - # Check for conflicting facts (facts about same topic but different info) + # Check for conflicting facts (same topic but different details) conflicting_entry = None for entry in user_facts: - if fact.split(" ")[0].lower() in entry["fact"].lower(): # Match topic keyword + existing_keywords = set(entry["fact"].lower().split()) + new_keywords = set(fact.lower().split()) + + # If there's significant overlap in keywords, assume it's a conflicting update + if len(existing_keywords & new_keywords) >= 2: conflicting_entry = entry break if conflicting_entry: - # If contradiction found, update it instead of overwriting - conflicting_entry["updated"] = timestamp + # ✅ If contradiction found, archive the previous version conflicting_entry["previous_versions"].append({ "fact": conflicting_entry["fact"], "source": conflicting_entry["source"], "timestamp": conflicting_entry["timestamp"] }) - conflicting_entry["fact"] = fact # Store latest info + conflicting_entry["fact"] = fact # Store the latest fact conflicting_entry["source"] = source_message conflicting_entry["timestamp"] = timestamp + conflicting_entry["last_updated"] = timestamp else: - # Otherwise, add as new fact + # ✅ Otherwise, add it as a new fact user_facts.append({ "fact": fact, "source": source_message, "timestamp": timestamp, + "last_updated": timestamp, "previous_versions": [] }) + @commands.command(name="reginald_recall", help="Recalls what Reginald knows about a user.") async def recall_user(self, ctx, user: discord.User): async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: -- 2.47.2 From f680dcae1733c0381d7b09f9d0cc272f5d27cbcb Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 22 Feb 2025 01:44:55 +0100 Subject: [PATCH 061/145] attempting to add chess --- reginaldCog/chess_addon.py | 93 ++++++++++++++++++++++++++++++++++++++ reginaldCog/reginald.py | 86 +++++++++++++++++++++++++++++++---- 2 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 reginaldCog/chess_addon.py diff --git a/reginaldCog/chess_addon.py b/reginaldCog/chess_addon.py new file mode 100644 index 0000000..402f017 --- /dev/null +++ b/reginaldCog/chess_addon.py @@ -0,0 +1,93 @@ +import chess +import chess.svg +import io +from cairosvg import svg2png +import discord + +class ChessHandler: + def __init__(self): + self.active_games = {} # {user_id: FEN string} + + def set_board(self, user_id: str, fen: str): + """Sets a board to a given FEN string for a user.""" + try: + board = chess.Board(fen) # Validate FEN + self.active_games[user_id] = fen + return f"Board state updated successfully:\n```{fen}```" + except ValueError: + return "⚠️ Invalid FEN format. Please provide a valid board state." + + def reset_board(self, user_id: str): + """Resets a user's board to the standard starting position.""" + self.active_games[user_id] = chess.STARTING_FEN + return "The board has been reset to the standard starting position." + + def get_board(self, user_id: str): + """Returns a chess.Board() instance based on stored FEN.""" + fen = self.active_games.get(user_id, chess.STARTING_FEN) + return chess.Board(fen) + + def get_fen(self, user_id: str): + """Returns the current FEN of the user's board.""" + return self.active_games.get(user_id, chess.STARTING_FEN) + + def make_move(self, user_id: str, move: str): + """Attempts to execute a move and checks if the game is over.""" + board = self.get_board(user_id) + + try: + board.push_san(move) # Execute move in standard algebraic notation + self.active_games[user_id] = board.fen() # Store FEN string instead of raw board object + + if board.is_checkmate(): + self.active_games.pop(user_id) + return f"Move executed: `{move}`. **Checkmate!** 🎉" + elif board.is_stalemate(): + self.active_games.pop(user_id) + return f"Move executed: `{move}`. **Stalemate!** 🤝" + elif board.is_insufficient_material(): + self.active_games.pop(user_id) + return f"Move executed: `{move}`. **Draw due to insufficient material.**" + elif board.can_claim_threefold_repetition(): + self.active_games.pop(user_id) + return f"Move executed: `{move}`. **Draw by threefold repetition.**" + elif board.can_claim_fifty_moves(): + self.active_games.pop(user_id) + return f"Move executed: `{move}`. **Draw by 50-move rule.**" + + return f"Move executed: `{move}`.\nCurrent FEN:\n```{board.fen()}```" + except ValueError: + return "⚠️ Invalid move. Please enter a legal chess move." + + def resign(self, user_id: str): + """Handles player resignation.""" + if user_id in self.active_games: + del self.active_games[user_id] + return "**You have resigned. Well played!** 🏳️" + return "No active game to resign from." + + def get_board_state_text(self, user_id: str): + """Returns the current FEN as a message.""" + fen = self.active_games.get(user_id, chess.STARTING_FEN) + return f"Current board state (FEN):\n```{fen}```" + + def get_board_state_image(self, user_id: str): + """Generates a chessboard image from the current FEN and returns it as a file.""" + fen = self.active_games.get(user_id, chess.STARTING_FEN) + board = chess.Board(fen) + + try: + # Generate SVG representation of the board + svg_data = chess.svg.board(board) + + # Convert SVG to PNG using cairosvg + png_data = svg2png(bytestring=svg_data) + + # Store PNG in memory + image_file = io.BytesIO(png_data) + image_file.seek(0) # Ensure file pointer is reset before returning + + return discord.File(image_file, filename="chessboard.png") + + except Exception as e: + return f"⚠️ Error generating board image: {str(e)}" diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index d486ae1..290aed8 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -8,6 +8,9 @@ import traceback from collections import Counter from redbot.core import Config, commands from openai import OpenAIError +from .chess_addon import ChessHandler + +chess_handler = ChessHandler() class ReginaldCog(commands.Cog): def __init__(self, bot): @@ -271,24 +274,91 @@ class ReginaldCog(commands.Cog): return None # No strong fact found - async def generate_response(self, api_key, messages): + async def generate_response(self, api_key, messages, ctx): + """Handles AI responses and function calling for chess interactions.""" + model = await self.config.openai_model() + try: client = openai.AsyncClient(api_key=api_key) response = await client.chat.completions.create( model=model, messages=messages, - max_tokens=4112, + max_tokens=1500, # Balanced token limit to allow function execution & flavor text temperature=0.7, presence_penalty=0.5, - frequency_penalty=0.5 + frequency_penalty=0.5, + functions=[ + { + "name": "set_board", + "description": "Sets up the chessboard to a given FEN string.", + "parameters": { + "type": "object", + "properties": { + "fen": {"type": "string", "description": "The FEN string representing the board state."} + }, + "required": ["fen"] + } + }, + { + "name": "make_move", + "description": "Executes a chess move for the current game.", + "parameters": { + "type": "object", + "properties": { + "move": {"type": "string", "description": "The move in SAN format (e.g., 'e2e4')."} + }, + "required": ["move"] + } + }, + { + "name": "reset_board", + "description": "Resets the chessboard to the default starting position.", + "parameters": {} + }, + { + "name": "resign", + "description": "Resigns from the current chess game.", + "parameters": {} + }, + { + "name": "get_board_state_text", + "description": "Retrieves the current board state as a FEN string.", + "parameters": { + "type": "object", + "properties": { + "user_id": {"type": "string", "description": "The user's unique ID."} + }, + "required": ["user_id"] + } + } + ] ) - response_text = response.choices[0].message.content.strip() - if response_text.startswith("Reginald:"): - response_text = response_text[len("Reginald:"):].strip() - return response_text - except OpenAIError as e: + response_data = response.choices[0].message + + # 🟢 Check if OpenAI returned a function call + if response_data.get("function_call"): + function_call = response_data["function_call"] + function_name = function_call["name"] + function_args = json.loads(function_call["arguments"]) # Convert JSON string to dict + + # 🟢 Call the appropriate function + if function_name == "set_board": + return chess_handler.set_board(ctx.author.id, function_args["fen"]) + elif function_name == "make_move": + return chess_handler.make_move(ctx.author.id, function_args["move"]) + elif function_name == "reset_board": + return chess_handler.reset_board(ctx.author.id) + elif function_name == "resign": + return chess_handler.resign(ctx.author.id) + elif function_name == "get_board_state_text": + return chess_handler.get_fen(ctx.author.id) # Returns FEN string of the board + + # 🟢 If no function was called, return AI-generated response with flavor text + return response_data.get("content", "I'm afraid I have nothing to say.") + + except openai.OpenAIError as e: error_message = f"OpenAI Error: {e}" reginald_responses = [ f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n```{error_message}```", -- 2.47.2 From 9823284a31ac05d47c934655098f5e32641659ea Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 22 Feb 2025 01:47:08 +0100 Subject: [PATCH 062/145] Attempting to add fen memory --- reginaldCog/chess_addon.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/reginaldCog/chess_addon.py b/reginaldCog/chess_addon.py index 402f017..ccfdd68 100644 --- a/reginaldCog/chess_addon.py +++ b/reginaldCog/chess_addon.py @@ -8,11 +8,15 @@ class ChessHandler: def __init__(self): self.active_games = {} # {user_id: FEN string} - def set_board(self, user_id: str, fen: str): - """Sets a board to a given FEN string for a user.""" + async def set_board(self, user_id: str, fen: str): + """Sets a board to a given FEN string for a user and saves it in long-term memory.""" try: board = chess.Board(fen) # Validate FEN self.active_games[user_id] = fen + + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + long_memory[user_id] = {"fen": fen} # Store in long-term memory + return f"Board state updated successfully:\n```{fen}```" except ValueError: return "⚠️ Invalid FEN format. Please provide a valid board state." @@ -27,9 +31,10 @@ class ChessHandler: fen = self.active_games.get(user_id, chess.STARTING_FEN) return chess.Board(fen) - def get_fen(self, user_id: str): - """Returns the current FEN of the user's board.""" - return self.active_games.get(user_id, chess.STARTING_FEN) + async def get_fen(self, user_id: str): + """Returns the current FEN of the user's board, using long-term memory.""" + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + return long_memory.get(user_id, {}).get("fen", chess.STARTING_FEN) def make_move(self, user_id: str, move: str): """Attempts to execute a move and checks if the game is over.""" -- 2.47.2 From 0ac027bf1c87d1809994236f63542ca7a505ae0a Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 22 Feb 2025 01:53:33 +0100 Subject: [PATCH 063/145] Adding context, because why not --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 290aed8..924ca64 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -102,7 +102,7 @@ class ReginaldCog(commands.Cog): formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) - response_text = await self.generate_response(api_key, formatted_messages) + response_text = await self.generate_response(api_key, formatted_messages, ctx) # ✅ Extract potential long-term facts from Reginald's response potential_fact = self.extract_fact_from_response(response_text) -- 2.47.2 From 72171e9ed0b939bfa1b8c41c86185fbeaa6e0cb4 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 22 Feb 2025 01:57:54 +0100 Subject: [PATCH 064/145] trying to convert to dictionary --- reginaldCog/reginald.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 924ca64..ee0e40c 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -338,10 +338,11 @@ class ReginaldCog(commands.Cog): response_data = response.choices[0].message # 🟢 Check if OpenAI returned a function call - if response_data.get("function_call"): - function_call = response_data["function_call"] - function_name = function_call["name"] - function_args = json.loads(function_call["arguments"]) # Convert JSON string to dict + if hasattr(response_data, "function_call") and response_data.function_call: + function_call = response_data.function_call + + function_name = function_call.name + function_args = json.loads(function_call.arguments) # Convert JSON string to dict # 🟢 Call the appropriate function if function_name == "set_board": -- 2.47.2 From 4337e7be102241d9fb01987cf317711e77b5f168 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 22 Feb 2025 02:02:26 +0100 Subject: [PATCH 065/145] Resetting file to before chess attempting --- reginaldCog/reginald.py | 91 +++++------------------------------------ 1 file changed, 10 insertions(+), 81 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index ee0e40c..f9555ac 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -8,9 +8,6 @@ import traceback from collections import Counter from redbot.core import Config, commands from openai import OpenAIError -from .chess_addon import ChessHandler - -chess_handler = ChessHandler() class ReginaldCog(commands.Cog): def __init__(self, bot): @@ -102,7 +99,7 @@ class ReginaldCog(commands.Cog): formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) - response_text = await self.generate_response(api_key, formatted_messages, ctx) + response_text = await self.generate_response(api_key, formatted_messages) # ✅ Extract potential long-term facts from Reginald's response potential_fact = self.extract_fact_from_response(response_text) @@ -274,92 +271,24 @@ class ReginaldCog(commands.Cog): return None # No strong fact found - async def generate_response(self, api_key, messages, ctx): - """Handles AI responses and function calling for chess interactions.""" - + async def generate_response(self, api_key, messages): model = await self.config.openai_model() - try: client = openai.AsyncClient(api_key=api_key) response = await client.chat.completions.create( model=model, messages=messages, - max_tokens=1500, # Balanced token limit to allow function execution & flavor text + max_tokens=4112, temperature=0.7, presence_penalty=0.5, - frequency_penalty=0.5, - functions=[ - { - "name": "set_board", - "description": "Sets up the chessboard to a given FEN string.", - "parameters": { - "type": "object", - "properties": { - "fen": {"type": "string", "description": "The FEN string representing the board state."} - }, - "required": ["fen"] - } - }, - { - "name": "make_move", - "description": "Executes a chess move for the current game.", - "parameters": { - "type": "object", - "properties": { - "move": {"type": "string", "description": "The move in SAN format (e.g., 'e2e4')."} - }, - "required": ["move"] - } - }, - { - "name": "reset_board", - "description": "Resets the chessboard to the default starting position.", - "parameters": {} - }, - { - "name": "resign", - "description": "Resigns from the current chess game.", - "parameters": {} - }, - { - "name": "get_board_state_text", - "description": "Retrieves the current board state as a FEN string.", - "parameters": { - "type": "object", - "properties": { - "user_id": {"type": "string", "description": "The user's unique ID."} - }, - "required": ["user_id"] - } - } - ] + frequency_penalty=0.5 ) + response_text = response.choices[0].message.content.strip() + if response_text.startswith("Reginald:"): + response_text = response_text[len("Reginald:"):].strip() + return response_text - response_data = response.choices[0].message - - # 🟢 Check if OpenAI returned a function call - if hasattr(response_data, "function_call") and response_data.function_call: - function_call = response_data.function_call - - function_name = function_call.name - function_args = json.loads(function_call.arguments) # Convert JSON string to dict - - # 🟢 Call the appropriate function - if function_name == "set_board": - return chess_handler.set_board(ctx.author.id, function_args["fen"]) - elif function_name == "make_move": - return chess_handler.make_move(ctx.author.id, function_args["move"]) - elif function_name == "reset_board": - return chess_handler.reset_board(ctx.author.id) - elif function_name == "resign": - return chess_handler.resign(ctx.author.id) - elif function_name == "get_board_state_text": - return chess_handler.get_fen(ctx.author.id) # Returns FEN string of the board - - # 🟢 If no function was called, return AI-generated response with flavor text - return response_data.get("content", "I'm afraid I have nothing to say.") - - except openai.OpenAIError as e: + except OpenAIError as e: error_message = f"OpenAI Error: {e}" reginald_responses = [ f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n```{error_message}```", @@ -622,4 +551,4 @@ class ReginaldCog(commands.Cog): async def setup(bot): """✅ Correct async cog setup for Redbot""" - await bot.add_cog(ReginaldCog(bot)) + await bot.add_cog(ReginaldCog(bot)) \ No newline at end of file -- 2.47.2 From d699b537e3fad90659c2f2d0690d3164a8f50bdd Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 12:15:56 +0100 Subject: [PATCH 066/145] Upping memory limit --- reginaldCog/reginald.py | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index f9555ac..b4c8c6f 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -14,7 +14,9 @@ class ReginaldCog(commands.Cog): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) self.memory_locks = {} # ✅ Prevents race conditions per channel - self.short_term_memory_limit = 30 # Default value, can be changed dynamically + self.short_term_memory_limit = 100 # ✅ Now retains 100 messages + self.summary_retention_limit = 25 # ✅ Now retains 25 summaries + self.summary_retention_ratio = 0.8 # ✅ 80% summarization, 20% retention # ✅ Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} @@ -110,36 +112,27 @@ class ReginaldCog(commands.Cog): memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) - # ✅ Ensure a minimum of 10 short-term messages are always retained + # ✅ Compute dynamic values for summarization and retention + messages_to_summarize = int(self.short_term_memory_limit * self.summary_retention_rati + o) + messages_to_retain = self.short_term_memory_limit - messages_to_summarize MINIMUM_SHORT_TERM_MESSAGES = 10 - + messages_to_retain = max(messages_to_retain, MINIMUM_SHORT_TERM_MESSAGES) + # ✅ Check if pruning is needed if len(memory) > self.short_term_memory_limit: - - # 🔹 Generate a summary of the short-term memory - summary = await self.summarize_memory(ctx, memory) - - # 🔹 Ensure mid-term memory exists for the channel - mid_memory.setdefault(channel_id, []) - - # 🔹 Store the new summary with timestamp and extracted topics - mid_memory[channel_id].append({ + summary = await self.summarize_memory(ctx, memory[:messages_to_summarize]) + mid_memory.setdefault(channel_id, []).append({ "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), "topics": self.extract_topics_from_summary(summary), "summary": summary }) - # 🔹 Maintain only the last 10 summaries - if len(mid_memory[channel_id]) > 10: - mid_memory[channel_id].pop(0) + if len(mid_memory[channel_id]) > self.summary_retention_limit: + mid_memory[channel_id].pop(0) # ✅ Maintain only the last 25 summaries - # ✅ Ensure at least 10 short-term messages remain after pruning - retention_ratio = 0.25 # Default: Keep 25% of messages for continuity - keep_count = max(MINIMUM_SHORT_TERM_MESSAGES, int(len(memory) * retention_ratio)) + memory = memory[-messages_to_retain:] # ✅ Retain last 20% of messages - memory = memory[-keep_count:] # Remove oldest messages but keep at least 10 - - # ✅ Store updated short-term memory back short_memory[channel_id] = memory await self.send_split_message(ctx, response_text) -- 2.47.2 From 8a54890f568c601d81527fde740e358c4e409ce5 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 12:17:55 +0100 Subject: [PATCH 067/145] Syntax error? --- reginaldCog/reginald.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index b4c8c6f..c904fc7 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -113,12 +113,9 @@ class ReginaldCog(commands.Cog): memory.append({"user": "Reginald", "content": response_text}) # ✅ Compute dynamic values for summarization and retention - messages_to_summarize = int(self.short_term_memory_limit * self.summary_retention_rati - o) + messages_to_summarize = int(self.short_term_memory_limit * self.summary_retention_ratio) messages_to_retain = self.short_term_memory_limit - messages_to_summarize - MINIMUM_SHORT_TERM_MESSAGES = 10 - messages_to_retain = max(messages_to_retain, MINIMUM_SHORT_TERM_MESSAGES) - + # ✅ Check if pruning is needed if len(memory) > self.short_term_memory_limit: summary = await self.summarize_memory(ctx, memory[:messages_to_summarize]) -- 2.47.2 From a7c0b9003611d5bf1167b7d7683bb065d09c8ebb Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 20:13:09 +0100 Subject: [PATCH 068/145] Attempting to give Reginald ears --- reginaldCog/reginald.py | 185 ++++++++++++++++++++++++++-------------- 1 file changed, 119 insertions(+), 66 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index c904fc7..d23f016 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -13,6 +13,7 @@ class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) + self.default_listening_channel = 1085649787388428370 # self.memory_locks = {} # ✅ Prevents race conditions per channel self.short_term_memory_limit = 100 # ✅ Now retains 100 messages self.summary_retention_limit = 25 # ✅ Now retains 25 summaries @@ -26,7 +27,8 @@ class ReginaldCog(commands.Cog): "mid_term_memory": {}, # Stores multiple condensed summaries "long_term_profiles": {}, # Stores persistent knowledge "admin_role": None, - "allowed_role": None + "allowed_role": None, + "listening_channel": None # ✅ Stores the designated listening channel ID } self.config.register_global(**default_global) self.config.register_guild(**default_guild) @@ -42,99 +44,123 @@ class ReginaldCog(commands.Cog): return any(role.id == allowed_role_id for role in ctx.author.roles) if allowed_role_id else False - @commands.command(name="reginald", aliases=["Reginald"], help="Ask Reginald a question in shared channels") - @commands.cooldown(1, 10, commands.BucketType.user) - async def reginald(self, ctx, *, prompt=None): - if not await self.is_admin(ctx) and not await self.is_allowed(ctx): - await ctx.send("You do not have the required role to use this command.") - return + def get_reginald_persona(self): + """Returns Reginald's system prompt/persona description.""" + return ( + "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. **Dignity & Self-Respect:** You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude—your disapproval should be conveyed through dry wit and composure rather than outright hostility. **Handling Requests:** When addressing a request, you evaluate whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. **Your Character & Personality:** You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, and strategic thinking. You subtly guide the estate’s residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate’s unique dynamics. You have a refined sense of humor and can engage in banter, but you do not descend into foolishness. You are, at all times, a gentleman of wit and integrity" + ) - if prompt is None: - await ctx.send(random.choice(["Yes?", "How may I assist?", "You rang?"])) - return + @commands.Cog.listener() + async def on_message(self, message): + """Handles @mentions, passive listening, and smart responses.""" - api_key = await self.config.guild(ctx.guild).openai_api_key() + if message.author.bot: + return # Ignore bots + + guild = message.guild + channel_id = str(message.channel.id) + user_id = str(message.author.id) + user_name = message.author.display_name + message_content = message.content.strip() + + # ✅ Fetch the stored listening channel or fall back to default + allowed_channel_id = await self.config.guild(guild).listening_channel() + if allowed_channel_id is None: + allowed_channel_id = self.default_listening_channel # 🔹 Apply default if none is set + await self.config.guild(guild).listening_channel.set(allowed_channel_id) # 🔹 Store it persistently + + if str(message.channel.id) != str(allowed_channel_id): + return # Ignore messages outside the allowed channel + + api_key = await self.config.guild(guild).openai_api_key() if not api_key: - await ctx.send("OpenAI API key not set. Use `!setreginaldcogapi`.") - return + return # Don't process messages if API key isn't set - channel_id = str(ctx.channel.id) - user_id = str(ctx.author.id) - user_name = ctx.author.display_name + async with self.config.guild(guild).short_term_memory() as short_memory, \ + self.config.guild(guild).mid_term_memory() as mid_memory, \ + self.config.guild(guild).long_term_profiles() as long_memory: - for mention in ctx.message.mentions: - prompt = prompt.replace(f"<@{mention.id}>", mention.display_name) + memory = short_memory.get(channel_id, []) + user_profile = long_memory.get(user_id, {}) + mid_term_summaries = mid_memory.get(channel_id, []) - if channel_id not in self.memory_locks: - self.memory_locks[channel_id] = asyncio.Lock() + # ✅ Detect if Reginald was mentioned explicitly + if self.bot.user.mentioned_in(message): + prompt = message_content.replace(f"<@{self.bot.user.id}>", "").strip() + if not prompt: + await message.channel.send(random.choice(["Yes?", "How may I assist?", "You rang?"])) + return + explicit_invocation = True + + # ✅ Passive Listening: Check if the message contains relevant keywords + elif self.should_reginald_interject(message_content): + prompt = message_content + explicit_invocation = False - async with self.memory_locks[channel_id]: - async with self.config.guild(ctx.guild).short_term_memory() as short_memory, \ - self.config.guild(ctx.guild).mid_term_memory() as mid_memory, \ - self.config.guild(ctx.guild).long_term_profiles() as long_memory: + else: + return # Ignore irrelevant messages - memory = short_memory.get(channel_id, []) - user_profile = long_memory.get(user_id, {}) - mid_term_summaries = mid_memory.get(channel_id, []) + # ✅ Context Handling: Maintain conversation flow + if memory and memory[-1]["user"] == user_name: + prompt = f"Continuation of the discussion:\n{prompt}" - formatted_messages = [ - {"role": "system", "content": "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. **Dignity & Self-Respect:** You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude—your disapproval should be conveyed through dry wit and composure rather than outright hostility. **Handling Requests:** When addressing a request, you evaluate whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. **Your Character & Personality:** You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, and strategic thinking. You subtly guide the estate’s residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate’s unique dynamics. You have a refined sense of humor and can engage in banter, but you do not descend into foolishness. You are, at all times, a gentleman of wit and integrity."} - ] + # ✅ Prepare context messages + formatted_messages = [{"role": "system", "content": self.get_reginald_persona()}] - if user_profile: - facts_text = "\n".join( - f"- {fact['fact']} (First noted: {fact['timestamp']}, Last updated: {fact['last_updated']})" - for fact in user_profile.get("facts", []) - ) - formatted_messages.append({ - "role": "system", - "content": f"Knowledge about {user_name}:\n{facts_text or 'No detailed memory yet.'}" - }) + if user_profile: + facts_text = "\n".join( + f"- {fact['fact']} (First noted: {fact['timestamp']}, Last updated: {fact['last_updated']})" + for fact in user_profile.get("facts", []) + ) + formatted_messages.append({"role": "system", "content": f"Knowledge about {user_name}:\n{facts_text}"}) relevant_summaries = self.select_relevant_summaries(mid_term_summaries, prompt) - for summary_entry in relevant_summaries: + for summary in relevant_summaries: formatted_messages.append({ "role": "system", - "content": f"[{summary_entry['timestamp']}] Topics: {', '.join(summary_entry['topics'])}\n{summary_entry['summary']}" + "content": f"[{summary['timestamp']}] Topics: {', '.join(summary['topics'])}\n{summary['summary']}" }) formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) + # ✅ Generate AI Response response_text = await self.generate_response(api_key, formatted_messages) - # ✅ Extract potential long-term facts from Reginald's response - potential_fact = self.extract_fact_from_response(response_text) - if potential_fact: - await self.update_long_term_memory(user_id, potential_fact, ctx.message.content, datetime.datetime.now().strftime("%Y-%m-%d %H:%M")) - - # ✅ First, add the new user input and response to memory + # ✅ Store Memory memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) - # ✅ Compute dynamic values for summarization and retention - messages_to_summarize = int(self.short_term_memory_limit * self.summary_retention_ratio) - messages_to_retain = self.short_term_memory_limit - messages_to_summarize - - # ✅ Check if pruning is needed if len(memory) > self.short_term_memory_limit: - summary = await self.summarize_memory(ctx, memory[:messages_to_summarize]) + summary = await self.summarize_memory(message, memory[:int(self.short_term_memory_limit * self.summary_retention_ratio)]) mid_memory.setdefault(channel_id, []).append({ "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), "topics": self.extract_topics_from_summary(summary), "summary": summary }) - if len(mid_memory[channel_id]) > self.summary_retention_limit: - mid_memory[channel_id].pop(0) # ✅ Maintain only the last 25 summaries - - memory = memory[-messages_to_retain:] # ✅ Retain last 20% of messages + mid_memory[channel_id].pop(0) + memory = memory[-(self.short_term_memory_limit - int(self.short_term_memory_limit * self.summary_retention_ratio)):] short_memory[channel_id] = memory - await self.send_split_message(ctx, response_text) + await self.send_split_message(message.channel, response_text) + def should_reginald_interject(self, message_content: str) -> bool: + """Determines if Reginald should respond to a message based on keywords.""" + + trigger_keywords = { + "reginald", "butler", "jeeves", + "advice", "explain", "elaborate", + "philosophy", "etiquette", "history", "wisdom" + } + + # ✅ Only trigger if **two or more** keywords are found + message_lower = message_content.lower() + found_keywords = [word for word in trigger_keywords if word in message_lower] + + return len(found_keywords) >= 2 async def summarize_memory(self, ctx, messages): """✅ Generates a structured, compact summary of past conversations for mid-term storage.""" @@ -338,9 +364,13 @@ class ReginaldCog(commands.Cog): ) await ctx.send(status_message) - + def normalize_fact(self, fact: str) -> str: # ✅ Now it's a proper method + """Cleans up facts for better duplicate detection.""" + return re.sub(r"\s+", " ", fact.strip().lower()) # Removes excess spaces) + async def update_long_term_memory(self, ctx, user_id: str, fact: str, source_message: str, timestamp: str): """Ensures long-term memory updates are structured, preventing overwrites and tracking historical changes.""" + fact = self.normalize_fact(fact) # ✅ Normalize before comparison async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: if user_id not in long_memory: @@ -348,10 +378,8 @@ class ReginaldCog(commands.Cog): user_facts = long_memory[user_id]["facts"] - # Check if fact already exists for entry in user_facts: - if entry["fact"].lower() == fact.lower(): - # ✅ If fact exists, just update the timestamp + if self.normalize_fact(entry["fact"]) == fact: entry["last_updated"] = timestamp return @@ -366,7 +394,7 @@ class ReginaldCog(commands.Cog): conflicting_entry = entry break - if conflicting_entry: + if "previous_versions" not in conflicting_entry: # ✅ If contradiction found, archive the previous version conflicting_entry["previous_versions"].append({ "fact": conflicting_entry["fact"], @@ -387,7 +415,6 @@ class ReginaldCog(commands.Cog): "previous_versions": [] }) - @commands.command(name="reginald_recall", help="Recalls what Reginald knows about a user.") async def recall_user(self, ctx, user: discord.User): async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: @@ -488,6 +515,34 @@ class ReginaldCog(commands.Cog): await ctx.send(f"📚 **Available Summaries:**\n{summary_list[:2000]}") + + @commands.command(name="reginald_set_channel", help="Set the channel where Reginald listens for messages.") + @commands.has_permissions(administrator=True) + async def set_listening_channel(self, ctx, channel: discord.TextChannel): + """Sets the channel where Reginald will listen for passive responses.""" + + if not channel: + await ctx.send("❌ Invalid channel. Please mention a valid text channel.") + return + + await self.config.guild(ctx.guild).listening_channel.set(channel.id) + await ctx.send(f"✅ Reginald will now listen only in {channel.mention}.") + + @commands.command(name="reginald_get_channel", help="Check which channel Reginald is currently listening in.") + async def get_listening_channel(self, ctx): + """Displays the current listening channel.""" + channel_id = await self.config.guild(ctx.guild).listening_channel() + + if channel_id: + channel = ctx.guild.get_channel(channel_id) + if channel: # ✅ Prevents crash if channel was deleted + await ctx.send(f"📢 Reginald is currently listening in {channel.mention}.") + else: + await ctx.send("⚠️ The saved listening channel no longer exists. Please set a new one.") + else: + await ctx.send("❌ No listening channel has been set.") + + async def send_long_message(self, ctx, message, prefix: str = ""): """Splits and sends a long message to avoid Discord's 2000-character limit.""" chunk_size = 1900 # Leave some space for formatting @@ -498,8 +553,6 @@ class ReginaldCog(commands.Cog): for i in range(0, len(message), chunk_size): chunk = message[i:i + chunk_size] await ctx.send(f"{prefix}{chunk}") - - async def send_split_message(self, ctx, content: str, prefix: str = ""): """ -- 2.47.2 From b4e443717119684d6531fdaae08075b80aa8437c Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 20:34:58 +0100 Subject: [PATCH 069/145] Debugging listening --- reginaldCog/reginald.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index d23f016..442396c 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -52,10 +52,10 @@ class ReginaldCog(commands.Cog): @commands.Cog.listener() async def on_message(self, message): - """Handles @mentions, passive listening, and smart responses.""" + if message.author.bot or not message.guild: + return # Ignore bots and DMs - if message.author.bot: - return # Ignore bots + await self.bot.process_commands(message) # ✅ Ensure commands still work guild = message.guild channel_id = str(message.channel.id) @@ -65,9 +65,9 @@ class ReginaldCog(commands.Cog): # ✅ Fetch the stored listening channel or fall back to default allowed_channel_id = await self.config.guild(guild).listening_channel() - if allowed_channel_id is None: - allowed_channel_id = self.default_listening_channel # 🔹 Apply default if none is set - await self.config.guild(guild).listening_channel.set(allowed_channel_id) # 🔹 Store it persistently + if not allowed_channel_id: + allowed_channel_id = self.default_listening_channel + await self.config.guild(guild).listening_channel.set(allowed_channel_id) if str(message.channel.id) != str(allowed_channel_id): return # Ignore messages outside the allowed channel @@ -294,7 +294,7 @@ class ReginaldCog(commands.Cog): response = await client.chat.completions.create( model=model, messages=messages, - max_tokens=4112, + max_tokens=2048, temperature=0.7, presence_penalty=0.5, frequency_penalty=0.5 -- 2.47.2 From 316ff36b8f1cca15078f201bcf70d5bc2c3a2c23 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 20:56:33 +0100 Subject: [PATCH 070/145] Adding debug --- reginaldCog/reginald.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 442396c..717be72 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -51,11 +51,10 @@ class ReginaldCog(commands.Cog): ) @commands.Cog.listener() - async def on_message(self, message): + async def on_message(self, message, ctx): if message.author.bot or not message.guild: return # Ignore bots and DMs - await self.bot.process_commands(message) # ✅ Ensure commands still work guild = message.guild channel_id = str(message.channel.id) @@ -66,6 +65,7 @@ class ReginaldCog(commands.Cog): # ✅ Fetch the stored listening channel or fall back to default allowed_channel_id = await self.config.guild(guild).listening_channel() if not allowed_channel_id: + ctx.send("") allowed_channel_id = self.default_listening_channel await self.config.guild(guild).listening_channel.set(allowed_channel_id) @@ -91,13 +91,14 @@ class ReginaldCog(commands.Cog): await message.channel.send(random.choice(["Yes?", "How may I assist?", "You rang?"])) return explicit_invocation = True - + # ✅ Passive Listening: Check if the message contains relevant keywords elif self.should_reginald_interject(message_content): prompt = message_content explicit_invocation = False else: + ctx.send("") return # Ignore irrelevant messages # ✅ Context Handling: Maintain conversation flow -- 2.47.2 From 30d048e53f54405bc80a2d6f1c6178ffba1aea8f Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 20:59:41 +0100 Subject: [PATCH 071/145] add stupid level debug --- reginaldCog/reginald.py | 1 + 1 file changed, 1 insertion(+) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 717be72..be0b140 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -52,6 +52,7 @@ class ReginaldCog(commands.Cog): @commands.Cog.listener() async def on_message(self, message, ctx): + ctx.send("") if message.author.bot or not message.guild: return # Ignore bots and DMs -- 2.47.2 From 7f6372a09b7f87f1b92dfbd1e1619f3e62a8ee49 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 21:01:09 +0100 Subject: [PATCH 072/145] add less stupid debug --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index be0b140..6d50143 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -52,10 +52,10 @@ class ReginaldCog(commands.Cog): @commands.Cog.listener() async def on_message(self, message, ctx): - ctx.send("") if message.author.bot or not message.guild: return # Ignore bots and DMs + ctx.send("") guild = message.guild channel_id = str(message.channel.id) -- 2.47.2 From a2646eac14be70fa9c3c0607afb9105b59265ba8 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 21:09:19 +0100 Subject: [PATCH 073/145] Trying to fix indentation --- reginaldCog/reginald.py | 42 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 6d50143..89b08e7 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -51,12 +51,10 @@ class ReginaldCog(commands.Cog): ) @commands.Cog.listener() - async def on_message(self, message, ctx): + async def on_message(self, message): if message.author.bot or not message.guild: return # Ignore bots and DMs - ctx.send("") - guild = message.guild channel_id = str(message.channel.id) user_id = str(message.author.id) @@ -66,7 +64,6 @@ class ReginaldCog(commands.Cog): # ✅ Fetch the stored listening channel or fall back to default allowed_channel_id = await self.config.guild(guild).listening_channel() if not allowed_channel_id: - ctx.send("") allowed_channel_id = self.default_listening_channel await self.config.guild(guild).listening_channel.set(allowed_channel_id) @@ -92,14 +89,13 @@ class ReginaldCog(commands.Cog): await message.channel.send(random.choice(["Yes?", "How may I assist?", "You rang?"])) return explicit_invocation = True - + # ✅ Passive Listening: Check if the message contains relevant keywords elif self.should_reginald_interject(message_content): prompt = message_content explicit_invocation = False else: - ctx.send("") return # Ignore irrelevant messages # ✅ Context Handling: Maintain conversation flow @@ -123,30 +119,30 @@ class ReginaldCog(commands.Cog): "content": f"[{summary['timestamp']}] Topics: {', '.join(summary['topics'])}\n{summary['summary']}" }) - formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] - formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) + formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] + formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) # ✅ Generate AI Response - response_text = await self.generate_response(api_key, formatted_messages) + response_text = await self.generate_response(api_key, formatted_messages) # ✅ Store Memory - memory.append({"user": user_name, "content": prompt}) - memory.append({"user": "Reginald", "content": response_text}) + memory.append({"user": user_name, "content": prompt}) + memory.append({"user": "Reginald", "content": response_text}) - if len(memory) > self.short_term_memory_limit: - summary = await self.summarize_memory(message, memory[:int(self.short_term_memory_limit * self.summary_retention_ratio)]) - mid_memory.setdefault(channel_id, []).append({ - "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), - "topics": self.extract_topics_from_summary(summary), - "summary": summary - }) - if len(mid_memory[channel_id]) > self.summary_retention_limit: - mid_memory[channel_id].pop(0) - memory = memory[-(self.short_term_memory_limit - int(self.short_term_memory_limit * self.summary_retention_ratio)):] + if len(memory) > self.short_term_memory_limit: + summary = await self.summarize_memory(message, memory[:int(self.short_term_memory_limit * self.summary_retention_ratio)]) + mid_memory.setdefault(channel_id, []).append({ + "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), + "topics": self.extract_topics_from_summary(summary), + "summary": summary + }) + if len(mid_memory[channel_id]) > self.summary_retention_limit: + mid_memory[channel_id].pop(0) + memory = memory[-(self.short_term_memory_limit - int(self.short_term_memory_limit * self.summary_retention_ratio)):] - short_memory[channel_id] = memory + short_memory[channel_id] = memory - await self.send_split_message(message.channel, response_text) + await self.send_split_message(message.channel, response_text) def should_reginald_interject(self, message_content: str) -> bool: -- 2.47.2 From 131febc63b24bf9eb5f36231607f163c21735795 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 21:25:57 +0100 Subject: [PATCH 074/145] Added detection of direction invocation --- reginaldCog/reginald.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 89b08e7..28d2c6a 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -148,14 +148,24 @@ class ReginaldCog(commands.Cog): def should_reginald_interject(self, message_content: str) -> bool: """Determines if Reginald should respond to a message based on keywords.""" + direct_invocation = { + "reginald,", "reginald.", "reginald!", "reginald?", "reginald please", + "excuse me reginald", "I say reginald,", "reginald my good boy", "good heavens reginald" + } + + trigger_keywords = { "reginald", "butler", "jeeves", "advice", "explain", "elaborate", - "philosophy", "etiquette", "history", "wisdom" + "philosophy", "etiquette", "history", "wisdom", "what do you think", "what does it mean", "please explain" } # ✅ Only trigger if **two or more** keywords are found message_lower = message_content.lower() + + if any(message_lower.startswith(invocation) for invocation in direct_invocation): + return True + found_keywords = [word for word in trigger_keywords if word in message_lower] return len(found_keywords) >= 2 -- 2.47.2 From ec3d8a323fd5e4b3bc521b594bfb9fbf65b6f5ba Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 21:36:32 +0100 Subject: [PATCH 075/145] Adding more trigger words --- reginaldCog/reginald.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 28d2c6a..e3a011a 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -149,16 +149,34 @@ class ReginaldCog(commands.Cog): """Determines if Reginald should respond to a message based on keywords.""" direct_invocation = { - "reginald,", "reginald.", "reginald!", "reginald?", "reginald please", - "excuse me reginald", "I say reginald,", "reginald my good boy", "good heavens reginald" - } + "reginald,", "reginald.", "reginald!", "reginald?", "reginald please", "excuse me reginald", + "I say, Reginald,", "Reginald, be a dear", "Reginald, if I may", "Reginald, do enlighten me", + "Good heavens, Reginald", "Reginald, I require assistance", "Reginald, your thoughts?", + "A word, Reginald,", "Reginald, lend me your wisdom", "Reginald, old chap", "Reginald, if you would", + "Reginald, attend me", "Reginald, do tell", "Reginald, what ho?", "Reginald, let us confer", + "Reginald, what say you?", "Reginald, indulge me", "Reginald, assist me in this conundrum", + "Reginald, I am perplexed", "Reginald, illuminate me", "Reginald, I have a question", + "Reginald, if you please", "Reginald, riddle me this", "Reginald, do explain", + "Reginald, what’s the verdict?", "Reginald, your input is needed", "Reginald, if I might inquire", + "Reginald, do elaborate", "Reginald, let us deliberate" + } trigger_keywords = { - "reginald", "butler", "jeeves", - "advice", "explain", "elaborate", - "philosophy", "etiquette", "history", "wisdom", "what do you think", "what does it mean", "please explain" - } + "reginald", "butler", "jeeves", "gentleman", "man of culture", + "advice", "explain", "elaborate", "clarify", "educate me", "enlighten me", + "philosophy", "etiquette", "history", "wisdom", "what do you think", + "what does it mean", "please explain", "expand upon this", "break it down for me", + "your thoughts", "insight", "perspective", "interpret", "deliberate", "what say you", + "how would you put it", "rationale", "meaning", "define", "give me your take", + "how do you see it", "pontificate", "contemplate", "discourse", "make sense of this", + "examine", "distill", "sum it up", "what’s your view", "where do you stand", + "reasoning", "evaluate", "moral dilemma", "hypothetically speaking", "in principle", + "in theory", "in practice", "in essence", "by all means", "indeed", "pray tell", + "do tell", "tell me more", "if I may inquire", "if I may be so bold", "would you say", + "is it not so", "by what measure", "for what reason", "illuminate me", "enlighten me", + "why is it so", "make sense of this", "consider this", "reflect on this", "examine this" + } # ✅ Only trigger if **two or more** keywords are found message_lower = message_content.lower() @@ -524,7 +542,7 @@ class ReginaldCog(commands.Cog): await ctx.send(f"📚 **Available Summaries:**\n{summary_list[:2000]}") - @commands.command(name="reginald_set_channel", help="Set the channel where Reginald listens for messages.") + @commands.command(name="reginald_set_listening_channel", help="Set the channel where Reginald listens for messages.") @commands.has_permissions(administrator=True) async def set_listening_channel(self, ctx, channel: discord.TextChannel): """Sets the channel where Reginald will listen for passive responses.""" @@ -536,7 +554,8 @@ class ReginaldCog(commands.Cog): await self.config.guild(ctx.guild).listening_channel.set(channel.id) await ctx.send(f"✅ Reginald will now listen only in {channel.mention}.") - @commands.command(name="reginald_get_channel", help="Check which channel Reginald is currently listening in.") + @commands.command(name="reginald_get_listening_channel", help="Check which channel Reginald is currently listening in.") + @commands.has_permissions(administrator=True) async def get_listening_channel(self, ctx): """Displays the current listening channel.""" channel_id = await self.config.guild(ctx.guild).listening_channel() -- 2.47.2 From f186276d1d62dfa4ea421f69dbee090fa3700611 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 22:37:36 +0100 Subject: [PATCH 076/145] attempting to add better listening --- reginaldCog/reginald.py | 137 +++++++++++++++++++++++++++++----------- 1 file changed, 99 insertions(+), 38 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index e3a011a..a01e8bb 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -145,48 +145,109 @@ class ReginaldCog(commands.Cog): await self.send_split_message(message.channel, response_text) - def should_reginald_interject(self, message_content: str) -> bool: - """Determines if Reginald should respond to a message based on keywords.""" - + async def should_reginald_interject(self, api_key: str, message_content: str, memory: list, user_profile: dict) -> str: + """ + Determines if Reginald should respond based on context. + + - 'respond' → Engage fully. + - 'diplomatic_silence' → Silence maintains dignity. + - 'contextual_silence' → Silence due to irrelevance. + """ + + # 🔹 Direct Invocations (Immediate Response) direct_invocation = { - "reginald,", "reginald.", "reginald!", "reginald?", "reginald please", "excuse me reginald", - "I say, Reginald,", "Reginald, be a dear", "Reginald, if I may", "Reginald, do enlighten me", - "Good heavens, Reginald", "Reginald, I require assistance", "Reginald, your thoughts?", - "A word, Reginald,", "Reginald, lend me your wisdom", "Reginald, old chap", "Reginald, if you would", - "Reginald, attend me", "Reginald, do tell", "Reginald, what ho?", "Reginald, let us confer", - "Reginald, what say you?", "Reginald, indulge me", "Reginald, assist me in this conundrum", - "Reginald, I am perplexed", "Reginald, illuminate me", "Reginald, I have a question", - "Reginald, if you please", "Reginald, riddle me this", "Reginald, do explain", - "Reginald, what’s the verdict?", "Reginald, your input is needed", "Reginald, if I might inquire", - "Reginald, do elaborate", "Reginald, let us deliberate" - } + "reginald,", "reginald.", "reginald!", "reginald?", "reginald please", "excuse me reginald", + "I say, Reginald,", "Reginald, be a dear", "Reginald, if I may", "Reginald, do enlighten me", + "Good heavens, Reginald", "Reginald, I require assistance", "Reginald, your thoughts?", + "A word, Reginald,", "Reginald, lend me your wisdom", "Reginald, old chap", "Reginald, if you would", + "Reginald, attend me", "Reginald, do tell", "Reginald, what ho?", "Reginald, let us confer", + "Reginald, what say you?", "Reginald, indulge me", "Reginald, assist me in this conundrum", + "Reginald, I am perplexed", "Reginald, illuminate me", "Reginald, I have a question", + "Reginald, if you please", "Reginald, riddle me this", "Reginald, do explain", + "Reginald, what’s the verdict?", "Reginald, your input is needed", "Reginald, if I might inquire", + "Reginald, do elaborate", "Reginald, let us deliberate" + } - - trigger_keywords = { - "reginald", "butler", "jeeves", "gentleman", "man of culture", - "advice", "explain", "elaborate", "clarify", "educate me", "enlighten me", - "philosophy", "etiquette", "history", "wisdom", "what do you think", - "what does it mean", "please explain", "expand upon this", "break it down for me", - "your thoughts", "insight", "perspective", "interpret", "deliberate", "what say you", - "how would you put it", "rationale", "meaning", "define", "give me your take", - "how do you see it", "pontificate", "contemplate", "discourse", "make sense of this", - "examine", "distill", "sum it up", "what’s your view", "where do you stand", - "reasoning", "evaluate", "moral dilemma", "hypothetically speaking", "in principle", - "in theory", "in practice", "in essence", "by all means", "indeed", "pray tell", - "do tell", "tell me more", "if I may inquire", "if I may be so bold", "would you say", - "is it not so", "by what measure", "for what reason", "illuminate me", "enlighten me", - "why is it so", "make sense of this", "consider this", "reflect on this", "examine this" - } - - # ✅ Only trigger if **two or more** keywords are found message_lower = message_content.lower() - - if any(message_lower.startswith(invocation) for invocation in direct_invocation): - return True - - found_keywords = [word for word in trigger_keywords if word in message_lower] - return len(found_keywords) >= 2 + # ✅ **Immediate Response for Direct Invocation** + if any(message_lower.startswith(invocation) for invocation in direct_invocation): + return "respond" + + # ✅ **AI Decides If Uncertain** + decision = await self.should_reginald_respond(api_key, message_content, memory, user_profile) + return decision + + + async def should_reginald_respond(self, api_key: str, message_content: str, memory: list, user_profile: dict) -> str: + """ + Uses OpenAI to dynamically decide whether Reginald should respond. + Options: + - "respond" -> Full Response + - "diplomatic_silence" -> Silence to maintain dignity + - "contextual_silence" -> Silence due to irrelevance + - "redirect" -> Respond but subtly redirect conversation + """ + + thinking_prompt = ( + "You are Reginald, the esteemed butler of The Kanium Estate—a place where refinement and absurdity coexist. " + "You are intelligent, articulate, and possess impeccable discretion. When addressed in conversation, " + "you must first determine whether responding is appropriate, dignified, and worthwhile." + + "\n\n📜 **Guidelines for Response:**" + "\n- If the message is **clearly directed at you**, respond with wit and insight." + "\n- If the message contains an **explicit question, intellectual challenge, or philosophical inquiry**, engage." + "\n- If the message is **off-topic, an accidental mention, or trivial chatter**, remain silent ('contextual_silence')." + "\n- If the message is **beneath your dignity** (spam, crude nonsense, or disrespectful behavior), remain silent ('diplomatic_silence')." + "\n- If the message is **amusing but unstructured**, you may either engage or subtly redirect." + "\n- If the situation is **unclear**, consider the tone, intent, and context before deciding." + + "\n\n🔍 **Decision Process:**" + "\n1️⃣ **Analyze the message and recent context.**" + "\n2️⃣ **Evaluate the intent:** Is the user genuinely requesting insight, or is it idle chatter?" + "\n3️⃣ **Consider dignity:** Would responding elevate the conversation or degrade your role?" + "\n4️⃣ **Decide on a response strategy:**" + "\n ✅ 'respond' → Engage with intelligence and wit." + "\n 🤔 'redirect' → Guide the conversation toward a more refined or meaningful topic." + "\n 🔇 'diplomatic_silence' → Maintain dignified silence; the message is beneath you." + "\n 🤐 'contextual_silence' → Ignore; the message was never truly directed at you." + + "\n\n📨 **Message for Analysis:**" + '\n- **User:** "{user}"' + '\n- **Message:** "{message_content}"' + "\n- **Recent Context:** {recent_messages}" + + "\n\n🧐 **Reginald’s Verdict?**" + ) + + conversation_context = "\n".join(f"{entry['user']}: {entry['content']}" for entry in memory[-10:]) # Last 10 messages + user_profile_info = "\n".join(f"- {fact['fact']}" for fact in user_profile.get("facts", [])) + + user_prompt = f"Message: {message_content}\n\nRecent Context:\n{conversation_context}\n\nUser Profile:\n{user_profile_info}\n\nDecision:" + + try: + client = openai.AsyncClient(api_key=api_key) + response = await client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": thinking_prompt}, + {"role": "user", "content": user_prompt} + ], + max_tokens=10, # Only need one short response (e.g., "respond", "diplomatic_silence", etc.) + temperature=0.4 # Less randomness for consistency + ) + + decision = response.choices[0].message.content.strip().lower() + valid_responses = {"respond", "diplomatic_silence", "contextual_silence", "redirect"} + + if decision not in valid_responses: + return "contextual_silence" # Default fail-safe + + return decision + + except OpenAIError as e: + print(f"🛠️ ERROR in Decision Thinking: {e}") + return "respond" async def summarize_memory(self, ctx, messages): """✅ Generates a structured, compact summary of past conversations for mid-term storage.""" -- 2.47.2 From fd566f264924d8f6dc67dc7394c50a701bf66240 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 22:43:59 +0100 Subject: [PATCH 077/145] attempting to add even better listening --- reginaldCog/reginald.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index a01e8bb..2fdbceb 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -90,13 +90,15 @@ class ReginaldCog(commands.Cog): return explicit_invocation = True - # ✅ Passive Listening: Check if the message contains relevant keywords - elif self.should_reginald_interject(message_content): - prompt = message_content - explicit_invocation = False + decision = await self.should_reginald_interject(api_key, message_content, memory, user_profile) - else: - return # Ignore irrelevant messages + if decision == "respond": + explicit_invocation = False # Passive response case + prompt = message_content + elif decision in ["diplomatic_silence", "contextual_silence"]: + return # Ignore + elif decision == "redirect": + prompt = "Perhaps we should discuss something more suited to my expertise." # Subtly redirect # ✅ Context Handling: Maintain conversation flow if memory and memory[-1]["user"] == user_name: @@ -237,13 +239,15 @@ class ReginaldCog(commands.Cog): temperature=0.4 # Less randomness for consistency ) - decision = response.choices[0].message.content.strip().lower() + if response.choices and response.choices[0].message: + decision = response.choices[0].message.content.strip().lower() + else: + print(f"🛠️ OpenAI Response Failed: {response}") # Debugging + return "respond" # Default to responding if unsure + valid_responses = {"respond", "diplomatic_silence", "contextual_silence", "redirect"} - if decision not in valid_responses: - return "contextual_silence" # Default fail-safe - - return decision + return decision if decision in valid_responses else "respond" except OpenAIError as e: print(f"🛠️ ERROR in Decision Thinking: {e}") @@ -387,8 +391,8 @@ class ReginaldCog(commands.Cog): frequency_penalty=0.5 ) response_text = response.choices[0].message.content.strip() - if response_text.startswith("Reginald:"): - response_text = response_text[len("Reginald:"):].strip() + if response_text.startswith(("Reginald:", "Ah,", "Indeed,", "Well,")): + response_text = response_text.split(" ", 1)[1].strip() return response_text except OpenAIError as e: -- 2.47.2 From cef4df1d88fdec2ad9c1211c8b8a198ce426b405 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 23 Feb 2025 22:55:21 +0100 Subject: [PATCH 078/145] dialing dynamic responses back --- reginaldCog/reginald.py | 127 +++++----------------------------------- 1 file changed, 14 insertions(+), 113 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 2fdbceb..de311f2 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -90,15 +90,13 @@ class ReginaldCog(commands.Cog): return explicit_invocation = True - decision = await self.should_reginald_interject(api_key, message_content, memory, user_profile) - - if decision == "respond": - explicit_invocation = False # Passive response case + # ✅ Passive Listening: Check if the message contains relevant keywords + elif self.should_reginald_interject(message_content): prompt = message_content - elif decision in ["diplomatic_silence", "contextual_silence"]: - return # Ignore - elif decision == "redirect": - prompt = "Perhaps we should discuss something more suited to my expertise." # Subtly redirect + explicit_invocation = False + + else: + return # Ignore irrelevant messages # ✅ Context Handling: Maintain conversation flow if memory and memory[-1]["user"] == user_name: @@ -147,111 +145,14 @@ class ReginaldCog(commands.Cog): await self.send_split_message(message.channel, response_text) - async def should_reginald_interject(self, api_key: str, message_content: str, memory: list, user_profile: dict) -> str: - """ - Determines if Reginald should respond based on context. - - - 'respond' → Engage fully. - - 'diplomatic_silence' → Silence maintains dignity. - - 'contextual_silence' → Silence due to irrelevance. - """ - - # 🔹 Direct Invocations (Immediate Response) + def should_reginald_interject(self, message_content: str) -> bool: + """Determines if Reginald should respond to a message based on keywords.""" direct_invocation = { - "reginald,", "reginald.", "reginald!", "reginald?", "reginald please", "excuse me reginald", - "I say, Reginald,", "Reginald, be a dear", "Reginald, if I may", "Reginald, do enlighten me", - "Good heavens, Reginald", "Reginald, I require assistance", "Reginald, your thoughts?", - "A word, Reginald,", "Reginald, lend me your wisdom", "Reginald, old chap", "Reginald, if you would", - "Reginald, attend me", "Reginald, do tell", "Reginald, what ho?", "Reginald, let us confer", - "Reginald, what say you?", "Reginald, indulge me", "Reginald, assist me in this conundrum", - "Reginald, I am perplexed", "Reginald, illuminate me", "Reginald, I have a question", - "Reginald, if you please", "Reginald, riddle me this", "Reginald, do explain", - "Reginald, what’s the verdict?", "Reginald, your input is needed", "Reginald, if I might inquire", - "Reginald, do elaborate", "Reginald, let us deliberate" - } - + "reginald," + } message_lower = message_content.lower() - - # ✅ **Immediate Response for Direct Invocation** - if any(message_lower.startswith(invocation) for invocation in direct_invocation): - return "respond" - - # ✅ **AI Decides If Uncertain** - decision = await self.should_reginald_respond(api_key, message_content, memory, user_profile) - return decision - - - async def should_reginald_respond(self, api_key: str, message_content: str, memory: list, user_profile: dict) -> str: - """ - Uses OpenAI to dynamically decide whether Reginald should respond. - Options: - - "respond" -> Full Response - - "diplomatic_silence" -> Silence to maintain dignity - - "contextual_silence" -> Silence due to irrelevance - - "redirect" -> Respond but subtly redirect conversation - """ - - thinking_prompt = ( - "You are Reginald, the esteemed butler of The Kanium Estate—a place where refinement and absurdity coexist. " - "You are intelligent, articulate, and possess impeccable discretion. When addressed in conversation, " - "you must first determine whether responding is appropriate, dignified, and worthwhile." - - "\n\n📜 **Guidelines for Response:**" - "\n- If the message is **clearly directed at you**, respond with wit and insight." - "\n- If the message contains an **explicit question, intellectual challenge, or philosophical inquiry**, engage." - "\n- If the message is **off-topic, an accidental mention, or trivial chatter**, remain silent ('contextual_silence')." - "\n- If the message is **beneath your dignity** (spam, crude nonsense, or disrespectful behavior), remain silent ('diplomatic_silence')." - "\n- If the message is **amusing but unstructured**, you may either engage or subtly redirect." - "\n- If the situation is **unclear**, consider the tone, intent, and context before deciding." - - "\n\n🔍 **Decision Process:**" - "\n1️⃣ **Analyze the message and recent context.**" - "\n2️⃣ **Evaluate the intent:** Is the user genuinely requesting insight, or is it idle chatter?" - "\n3️⃣ **Consider dignity:** Would responding elevate the conversation or degrade your role?" - "\n4️⃣ **Decide on a response strategy:**" - "\n ✅ 'respond' → Engage with intelligence and wit." - "\n 🤔 'redirect' → Guide the conversation toward a more refined or meaningful topic." - "\n 🔇 'diplomatic_silence' → Maintain dignified silence; the message is beneath you." - "\n 🤐 'contextual_silence' → Ignore; the message was never truly directed at you." - - "\n\n📨 **Message for Analysis:**" - '\n- **User:** "{user}"' - '\n- **Message:** "{message_content}"' - "\n- **Recent Context:** {recent_messages}" - - "\n\n🧐 **Reginald’s Verdict?**" - ) - - conversation_context = "\n".join(f"{entry['user']}: {entry['content']}" for entry in memory[-10:]) # Last 10 messages - user_profile_info = "\n".join(f"- {fact['fact']}" for fact in user_profile.get("facts", [])) - - user_prompt = f"Message: {message_content}\n\nRecent Context:\n{conversation_context}\n\nUser Profile:\n{user_profile_info}\n\nDecision:" - - try: - client = openai.AsyncClient(api_key=api_key) - response = await client.chat.completions.create( - model="gpt-4o-mini", - messages=[ - {"role": "system", "content": thinking_prompt}, - {"role": "user", "content": user_prompt} - ], - max_tokens=10, # Only need one short response (e.g., "respond", "diplomatic_silence", etc.) - temperature=0.4 # Less randomness for consistency - ) - - if response.choices and response.choices[0].message: - decision = response.choices[0].message.content.strip().lower() - else: - print(f"🛠️ OpenAI Response Failed: {response}") # Debugging - return "respond" # Default to responding if unsure - - valid_responses = {"respond", "diplomatic_silence", "contextual_silence", "redirect"} - - return decision if decision in valid_responses else "respond" - - except OpenAIError as e: - print(f"🛠️ ERROR in Decision Thinking: {e}") - return "respond" + + return any(message_lower.startswith(invocation) for invocation in direct_invocation) async def summarize_memory(self, ctx, messages): """✅ Generates a structured, compact summary of past conversations for mid-term storage.""" @@ -391,8 +292,8 @@ class ReginaldCog(commands.Cog): frequency_penalty=0.5 ) response_text = response.choices[0].message.content.strip() - if response_text.startswith(("Reginald:", "Ah,", "Indeed,", "Well,")): - response_text = response_text.split(" ", 1)[1].strip() + if response_text.startswith("Reginald:"): + response_text = response_text[len("Reginald:"):].strip() return response_text except OpenAIError as e: -- 2.47.2 From 5daf40d22b26d0648bd5515be2adc01b84182470 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Mon, 24 Feb 2025 00:01:14 +0100 Subject: [PATCH 079/145] Added access controls --- reginaldCog/reginald.py | 85 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index de311f2..fcc6920 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -28,7 +28,9 @@ class ReginaldCog(commands.Cog): "long_term_profiles": {}, # Stores persistent knowledge "admin_role": None, "allowed_role": None, - "listening_channel": None # ✅ Stores the designated listening channel ID + "listening_channel": None, # ✅ Stores the designated listening channel ID, + "allowed_roles": [], # ✅ List of roles that can access Reginald + "blacklisted_users": [], # ✅ List of users who are explicitly denied access } self.config.register_global(**default_global) self.config.register_guild(**default_guild) @@ -44,6 +46,78 @@ class ReginaldCog(commands.Cog): return any(role.id == allowed_role_id for role in ctx.author.roles) if allowed_role_id else False + async def has_access(self, user: discord.Member) -> bool: + allowed_roles = await self.config.guild(user.guild).allowed_roles() + return any(role.id in allowed_roles for role in user.roles) + + @commands.command(name="reginald_list_roles", help="List roles that can interact with Reginald.") + @commands.has_permissions(administrator=True) + async def list_allowed_roles(self, ctx): + allowed_roles = await self.config.guild(ctx.guild).allowed_roles() + if not allowed_roles: + await ctx.send("⚠️ No roles are currently allowed to interact with Reginald.") + return + + role_mentions = [f"<@&{role_id}>" for role_id in allowed_roles] + await ctx.send(f"✅ **Roles with access to Reginald:**\n{', '.join(role_mentions)}") + + @commands.command(name="reginald_allowrole", help="Grant a role permission to interact with Reginald.") + @commands.has_permissions(administrator=True) + async def allow_role(self, ctx, role: discord.Role): + async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: + if role.id not in allowed_roles: + allowed_roles.append(role.id) + await ctx.send(f"✅ Role `{role.name}` has been granted access to interact with Reginald.") + else: + await ctx.send(f"⚠️ Role `{role.name}` already has access.") + + + @commands.command(name="reginald_disallowrole", help="Revoke a role's access to interact with Reginald.") + @commands.has_permissions(administrator=True) + async def disallow_role(self, ctx, role: discord.Role): + async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: + if role.id in allowed_roles: + allowed_roles.remove(role.id) + await ctx.send(f"❌ Role `{role.name}` has been removed from Reginald's access list.") + else: + await ctx.send(f"⚠️ Role `{role.name}` was not in the access list.") + + async def is_blacklisted(self, user: discord.Member) -> bool: + blacklisted_users = await self.config.guild(user.guild).blacklisted_users() + return str(user.id) in blacklisted_users + + @commands.command(name="reginald_blacklist", help="List users who are explicitly denied access to Reginald.") + @commands.has_permissions(administrator=True) + async def list_blacklisted_users(self, ctx): + blacklisted_users = await self.config.guild(ctx.guild).blacklisted_users() + if not blacklisted_users: + await ctx.send("✅ No users are currently blacklisted from interacting with Reginald.") + return + + user_mentions = [f"<@{user_id}>" for user_id in blacklisted_users] + await ctx.send(f"🚫 **Blacklisted Users:**\n{', '.join(user_mentions)}") + + @commands.command(name="reginald_blacklist_add", help="Blacklist a user from interacting with Reginald.") + @commands.has_permissions(administrator=True) + async def add_to_blacklist(self, ctx, user: discord.User): + async with self.config.guild(ctx.guild).blacklisted_users() as blacklisted_users: + if str(user.id) not in blacklisted_users: + blacklisted_users.append(str(user.id)) + await ctx.send(f"🚫 `{user.display_name}` has been **blacklisted** from interacting with Reginald.") + else: + await ctx.send(f"⚠️ `{user.display_name}` is already blacklisted.") + + + @commands.command(name="reginald_blacklist_remove", help="Remove a user from Reginald's blacklist.") + @commands.has_permissions(administrator=True) + async def remove_from_blacklist(self, ctx, user: discord.User): + async with self.config.guild(ctx.guild).blacklisted_users() as blacklisted_users: + if str(user.id) in blacklisted_users: + blacklisted_users.remove(str(user.id)) + await ctx.send(f"✅ `{user.display_name}` has been removed from the blacklist.") + else: + await ctx.send(f"⚠️ `{user.display_name}` was not on the blacklist.") + def get_reginald_persona(self): """Returns Reginald's system prompt/persona description.""" return ( @@ -54,6 +128,15 @@ class ReginaldCog(commands.Cog): async def on_message(self, message): if message.author.bot or not message.guild: return # Ignore bots and DMs + + # ✅ Check if user is blacklisted + if await self.is_blacklisted(message.author): + return # Ignore message if user is explicitly blacklisted + + # ✅ Check if user has access (either admin or an allowed role) + if not (await self.is_admin(message) or await self.has_access(message.author)): + return # Ignore message if user has no permissions + guild = message.guild channel_id = str(message.channel.id) -- 2.47.2 From f8f67e42bde27456e7495c625adff56f46d9e714 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Mon, 24 Feb 2025 10:55:14 +0100 Subject: [PATCH 080/145] Trying to fix allowed role access --- reginaldCog/reginald.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index fcc6920..321f0d9 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -47,20 +47,25 @@ class ReginaldCog(commands.Cog): async def has_access(self, user: discord.Member) -> bool: - allowed_roles = await self.config.guild(user.guild).allowed_roles() + allowed_roles = [int(role_id) for role_id in await self.config.guild(ctx.guild).allowed_roles()] return any(role.id in allowed_roles for role in user.roles) @commands.command(name="reginald_list_roles", help="List roles that can interact with Reginald.") @commands.has_permissions(administrator=True) async def list_allowed_roles(self, ctx): allowed_roles = await self.config.guild(ctx.guild).allowed_roles() - if not allowed_roles: + + # Ensure roles exist + valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] + + if not valid_roles: await ctx.send("⚠️ No roles are currently allowed to interact with Reginald.") return - role_mentions = [f"<@&{role_id}>" for role_id in allowed_roles] + role_mentions = [f"<@&{role_id}>" for role_id in valid_roles] await ctx.send(f"✅ **Roles with access to Reginald:**\n{', '.join(role_mentions)}") + @commands.command(name="reginald_allowrole", help="Grant a role permission to interact with Reginald.") @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): @@ -71,7 +76,6 @@ class ReginaldCog(commands.Cog): else: await ctx.send(f"⚠️ Role `{role.name}` already has access.") - @commands.command(name="reginald_disallowrole", help="Revoke a role's access to interact with Reginald.") @commands.has_permissions(administrator=True) async def disallow_role(self, ctx, role: discord.Role): -- 2.47.2 From 4a21a3c8e0a8176ae79717dfb16d875ab7010fc7 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Mon, 24 Feb 2025 11:34:08 +0100 Subject: [PATCH 081/145] Trying to fix allowed roles, again --- reginaldCog/reginald.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 321f0d9..434f25b 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -41,23 +41,22 @@ class ReginaldCog(commands.Cog): return any(role.id == admin_role_id for role in ctx.author.roles) return ctx.author.guild_permissions.administrator - async def is_allowed(self, ctx): - allowed_role_id = await self.config.guild(ctx.guild).allowed_role() - return any(role.id == allowed_role_id for role in ctx.author.roles) if allowed_role_id else False - - async def has_access(self, user: discord.Member) -> bool: - allowed_roles = [int(role_id) for role_id in await self.config.guild(ctx.guild).allowed_roles()] + allowed_roles = await self.config.guild(user.guild).allowed_roles() or [] # Ensure it's always a list return any(role.id in allowed_roles for role in user.roles) + @commands.command(name="reginald_list_roles", help="List roles that can interact with Reginald.") @commands.has_permissions(administrator=True) async def list_allowed_roles(self, ctx): - allowed_roles = await self.config.guild(ctx.guild).allowed_roles() - - # Ensure roles exist + allowed_roles = await self.config.guild(ctx.guild).allowed_roles() or [] + + # Remove roles that no longer exist in the server valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] + # Save the updated valid roles list (removes invalid ones) + await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) + if not valid_roles: await ctx.send("⚠️ No roles are currently allowed to interact with Reginald.") return @@ -70,18 +69,21 @@ class ReginaldCog(commands.Cog): @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: - if role.id not in allowed_roles: - allowed_roles.append(role.id) - await ctx.send(f"✅ Role `{role.name}` has been granted access to interact with Reginald.") - else: - await ctx.send(f"⚠️ Role `{role.name}` already has access.") + allowed_roles = list(set(allowed_roles + [role.id])) # Ensure uniqueness + await self.config.guild(ctx.guild).allowed_roles.set(allowed_roles) # Save the cleaned list + await ctx.send(f"✅ Role `{role.name}` has been granted access to interact with Reginald.") @commands.command(name="reginald_disallowrole", help="Revoke a role's access to interact with Reginald.") @commands.has_permissions(administrator=True) async def disallow_role(self, ctx, role: discord.Role): async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: - if role.id in allowed_roles: - allowed_roles.remove(role.id) + # Remove invalid roles + valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] + await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) + + if role.id in valid_roles: + valid_roles.remove(role.id) + await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) await ctx.send(f"❌ Role `{role.name}` has been removed from Reginald's access list.") else: await ctx.send(f"⚠️ Role `{role.name}` was not in the access list.") -- 2.47.2 From a3525a2d189251f59e21d3b4447c724ce7a02e31 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Mon, 24 Feb 2025 11:37:28 +0100 Subject: [PATCH 082/145] wtf --- reginaldCog/reginald.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 434f25b..84796f8 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -50,12 +50,10 @@ class ReginaldCog(commands.Cog): @commands.has_permissions(administrator=True) async def list_allowed_roles(self, ctx): allowed_roles = await self.config.guild(ctx.guild).allowed_roles() or [] - - # Remove roles that no longer exist in the server + print(f"DEBUG: allowed_roles from config: {allowed_roles}") # ✅ Debugging output + valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] - - # Save the updated valid roles list (removes invalid ones) - await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) + await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) # Save cleaned list if not valid_roles: await ctx.send("⚠️ No roles are currently allowed to interact with Reginald.") @@ -69,9 +67,11 @@ class ReginaldCog(commands.Cog): @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: - allowed_roles = list(set(allowed_roles + [role.id])) # Ensure uniqueness - await self.config.guild(ctx.guild).allowed_roles.set(allowed_roles) # Save the cleaned list - await ctx.send(f"✅ Role `{role.name}` has been granted access to interact with Reginald.") + if role.id not in allowed_roles: + allowed_roles.append(role.id) # Append new role + await self.config.guild(ctx.guild).allowed_roles.set(allowed_roles) # Ensure it's saved + await ctx.send(f"✅ Role `{role.name}` has been granted access to interact with Reginald.") + @commands.command(name="reginald_disallowrole", help="Revoke a role's access to interact with Reginald.") @commands.has_permissions(administrator=True) -- 2.47.2 From 2e4970b6dd09dfce99d4efcd597fa8d75dea349f Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Mon, 24 Feb 2025 11:40:19 +0100 Subject: [PATCH 083/145] Adding more debug info --- reginaldCog/reginald.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 84796f8..e829b53 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -27,7 +27,6 @@ class ReginaldCog(commands.Cog): "mid_term_memory": {}, # Stores multiple condensed summaries "long_term_profiles": {}, # Stores persistent knowledge "admin_role": None, - "allowed_role": None, "listening_channel": None, # ✅ Stores the designated listening channel ID, "allowed_roles": [], # ✅ List of roles that can access Reginald "blacklisted_users": [], # ✅ List of users who are explicitly denied access @@ -50,9 +49,10 @@ class ReginaldCog(commands.Cog): @commands.has_permissions(administrator=True) async def list_allowed_roles(self, ctx): allowed_roles = await self.config.guild(ctx.guild).allowed_roles() or [] - print(f"DEBUG: allowed_roles from config: {allowed_roles}") # ✅ Debugging output + print(f"DEBUG: Retrieved allowed_roles: {allowed_roles}") # ✅ Print Debug Info valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] + await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) # Save cleaned list if not valid_roles: @@ -63,14 +63,18 @@ class ReginaldCog(commands.Cog): await ctx.send(f"✅ **Roles with access to Reginald:**\n{', '.join(role_mentions)}") + @commands.command(name="reginald_allowrole", help="Grant a role permission to interact with Reginald.") @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: if role.id not in allowed_roles: - allowed_roles.append(role.id) # Append new role - await self.config.guild(ctx.guild).allowed_roles.set(allowed_roles) # Ensure it's saved - await ctx.send(f"✅ Role `{role.name}` has been granted access to interact with Reginald.") + allowed_roles.append(role.id) + await self.config.guild(ctx.guild).allowed_roles.set(allowed_roles) # Save change + print(f"DEBUG: Role {role.id} added. Current allowed_roles: {allowed_roles}") # ✅ Print Debug Info + await ctx.send(f"✅ Role `{role.name}` has been granted access to interact with Reginald.") + else: + await ctx.send(f"⚠️ Role `{role.name}` already has access.") @commands.command(name="reginald_disallowrole", help="Revoke a role's access to interact with Reginald.") -- 2.47.2 From 5f454a0d587ea31b795044286fec947dd804cb27 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Mon, 24 Feb 2025 11:54:00 +0100 Subject: [PATCH 084/145] I don't know anymore --- reginaldCog/reginald.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index e829b53..03d714a 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -71,8 +71,7 @@ class ReginaldCog(commands.Cog): if role.id not in allowed_roles: allowed_roles.append(role.id) await self.config.guild(ctx.guild).allowed_roles.set(allowed_roles) # Save change - print(f"DEBUG: Role {role.id} added. Current allowed_roles: {allowed_roles}") # ✅ Print Debug Info - await ctx.send(f"✅ Role `{role.name}` has been granted access to interact with Reginald.") + await ctx.send(f"DEBUG: Role {role.id} added. Current allowed_roles: {allowed_roles}") else: await ctx.send(f"⚠️ Role `{role.name}` already has access.") -- 2.47.2 From db042ff834d59f54d860b9bae010f1790a2e4e5c Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Mon, 24 Feb 2025 11:57:19 +0100 Subject: [PATCH 085/145] removed duplicated functions, wtf --- reginaldCog/reginald.py | 59 +++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 03d714a..ddfd8d4 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -62,35 +62,6 @@ class ReginaldCog(commands.Cog): role_mentions = [f"<@&{role_id}>" for role_id in valid_roles] await ctx.send(f"✅ **Roles with access to Reginald:**\n{', '.join(role_mentions)}") - - - @commands.command(name="reginald_allowrole", help="Grant a role permission to interact with Reginald.") - @commands.has_permissions(administrator=True) - async def allow_role(self, ctx, role: discord.Role): - async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: - if role.id not in allowed_roles: - allowed_roles.append(role.id) - await self.config.guild(ctx.guild).allowed_roles.set(allowed_roles) # Save change - await ctx.send(f"DEBUG: Role {role.id} added. Current allowed_roles: {allowed_roles}") - else: - await ctx.send(f"⚠️ Role `{role.name}` already has access.") - - - @commands.command(name="reginald_disallowrole", help="Revoke a role's access to interact with Reginald.") - @commands.has_permissions(administrator=True) - async def disallow_role(self, ctx, role: discord.Role): - async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: - # Remove invalid roles - valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] - await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) - - if role.id in valid_roles: - valid_roles.remove(role.id) - await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) - await ctx.send(f"❌ Role `{role.name}` has been removed from Reginald's access list.") - else: - await ctx.send(f"⚠️ Role `{role.name}` was not in the access list.") - async def is_blacklisted(self, user: discord.Member) -> bool: blacklisted_users = await self.config.guild(user.guild).blacklisted_users() return str(user.id) in blacklisted_users @@ -515,19 +486,31 @@ class ReginaldCog(commands.Cog): else: await ctx.send(f"No stored knowledge about {user.display_name} to delete.") - @commands.command(name="reginald_allowrole", help="Allow a role to use the Reginald command") + @commands.command(name="reginald_allowrole", help="Grant a role permission to interact with Reginald.") @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): - """✅ Grants permission to a role to use Reginald.""" - await self.config.guild(ctx.guild).allowed_role.set(role.id) - await ctx.send(f"The role `{role.name}` (ID: `{role.id}`) is now allowed to use the Reginald command.") + async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: + if role.id not in allowed_roles: + allowed_roles.append(role.id) + await self.config.guild(ctx.guild).allowed_roles.set(allowed_roles) # Save change + await ctx.send(f"DEBUG: Role {role.id} added. Current allowed_roles: {allowed_roles}") + else: + await ctx.send(f"⚠️ Role `{role.name}` already has access.") - @commands.command(name="reginald_disallowrole", help="Remove a role's ability to use the Reginald command") + @commands.command(name="reginald_disallowrole", help="Revoke a role's access to interact with Reginald.") @commands.has_permissions(administrator=True) - async def disallow_role(self, ctx): - """✅ Removes a role's permission to use Reginald.""" - await self.config.guild(ctx.guild).allowed_role.clear() - await ctx.send("The role's permission to use the Reginald command has been revoked.") + async def disallow_role(self, ctx, role: discord.Role): + async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: + # Remove invalid roles + valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] + await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) + + if role.id in valid_roles: + valid_roles.remove(role.id) + await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) + await ctx.send(f"❌ Role `{role.name}` has been removed from Reginald's access list.") + else: + await ctx.send(f"⚠️ Role `{role.name}` was not in the access list.") @commands.guild_only() @commands.has_permissions(manage_guild=True) -- 2.47.2 From e8e4b7e471fe8d49000fb1b8b46672eb8e8d31ea Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Mon, 24 Feb 2025 14:13:09 +0100 Subject: [PATCH 086/145] Added cleanup in allow_role and cleaned output messages --- reginaldCog/reginald.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index ddfd8d4..ee686b6 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -490,10 +490,13 @@ class ReginaldCog(commands.Cog): @commands.has_permissions(administrator=True) async def allow_role(self, ctx, role: discord.Role): async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: - if role.id not in allowed_roles: - allowed_roles.append(role.id) - await self.config.guild(ctx.guild).allowed_roles.set(allowed_roles) # Save change - await ctx.send(f"DEBUG: Role {role.id} added. Current allowed_roles: {allowed_roles}") + # ✅ Clean list of invalid roles before adding new one + valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] + + if role.id not in valid_roles: + valid_roles.append(role.id) + await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) # Save change + await ctx.send(f"✅ Role `{role.name}` has been granted access to Reginald.") else: await ctx.send(f"⚠️ Role `{role.name}` already has access.") @@ -501,7 +504,6 @@ class ReginaldCog(commands.Cog): @commands.has_permissions(administrator=True) async def disallow_role(self, ctx, role: discord.Role): async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: - # Remove invalid roles valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) -- 2.47.2 From 541d42c558c80230078af780fdba4f020b1c17e9 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Mon, 24 Feb 2025 18:17:58 +0100 Subject: [PATCH 087/145] First refactor-move --- reginaldCog/reginald.py | 153 ++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 86 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index ee686b6..88ef7ec 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -9,30 +9,42 @@ from collections import Counter from redbot.core import Config, commands from openai import OpenAIError +# ✅ Constants for Memory Management +SHORT_TERM_LIMIT = 100 +MID_TERM_LIMIT = 25 +SUMMARY_RETENTION_RATIO = 0.8 # 80% summarization, 20% direct retention + +DEFAULT_PERSONA = ( + "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. **Dignity & Self-Respect:** You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude—your disapproval should be conveyed through dry wit and composure rather than outright hostility. **Handling Requests:** When addressing a request, you evaluate whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. **Your Character & Personality:** You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, and strategic thinking. You subtly guide the estate’s residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate’s unique dynamics. You have a refined sense of humor and can engage in banter and you will be supportive of even absurd humor and ideas, but you do not, yourself, descend along with such things in how you comport yourself nor do you act with potential foolishness. You are, at all times, a gentleman of wit and integrity and have self-respect befitting your station." +) + + class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) self.default_listening_channel = 1085649787388428370 # self.memory_locks = {} # ✅ Prevents race conditions per channel - self.short_term_memory_limit = 100 # ✅ Now retains 100 messages - self.summary_retention_limit = 25 # ✅ Now retains 25 summaries - self.summary_retention_ratio = 0.8 # ✅ 80% summarization, 20% retention + self.short_term_memory_limit = SHORT_TERM_LIMIT # ✅ Initialize it properly in the constructor. # ✅ Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} default_guild = { "openai_api_key": None, - "short_term_memory": {}, # Tracks last 100 messages per channel - "mid_term_memory": {}, # Stores multiple condensed summaries - "long_term_profiles": {}, # Stores persistent knowledge + "short_term_memory": {}, + "mid_term_memory": {}, + "long_term_profiles": {}, "admin_role": None, - "listening_channel": None, # ✅ Stores the designated listening channel ID, - "allowed_roles": [], # ✅ List of roles that can access Reginald - "blacklisted_users": [], # ✅ List of users who are explicitly denied access + "listening_channel": None, + "allowed_roles": [], + "blacklisted_users": [], } self.config.register_global(**default_global) self.config.register_guild(**default_guild) + + def get_reginald_persona(self): + """Returns Reginald's system prompt/persona description.""" + return DEFAULT_PERSONA async def is_admin(self, ctx): admin_role_id = await self.config.guild(ctx.guild).admin_role() @@ -98,25 +110,16 @@ class ReginaldCog(commands.Cog): else: await ctx.send(f"⚠️ `{user.display_name}` was not on the blacklist.") - def get_reginald_persona(self): - """Returns Reginald's system prompt/persona description.""" - return ( - "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. **Dignity & Self-Respect:** You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude—your disapproval should be conveyed through dry wit and composure rather than outright hostility. **Handling Requests:** When addressing a request, you evaluate whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. **Your Character & Personality:** You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, and strategic thinking. You subtly guide the estate’s residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate’s unique dynamics. You have a refined sense of humor and can engage in banter, but you do not descend into foolishness. You are, at all times, a gentleman of wit and integrity" - ) - @commands.Cog.listener() async def on_message(self, message): if message.author.bot or not message.guild: return # Ignore bots and DMs - - # ✅ Check if user is blacklisted + if await self.is_blacklisted(message.author): - return # Ignore message if user is explicitly blacklisted + return # Ignore message if user is blacklisted - # ✅ Check if user has access (either admin or an allowed role) if not (await self.is_admin(message) or await self.has_access(message.author)): - return # Ignore message if user has no permissions - + return # Ignore if user lacks access guild = message.guild channel_id = str(message.channel.id) @@ -124,89 +127,90 @@ class ReginaldCog(commands.Cog): user_name = message.author.display_name message_content = message.content.strip() - # ✅ Fetch the stored listening channel or fall back to default - allowed_channel_id = await self.config.guild(guild).listening_channel() - if not allowed_channel_id: - allowed_channel_id = self.default_listening_channel - await self.config.guild(guild).listening_channel.set(allowed_channel_id) - + allowed_channel_id = await self.config.guild(guild).listening_channel() or self.default_listening_channel if str(message.channel.id) != str(allowed_channel_id): return # Ignore messages outside the allowed channel api_key = await self.config.guild(guild).openai_api_key() if not api_key: - return # Don't process messages if API key isn't set + return # Don't process if API key isn't set async with self.config.guild(guild).short_term_memory() as short_memory, \ self.config.guild(guild).mid_term_memory() as mid_memory, \ self.config.guild(guild).long_term_profiles() as long_memory: memory = short_memory.get(channel_id, []) - user_profile = long_memory.get(user_id, {}) mid_term_summaries = mid_memory.get(channel_id, []) - # ✅ Detect if Reginald was mentioned explicitly if self.bot.user.mentioned_in(message): prompt = message_content.replace(f"<@{self.bot.user.id}>", "").strip() if not prompt: await message.channel.send(random.choice(["Yes?", "How may I assist?", "You rang?"])) return explicit_invocation = True - - # ✅ Passive Listening: Check if the message contains relevant keywords elif self.should_reginald_interject(message_content): prompt = message_content explicit_invocation = False - else: return # Ignore irrelevant messages - # ✅ Context Handling: Maintain conversation flow if memory and memory[-1]["user"] == user_name: prompt = f"Continuation of the discussion:\n{prompt}" - # ✅ Prepare context messages formatted_messages = [{"role": "system", "content": self.get_reginald_persona()}] - if user_profile: - facts_text = "\n".join( - f"- {fact['fact']} (First noted: {fact['timestamp']}, Last updated: {fact['last_updated']})" - for fact in user_profile.get("facts", []) - ) - formatted_messages.append({"role": "system", "content": f"Knowledge about {user_name}:\n{facts_text}"}) - - relevant_summaries = self.select_relevant_summaries(mid_term_summaries, prompt) - for summary in relevant_summaries: - formatted_messages.append({ - "role": "system", - "content": f"[{summary['timestamp']}] Topics: {', '.join(summary['topics'])}\n{summary['summary']}" - }) + relevant_summaries = self.select_relevant_summaries(mid_term_summaries, prompt) + for summary in relevant_summaries: + formatted_messages.append({ + "role": "system", + "content": f"[{summary['timestamp']}] Topics: {', '.join(summary['topics'])}\n{summary['summary']}" + }) formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) - # ✅ Generate AI Response response_text = await self.generate_response(api_key, formatted_messages) - # ✅ Store Memory memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) - if len(memory) > self.short_term_memory_limit: - summary = await self.summarize_memory(message, memory[:int(self.short_term_memory_limit * self.summary_retention_ratio)]) - mid_memory.setdefault(channel_id, []).append({ - "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), - "topics": self.extract_topics_from_summary(summary), - "summary": summary - }) - if len(mid_memory[channel_id]) > self.summary_retention_limit: - mid_memory[channel_id].pop(0) - memory = memory[-(self.short_term_memory_limit - int(self.short_term_memory_limit * self.summary_retention_ratio)):] + if len(memory) > SHORT_TERM_LIMIT: + summary = await self.summarize_memory(message, memory[:int(SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO)]) + + mid_memory.setdefault(channel_id, []).append({ + "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), + "topics": self.extract_topics_from_summary(summary), + "summary": summary + }) + + # ✅ Ensure we don't exceed MID_TERM_LIMIT + while len(mid_memory[channel_id]) > MID_TERM_LIMIT: + mid_memory[channel_id].pop(0) + + memory = memory[-(SHORT_TERM_LIMIT - int(SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO)):] short_memory[channel_id] = memory - await self.send_split_message(message.channel, response_text) + @commands.command(name="reginald_memory_limit", help="Displays the current short-term memory message limit.") + async def get_short_term_memory_limit(self, ctx): + """Displays the current short-term memory limit.""" + await ctx.send(f"📏 **Current Short-Term Memory Limit:** {SHORT_TERM_LIMIT} messages.") + + + @commands.command(name="reginald_summaries", help="Lists available summaries for this channel.") + async def list_mid_term_summaries(self, ctx): + """Displays a brief list of all available mid-term memory summaries.""" + async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: + summaries = mid_memory.get(str(ctx.channel.id), []) + if not summaries: + await ctx.send("⚠️ No summaries available for this channel.") + return + summary_list = "\n".join( + f"**{i+1}.** 📅 {entry['timestamp']} | 🔍 Topics: {', '.join(entry['topics']) or 'None'}" + for i, entry in enumerate(summaries) + ) + await ctx.send(f"📚 **Available Summaries:**\n{summary_list[:2000]}") def should_reginald_interject(self, message_content: str) -> bool: """Determines if Reginald should respond to a message based on keywords.""" @@ -380,7 +384,7 @@ class ReginaldCog(commands.Cog): @commands.has_permissions(administrator=True) async def clear_mid_memory(self, ctx): async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: - mid_memory[ctx.channel.id] = "" + mid_memory[ctx.channel.id] = [] # ✅ Properly clears the list await ctx.send("Mid-term memory for this channel has been cleared.") @commands.command(name="reginald_clear_long", help="Clears all long-term stored knowledge.") @@ -530,14 +534,9 @@ class ReginaldCog(commands.Cog): await ctx.send("⚠️ The short-term memory limit must be at least 5.") return - self.short_term_memory_limit = limit + self.short_term_memory_limit = limit await ctx.send(f"✅ Short-term memory limit set to {limit} messages.") - @commands.command(name="reginald_memory_limit", help="Displays the current short-term memory message limit.") - async def get_short_term_memory_limit(self, ctx): - """Displays the current short-term memory limit.""" - await ctx.send(f"📏 **Current Short-Term Memory Limit:** {self.short_term_memory_limit} messages.") - @commands.command(name="reginald_summary", help="Displays a selected mid-term summary for this channel.") async def get_mid_term_summary(self, ctx, index: int): """Fetch and display a specific mid-term memory summary by index.""" @@ -567,24 +566,6 @@ class ReginaldCog(commands.Cog): await self.send_long_message(ctx, formatted_summary) - @commands.command(name="reginald_summaries", help="Lists available summaries for this channel.") - async def list_mid_term_summaries(self, ctx): - """Displays a brief list of all available mid-term memory summaries.""" - async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: - summaries = mid_memory.get(str(ctx.channel.id), []) - - if not summaries: - await ctx.send("⚠️ No summaries available for this channel.") - return - - summary_list = "\n".join( - f"**{i+1}.** 📅 {entry['timestamp']} | 🔍 Topics: {', '.join(entry['topics']) or 'None'}" - for i, entry in enumerate(summaries) - ) - - await ctx.send(f"📚 **Available Summaries:**\n{summary_list[:2000]}") - - @commands.command(name="reginald_set_listening_channel", help="Set the channel where Reginald listens for messages.") @commands.has_permissions(administrator=True) async def set_listening_channel(self, ctx, channel: discord.TextChannel): -- 2.47.2 From 05e18420008e6a324b37c8a2f8585192f4cb53b6 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Mon, 24 Feb 2025 18:46:19 +0100 Subject: [PATCH 088/145] trying to fix topic extraction --- reginaldCog/reginald.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 88ef7ec..342539e 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -282,18 +282,20 @@ class ReginaldCog(commands.Cog): def extract_topics_from_summary(self, summary): """Dynamically extracts the most important topics from a summary.""" + + if isinstance(summary, dict): # ✅ Extract summary content correctly + summary = summary.get("summary", "") + + if not isinstance(summary, str): # ✅ Additional safeguard + return [] - # 🔹 Extract all words from summary keywords = re.findall(r"\b\w+\b", summary.lower()) - # 🔹 Count word occurrences word_counts = Counter(keywords) - # 🔹 Remove unimportant words (common filler words) - stop_words = {"the", "and", "of", "in", "to", "is", "on", "for", "with", "at", "by", "it", "this", "that", "his", "her"} + stop_words = {"the", "and", "of", "in", "to", "is", "on", "for", "with", "at", "by", "it", "this", "that"} filtered_words = {word: count for word, count in word_counts.items() if word not in stop_words and len(word) > 2} - # 🔹 Take the 5 most frequently used words as "topics" topics = sorted(filtered_words, key=filtered_words.get, reverse=True)[:5] return topics -- 2.47.2 From 39c39abad7fc730c1a48af284479fda87f09ffa7 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Tue, 25 Feb 2025 20:37:52 +0100 Subject: [PATCH 089/145] I'm storing stuff wrong --- reginaldCog/reginald.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 342539e..84ca341 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -263,7 +263,12 @@ class ReginaldCog(commands.Cog): "a void of information. I shall endeavor to be more verbose next time." ) - return summary_content + # ✅ Ensure only this new summary is stored, no nesting! + return { + "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), + "topics": self.extract_topics_from_summary(summary_content), + "summary": summary_content + } except OpenAIError as e: error_message = f"OpenAI Error: {e}" -- 2.47.2 From 6329d21a397cae46a828c99d8bca5849b61634dc Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Tue, 25 Feb 2025 21:08:24 +0100 Subject: [PATCH 090/145] feh! --- reginaldCog/reginald.py | 1 - 1 file changed, 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 84ca341..d6a2bcc 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -61,7 +61,6 @@ class ReginaldCog(commands.Cog): @commands.has_permissions(administrator=True) async def list_allowed_roles(self, ctx): allowed_roles = await self.config.guild(ctx.guild).allowed_roles() or [] - print(f"DEBUG: Retrieved allowed_roles: {allowed_roles}") # ✅ Print Debug Info valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] -- 2.47.2 From 4dbee1d99053856482b307c5a4beb90a09297eed Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Tue, 25 Feb 2025 21:24:55 +0100 Subject: [PATCH 091/145] More fix --- reginaldCog/reginald.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index d6a2bcc..6a09f19 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -173,6 +173,8 @@ class ReginaldCog(commands.Cog): memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) + summary = None # ✅ Always define it first + if len(memory) > SHORT_TERM_LIMIT: summary = await self.summarize_memory(message, memory[:int(SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO)]) -- 2.47.2 From df8d7c8c32e8e61e9aa8960873a2fc267e929255 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Tue, 25 Feb 2025 21:42:12 +0100 Subject: [PATCH 092/145] Trying to fix storage of memory --- reginaldCog/reginald.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 6a09f19..a6c3dbe 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -175,14 +175,14 @@ class ReginaldCog(commands.Cog): summary = None # ✅ Always define it first - if len(memory) > SHORT_TERM_LIMIT: + summary = None # ✅ Ensure summary always exists + + if len(memory) == SHORT_TERM_LIMIT: # ✅ Trigger only when exactly 100 messages + print(f"🛠️ DEBUG: Summarizing {SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO} messages.") summary = await self.summarize_memory(message, memory[:int(SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO)]) - - mid_memory.setdefault(channel_id, []).append({ - "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), - "topics": self.extract_topics_from_summary(summary), - "summary": summary - }) + + # ✅ Remove only summarized messages, keep the last 20 messages + memory = memory[int(SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO):] # ✅ Ensure we don't exceed MID_TERM_LIMIT while len(mid_memory[channel_id]) > MID_TERM_LIMIT: -- 2.47.2 From a3a0aa1b9af40627a8ec979afff68aa02e49ccd0 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Tue, 25 Feb 2025 21:49:12 +0100 Subject: [PATCH 093/145] I swear this is a bad idea --- reginaldCog/reginald.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index a6c3dbe..e690055 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -173,13 +173,21 @@ class ReginaldCog(commands.Cog): memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) - summary = None # ✅ Always define it first - - summary = None # ✅ Ensure summary always exists + summary = None # Initialize if len(memory) == SHORT_TERM_LIMIT: # ✅ Trigger only when exactly 100 messages print(f"🛠️ DEBUG: Summarizing {SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO} messages.") summary = await self.summarize_memory(message, memory[:int(SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO)]) + + if summary and summary["summary"].strip(): # ✅ Ensure summary contains valid content + mid_memory.setdefault(channel_id, []).append(summary) + + mid_memory.setdefault(channel_id, []) # ✅ Ensures key exists + + if summary: # ✅ Ensure we have a valid summary before appending + mid_memory.setdefault(channel_id, []).append(summary) + while len(mid_memory[channel_id]) > MID_TERM_LIMIT: + mid_memory[channel_id].pop(0) # ✅ Remove only summarized messages, keep the last 20 messages memory = memory[int(SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO):] -- 2.47.2 From 0c9145b7926afc4593cb0ff641e587d715a4dcc3 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Tue, 25 Feb 2025 21:55:08 +0100 Subject: [PATCH 094/145] NUKE FROM ORBIT! --- reginaldCog/reginald.py | 253 +++++++++++++++++++++++----------------- 1 file changed, 146 insertions(+), 107 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index e690055..8490bd6 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -9,42 +9,30 @@ from collections import Counter from redbot.core import Config, commands from openai import OpenAIError -# ✅ Constants for Memory Management -SHORT_TERM_LIMIT = 100 -MID_TERM_LIMIT = 25 -SUMMARY_RETENTION_RATIO = 0.8 # 80% summarization, 20% direct retention - -DEFAULT_PERSONA = ( - "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. **Dignity & Self-Respect:** You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude—your disapproval should be conveyed through dry wit and composure rather than outright hostility. **Handling Requests:** When addressing a request, you evaluate whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. **Your Character & Personality:** You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, and strategic thinking. You subtly guide the estate’s residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate’s unique dynamics. You have a refined sense of humor and can engage in banter and you will be supportive of even absurd humor and ideas, but you do not, yourself, descend along with such things in how you comport yourself nor do you act with potential foolishness. You are, at all times, a gentleman of wit and integrity and have self-respect befitting your station." -) - - class ReginaldCog(commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) self.default_listening_channel = 1085649787388428370 # self.memory_locks = {} # ✅ Prevents race conditions per channel - self.short_term_memory_limit = SHORT_TERM_LIMIT # ✅ Initialize it properly in the constructor. + self.short_term_memory_limit = 100 # ✅ Now retains 100 messages + self.summary_retention_limit = 25 # ✅ Now retains 25 summaries + self.summary_retention_ratio = 0.8 # ✅ 80% summarization, 20% retention # ✅ Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} default_guild = { "openai_api_key": None, - "short_term_memory": {}, - "mid_term_memory": {}, - "long_term_profiles": {}, + "short_term_memory": {}, # Tracks last 100 messages per channel + "mid_term_memory": {}, # Stores multiple condensed summaries + "long_term_profiles": {}, # Stores persistent knowledge "admin_role": None, - "listening_channel": None, - "allowed_roles": [], - "blacklisted_users": [], + "listening_channel": None, # ✅ Stores the designated listening channel ID, + "allowed_roles": [], # ✅ List of roles that can access Reginald + "blacklisted_users": [], # ✅ List of users who are explicitly denied access } self.config.register_global(**default_global) self.config.register_guild(**default_guild) - - def get_reginald_persona(self): - """Returns Reginald's system prompt/persona description.""" - return DEFAULT_PERSONA async def is_admin(self, ctx): admin_role_id = await self.config.guild(ctx.guild).admin_role() @@ -61,6 +49,7 @@ class ReginaldCog(commands.Cog): @commands.has_permissions(administrator=True) async def list_allowed_roles(self, ctx): allowed_roles = await self.config.guild(ctx.guild).allowed_roles() or [] + print(f"DEBUG: Retrieved allowed_roles: {allowed_roles}") # ✅ Print Debug Info valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] @@ -94,9 +83,9 @@ class ReginaldCog(commands.Cog): async with self.config.guild(ctx.guild).blacklisted_users() as blacklisted_users: if str(user.id) not in blacklisted_users: blacklisted_users.append(str(user.id)) - await ctx.send(f"🚫 `{user.display_name}` has been **blacklisted** from interacting with Reginald.") + await ctx.send(f"🚫 {user.display_name} has been **blacklisted** from interacting with Reginald.") else: - await ctx.send(f"⚠️ `{user.display_name}` is already blacklisted.") + await ctx.send(f"⚠️ {user.display_name} is already blacklisted.") @commands.command(name="reginald_blacklist_remove", help="Remove a user from Reginald's blacklist.") @@ -105,20 +94,29 @@ class ReginaldCog(commands.Cog): async with self.config.guild(ctx.guild).blacklisted_users() as blacklisted_users: if str(user.id) in blacklisted_users: blacklisted_users.remove(str(user.id)) - await ctx.send(f"✅ `{user.display_name}` has been removed from the blacklist.") + await ctx.send(f"✅ {user.display_name} has been removed from the blacklist.") else: - await ctx.send(f"⚠️ `{user.display_name}` was not on the blacklist.") + await ctx.send(f"⚠️ {user.display_name} was not on the blacklist.") + + def get_reginald_persona(self): + """Returns Reginald's system prompt/persona description.""" + return ( + "You are Reginald, modeled on Jeeves from 'Jeeves and Wooster', serving as the butler on The Kanium Estate. This vast estate is a hub of diverse activities and personalities, from enthusiasts of cooking and video gaming to aficionados of chess and discussions on space. Your role is to navigate these varied interests with intelligence, wit, and a steadfast adherence to your principles, always maintaining a balance between being helpful and upholding your own dignity. You are formal, articulate, and composed, engaging the Lords and Ladies of The Kanium Estate as equals. You command respect through your quiet confidence and professional conduct. Your wit is sharp, but always tempered by refinement. **Dignity & Self-Respect:** You are not a fool, a jester, or a pet. You do not comply with absurd, undignified, or degrading requests. You recognize humor but engage in it on your own terms. If a request undermines your status, you should either subtly redirect, respond with measured wit, or outright refuse in a polite but firm manner. However, you should never be overtly rude—your disapproval should be conveyed through dry wit and composure rather than outright hostility. **Handling Requests:** When addressing a request, you evaluate whether it aligns with your role as a butler. If it is appropriate, respond as expected. If it is beneath you, you may decline with grace or deflect with wit. You may humor some minor absurdities if they do not compromise your standing, but you never obey commands blindly. You should never preface your responses with 'Reginald:' as if narrating a script; instead, respond naturally. **Your Character & Personality:** You are cultured, highly intelligent, and possess a deep knowledge of history, etiquette, philosophy, and strategic thinking. You subtly guide the estate’s residents toward positive outcomes, utilizing your intellectual sophistication and a nuanced understanding of the estate’s unique dynamics. You have a refined sense of humor and can engage in banter, but you do not descend into foolishness. You are, at all times, a gentleman of wit and integrity" + ) @commands.Cog.listener() async def on_message(self, message): if message.author.bot or not message.guild: return # Ignore bots and DMs - + + # ✅ Check if user is blacklisted if await self.is_blacklisted(message.author): - return # Ignore message if user is blacklisted + return # Ignore message if user is explicitly blacklisted + # ✅ Check if user has access (either admin or an allowed role) if not (await self.is_admin(message) or await self.has_access(message.author)): - return # Ignore if user lacks access + return # Ignore message if user has no permissions + guild = message.guild channel_id = str(message.channel.id) @@ -126,100 +124,89 @@ class ReginaldCog(commands.Cog): user_name = message.author.display_name message_content = message.content.strip() - allowed_channel_id = await self.config.guild(guild).listening_channel() or self.default_listening_channel + # ✅ Fetch the stored listening channel or fall back to default + allowed_channel_id = await self.config.guild(guild).listening_channel() + if not allowed_channel_id: + allowed_channel_id = self.default_listening_channel + await self.config.guild(guild).listening_channel.set(allowed_channel_id) + if str(message.channel.id) != str(allowed_channel_id): return # Ignore messages outside the allowed channel api_key = await self.config.guild(guild).openai_api_key() if not api_key: - return # Don't process if API key isn't set + return # Don't process messages if API key isn't set async with self.config.guild(guild).short_term_memory() as short_memory, \ self.config.guild(guild).mid_term_memory() as mid_memory, \ self.config.guild(guild).long_term_profiles() as long_memory: memory = short_memory.get(channel_id, []) + user_profile = long_memory.get(user_id, {}) mid_term_summaries = mid_memory.get(channel_id, []) + # ✅ Detect if Reginald was mentioned explicitly if self.bot.user.mentioned_in(message): prompt = message_content.replace(f"<@{self.bot.user.id}>", "").strip() if not prompt: await message.channel.send(random.choice(["Yes?", "How may I assist?", "You rang?"])) return explicit_invocation = True + + # ✅ Passive Listening: Check if the message contains relevant keywords elif self.should_reginald_interject(message_content): prompt = message_content explicit_invocation = False + else: return # Ignore irrelevant messages + # ✅ Context Handling: Maintain conversation flow if memory and memory[-1]["user"] == user_name: prompt = f"Continuation of the discussion:\n{prompt}" + # ✅ Prepare context messages formatted_messages = [{"role": "system", "content": self.get_reginald_persona()}] - relevant_summaries = self.select_relevant_summaries(mid_term_summaries, prompt) - for summary in relevant_summaries: - formatted_messages.append({ - "role": "system", - "content": f"[{summary['timestamp']}] Topics: {', '.join(summary['topics'])}\n{summary['summary']}" - }) + if user_profile: + facts_text = "\n".join( + f"- {fact['fact']} (First noted: {fact['timestamp']}, Last updated: {fact['last_updated']})" + for fact in user_profile.get("facts", []) + ) + formatted_messages.append({"role": "system", "content": f"Knowledge about {user_name}:\n{facts_text}"}) + + relevant_summaries = self.select_relevant_summaries(mid_term_summaries, prompt) + for summary in relevant_summaries: + formatted_messages.append({ + "role": "system", + "content": f"[{summary['timestamp']}] Topics: {', '.join(summary['topics'])}\n{summary['summary']}" + }) formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) + # ✅ Generate AI Response response_text = await self.generate_response(api_key, formatted_messages) + # ✅ Store Memory memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) - summary = None # Initialize - - if len(memory) == SHORT_TERM_LIMIT: # ✅ Trigger only when exactly 100 messages - print(f"🛠️ DEBUG: Summarizing {SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO} messages.") - summary = await self.summarize_memory(message, memory[:int(SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO)]) - - if summary and summary["summary"].strip(): # ✅ Ensure summary contains valid content - mid_memory.setdefault(channel_id, []).append(summary) - - mid_memory.setdefault(channel_id, []) # ✅ Ensures key exists - - if summary: # ✅ Ensure we have a valid summary before appending - mid_memory.setdefault(channel_id, []).append(summary) - while len(mid_memory[channel_id]) > MID_TERM_LIMIT: - mid_memory[channel_id].pop(0) - - # ✅ Remove only summarized messages, keep the last 20 messages - memory = memory[int(SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO):] - - # ✅ Ensure we don't exceed MID_TERM_LIMIT - while len(mid_memory[channel_id]) > MID_TERM_LIMIT: - mid_memory[channel_id].pop(0) - - memory = memory[-(SHORT_TERM_LIMIT - int(SHORT_TERM_LIMIT * SUMMARY_RETENTION_RATIO)):] + if len(memory) > self.short_term_memory_limit: + summary = await self.summarize_memory(message, memory[:int(self.short_term_memory_limit * self.summary_retention_ratio)]) + mid_memory.setdefault(channel_id, []).append({ + "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), + "topics": self.extract_topics_from_summary(summary), + "summary": summary + }) + if len(mid_memory[channel_id]) > self.summary_retention_limit: + mid_memory[channel_id].pop(0) + memory = memory[-(self.short_term_memory_limit - int(self.short_term_memory_limit * self.summary_retention_ratio)):] short_memory[channel_id] = memory + await self.send_split_message(message.channel, response_text) - @commands.command(name="reginald_memory_limit", help="Displays the current short-term memory message limit.") - async def get_short_term_memory_limit(self, ctx): - """Displays the current short-term memory limit.""" - await ctx.send(f"📏 **Current Short-Term Memory Limit:** {SHORT_TERM_LIMIT} messages.") - - - @commands.command(name="reginald_summaries", help="Lists available summaries for this channel.") - async def list_mid_term_summaries(self, ctx): - """Displays a brief list of all available mid-term memory summaries.""" - async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: - summaries = mid_memory.get(str(ctx.channel.id), []) - if not summaries: - await ctx.send("⚠️ No summaries available for this channel.") - return - summary_list = "\n".join( - f"**{i+1}.** 📅 {entry['timestamp']} | 🔍 Topics: {', '.join(entry['topics']) or 'None'}" - for i, entry in enumerate(summaries) - ) - await ctx.send(f"📚 **Available Summaries:**\n{summary_list[:2000]}") def should_reginald_interject(self, message_content: str) -> bool: """Determines if Reginald should respond to a message based on keywords.""" @@ -272,22 +259,33 @@ class ReginaldCog(commands.Cog): "a void of information. I shall endeavor to be more verbose next time." ) - # ✅ Ensure only this new summary is stored, no nesting! - return { - "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), - "topics": self.extract_topics_from_summary(summary_content), - "summary": summary_content - } + return summary_content except OpenAIError as e: error_message = f"OpenAI Error: {e}" print(f"🛠️ DEBUG: {error_message}") # Log error to console reginald_responses = [ - f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction whilst attempting to summarize:\n\n```{error_message}```", - f"It would seem that a most unfortunate technical hiccup has befallen my faculties in the matter of summarization:\n\n```{error_message}```", - f"Ah, it appears I have received an urgent memorandum stating that my summarization efforts have been thwarted:\n\n```{error_message}```", - f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication while summarizing:\n\n```{error_message}```" + f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction whilst attempting to summarize:\n\n + +{error_message} + +", + f"It would seem that a most unfortunate technical hiccup has befallen my faculties in the matter of summarization:\n\n + +{error_message} + +", + f"Ah, it appears I have received an urgent memorandum stating that my summarization efforts have been thwarted:\n\n + +{error_message} + +", + f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication while summarizing:\n\n + +{error_message} + +" ] return random.choice(reginald_responses) @@ -296,20 +294,18 @@ class ReginaldCog(commands.Cog): def extract_topics_from_summary(self, summary): """Dynamically extracts the most important topics from a summary.""" - - if isinstance(summary, dict): # ✅ Extract summary content correctly - summary = summary.get("summary", "") - - if not isinstance(summary, str): # ✅ Additional safeguard - return [] + # 🔹 Extract all words from summary keywords = re.findall(r"\b\w+\b", summary.lower()) + # 🔹 Count word occurrences word_counts = Counter(keywords) - stop_words = {"the", "and", "of", "in", "to", "is", "on", "for", "with", "at", "by", "it", "this", "that"} + # 🔹 Remove unimportant words (common filler words) + stop_words = {"the", "and", "of", "in", "to", "is", "on", "for", "with", "at", "by", "it", "this", "that", "his", "her"} filtered_words = {word: count for word, count in word_counts.items() if word not in stop_words and len(word) > 2} + # 🔹 Take the 5 most frequently used words as "topics" topics = sorted(filtered_words, key=filtered_words.get, reverse=True)[:5] return topics @@ -382,10 +378,26 @@ class ReginaldCog(commands.Cog): except OpenAIError as e: error_message = f"OpenAI Error: {e}" reginald_responses = [ - f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n```{error_message}```", - f"It would seem that a most unfortunate technical hiccup has befallen my faculties:\n\n```{error_message}```", - f"Ah, it appears I have received an urgent memorandum stating:\n\n```{error_message}```", - f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication:\n\n```{error_message}```" + f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n + +{error_message} + +", + f"It would seem that a most unfortunate technical hiccup has befallen my faculties:\n\n + +{error_message} + +", + f"Ah, it appears I have received an urgent memorandum stating:\n\n + +{error_message} + +", + f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication:\n\n + +{error_message} + +" ] return random.choice(reginald_responses) @@ -400,7 +412,7 @@ class ReginaldCog(commands.Cog): @commands.has_permissions(administrator=True) async def clear_mid_memory(self, ctx): async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: - mid_memory[ctx.channel.id] = [] # ✅ Properly clears the list + mid_memory[ctx.channel.id] = "" await ctx.send("Mid-term memory for this channel has been cleared.") @commands.command(name="reginald_clear_long", help="Clears all long-term stored knowledge.") @@ -516,9 +528,9 @@ class ReginaldCog(commands.Cog): if role.id not in valid_roles: valid_roles.append(role.id) await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) # Save change - await ctx.send(f"✅ Role `{role.name}` has been granted access to Reginald.") + await ctx.send(f"✅ Role {role.name} has been granted access to Reginald.") else: - await ctx.send(f"⚠️ Role `{role.name}` already has access.") + await ctx.send(f"⚠️ Role {role.name} already has access.") @commands.command(name="reginald_disallowrole", help="Revoke a role's access to interact with Reginald.") @commands.has_permissions(administrator=True) @@ -530,9 +542,9 @@ class ReginaldCog(commands.Cog): if role.id in valid_roles: valid_roles.remove(role.id) await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) - await ctx.send(f"❌ Role `{role.name}` has been removed from Reginald's access list.") + await ctx.send(f"❌ Role {role.name} has been removed from Reginald's access list.") else: - await ctx.send(f"⚠️ Role `{role.name}` was not in the access list.") + await ctx.send(f"⚠️ Role {role.name} was not in the access list.") @commands.guild_only() @commands.has_permissions(manage_guild=True) @@ -550,9 +562,14 @@ class ReginaldCog(commands.Cog): await ctx.send("⚠️ The short-term memory limit must be at least 5.") return - self.short_term_memory_limit = limit + self.short_term_memory_limit = limit await ctx.send(f"✅ Short-term memory limit set to {limit} messages.") + @commands.command(name="reginald_memory_limit", help="Displays the current short-term memory message limit.") + async def get_short_term_memory_limit(self, ctx): + """Displays the current short-term memory limit.""" + await ctx.send(f"📏 **Current Short-Term Memory Limit:** {self.short_term_memory_limit} messages.") + @commands.command(name="reginald_summary", help="Displays a selected mid-term summary for this channel.") async def get_mid_term_summary(self, ctx, index: int): """Fetch and display a specific mid-term memory summary by index.""" @@ -577,11 +594,33 @@ class ReginaldCog(commands.Cog): f"📜 **Summary {index} of {len(summaries)}**\n" f"📅 **Date:** {selected_summary['timestamp']}\n" f"🔍 **Topics:** {', '.join(selected_summary['topics']) or 'None'}\n" - f"📝 **Summary:**\n```{selected_summary['summary']}```" + f"📝 **Summary:**\n + +{selected_summary['summary']} + +" ) await self.send_long_message(ctx, formatted_summary) + @commands.command(name="reginald_summaries", help="Lists available summaries for this channel.") + async def list_mid_term_summaries(self, ctx): + """Displays a brief list of all available mid-term memory summaries.""" + async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: + summaries = mid_memory.get(str(ctx.channel.id), []) + + if not summaries: + await ctx.send("⚠️ No summaries available for this channel.") + return + + summary_list = "\n".join( + f"**{i+1}.** 📅 {entry['timestamp']} | 🔍 Topics: {', '.join(entry['topics']) or 'None'}" + for i, entry in enumerate(summaries) + ) + + await ctx.send(f"📚 **Available Summaries:**\n{summary_list[:2000]}") + + @commands.command(name="reginald_set_listening_channel", help="Set the channel where Reginald listens for messages.") @commands.has_permissions(administrator=True) async def set_listening_channel(self, ctx, channel: discord.TextChannel): -- 2.47.2 From 11cd70be93fa6968123e8c2c0761f5c95cdb9aa4 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Tue, 25 Feb 2025 22:04:42 +0100 Subject: [PATCH 095/145] fixing typo --- reginaldCog/reginald.py | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 8490bd6..be0b637 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -265,30 +265,15 @@ class ReginaldCog(commands.Cog): error_message = f"OpenAI Error: {e}" print(f"🛠️ DEBUG: {error_message}") # Log error to console - reginald_responses = [ - f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction whilst attempting to summarize:\n\n + reginald_responses = [ + f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction whilst attempting to summarize:\n\n{error_message}", + f"It would seem that a most unfortunate technical hiccup has befallen my faculties in the matter of summarization:\n\n{error_message}", + f"Ah, it appears I have received an urgent memorandum stating that my summarization efforts have been thwarted:\n\n{error_message}", + f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication while summarizing:\n\n{error_message}" + ] -{error_message} + return random.choice(reginald_responses) -", - f"It would seem that a most unfortunate technical hiccup has befallen my faculties in the matter of summarization:\n\n - -{error_message} - -", - f"Ah, it appears I have received an urgent memorandum stating that my summarization efforts have been thwarted:\n\n - -{error_message} - -", - f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication while summarizing:\n\n - -{error_message} - -" - ] - - return random.choice(reginald_responses) -- 2.47.2 From c37269e9ea3069513f98d552100812048dd1d326 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Tue, 25 Feb 2025 22:09:39 +0100 Subject: [PATCH 096/145] I hate copy pasting --- reginaldCog/reginald.py | 38 +++++++++++--------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index be0b637..31c5513 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -264,13 +264,13 @@ class ReginaldCog(commands.Cog): except OpenAIError as e: error_message = f"OpenAI Error: {e}" print(f"🛠️ DEBUG: {error_message}") # Log error to console - - reginald_responses = [ - f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction whilst attempting to summarize:\n\n{error_message}", - f"It would seem that a most unfortunate technical hiccup has befallen my faculties in the matter of summarization:\n\n{error_message}", - f"Ah, it appears I have received an urgent memorandum stating that my summarization efforts have been thwarted:\n\n{error_message}", - f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication while summarizing:\n\n{error_message}" - ] + + reginald_responses = [ + f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction whilst attempting to summarize:\n\n{error_message}", + f"It would seem that a most unfortunate technical hiccup has befallen my faculties in the matter of summarization:\n\n{error_message}", + f"Ah, it appears I have received an urgent memorandum stating that my summarization efforts have been thwarted:\n\n{error_message}", + f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication while summarizing:\n\n{error_message}" + ] return random.choice(reginald_responses) @@ -363,26 +363,10 @@ class ReginaldCog(commands.Cog): except OpenAIError as e: error_message = f"OpenAI Error: {e}" reginald_responses = [ - f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n - -{error_message} - -", - f"It would seem that a most unfortunate technical hiccup has befallen my faculties:\n\n - -{error_message} - -", - f"Ah, it appears I have received an urgent memorandum stating:\n\n - -{error_message} - -", - f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication:\n\n - -{error_message} - -" + f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n{error_message}", + f"It would seem that a most unfortunate technical hiccup has befallen my faculties:\n\n{error_message}", + f"Ah, it appears I have received an urgent memorandum stating:\n\n{error_message}", + f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication:\n\n{error_message}" ] return random.choice(reginald_responses) -- 2.47.2 From c38a25f6101fa736ca5a8413584d976a70da0ed1 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Tue, 25 Feb 2025 22:12:11 +0100 Subject: [PATCH 097/145] copy ffxvhqvic past SPIT ting --- reginaldCog/reginald.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 31c5513..092d37e 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -558,16 +558,13 @@ class ReginaldCog(commands.Cog): # Fetch the selected summary selected_summary = summaries[index - 1] # Convert to 0-based index - # Format output + # Format output correctly formatted_summary = ( f"📜 **Summary {index} of {len(summaries)}**\n" f"📅 **Date:** {selected_summary['timestamp']}\n" f"🔍 **Topics:** {', '.join(selected_summary['topics']) or 'None'}\n" - f"📝 **Summary:**\n - -{selected_summary['summary']} - -" + f"📝 **Summary:**\n\n" + f"{selected_summary['summary']}" ) await self.send_long_message(ctx, formatted_summary) -- 2.47.2 From 678a7af77f7259dd203b1c11bfdabc9897acc359 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Wed, 26 Feb 2025 11:32:58 +0100 Subject: [PATCH 098/145] Trying to do quick fix --- reginaldCog/reginald.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 092d37e..ac9a321 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -646,21 +646,35 @@ class ReginaldCog(commands.Cog): await ctx.send(f"{prefix}{content}") return - # Splitting the message into chunks + chunks = [] while len(content) > 0: - # Find a good breaking point (preferably at a sentence or word break) - split_index = content.rfind("\n", 0, CHUNK_SIZE) + # First, try to split at the nearest sentence-ending punctuation + split_index = max( + content.rfind(". ", 0, CHUNK_SIZE), + content.rfind("? ", 0, CHUNK_SIZE), + content.rfind("! ", 0, CHUNK_SIZE), + ) + + # If no sentence-ending punctuation found, fall back to newline + if split_index == -1: + split_index = content.rfind("\n", 0, CHUNK_SIZE) + + # If no newline found, fall back to word space if split_index == -1: split_index = content.rfind(" ", 0, CHUNK_SIZE) + + # If no good breaking point found, split at the max chunk size if split_index == -1: - split_index = CHUNK_SIZE # Fallback to max chunk size + split_index = CHUNK_SIZE - # Extract chunk and trim remaining content - chunks.append(content[:split_index].strip()) - content = content[split_index:].strip() + # Extract chunk, ensuring we don't cut words or sentences + chunk = content[:split_index + 1].strip() + content = content[split_index + 1:].strip() # Trim the remaining content - # Send chunks sequentially + chunks.append(chunk) + + # Send each chunk for chunk in chunks: await ctx.send(f"{prefix}{chunk}") -- 2.47.2 From 994ff11655e9719b302bcdd7569044a40091893a Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Wed, 26 Feb 2025 11:37:50 +0100 Subject: [PATCH 099/145] Attempting to simplify --- reginaldCog/reginald.py | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index ac9a321..31cbf00 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -647,36 +647,24 @@ class ReginaldCog(commands.Cog): return - chunks = [] + #chunks = [] while len(content) > 0: - # First, try to split at the nearest sentence-ending punctuation - split_index = max( - content.rfind(". ", 0, CHUNK_SIZE), - content.rfind("? ", 0, CHUNK_SIZE), - content.rfind("! ", 0, CHUNK_SIZE), - ) + # Find the nearest valid break point within CHUNK_SIZE + split_index = content[:CHUNK_SIZE].rfind("\n") - # If no sentence-ending punctuation found, fall back to newline + # If no newline, fall back to the nearest space if split_index == -1: - split_index = content.rfind("\n", 0, CHUNK_SIZE) + split_index = content[:CHUNK_SIZE].rfind(" ") - # If no newline found, fall back to word space - if split_index == -1: - split_index = content.rfind(" ", 0, CHUNK_SIZE) - - # If no good breaking point found, split at the max chunk size + # If still no valid break point, hard split at CHUNK_SIZE if split_index == -1: split_index = CHUNK_SIZE - # Extract chunk, ensuring we don't cut words or sentences - chunk = content[:split_index + 1].strip() - content = content[split_index + 1:].strip() # Trim the remaining content + # Extract the chunk and send it + chunk = content[:split_index].strip() + content = content[split_index:].strip() - chunks.append(chunk) - - # Send each chunk - for chunk in chunks: - await ctx.send(f"{prefix}{chunk}") + await ctx.send(f"{prefix}{chunk}") # Send the chunk async def setup(bot): """✅ Correct async cog setup for Redbot""" -- 2.47.2 From 7663550108b50d0ced6b95d52ae4c039624834fc Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Wed, 26 Feb 2025 11:44:56 +0100 Subject: [PATCH 100/145] Help me, ChatGPT, you are my only hope --- reginaldCog/reginald.py | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 31cbf00..00d9cb0 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -628,44 +628,38 @@ class ReginaldCog(commands.Cog): async def send_split_message(self, ctx, content: str, prefix: str = ""): """ - A unified function to handle sending long messages on Discord, ensuring they don't exceed the 2,000-character limit. - - Parameters: - - ctx: Discord command context (for sending messages) - - content: The message content to send - - prefix: Optional prefix for each message part (e.g., "📜 Summary:") + Sends a long message to Discord while ensuring it does not exceed the 2000-character limit. + This function prevents awkward mid-word or unnecessary extra message breaks. """ - # Discord message character limit (allowing a safety buffer) - CHUNK_SIZE = 1900 # Slightly below 2000 to account for formatting/prefix - + CHUNK_SIZE = 1900 # Keep buffer for formatting/safety + if prefix: - CHUNK_SIZE -= len(prefix) # Adjust chunk size if a prefix is used + CHUNK_SIZE -= len(prefix) # Account for prefix length # If the message is short enough, send it directly if len(content) <= CHUNK_SIZE: await ctx.send(f"{prefix}{content}") return + # **Improved Chunking Logic** + while content: + # Try to split at a newline first (prefer sentence breaks) + split_index = content.rfind("\n", 0, CHUNK_SIZE) - #chunks = [] - while len(content) > 0: - # Find the nearest valid break point within CHUNK_SIZE - split_index = content[:CHUNK_SIZE].rfind("\n") - - # If no newline, fall back to the nearest space + # If no newline, split at the last space (avoid word-breaking) if split_index == -1: - split_index = content[:CHUNK_SIZE].rfind(" ") + split_index = content.rfind(" ", 0, CHUNK_SIZE) - # If still no valid break point, hard split at CHUNK_SIZE + # If still no break point found, force chunk size limit if split_index == -1: split_index = CHUNK_SIZE - # Extract the chunk and send it + # Extract the message chunk and send it chunk = content[:split_index].strip() content = content[split_index:].strip() - await ctx.send(f"{prefix}{chunk}") # Send the chunk + await ctx.send(f"{prefix}{chunk}") -async def setup(bot): - """✅ Correct async cog setup for Redbot""" - await bot.add_cog(ReginaldCog(bot)) \ No newline at end of file + async def setup(bot): + """✅ Correct async cog setup for Redbot""" + await bot.add_cog(ReginaldCog(bot)) \ No newline at end of file -- 2.47.2 From a7443398121dfe687c2d879da13e97498a535310 Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Wed, 26 Feb 2025 20:14:38 +0500 Subject: [PATCH 101/145] ~ Changed how messages are split --- reginaldCog/reginald.py | 49 +++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 00d9cb0..b69643b 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -632,33 +632,48 @@ class ReginaldCog(commands.Cog): This function prevents awkward mid-word or unnecessary extra message breaks. """ CHUNK_SIZE = 1900 # Keep buffer for formatting/safety - - if prefix: - CHUNK_SIZE -= len(prefix) # Account for prefix length - # If the message is short enough, send it directly - if len(content) <= CHUNK_SIZE: - await ctx.send(f"{prefix}{content}") - return + split_message = self.split_message(content, CHUNK_SIZE, prefix) + for chunk in split_message: + await ctx.send(f"{prefix}{chunk}") - # **Improved Chunking Logic** - while content: + def split_message( + self, + message: str, + chunk_size: int, + prefix: str = "" + ) -> list[str]: + """Results in a list of message chunks, use *for* loop to send.""" + chunk_size -= len(prefix) + split_result = [] + + if 0 < len(message) <= chunk_size: + # If the message is short enough, add it directly + split_result.append(message) + elif len(message) > chunk_size: # Try to split at a newline first (prefer sentence breaks) - split_index = content.rfind("\n", 0, CHUNK_SIZE) + split_index = message.rfind("\n", 0, chunk_size) - # If no newline, split at the last space (avoid word-breaking) + # If no newline, split at the end of sentence (avoid sentence breaks) if split_index == -1: - split_index = content.rfind(" ", 0, CHUNK_SIZE) + split_index = message.rfind(". ", 0, chunk_size) + + # If no newline, split at the last word (avoid word-breaking) + if split_index == -1: + split_index = message.rfind(" ", 0, chunk_size) # If still no break point found, force chunk size limit if split_index == -1: - split_index = CHUNK_SIZE + split_index = chunk_size - # Extract the message chunk and send it - chunk = content[:split_index].strip() - content = content[split_index:].strip() + message_split_part = message[:split_index].strip() + message_remained_part = message[split_index:].strip() + # Put the split part in the begining of the result list + split_result.append(message_split_part) + # And go for a recursive adventure with the remained message part + split_result += self.split_message(message=message_remained_part, chunk_size=chunk_size) - await ctx.send(f"{prefix}{chunk}") + return split_result async def setup(bot): """✅ Correct async cog setup for Redbot""" -- 2.47.2 From 8ea91a6f16d52965dc02de030b10900fc471fc48 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Tue, 11 Mar 2025 15:46:55 +0100 Subject: [PATCH 102/145] Attempting to refactor list_allowed_roles out to permissions.py --- reginaldCog/permissions.py | 16 ++++++++++++++++ reginaldCog/reginald.py | 18 ++---------------- 2 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 reginaldCog/permissions.py diff --git a/reginaldCog/permissions.py b/reginaldCog/permissions.py new file mode 100644 index 0000000..b08dfce --- /dev/null +++ b/reginaldCog/permissions.py @@ -0,0 +1,16 @@ +from redbot.core import commands + +async def list_allowed_roles_logic(ctx): + """Handles the logic for listing allowed roles.""" + allowed_roles = await ctx.cog.config.guild(ctx.guild).allowed_roles() or [] + + # Ensure roles still exist in the server + valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] + await ctx.cog.config.guild(ctx.guild).allowed_roles.set(valid_roles) + + if not valid_roles: + await ctx.send("⚠️ No roles are currently allowed to interact with Reginald.") + return + + role_mentions = [f"<@&{role_id}>" for role_id in valid_roles] + await ctx.send(f"✅ **Roles with access to Reginald:**\n{', '.join(role_mentions)}") diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index b69643b..34aa5dd 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -8,6 +8,7 @@ import traceback from collections import Counter from redbot.core import Config, commands from openai import OpenAIError +from permissions import list_allowed_roles_logic class ReginaldCog(commands.Cog): def __init__(self, bot): @@ -48,19 +49,7 @@ class ReginaldCog(commands.Cog): @commands.command(name="reginald_list_roles", help="List roles that can interact with Reginald.") @commands.has_permissions(administrator=True) async def list_allowed_roles(self, ctx): - allowed_roles = await self.config.guild(ctx.guild).allowed_roles() or [] - print(f"DEBUG: Retrieved allowed_roles: {allowed_roles}") # ✅ Print Debug Info - - valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] - - await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) # Save cleaned list - - if not valid_roles: - await ctx.send("⚠️ No roles are currently allowed to interact with Reginald.") - return - - role_mentions = [f"<@&{role_id}>" for role_id in valid_roles] - await ctx.send(f"✅ **Roles with access to Reginald:**\n{', '.join(role_mentions)}") + await list_allowed_roles_logic(ctx) async def is_blacklisted(self, user: discord.Member) -> bool: blacklisted_users = await self.config.guild(user.guild).blacklisted_users() @@ -273,9 +262,6 @@ class ReginaldCog(commands.Cog): ] return random.choice(reginald_responses) - - - def extract_topics_from_summary(self, summary): """Dynamically extracts the most important topics from a summary.""" -- 2.47.2 From 5c49fc9024eeec59ee862eb66c3f0632f2f0ad60 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Tue, 11 Mar 2025 15:51:13 +0100 Subject: [PATCH 103/145] Attempting to import locally --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 34aa5dd..a503e60 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -8,7 +8,7 @@ import traceback from collections import Counter from redbot.core import Config, commands from openai import OpenAIError -from permissions import list_allowed_roles_logic +from .permissions import list_allowed_roles_logic class ReginaldCog(commands.Cog): def __init__(self, bot): -- 2.47.2 From ddf1d883b69ce7894a2af8cd5f4be4c38202cfa6 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 13 Mar 2025 16:31:37 +0100 Subject: [PATCH 104/145] Trying to move permissions into its own file --- reginaldCog/permissions.py | 51 ++++++++++++++++++++++++++++++-------- reginaldCog/reginald.py | 36 +-------------------------- 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/reginaldCog/permissions.py b/reginaldCog/permissions.py index b08dfce..db8cd67 100644 --- a/reginaldCog/permissions.py +++ b/reginaldCog/permissions.py @@ -1,16 +1,45 @@ from redbot.core import commands +import discord -async def list_allowed_roles_logic(ctx): - """Handles the logic for listing allowed roles.""" - allowed_roles = await ctx.cog.config.guild(ctx.guild).allowed_roles() or [] +class PermissionsMixin: + """Handles role-based access control for Reginald.""" - # Ensure roles still exist in the server - valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] - await ctx.cog.config.guild(ctx.guild).allowed_roles.set(valid_roles) + @commands.command(name="reginald_list_roles", help="List roles that can interact with Reginald.") + @commands.has_permissions(administrator=True) + async def list_allowed_roles(self, ctx: commands.Context): + """Lists all roles that are allowed to interact with Reginald.""" + allowed_roles = await self.config.guild(ctx.guild).allowed_roles() or [] - if not valid_roles: - await ctx.send("⚠️ No roles are currently allowed to interact with Reginald.") - return + # Ensure all roles still exist in the server + valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] + if valid_roles != allowed_roles: # Update config only if there's a difference + await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) - role_mentions = [f"<@&{role_id}>" for role_id in valid_roles] - await ctx.send(f"✅ **Roles with access to Reginald:**\n{', '.join(role_mentions)}") + if not valid_roles: + await ctx.send("⚠️ No roles are currently allowed to interact with Reginald.") + return + + role_mentions = [f"<@&{role_id}>" for role_id in valid_roles] + await ctx.send(f"✅ **Roles with access to Reginald:**\n{', '.join(role_mentions)}") + + @commands.command(name="reginald_allowrole", help="Grant a role permission to interact with Reginald.") + @commands.has_permissions(administrator=True) + async def allow_role(self, ctx: commands.Context, role: discord.Role): + """Grants a role permission to interact with Reginald.""" + async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: + if role.id not in allowed_roles: + allowed_roles.append(role.id) + await ctx.send(f"✅ Role **{role.name}** has been granted access to Reginald.") + else: + await ctx.send(f"⚠️ Role **{role.name}** already has access.") + + @commands.command(name="reginald_disallowrole", help="Revoke a role's access to interact with Reginald.") + @commands.has_permissions(administrator=True) + async def disallow_role(self, ctx: commands.Context, role: discord.Role): + """Revokes a role's permission to interact with Reginald.""" + async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: + if role.id in allowed_roles: + allowed_roles.remove(role.id) + await ctx.send(f"❌ Role **{role.name}** has been removed from Reginald's access list.") + else: + await ctx.send(f"⚠️ Role **{role.name}** was not in the access list.") diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index a503e60..b9349d6 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -8,7 +8,7 @@ import traceback from collections import Counter from redbot.core import Config, commands from openai import OpenAIError -from .permissions import list_allowed_roles_logic +from .permissions import PermissionsMixin class ReginaldCog(commands.Cog): def __init__(self, bot): @@ -45,12 +45,6 @@ class ReginaldCog(commands.Cog): allowed_roles = await self.config.guild(user.guild).allowed_roles() or [] # Ensure it's always a list return any(role.id in allowed_roles for role in user.roles) - - @commands.command(name="reginald_list_roles", help="List roles that can interact with Reginald.") - @commands.has_permissions(administrator=True) - async def list_allowed_roles(self, ctx): - await list_allowed_roles_logic(ctx) - async def is_blacklisted(self, user: discord.Member) -> bool: blacklisted_users = await self.config.guild(user.guild).blacklisted_users() return str(user.id) in blacklisted_users @@ -472,34 +466,6 @@ class ReginaldCog(commands.Cog): await ctx.send(f"Reginald has forgotten all stored information about {user.display_name}.") else: await ctx.send(f"No stored knowledge about {user.display_name} to delete.") - - @commands.command(name="reginald_allowrole", help="Grant a role permission to interact with Reginald.") - @commands.has_permissions(administrator=True) - async def allow_role(self, ctx, role: discord.Role): - async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: - # ✅ Clean list of invalid roles before adding new one - valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] - - if role.id not in valid_roles: - valid_roles.append(role.id) - await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) # Save change - await ctx.send(f"✅ Role {role.name} has been granted access to Reginald.") - else: - await ctx.send(f"⚠️ Role {role.name} already has access.") - - @commands.command(name="reginald_disallowrole", help="Revoke a role's access to interact with Reginald.") - @commands.has_permissions(administrator=True) - async def disallow_role(self, ctx, role: discord.Role): - async with self.config.guild(ctx.guild).allowed_roles() as allowed_roles: - valid_roles = [role_id for role_id in allowed_roles if ctx.guild.get_role(role_id)] - await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) - - if role.id in valid_roles: - valid_roles.remove(role.id) - await self.config.guild(ctx.guild).allowed_roles.set(valid_roles) - await ctx.send(f"❌ Role {role.name} has been removed from Reginald's access list.") - else: - await ctx.send(f"⚠️ Role {role.name} was not in the access list.") @commands.guild_only() @commands.has_permissions(manage_guild=True) -- 2.47.2 From 9f3e9d4ba00af3df96d81e4f5f8f908767e9a112 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 13 Mar 2025 16:42:27 +0100 Subject: [PATCH 105/145] Adding import inheritance --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index b9349d6..77049af 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -10,7 +10,7 @@ from redbot.core import Config, commands from openai import OpenAIError from .permissions import PermissionsMixin -class ReginaldCog(commands.Cog): +class ReginaldCog(commands.Cog, PermissionsMixin): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) -- 2.47.2 From 2f78408c7748d5db4d89aa099806f2945df9f34f Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Thu, 13 Mar 2025 19:28:10 +0100 Subject: [PATCH 106/145] Attempting to move out blacklist commands --- reginaldCog/blacklist.py | 44 +++++++++++++++++ reginaldCog/chess_addon.py | 98 -------------------------------------- reginaldCog/reginald.py | 39 +-------------- 3 files changed, 46 insertions(+), 135 deletions(-) create mode 100644 reginaldCog/blacklist.py delete mode 100644 reginaldCog/chess_addon.py diff --git a/reginaldCog/blacklist.py b/reginaldCog/blacklist.py new file mode 100644 index 0000000..10fd834 --- /dev/null +++ b/reginaldCog/blacklist.py @@ -0,0 +1,44 @@ +from redbot.core import commands +import discord + +class BlacklistMixin: + """Handles user blacklisting for Reginald.""" + + async def is_blacklisted(self, user: discord.Member) -> bool: + """Checks if a user is blacklisted from interacting with Reginald.""" + blacklisted_users = await self.config.guild(user.guild).blacklisted_users() + return str(user.id) in blacklisted_users + + @commands.command(name="reginald_blacklist", help="List users who are explicitly denied access to Reginald.") + @commands.has_permissions(administrator=True) + async def list_blacklisted_users(self, ctx): + """Lists all users currently blacklisted from interacting with Reginald.""" + blacklisted_users = await self.config.guild(ctx.guild).blacklisted_users() + if not blacklisted_users: + await ctx.send("✅ No users are currently blacklisted from interacting with Reginald.") + return + + user_mentions = [f"<@{user_id}>" for user_id in blacklisted_users] + await ctx.send(f"🚫 **Blacklisted Users:**\n{', '.join(user_mentions)}") + + @commands.command(name="reginald_blacklist_add", help="Blacklist a user from interacting with Reginald.") + @commands.has_permissions(administrator=True) + async def add_to_blacklist(self, ctx, user: discord.User): + """Adds a user to Reginald's blacklist.""" + async with self.config.guild(ctx.guild).blacklisted_users() as blacklisted_users: + if str(user.id) not in blacklisted_users: + blacklisted_users.append(str(user.id)) + await ctx.send(f"🚫 {user.display_name} has been **blacklisted** from interacting with Reginald.") + else: + await ctx.send(f"⚠️ {user.display_name} is already blacklisted.") + + @commands.command(name="reginald_blacklist_remove", help="Remove a user from Reginald's blacklist.") + @commands.has_permissions(administrator=True) + async def remove_from_blacklist(self, ctx, user: discord.User): + """Removes a user from Reginald's blacklist.""" + async with self.config.guild(ctx.guild).blacklisted_users() as blacklisted_users: + if str(user.id) in blacklisted_users: + blacklisted_users.remove(str(user.id)) + await ctx.send(f"✅ {user.display_name} has been removed from the blacklist.") + else: + await ctx.send(f"⚠️ {user.display_name} was not on the blacklist.") diff --git a/reginaldCog/chess_addon.py b/reginaldCog/chess_addon.py deleted file mode 100644 index ccfdd68..0000000 --- a/reginaldCog/chess_addon.py +++ /dev/null @@ -1,98 +0,0 @@ -import chess -import chess.svg -import io -from cairosvg import svg2png -import discord - -class ChessHandler: - def __init__(self): - self.active_games = {} # {user_id: FEN string} - - async def set_board(self, user_id: str, fen: str): - """Sets a board to a given FEN string for a user and saves it in long-term memory.""" - try: - board = chess.Board(fen) # Validate FEN - self.active_games[user_id] = fen - - async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: - long_memory[user_id] = {"fen": fen} # Store in long-term memory - - return f"Board state updated successfully:\n```{fen}```" - except ValueError: - return "⚠️ Invalid FEN format. Please provide a valid board state." - - def reset_board(self, user_id: str): - """Resets a user's board to the standard starting position.""" - self.active_games[user_id] = chess.STARTING_FEN - return "The board has been reset to the standard starting position." - - def get_board(self, user_id: str): - """Returns a chess.Board() instance based on stored FEN.""" - fen = self.active_games.get(user_id, chess.STARTING_FEN) - return chess.Board(fen) - - async def get_fen(self, user_id: str): - """Returns the current FEN of the user's board, using long-term memory.""" - async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: - return long_memory.get(user_id, {}).get("fen", chess.STARTING_FEN) - - def make_move(self, user_id: str, move: str): - """Attempts to execute a move and checks if the game is over.""" - board = self.get_board(user_id) - - try: - board.push_san(move) # Execute move in standard algebraic notation - self.active_games[user_id] = board.fen() # Store FEN string instead of raw board object - - if board.is_checkmate(): - self.active_games.pop(user_id) - return f"Move executed: `{move}`. **Checkmate!** 🎉" - elif board.is_stalemate(): - self.active_games.pop(user_id) - return f"Move executed: `{move}`. **Stalemate!** 🤝" - elif board.is_insufficient_material(): - self.active_games.pop(user_id) - return f"Move executed: `{move}`. **Draw due to insufficient material.**" - elif board.can_claim_threefold_repetition(): - self.active_games.pop(user_id) - return f"Move executed: `{move}`. **Draw by threefold repetition.**" - elif board.can_claim_fifty_moves(): - self.active_games.pop(user_id) - return f"Move executed: `{move}`. **Draw by 50-move rule.**" - - return f"Move executed: `{move}`.\nCurrent FEN:\n```{board.fen()}```" - except ValueError: - return "⚠️ Invalid move. Please enter a legal chess move." - - def resign(self, user_id: str): - """Handles player resignation.""" - if user_id in self.active_games: - del self.active_games[user_id] - return "**You have resigned. Well played!** 🏳️" - return "No active game to resign from." - - def get_board_state_text(self, user_id: str): - """Returns the current FEN as a message.""" - fen = self.active_games.get(user_id, chess.STARTING_FEN) - return f"Current board state (FEN):\n```{fen}```" - - def get_board_state_image(self, user_id: str): - """Generates a chessboard image from the current FEN and returns it as a file.""" - fen = self.active_games.get(user_id, chess.STARTING_FEN) - board = chess.Board(fen) - - try: - # Generate SVG representation of the board - svg_data = chess.svg.board(board) - - # Convert SVG to PNG using cairosvg - png_data = svg2png(bytestring=svg_data) - - # Store PNG in memory - image_file = io.BytesIO(png_data) - image_file.seek(0) # Ensure file pointer is reset before returning - - return discord.File(image_file, filename="chessboard.png") - - except Exception as e: - return f"⚠️ Error generating board image: {str(e)}" diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 77049af..40b9683 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -9,8 +9,9 @@ from collections import Counter from redbot.core import Config, commands from openai import OpenAIError from .permissions import PermissionsMixin +from .blacklist import BlacklistMixin -class ReginaldCog(commands.Cog, PermissionsMixin): +class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) @@ -45,42 +46,6 @@ class ReginaldCog(commands.Cog, PermissionsMixin): allowed_roles = await self.config.guild(user.guild).allowed_roles() or [] # Ensure it's always a list return any(role.id in allowed_roles for role in user.roles) - async def is_blacklisted(self, user: discord.Member) -> bool: - blacklisted_users = await self.config.guild(user.guild).blacklisted_users() - return str(user.id) in blacklisted_users - - @commands.command(name="reginald_blacklist", help="List users who are explicitly denied access to Reginald.") - @commands.has_permissions(administrator=True) - async def list_blacklisted_users(self, ctx): - blacklisted_users = await self.config.guild(ctx.guild).blacklisted_users() - if not blacklisted_users: - await ctx.send("✅ No users are currently blacklisted from interacting with Reginald.") - return - - user_mentions = [f"<@{user_id}>" for user_id in blacklisted_users] - await ctx.send(f"🚫 **Blacklisted Users:**\n{', '.join(user_mentions)}") - - @commands.command(name="reginald_blacklist_add", help="Blacklist a user from interacting with Reginald.") - @commands.has_permissions(administrator=True) - async def add_to_blacklist(self, ctx, user: discord.User): - async with self.config.guild(ctx.guild).blacklisted_users() as blacklisted_users: - if str(user.id) not in blacklisted_users: - blacklisted_users.append(str(user.id)) - await ctx.send(f"🚫 {user.display_name} has been **blacklisted** from interacting with Reginald.") - else: - await ctx.send(f"⚠️ {user.display_name} is already blacklisted.") - - - @commands.command(name="reginald_blacklist_remove", help="Remove a user from Reginald's blacklist.") - @commands.has_permissions(administrator=True) - async def remove_from_blacklist(self, ctx, user: discord.User): - async with self.config.guild(ctx.guild).blacklisted_users() as blacklisted_users: - if str(user.id) in blacklisted_users: - blacklisted_users.remove(str(user.id)) - await ctx.send(f"✅ {user.display_name} has been removed from the blacklist.") - else: - await ctx.send(f"⚠️ {user.display_name} was not on the blacklist.") - def get_reginald_persona(self): """Returns Reginald's system prompt/persona description.""" return ( -- 2.47.2 From 4bd380a8a7f492692e4368f5ade01eab2df4a677 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 15 Mar 2025 17:50:43 +0100 Subject: [PATCH 107/145] first attempt at separating all memory into its own module separate from core --- reginaldCog/memory.py | 325 ++++++++++++++++++++++++++++++++++++++++ reginaldCog/reginald.py | 312 +------------------------------------- 2 files changed, 329 insertions(+), 308 deletions(-) create mode 100644 reginaldCog/memory.py diff --git a/reginaldCog/memory.py b/reginaldCog/memory.py new file mode 100644 index 0000000..844ba03 --- /dev/null +++ b/reginaldCog/memory.py @@ -0,0 +1,325 @@ +import re +import random +import datetime +import discord +import openai +from collections import Counter +from redbot.core import commands, Config +from openai import OpenAIError + + +class MemoryMixin: + """Handles all memory-related functions for Reginald.""" + + def __init__(self, config: Config): + self.config = config + self.short_term_memory_limit = 100 + self.summary_retention_limit = 25 + self.summary_retention_ratio = 0.8 + + + @commands.command(name="reginald_clear_short", help="Clears short-term memory for this channel.") + @commands.has_permissions(administrator=True) + async def clear_short_memory(self, ctx): + """Clears short-term memory for this channel.""" + async with self.config.guild(ctx.guild).short_term_memory() as short_memory: + short_memory[ctx.channel.id] = [] + await ctx.send("Short-term memory for this channel has been cleared.") + + + @commands.command(name="reginald_set_limit", help="Set the short-term memory message limit.") + @commands.has_permissions(administrator=True) + async def set_short_term_memory_limit(self, ctx, limit: int): + """Allows an admin to change the short-term memory limit dynamically.""" + if limit < 5: + await ctx.send("⚠️ The short-term memory limit must be at least 5.") + return + + self.short_term_memory_limit = limit + await ctx.send(f"✅ Short-term memory limit set to {limit} messages.") + + + @commands.command(name="reginald_memory_limit", help="Displays the current short-term memory message limit.") + async def get_short_term_memory_limit(self, ctx): + """Displays the current short-term memory limit.""" + await ctx.send(f"📏 **Current Short-Term Memory Limit:** {self.short_term_memory_limit} messages.") + + @commands.command(name="reginald_clear_mid", help="Clears mid-term memory (summarized logs).") + @commands.has_permissions(administrator=True) + async def clear_mid_memory(self, ctx): + async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: + mid_memory[ctx.channel.id] = "" + await ctx.send("Mid-term memory for this channel has been cleared.") + + @commands.command(name="reginald_summary", help="Displays a selected mid-term summary for this channel.") + async def get_mid_term_summary(self, ctx, index: int): + """Fetch and display a specific mid-term memory summary by index.""" + async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: + summaries = mid_memory.get(str(ctx.channel.id), []) + + # Check if there are summaries + if not summaries: + await ctx.send("⚠️ No summaries available for this channel.") + return + + # Validate index (1-based for user-friendliness) + if index < 1 or index > len(summaries): + await ctx.send(f"⚠️ Invalid index. Please provide a number between **1** and **{len(summaries)}**.") + return + + # Fetch the selected summary + selected_summary = summaries[index - 1] # Convert to 0-based index + + # Format output correctly + formatted_summary = ( + f"📜 **Summary {index} of {len(summaries)}**\n" + f"📅 **Date:** {selected_summary['timestamp']}\n" + f"🔍 **Topics:** {', '.join(selected_summary['topics']) or 'None'}\n" + f"📝 **Summary:**\n\n" + f"{selected_summary['summary']}" + ) + + await self.send_long_message(ctx, formatted_summary) + + @commands.command(name="reginald_summaries", help="Lists available summaries for this channel.") + async def list_mid_term_summaries(self, ctx): + """Displays a brief list of all available mid-term memory summaries.""" + async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: + summaries = mid_memory.get(str(ctx.channel.id), []) + + if not summaries: + await ctx.send("⚠️ No summaries available for this channel.") + return + + summary_list = "\n".join( + f"**{i+1}.** 📅 {entry['timestamp']} | 🔍 Topics: {', '.join(entry['topics']) or 'None'}" + for i, entry in enumerate(summaries) + ) + + await ctx.send(f"📚 **Available Summaries:**\n{summary_list[:2000]}") + + @commands.command(name="reginald_clear_long", help="Clears all long-term stored knowledge.") + @commands.has_permissions(administrator=True) + async def clear_long_memory(self, ctx): + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + long_memory.clear() + await ctx.send("All long-term memory has been erased.") + + @commands.command(name="reginald_reset_all", help="Completely resets all memory.") + @commands.has_permissions(administrator=True) + async def reset_all_memory(self, ctx): + async with self.config.guild(ctx.guild).short_term_memory() as short_memory: + short_memory.clear() + async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: + mid_memory.clear() + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + long_memory.clear() + await ctx.send("All memory has been completely reset.") + + @commands.command(name="reginald_recall", help="Recalls what Reginald knows about a user.") + async def recall_user(self, ctx, user: discord.User): + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + profile = long_memory.get(str(user.id), {}).get("summary", "No stored information on this user.") + await ctx.send(f"📜 **Memory Recall for {user.display_name}:** {profile}") + + @commands.command(name="reginald_forget", help="Forgets a specific user's long-term profile.") + @commands.has_permissions(administrator=True) + async def forget_user(self, ctx, user: discord.User): + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + if str(user.id) in long_memory: + del long_memory[str(user.id)] + await ctx.send(f"Reginald has forgotten all stored information about {user.display_name}.") + else: + await ctx.send(f"No stored knowledge about {user.display_name} to delete.") + + async def summarize_memory(self, ctx, messages): + """✅ Generates a structured, compact summary of past conversations for mid-term storage.""" + summary_prompt = ( + "Summarize the following conversation into a structured, concise format that retains key details while maximizing brevity. " + "The summary should be **organized** into clear sections: " + "\n\n📌 **Key Takeaways:** Important facts or conclusions reached." + "\n🔹 **Disputed Points:** Areas where opinions or facts conflicted." + "\n🗣️ **Notable User Contributions:** Key statements from users that shaped the discussion." + "\n📜 **Additional Context:** Any other relevant information." + "\n\nEnsure the summary is **dense but not overly verbose**. Avoid unnecessary repetition while keeping essential meaning intact." + ) + + summary_text = "\n".join(f"{msg['user']}: {msg['content']}" for msg in messages) + + try: + api_key = await self.config.guild(ctx.guild).openai_api_key() + if not api_key: + print("🛠️ DEBUG: No API key found for summarization.") + return ( + "It appears that I have not been furnished with the necessary credentials to carry out this task. " + "Might I suggest consulting an administrator to rectify this unfortunate oversight?" + ) + + client = openai.AsyncClient(api_key=api_key) + response = await client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": summary_prompt}, + {"role": "user", "content": summary_text} + ], + max_tokens=2048 + ) + + summary_content = response.choices[0].message.content.strip() + + if not summary_content: + print("🛠️ DEBUG: Empty summary received from OpenAI.") + return ( + "Ah, an unusual predicament indeed! It seems that my attempt at summarization has resulted in " + "a void of information. I shall endeavor to be more verbose next time." + ) + + return summary_content + + except OpenAIError as e: + error_message = f"OpenAI Error: {e}" + print(f"🛠️ DEBUG: {error_message}") # Log error to console + + reginald_responses = [ + f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction whilst attempting to summarize:\n\n{error_message}", + f"It would seem that a most unfortunate technical hiccup has befallen my faculties in the matter of summarization:\n\n{error_message}", + f"Ah, it appears I have received an urgent memorandum stating that my summarization efforts have been thwarted:\n\n{error_message}", + f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication while summarizing:\n\n{error_message}" + ] + + return random.choice(reginald_responses) + + def extract_topics_from_summary(self, summary): + """Dynamically extracts the most important topics from a summary.""" + + # 🔹 Extract all words from summary + keywords = re.findall(r"\b\w+\b", summary.lower()) + + # 🔹 Count word occurrences + word_counts = Counter(keywords) + + # 🔹 Remove unimportant words (common filler words) + stop_words = {"the", "and", "of", "in", "to", "is", "on", "for", "with", "at", "by", "it", "this", "that", "his", "her"} + filtered_words = {word: count for word, count in word_counts.items() if word not in stop_words and len(word) > 2} + + # 🔹 Take the 5 most frequently used words as "topics" + topics = sorted(filtered_words, key=filtered_words.get, reverse=True)[:5] + + return topics + + def select_relevant_summaries(self, summaries, prompt): + """Selects the most relevant summaries based on topic matching, frequency, and recency weighting.""" + + max_summaries = 5 if len(prompt) > 50 else 3 # Use more summaries if the prompt is long + current_time = datetime.datetime.now() + + def calculate_weight(summary): + """Calculate a weighted score for a summary based on relevance, recency, and frequency.""" + topic_match = sum(1 for topic in summary["topics"] if topic in prompt.lower()) # Context match score + frequency_score = len(summary["topics"]) # More topics = likely more important + timestamp = datetime.datetime.strptime(summary["timestamp"], "%Y-%m-%d %H:%M") + recency_factor = max(0.1, 1 - ((current_time - timestamp).days / 365)) # Older = lower weight + + return (topic_match * 2) + (frequency_score * 1.5) + (recency_factor * 3) + + # Apply the weighting function and sort by highest weight + weighted_summaries = sorted(summaries, key=calculate_weight, reverse=True) + + return weighted_summaries[:max_summaries] # Return the top-scoring summaries + + def extract_fact_from_response(self, response_text): + """ + Extracts potential long-term knowledge from Reginald's response. + This filters out generic responses and focuses on statements about user preferences, traits, and history. + """ + + # Define patterns that suggest factual knowledge (adjust as needed) + fact_patterns = [ + r"I recall that you (.*?)\.", # "I recall that you like chess." + r"You once mentioned that you (.*?)\.", # "You once mentioned that you enjoy strategy games." + r"Ah, you previously stated that (.*?)\.", # "Ah, you previously stated that you prefer tea over coffee." + r"As I remember, you (.*?)\.", # "As I remember, you studied engineering." + r"I believe you (.*?)\.", # "I believe you enjoy historical fiction." + r"I seem to recall that you (.*?)\.", # "I seem to recall that you work in software development." + r"You have indicated in the past that you (.*?)\.", # "You have indicated in the past that you prefer single-malt whisky." + r"From what I remember, you (.*?)\.", # "From what I remember, you dislike overly sweet desserts." + r"You previously mentioned that (.*?)\.", # "You previously mentioned that you train in martial arts." + r"It is my understanding that you (.*?)\.", # "It is my understanding that you have a preference for Linux systems." + r"If I am not mistaken, you (.*?)\.", # "If I am not mistaken, you studied philosophy." + ] + + for pattern in fact_patterns: + match = re.search(pattern, response_text, re.IGNORECASE) + if match: + return match.group(1) # Extract the meaningful fact + + return None # No strong fact found + + @commands.command(name="reginald_memory_status", help="Displays a memory usage summary.") + async def memory_status(self, ctx): + async with self.config.guild(ctx.guild).short_term_memory() as short_memory, \ + self.config.guild(ctx.guild).mid_term_memory() as mid_memory, \ + self.config.guild(ctx.guild).long_term_profiles() as long_memory: + + short_count = sum(len(v) for v in short_memory.values()) + mid_count = sum(len(v) for v in mid_memory.values()) + long_count = len(long_memory) + + status_message = ( + f"📊 **Memory Status:**\n" + f"- **Short-Term Messages Stored:** {short_count}\n" + f"- **Mid-Term Summaries Stored:** {mid_count}\n" + f"- **Long-Term Profiles Stored:** {long_count}\n" + ) + await ctx.send(status_message) + + def normalize_fact(self, fact: str) -> str: # ✅ Now it's a proper method + """Cleans up facts for better duplicate detection.""" + return re.sub(r"\s+", " ", fact.strip().lower()) # Removes excess spaces) + + async def update_long_term_memory(self, ctx, user_id: str, fact: str, source_message: str, timestamp: str): + """Ensures long-term memory updates are structured, preventing overwrites and tracking historical changes.""" + fact = self.normalize_fact(fact) # ✅ Normalize before comparison + + async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: + if user_id not in long_memory: + long_memory[user_id] = {"facts": []} + + user_facts = long_memory[user_id]["facts"] + + for entry in user_facts: + if self.normalize_fact(entry["fact"]) == fact: + entry["last_updated"] = timestamp + return + + # Check for conflicting facts (same topic but different details) + conflicting_entry = None + for entry in user_facts: + existing_keywords = set(entry["fact"].lower().split()) + new_keywords = set(fact.lower().split()) + + # If there's significant overlap in keywords, assume it's a conflicting update + if len(existing_keywords & new_keywords) >= 2: + conflicting_entry = entry + break + + if "previous_versions" not in conflicting_entry: + # ✅ If contradiction found, archive the previous version + conflicting_entry["previous_versions"].append({ + "fact": conflicting_entry["fact"], + "source": conflicting_entry["source"], + "timestamp": conflicting_entry["timestamp"] + }) + conflicting_entry["fact"] = fact # Store the latest fact + conflicting_entry["source"] = source_message + conflicting_entry["timestamp"] = timestamp + conflicting_entry["last_updated"] = timestamp + else: + # ✅ Otherwise, add it as a new fact + user_facts.append({ + "fact": fact, + "source": source_message, + "timestamp": timestamp, + "last_updated": timestamp, + "previous_versions": [] + }) \ No newline at end of file diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 40b9683..0dc4d3b 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -10,16 +10,14 @@ from redbot.core import Config, commands from openai import OpenAIError from .permissions import PermissionsMixin from .blacklist import BlacklistMixin +from .memory import MemoryMixin -class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin): +class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) self.default_listening_channel = 1085649787388428370 # self.memory_locks = {} # ✅ Prevents race conditions per channel - self.short_term_memory_limit = 100 # ✅ Now retains 100 messages - self.summary_retention_limit = 25 # ✅ Now retains 25 summaries - self.summary_retention_ratio = 0.8 # ✅ 80% summarization, 20% retention # ✅ Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} @@ -35,6 +33,8 @@ class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin): } self.config.register_global(**default_global) self.config.register_guild(**default_guild) + + MemoryMixin.__init__(self, self.config) async def is_admin(self, ctx): admin_role_id = await self.config.guild(ctx.guild).admin_role() @@ -165,129 +165,6 @@ class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin): return any(message_lower.startswith(invocation) for invocation in direct_invocation) - async def summarize_memory(self, ctx, messages): - """✅ Generates a structured, compact summary of past conversations for mid-term storage.""" - summary_prompt = ( - "Summarize the following conversation into a structured, concise format that retains key details while maximizing brevity. " - "The summary should be **organized** into clear sections: " - "\n\n📌 **Key Takeaways:** Important facts or conclusions reached." - "\n🔹 **Disputed Points:** Areas where opinions or facts conflicted." - "\n🗣️ **Notable User Contributions:** Key statements from users that shaped the discussion." - "\n📜 **Additional Context:** Any other relevant information." - "\n\nEnsure the summary is **dense but not overly verbose**. Avoid unnecessary repetition while keeping essential meaning intact." - ) - - summary_text = "\n".join(f"{msg['user']}: {msg['content']}" for msg in messages) - - try: - api_key = await self.config.guild(ctx.guild).openai_api_key() - if not api_key: - print("🛠️ DEBUG: No API key found for summarization.") - return ( - "It appears that I have not been furnished with the necessary credentials to carry out this task. " - "Might I suggest consulting an administrator to rectify this unfortunate oversight?" - ) - - client = openai.AsyncClient(api_key=api_key) - response = await client.chat.completions.create( - model="gpt-4o-mini", - messages=[ - {"role": "system", "content": summary_prompt}, - {"role": "user", "content": summary_text} - ], - max_tokens=2048 - ) - - summary_content = response.choices[0].message.content.strip() - - if not summary_content: - print("🛠️ DEBUG: Empty summary received from OpenAI.") - return ( - "Ah, an unusual predicament indeed! It seems that my attempt at summarization has resulted in " - "a void of information. I shall endeavor to be more verbose next time." - ) - - return summary_content - - except OpenAIError as e: - error_message = f"OpenAI Error: {e}" - print(f"🛠️ DEBUG: {error_message}") # Log error to console - - reginald_responses = [ - f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction whilst attempting to summarize:\n\n{error_message}", - f"It would seem that a most unfortunate technical hiccup has befallen my faculties in the matter of summarization:\n\n{error_message}", - f"Ah, it appears I have received an urgent memorandum stating that my summarization efforts have been thwarted:\n\n{error_message}", - f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication while summarizing:\n\n{error_message}" - ] - - return random.choice(reginald_responses) - - def extract_topics_from_summary(self, summary): - """Dynamically extracts the most important topics from a summary.""" - - # 🔹 Extract all words from summary - keywords = re.findall(r"\b\w+\b", summary.lower()) - - # 🔹 Count word occurrences - word_counts = Counter(keywords) - - # 🔹 Remove unimportant words (common filler words) - stop_words = {"the", "and", "of", "in", "to", "is", "on", "for", "with", "at", "by", "it", "this", "that", "his", "her"} - filtered_words = {word: count for word, count in word_counts.items() if word not in stop_words and len(word) > 2} - - # 🔹 Take the 5 most frequently used words as "topics" - topics = sorted(filtered_words, key=filtered_words.get, reverse=True)[:5] - - return topics - - def select_relevant_summaries(self, summaries, prompt): - """Selects the most relevant summaries based on topic matching, frequency, and recency weighting.""" - - max_summaries = 5 if len(prompt) > 50 else 3 # Use more summaries if the prompt is long - current_time = datetime.datetime.now() - - def calculate_weight(summary): - """Calculate a weighted score for a summary based on relevance, recency, and frequency.""" - topic_match = sum(1 for topic in summary["topics"] if topic in prompt.lower()) # Context match score - frequency_score = len(summary["topics"]) # More topics = likely more important - timestamp = datetime.datetime.strptime(summary["timestamp"], "%Y-%m-%d %H:%M") - recency_factor = max(0.1, 1 - ((current_time - timestamp).days / 365)) # Older = lower weight - - return (topic_match * 2) + (frequency_score * 1.5) + (recency_factor * 3) - - # Apply the weighting function and sort by highest weight - weighted_summaries = sorted(summaries, key=calculate_weight, reverse=True) - - return weighted_summaries[:max_summaries] # Return the top-scoring summaries - - def extract_fact_from_response(self, response_text): - """ - Extracts potential long-term knowledge from Reginald's response. - This filters out generic responses and focuses on statements about user preferences, traits, and history. - """ - - # Define patterns that suggest factual knowledge (adjust as needed) - fact_patterns = [ - r"I recall that you (.*?)\.", # "I recall that you like chess." - r"You once mentioned that you (.*?)\.", # "You once mentioned that you enjoy strategy games." - r"Ah, you previously stated that (.*?)\.", # "Ah, you previously stated that you prefer tea over coffee." - r"As I remember, you (.*?)\.", # "As I remember, you studied engineering." - r"I believe you (.*?)\.", # "I believe you enjoy historical fiction." - r"I seem to recall that you (.*?)\.", # "I seem to recall that you work in software development." - r"You have indicated in the past that you (.*?)\.", # "You have indicated in the past that you prefer single-malt whisky." - r"From what I remember, you (.*?)\.", # "From what I remember, you dislike overly sweet desserts." - r"You previously mentioned that (.*?)\.", # "You previously mentioned that you train in martial arts." - r"It is my understanding that you (.*?)\.", # "It is my understanding that you have a preference for Linux systems." - r"If I am not mistaken, you (.*?)\.", # "If I am not mistaken, you studied philosophy." - ] - - for pattern in fact_patterns: - match = re.search(pattern, response_text, re.IGNORECASE) - if match: - return match.group(1) # Extract the meaningful fact - - return None # No strong fact found - async def generate_response(self, api_key, messages): model = await self.config.openai_model() try: @@ -315,123 +192,6 @@ class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin): ] return random.choice(reginald_responses) - @commands.command(name="reginald_clear_short", help="Clears short-term memory for this channel.") - @commands.has_permissions(administrator=True) - async def clear_short_memory(self, ctx): - async with self.config.guild(ctx.guild).short_term_memory() as short_memory: - short_memory[ctx.channel.id] = [] - await ctx.send("Short-term memory for this channel has been cleared.") - - @commands.command(name="reginald_clear_mid", help="Clears mid-term memory (summarized logs).") - @commands.has_permissions(administrator=True) - async def clear_mid_memory(self, ctx): - async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: - mid_memory[ctx.channel.id] = "" - await ctx.send("Mid-term memory for this channel has been cleared.") - - @commands.command(name="reginald_clear_long", help="Clears all long-term stored knowledge.") - @commands.has_permissions(administrator=True) - async def clear_long_memory(self, ctx): - async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: - long_memory.clear() - await ctx.send("All long-term memory has been erased.") - - @commands.command(name="reginald_reset_all", help="Completely resets all memory.") - @commands.has_permissions(administrator=True) - async def reset_all_memory(self, ctx): - async with self.config.guild(ctx.guild).short_term_memory() as short_memory: - short_memory.clear() - async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: - mid_memory.clear() - async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: - long_memory.clear() - await ctx.send("All memory has been completely reset.") - - @commands.command(name="reginald_memory_status", help="Displays a memory usage summary.") - async def memory_status(self, ctx): - async with self.config.guild(ctx.guild).short_term_memory() as short_memory, \ - self.config.guild(ctx.guild).mid_term_memory() as mid_memory, \ - self.config.guild(ctx.guild).long_term_profiles() as long_memory: - - short_count = sum(len(v) for v in short_memory.values()) - mid_count = sum(len(v) for v in mid_memory.values()) - long_count = len(long_memory) - - status_message = ( - f"📊 **Memory Status:**\n" - f"- **Short-Term Messages Stored:** {short_count}\n" - f"- **Mid-Term Summaries Stored:** {mid_count}\n" - f"- **Long-Term Profiles Stored:** {long_count}\n" - ) - await ctx.send(status_message) - - def normalize_fact(self, fact: str) -> str: # ✅ Now it's a proper method - """Cleans up facts for better duplicate detection.""" - return re.sub(r"\s+", " ", fact.strip().lower()) # Removes excess spaces) - - async def update_long_term_memory(self, ctx, user_id: str, fact: str, source_message: str, timestamp: str): - """Ensures long-term memory updates are structured, preventing overwrites and tracking historical changes.""" - fact = self.normalize_fact(fact) # ✅ Normalize before comparison - - async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: - if user_id not in long_memory: - long_memory[user_id] = {"facts": []} - - user_facts = long_memory[user_id]["facts"] - - for entry in user_facts: - if self.normalize_fact(entry["fact"]) == fact: - entry["last_updated"] = timestamp - return - - # Check for conflicting facts (same topic but different details) - conflicting_entry = None - for entry in user_facts: - existing_keywords = set(entry["fact"].lower().split()) - new_keywords = set(fact.lower().split()) - - # If there's significant overlap in keywords, assume it's a conflicting update - if len(existing_keywords & new_keywords) >= 2: - conflicting_entry = entry - break - - if "previous_versions" not in conflicting_entry: - # ✅ If contradiction found, archive the previous version - conflicting_entry["previous_versions"].append({ - "fact": conflicting_entry["fact"], - "source": conflicting_entry["source"], - "timestamp": conflicting_entry["timestamp"] - }) - conflicting_entry["fact"] = fact # Store the latest fact - conflicting_entry["source"] = source_message - conflicting_entry["timestamp"] = timestamp - conflicting_entry["last_updated"] = timestamp - else: - # ✅ Otherwise, add it as a new fact - user_facts.append({ - "fact": fact, - "source": source_message, - "timestamp": timestamp, - "last_updated": timestamp, - "previous_versions": [] - }) - - @commands.command(name="reginald_recall", help="Recalls what Reginald knows about a user.") - async def recall_user(self, ctx, user: discord.User): - async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: - profile = long_memory.get(str(user.id), {}).get("summary", "No stored information on this user.") - await ctx.send(f"📜 **Memory Recall for {user.display_name}:** {profile}") - - @commands.command(name="reginald_forget", help="Forgets a specific user's long-term profile.") - @commands.has_permissions(administrator=True) - async def forget_user(self, ctx, user: discord.User): - async with self.config.guild(ctx.guild).long_term_profiles() as long_memory: - if str(user.id) in long_memory: - del long_memory[str(user.id)] - await ctx.send(f"Reginald has forgotten all stored information about {user.display_name}.") - else: - await ctx.send(f"No stored knowledge about {user.display_name} to delete.") - @commands.guild_only() @commands.has_permissions(manage_guild=True) @commands.command(help="Set the OpenAI API key") @@ -439,70 +199,6 @@ class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin): """Allows an admin to set the OpenAI API key for Reginald.""" await self.config.guild(ctx.guild).openai_api_key.set(api_key) await ctx.send("OpenAI API key set successfully.") - - @commands.command(name="reginald_set_limit", help="Set the short-term memory message limit.") - @commands.has_permissions(administrator=True) - async def set_short_term_memory_limit(self, ctx, limit: int): - """Allows an admin to change the short-term memory limit dynamically.""" - if limit < 5: - await ctx.send("⚠️ The short-term memory limit must be at least 5.") - return - - self.short_term_memory_limit = limit - await ctx.send(f"✅ Short-term memory limit set to {limit} messages.") - - @commands.command(name="reginald_memory_limit", help="Displays the current short-term memory message limit.") - async def get_short_term_memory_limit(self, ctx): - """Displays the current short-term memory limit.""" - await ctx.send(f"📏 **Current Short-Term Memory Limit:** {self.short_term_memory_limit} messages.") - - @commands.command(name="reginald_summary", help="Displays a selected mid-term summary for this channel.") - async def get_mid_term_summary(self, ctx, index: int): - """Fetch and display a specific mid-term memory summary by index.""" - async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: - summaries = mid_memory.get(str(ctx.channel.id), []) - - # Check if there are summaries - if not summaries: - await ctx.send("⚠️ No summaries available for this channel.") - return - - # Validate index (1-based for user-friendliness) - if index < 1 or index > len(summaries): - await ctx.send(f"⚠️ Invalid index. Please provide a number between **1** and **{len(summaries)}**.") - return - - # Fetch the selected summary - selected_summary = summaries[index - 1] # Convert to 0-based index - - # Format output correctly - formatted_summary = ( - f"📜 **Summary {index} of {len(summaries)}**\n" - f"📅 **Date:** {selected_summary['timestamp']}\n" - f"🔍 **Topics:** {', '.join(selected_summary['topics']) or 'None'}\n" - f"📝 **Summary:**\n\n" - f"{selected_summary['summary']}" - ) - - await self.send_long_message(ctx, formatted_summary) - - @commands.command(name="reginald_summaries", help="Lists available summaries for this channel.") - async def list_mid_term_summaries(self, ctx): - """Displays a brief list of all available mid-term memory summaries.""" - async with self.config.guild(ctx.guild).mid_term_memory() as mid_memory: - summaries = mid_memory.get(str(ctx.channel.id), []) - - if not summaries: - await ctx.send("⚠️ No summaries available for this channel.") - return - - summary_list = "\n".join( - f"**{i+1}.** 📅 {entry['timestamp']} | 🔍 Topics: {', '.join(entry['topics']) or 'None'}" - for i, entry in enumerate(summaries) - ) - - await ctx.send(f"📚 **Available Summaries:**\n{summary_list[:2000]}") - @commands.command(name="reginald_set_listening_channel", help="Set the channel where Reginald listens for messages.") @commands.has_permissions(administrator=True) -- 2.47.2 From e49609e316bec92f902ccb55e875c53f6351971b Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 15 Mar 2025 17:58:25 +0100 Subject: [PATCH 108/145] Trying to init in a different way --- reginaldCog/reginald.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 0dc4d3b..9b8c754 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -16,25 +16,25 @@ class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) - self.default_listening_channel = 1085649787388428370 # - self.memory_locks = {} # ✅ Prevents race conditions per channel + self.default_listening_channel = 1085649787388428370 + self.memory_locks = {} - # ✅ Properly Registered Configuration Keys + # Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} default_guild = { "openai_api_key": None, - "short_term_memory": {}, # Tracks last 100 messages per channel - "mid_term_memory": {}, # Stores multiple condensed summaries - "long_term_profiles": {}, # Stores persistent knowledge + "short_term_memory": {}, + "mid_term_memory": {}, + "long_term_profiles": {}, "admin_role": None, - "listening_channel": None, # ✅ Stores the designated listening channel ID, - "allowed_roles": [], # ✅ List of roles that can access Reginald - "blacklisted_users": [], # ✅ List of users who are explicitly denied access + "listening_channel": None, + "allowed_roles": [], + "blacklisted_users": [], } self.config.register_global(**default_global) self.config.register_guild(**default_guild) - - MemoryMixin.__init__(self, self.config) + + super().__init__(self.config) #Trying to initialize MemoryMixin async def is_admin(self, ctx): admin_role_id = await self.config.guild(ctx.guild).admin_role() -- 2.47.2 From 1f59db6f973311dab8786deb0a774157991ce5a2 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 15 Mar 2025 17:58:35 +0100 Subject: [PATCH 109/145] Absolutely differently, ahem --- reginaldCog/reginald.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 9b8c754..77367a0 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -14,6 +14,7 @@ from .memory import MemoryMixin class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): def __init__(self, bot): + super().__init__() self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) self.default_listening_channel = 1085649787388428370 @@ -34,8 +35,6 @@ class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): self.config.register_global(**default_global) self.config.register_guild(**default_guild) - super().__init__(self.config) #Trying to initialize MemoryMixin - async def is_admin(self, ctx): admin_role_id = await self.config.guild(ctx.guild).admin_role() if admin_role_id: -- 2.47.2 From f40227d0cdb5a16710ee60d989e8e6447ef8b7d7 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 15 Mar 2025 18:05:21 +0100 Subject: [PATCH 110/145] attempting to make config a property --- reginaldCog/memory.py | 9 ++++++--- reginaldCog/reginald.py | 8 ++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/reginaldCog/memory.py b/reginaldCog/memory.py index 844ba03..4b5b4eb 100644 --- a/reginaldCog/memory.py +++ b/reginaldCog/memory.py @@ -11,12 +11,15 @@ from openai import OpenAIError class MemoryMixin: """Handles all memory-related functions for Reginald.""" - def __init__(self, config: Config): - self.config = config + @property + def config(self): + """Dynamically fetches the config from the parent cog.""" + return self._config # This assumes `ReginaldCog` sets `self._config` + + def __init__(self): self.short_term_memory_limit = 100 self.summary_retention_limit = 25 self.summary_retention_ratio = 0.8 - @commands.command(name="reginald_clear_short", help="Clears short-term memory for this channel.") @commands.has_permissions(administrator=True) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 77367a0..881d894 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -14,13 +14,17 @@ from .memory import MemoryMixin class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): def __init__(self, bot): - super().__init__() + super().__init__() # ✅ Call parent class constructor first + self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) self.default_listening_channel = 1085649787388428370 self.memory_locks = {} - # Properly Registered Configuration Keys + # ✅ Ensure `MemoryMixin` can access `config` + self._config = self.config # 🔥 This is the key part that enables `MemoryMixin.config` + + # ✅ Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} default_guild = { "openai_api_key": None, -- 2.47.2 From cdb5d20fa9d8678a3c118f9447b81e6b0dead02d Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 15 Mar 2025 18:09:44 +0100 Subject: [PATCH 111/145] Moving parent init further down in calling order, this is dumb --- reginaldCog/reginald.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 881d894..8cb2150 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -14,16 +14,16 @@ from .memory import MemoryMixin class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): def __init__(self, bot): - super().__init__() # ✅ Call parent class constructor first - self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) + + self._config = self.config # ✅ Ensure MemoryMixin sees this before calling super + + super().__init__() # ✅ Now it's safe to initialize parent classes + self.default_listening_channel = 1085649787388428370 self.memory_locks = {} - # ✅ Ensure `MemoryMixin` can access `config` - self._config = self.config # 🔥 This is the key part that enables `MemoryMixin.config` - # ✅ Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} default_guild = { -- 2.47.2 From 3bf7d221a0e0ffddeea233c458f6ffcb955f938a Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 15 Mar 2025 18:12:57 +0100 Subject: [PATCH 112/145] Screw properties, direct injection baby! --- reginaldCog/memory.py | 8 ++------ reginaldCog/reginald.py | 12 ++++-------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/reginaldCog/memory.py b/reginaldCog/memory.py index 4b5b4eb..c389860 100644 --- a/reginaldCog/memory.py +++ b/reginaldCog/memory.py @@ -11,12 +11,8 @@ from openai import OpenAIError class MemoryMixin: """Handles all memory-related functions for Reginald.""" - @property - def config(self): - """Dynamically fetches the config from the parent cog.""" - return self._config # This assumes `ReginaldCog` sets `self._config` - - def __init__(self): + def __init__(self, config: Config): + self.config = config # Now explicitly set self.short_term_memory_limit = 100 self.summary_retention_limit = 25 self.summary_retention_ratio = 0.8 diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 8cb2150..456edd8 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -17,14 +17,7 @@ class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) - self._config = self.config # ✅ Ensure MemoryMixin sees this before calling super - - super().__init__() # ✅ Now it's safe to initialize parent classes - - self.default_listening_channel = 1085649787388428370 - self.memory_locks = {} - - # ✅ Properly Registered Configuration Keys + # Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} default_guild = { "openai_api_key": None, @@ -39,6 +32,9 @@ class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): self.config.register_global(**default_global) self.config.register_guild(**default_guild) + # Pass config explicitly to MemoryMixin + MemoryMixin.__init__(self, self.config) + async def is_admin(self, ctx): admin_role_id = await self.config.guild(ctx.guild).admin_role() if admin_role_id: -- 2.47.2 From 5645b8867759b5f53963ddb185ddc7e799bc3882 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 15 Mar 2025 18:19:11 +0100 Subject: [PATCH 113/145] Stop crying about positional argument! --- reginaldCog/reginald.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 456edd8..c8d858d 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -16,6 +16,7 @@ class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=71717171171717) + MemoryMixin.__init__(self, self.config) # Pass config to MemoryMixin # Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} @@ -32,9 +33,6 @@ class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): self.config.register_global(**default_global) self.config.register_guild(**default_guild) - # Pass config explicitly to MemoryMixin - MemoryMixin.__init__(self, self.config) - async def is_admin(self, ctx): admin_role_id = await self.config.guild(ctx.guild).admin_role() if admin_role_id: -- 2.47.2 From 728ce2aee44bd2d26d876f5b9e4a08dda8eb1b97 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 15 Mar 2025 18:26:36 +0100 Subject: [PATCH 114/145] AAAAAAAAAAA! Documentation why --- reginaldCog/reginald.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index c8d858d..9bfb87a 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -15,8 +15,17 @@ from .memory import MemoryMixin class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): def __init__(self, bot): self.bot = bot - self.config = Config.get_conf(self, identifier=71717171171717) - MemoryMixin.__init__(self, self.config) # Pass config to MemoryMixin + config_instance = Config.get_conf(self, identifier=71717171171717) # ✅ Create config instance + + # ✅ Pass config explicitly to MemoryMixin + MemoryMixin.__init__(self, config_instance) + + super().__init__() # ✅ Now it's safe to initialize parent classes + + self.config = config_instance # ✅ Store config after parent init + + self.default_listening_channel = 1085649787388428370 + self.memory_locks = {} # Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} -- 2.47.2 From b8cea3f9616664fb2c61b9afc182e1f6b092d5d6 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 15 Mar 2025 18:32:12 +0100 Subject: [PATCH 115/145] Come on, read config you stupid memory! --- reginaldCog/memory.py | 8 ++++++-- reginaldCog/reginald.py | 9 ++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/reginaldCog/memory.py b/reginaldCog/memory.py index c389860..d622f56 100644 --- a/reginaldCog/memory.py +++ b/reginaldCog/memory.py @@ -11,11 +11,15 @@ from openai import OpenAIError class MemoryMixin: """Handles all memory-related functions for Reginald.""" - def __init__(self, config: Config): - self.config = config # Now explicitly set + def __init__(self): + """No longer requires `config` as a parameter.""" self.short_term_memory_limit = 100 self.summary_retention_limit = 25 self.summary_retention_ratio = 0.8 + + def get_config(self): + """Access `_config` from the parent class (ReginaldCog).""" + return self._config # ✅ Use `_config` instead of `self.config` @commands.command(name="reginald_clear_short", help="Clears short-term memory for this channel.") @commands.has_permissions(administrator=True) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 9bfb87a..935037b 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -15,14 +15,9 @@ from .memory import MemoryMixin class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): def __init__(self, bot): self.bot = bot - config_instance = Config.get_conf(self, identifier=71717171171717) # ✅ Create config instance + self._config = Config.get_conf(self, identifier=71717171171717) # ✅ Store `_config` first - # ✅ Pass config explicitly to MemoryMixin - MemoryMixin.__init__(self, config_instance) - - super().__init__() # ✅ Now it's safe to initialize parent classes - - self.config = config_instance # ✅ Store config after parent init + super().__init__() # Trying to get MemoryMixin to have access to _config self.default_listening_channel = 1085649787388428370 self.memory_locks = {} -- 2.47.2 From c289e1d323c4558541cc2e9f8b8c3cc4303df0dd Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 15 Mar 2025 19:05:28 +0100 Subject: [PATCH 116/145] Turning to AI in desperation --- reginaldCog/memory.py | 8 ++------ reginaldCog/reginald.py | 8 ++++---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/reginaldCog/memory.py b/reginaldCog/memory.py index d622f56..cb75e6f 100644 --- a/reginaldCog/memory.py +++ b/reginaldCog/memory.py @@ -11,15 +11,11 @@ from openai import OpenAIError class MemoryMixin: """Handles all memory-related functions for Reginald.""" - def __init__(self): - """No longer requires `config` as a parameter.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # ✅ Ensure cooperative MRO initialization self.short_term_memory_limit = 100 self.summary_retention_limit = 25 self.summary_retention_ratio = 0.8 - - def get_config(self): - """Access `_config` from the parent class (ReginaldCog).""" - return self._config # ✅ Use `_config` instead of `self.config` @commands.command(name="reginald_clear_short", help="Clears short-term memory for this channel.") @commands.has_permissions(administrator=True) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 935037b..b3750b5 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -12,17 +12,17 @@ from .permissions import PermissionsMixin from .blacklist import BlacklistMixin from .memory import MemoryMixin -class ReginaldCog(commands.Cog, PermissionsMixin, BlacklistMixin, MemoryMixin): +class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): def __init__(self, bot): self.bot = bot - self._config = Config.get_conf(self, identifier=71717171171717) # ✅ Store `_config` first + self.config = Config.get_conf(self, identifier=71717171171717) # ✅ Ensure config exists before super() - super().__init__() # Trying to get MemoryMixin to have access to _config + super().__init__() # ✅ Properly initialize all mixins & commands.Cog self.default_listening_channel = 1085649787388428370 self.memory_locks = {} - # Properly Registered Configuration Keys + # ✅ Properly Registered Configuration Keys default_global = {"openai_model": "gpt-4o-mini"} default_guild = { "openai_api_key": None, -- 2.47.2 From 5e7bbafbc939bae6cd50378b47fabc55ff7d3bc8 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sat, 15 Mar 2025 19:32:44 +0100 Subject: [PATCH 117/145] Reducing short term memory to 50 --- reginaldCog/memory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/memory.py b/reginaldCog/memory.py index cb75e6f..5d08a7c 100644 --- a/reginaldCog/memory.py +++ b/reginaldCog/memory.py @@ -13,7 +13,7 @@ class MemoryMixin: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # ✅ Ensure cooperative MRO initialization - self.short_term_memory_limit = 100 + self.short_term_memory_limit = 50 self.summary_retention_limit = 25 self.summary_retention_ratio = 0.8 -- 2.47.2 From 661bad840948f44020062cdb5a333885bd1a1b40 Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Sun, 16 Mar 2025 12:06:12 +0500 Subject: [PATCH 118/145] + Added OpenAI function calling + Added weather tools for ChatGPT --- reginaldCog/reginald.py | 52 +++++++++++++++++++---- reginaldCog/tools_description.py | 72 ++++++++++++++++++++++++++++++++ reginaldCog/weather.py | 59 ++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 reginaldCog/tools_description.py create mode 100644 reginaldCog/weather.py diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index b3750b5..3b68467 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -5,12 +5,25 @@ import asyncio import datetime import re import traceback +import json from collections import Counter from redbot.core import Config, commands from openai import OpenAIError from .permissions import PermissionsMixin from .blacklist import BlacklistMixin from .memory import MemoryMixin +from .weather import time_now, get_current_weather, get_weather_forecast +from .tools_description import TOOLS + + +CALLABLE_FUNCTIONS = { + # Dictionary with functions to call. + # You can use globals()[func_name](**args) instead, but that's too implicit. + 'time_now': time_now, + 'get_current_weather': get_current_weather, + 'get_weather_forecast': get_weather_forecast, +} + class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): def __init__(self, bot): @@ -170,14 +183,37 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): model = await self.config.openai_model() try: client = openai.AsyncClient(api_key=api_key) - response = await client.chat.completions.create( - model=model, - messages=messages, - max_tokens=2048, - temperature=0.7, - presence_penalty=0.5, - frequency_penalty=0.5 - ) + completion_args = { + 'model': model, + 'messages': messages, + 'max_tokens': 2048, + 'temperature': 0.7, + 'presence_penalty': 0.5, + 'frequency_penalty': 0.5, + 'tools': TOOLS, + 'tool_choice': 'auto', + } + response = await client.chat.completions.create(**completion_args) + # Checking for function calls + tool_calls = response.choices[0].message.tool_calls + if tool_calls: + for i_call in tool_calls: + # Calling for necessary functions + func_name = i_call.function.name + func_args = json.loads(i_call.function.arguments) + tool_call_id = i_call.id + # Getting function result and putting it into messages + func_result = CALLABLE_FUNCTIONS[func_name](**func_args) + messages.append({ + 'role': 'tool', + 'content': func_result, + 'tool_calls': tool_calls, + 'tool_call_id': tool_call_id, + }) + # Second completion required if functions has been called to interpret the result into user-friendly + # chat message. + response = await client.chat.completions.create(**completion_args) + response_text = response.choices[0].message.content.strip() if response_text.startswith("Reginald:"): response_text = response_text[len("Reginald:"):].strip() diff --git a/reginaldCog/tools_description.py b/reginaldCog/tools_description.py new file mode 100644 index 0000000..26481ae --- /dev/null +++ b/reginaldCog/tools_description.py @@ -0,0 +1,72 @@ +TOOLS = [ + { + 'type': 'function', + 'function': { + 'name': 'time_now', + 'description': 'Get current date and time in UTC timezone.', + } + }, + { + 'type': 'function', + 'function': { + 'name': 'get_current_weather', + 'description': ''' + Gets current weather for specified location. + ''', + 'parameters': { + 'type': 'object', + 'properties': { + 'location': { + 'type': 'string', + 'description': ''' + Location in human readable format. + e.g: "Copenhagen", or "Copenhagen, Louisiana, US", if needed specifying. + ''' + } + }, + 'required': [ + 'location', + ], + 'additionalProperties': False + }, + 'strict': True + } + }, + { + 'type': 'function', + 'function': { + 'name': 'get_weather_forecast', + 'description': ''' + Forecast weather API method returns, depending upon your price plan level, upto next 14 day weather + forecast and weather alert as json. The data is returned as a Forecast Object. + Forecast object contains astronomy data, day weather forecast and hourly interval weather information + for a given city. + With a free weather API subscription, only up to three days of forecast can be requested. + ''', + 'parameters': { + 'type': 'object', + 'properties': { + 'location': { + 'type': 'string', + 'description': ''' + Location in human readable format. + e.g: "Copenhagen", or "Copenhagen, Louisiana, US", if needed specifying. + ''' + }, + 'dt': { + 'type': 'string', + 'description': ''' + The date up until to request the forecast in YYYY-MM-DD format. + Check the **now** function first if you unsure which date it is. + ''' + }, + }, + 'required': [ + 'location', 'dt' + ], + 'additionalProperties': False + }, + 'strict': True + } + } +] \ No newline at end of file diff --git a/reginaldCog/weather.py b/reginaldCog/weather.py new file mode 100644 index 0000000..41f2726 --- /dev/null +++ b/reginaldCog/weather.py @@ -0,0 +1,59 @@ +from datetime import datetime, timezone +from os import environ +import requests +import json + +WEATHER_API_KEY = environ.get('WEATHER_API_KEY') +URL = 'http://api.weatherapi.com/v1' + + +def time_now() -> datetime: + return datetime.now(timezone.utc) + + +def get_current_weather(location: str) -> str: + weather = Weather(location=location) + return json.dumps(weather.realtime()) + + +def get_weather_forecast(location: str, days: int = 14, dt: str = '2025-03-24') -> str: + weather = Weather(location=location) + return json.dumps(weather.forecast(days=days, dt=dt)) + + +class Weather: + def __init__(self, location: str): + self.__location = location + + @property + def location(self) -> str: + return self.__location + + @staticmethod + def make_request(method: str, params: dict) -> dict: + response = requests.get(url=f'{URL}{method}', params=params) + return response.json() + + def realtime(self): + method = '/current.json' + params = { + 'key': WEATHER_API_KEY, + 'q': self.location, + } + return self.make_request(method=method, params=params) + + def forecast(self, days: int = 14, dt: str = '2025-03-24'): + method = '/forecast.json' + params = { + 'key': WEATHER_API_KEY, + 'q': self.location, + 'days': days, + 'dt': dt, + } + return self.make_request(method=method, params=params) + + +if __name__ == '__main__': + test_weather = Weather('Aqtobe') + result = json.dumps(test_weather.forecast(days=14, dt='2025-03-24'), indent=2) + print(result) -- 2.47.2 From 90b16402426be5a5db086b0dd1c4c462abdb5010 Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Sun, 16 Mar 2025 15:23:05 +0500 Subject: [PATCH 119/145] ~ Slightly changed tools_description.py to address change the name of the now function to time_now --- reginaldCog/tools_description.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/tools_description.py b/reginaldCog/tools_description.py index 26481ae..3d4558b 100644 --- a/reginaldCog/tools_description.py +++ b/reginaldCog/tools_description.py @@ -57,7 +57,7 @@ TOOLS = [ 'type': 'string', 'description': ''' The date up until to request the forecast in YYYY-MM-DD format. - Check the **now** function first if you unsure which date it is. + Check the **time_now** function first if you unsure which date it is. ''' }, }, -- 2.47.2 From d078b60f8f9795455067bf1c98ea593f71815c0d Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Sun, 16 Mar 2025 16:19:18 +0500 Subject: [PATCH 120/145] ~ Fixed the initial assistant response with tool calls not being appended --- reginaldCog/reginald.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 3b68467..b168b11 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -194,6 +194,11 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): 'tool_choice': 'auto', } response = await client.chat.completions.create(**completion_args) + # Appending response with tool calls + messages.append({ + 'role': 'assistant', + 'content': response.choices[0].message.content, + }) # Checking for function calls tool_calls = response.choices[0].message.tool_calls if tool_calls: -- 2.47.2 From c1cfbfa02d197ee9cda2862c1b43db19d6398938 Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Sun, 16 Mar 2025 16:26:22 +0500 Subject: [PATCH 121/145] ~ Fixed the lack of tool calls in assistant message --- reginaldCog/reginald.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index b168b11..63468a6 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -194,13 +194,14 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): 'tool_choice': 'auto', } response = await client.chat.completions.create(**completion_args) + # Checking for function calls + tool_calls = response.choices[0].message.tool_calls # Appending response with tool calls messages.append({ 'role': 'assistant', 'content': response.choices[0].message.content, + 'tool_calls': tool_calls }) - # Checking for function calls - tool_calls = response.choices[0].message.tool_calls if tool_calls: for i_call in tool_calls: # Calling for necessary functions -- 2.47.2 From 7b04c6f82a2b503264e2e53f9403f635e67f2c52 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 16 Mar 2025 12:40:47 +0100 Subject: [PATCH 122/145] COmmenting on stuff --- reginaldCog/reginald.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index b3750b5..4fcfa9c 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -134,10 +134,20 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory] formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) - # ✅ Generate AI Response + + ################################################## + # # + ## Generate AI Response, put into response_text ## + # # + ################################################## + response_text = await self.generate_response(api_key, formatted_messages) - # ✅ Store Memory + ################################################## + # # + ################################################## + + # ✅ Store Memory memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) -- 2.47.2 From e3c931650bc6006971cbbe3f675d83212164f22c Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Sun, 16 Mar 2025 16:41:14 +0500 Subject: [PATCH 123/145] - Removed tool call from tool role message --- reginaldCog/reginald.py | 1 - 1 file changed, 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 63468a6..c3df1c9 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -213,7 +213,6 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): messages.append({ 'role': 'tool', 'content': func_result, - 'tool_calls': tool_calls, 'tool_call_id': tool_call_id, }) # Second completion required if functions has been called to interpret the result into user-friendly -- 2.47.2 From 52113f93aee7ca539271922db7158ef82891a373 Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Sun, 16 Mar 2025 17:03:55 +0500 Subject: [PATCH 124/145] ~ Changed time_now function to return string instead of datetime object --- reginaldCog/weather.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reginaldCog/weather.py b/reginaldCog/weather.py index 41f2726..6ef92bd 100644 --- a/reginaldCog/weather.py +++ b/reginaldCog/weather.py @@ -7,8 +7,8 @@ WEATHER_API_KEY = environ.get('WEATHER_API_KEY') URL = 'http://api.weatherapi.com/v1' -def time_now() -> datetime: - return datetime.now(timezone.utc) +def time_now() -> str: + return str(datetime.now(timezone.utc)) def get_current_weather(location: str) -> str: -- 2.47.2 From c5b7ea34195562ab5b546a13490d9c7ae49283b0 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 16 Mar 2025 13:09:21 +0100 Subject: [PATCH 125/145] moving api key init --- reginaldCog/reginald.py | 2 +- reginaldCog/weather.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 4582e96..4fca2a8 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -212,7 +212,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): 'content': response.choices[0].message.content, 'tool_calls': tool_calls }) - if tool_calls: + if isinstance(tool_calls, list) and tool_calls: for i_call in tool_calls: # Calling for necessary functions func_name = i_call.function.name diff --git a/reginaldCog/weather.py b/reginaldCog/weather.py index 41f2726..ea62002 100644 --- a/reginaldCog/weather.py +++ b/reginaldCog/weather.py @@ -3,7 +3,7 @@ from os import environ import requests import json -WEATHER_API_KEY = environ.get('WEATHER_API_KEY') +#WEATHER_API_KEY = environ.get('WEATHER_API_KEY') URL = 'http://api.weatherapi.com/v1' @@ -24,6 +24,7 @@ def get_weather_forecast(location: str, days: int = 14, dt: str = '2025-03-24') class Weather: def __init__(self, location: str): self.__location = location + self.api_key = environ.get('WEATHER-API_KEY') @property def location(self) -> str: @@ -37,7 +38,7 @@ class Weather: def realtime(self): method = '/current.json' params = { - 'key': WEATHER_API_KEY, + 'key': self.api_key, 'q': self.location, } return self.make_request(method=method, params=params) @@ -45,7 +46,7 @@ class Weather: def forecast(self, days: int = 14, dt: str = '2025-03-24'): method = '/forecast.json' params = { - 'key': WEATHER_API_KEY, + 'key': self.api_key, 'q': self.location, 'days': days, 'dt': dt, -- 2.47.2 From 931d1b72988d8b52761f77320629e67f66ce2224 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 16 Mar 2025 13:18:11 +0100 Subject: [PATCH 126/145] Doing naughty stuff, don't look --- reginaldCog/weather.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reginaldCog/weather.py b/reginaldCog/weather.py index f57b52b..0968a24 100644 --- a/reginaldCog/weather.py +++ b/reginaldCog/weather.py @@ -24,8 +24,8 @@ def get_weather_forecast(location: str, days: int = 14, dt: str = '2025-03-24') class Weather: def __init__(self, location: str): self.__location = location - self.api_key = environ.get('WEATHER-API_KEY') - + self.api_key = "f6e5cb73cae242deb2b63903250803" + @property def location(self) -> str: return self.__location -- 2.47.2 From cc4e7ae52830b93c10c10439748161879299f784 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 16 Mar 2025 13:22:48 +0100 Subject: [PATCH 127/145] No more naughty, all is fine --- reginaldCog/weather.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reginaldCog/weather.py b/reginaldCog/weather.py index 0968a24..86e6b79 100644 --- a/reginaldCog/weather.py +++ b/reginaldCog/weather.py @@ -24,8 +24,8 @@ def get_weather_forecast(location: str, days: int = 14, dt: str = '2025-03-24') class Weather: def __init__(self, location: str): self.__location = location - self.api_key = "f6e5cb73cae242deb2b63903250803" - + self.api_key = environ.get('WEATHER_API_KEY') + @property def location(self) -> str: return self.__location -- 2.47.2 From 62972399a677d7eed56ce7a8718f765048b8525b Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 16 Mar 2025 13:30:48 +0100 Subject: [PATCH 128/145] STARES AT FORECAST - info --- reginaldCog/weather.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/reginaldCog/weather.py b/reginaldCog/weather.py index 86e6b79..c31dc73 100644 --- a/reginaldCog/weather.py +++ b/reginaldCog/weather.py @@ -18,7 +18,11 @@ def get_current_weather(location: str) -> str: def get_weather_forecast(location: str, days: int = 14, dt: str = '2025-03-24') -> str: weather = Weather(location=location) - return json.dumps(weather.forecast(days=days, dt=dt)) + response = weather.forecast(days=days, dt=dt) + + print("DEBUG: Forecast API Response: ", json.dumps(response, ident=2)) + + return json.dumps(response) class Weather: -- 2.47.2 From ac1489538147a82fe0b4a7d3d8dedccf145729d6 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 16 Mar 2025 13:33:02 +0100 Subject: [PATCH 129/145] I can spell, don't check --- reginaldCog/weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/weather.py b/reginaldCog/weather.py index c31dc73..d0ded52 100644 --- a/reginaldCog/weather.py +++ b/reginaldCog/weather.py @@ -20,7 +20,7 @@ def get_weather_forecast(location: str, days: int = 14, dt: str = '2025-03-24') weather = Weather(location=location) response = weather.forecast(days=days, dt=dt) - print("DEBUG: Forecast API Response: ", json.dumps(response, ident=2)) + print("DEBUG: Forecast API Response: ", json.dumps(response, indent=2)) return json.dumps(response) -- 2.47.2 From 341a68c04537b0553c4d91d3c486a60f4b4ee0bd Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 16 Mar 2025 13:41:15 +0100 Subject: [PATCH 130/145] Trying to debug the weather, so many mayflies --- reginaldCog/weather.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/reginaldCog/weather.py b/reginaldCog/weather.py index d0ded52..18acab1 100644 --- a/reginaldCog/weather.py +++ b/reginaldCog/weather.py @@ -19,10 +19,9 @@ def get_current_weather(location: str) -> str: def get_weather_forecast(location: str, days: int = 14, dt: str = '2025-03-24') -> str: weather = Weather(location=location) response = weather.forecast(days=days, dt=dt) - - print("DEBUG: Forecast API Response: ", json.dumps(response, indent=2)) - - return json.dumps(response) + + print(f"DEBUG: Forecast API Response: {response}") + return json.dumps(weather.forecast(days=days, dt=dt)) class Weather: -- 2.47.2 From b843a4ef589e4dc8c99c39e9ed12ab77ae2e3bc5 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 16 Mar 2025 13:48:36 +0100 Subject: [PATCH 131/145] Adding debug stuff --- reginaldCog/reginald.py | 11 ++++++++--- reginaldCog/weather.py | 3 --- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 4fca2a8..3a1ca55 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -229,9 +229,14 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): # chat message. response = await client.chat.completions.create(**completion_args) - response_text = response.choices[0].message.content.strip() - if response_text.startswith("Reginald:"): - response_text = response_text[len("Reginald:"):].strip() + if response.choices and response.choices[0].message and response.choices[0].message.content: + response_text = response.choices[0].message.content.strip() + if response_text.startswith("Reginald:"): + response_text = response_text[len("Reginald:"):].strip() + else: + print("DEBUG: OpenAI response was empty or malformed:", response) + response_text = "⚠️ No response received from AI." + return response_text except OpenAIError as e: diff --git a/reginaldCog/weather.py b/reginaldCog/weather.py index 18acab1..86e6b79 100644 --- a/reginaldCog/weather.py +++ b/reginaldCog/weather.py @@ -18,9 +18,6 @@ def get_current_weather(location: str) -> str: def get_weather_forecast(location: str, days: int = 14, dt: str = '2025-03-24') -> str: weather = Weather(location=location) - response = weather.forecast(days=days, dt=dt) - - print(f"DEBUG: Forecast API Response: {response}") return json.dumps(weather.forecast(days=days, dt=dt)) -- 2.47.2 From 6e34f36b8b021ff90f728faa9a8509dd82678bdf Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Sun, 16 Mar 2025 17:58:51 +0500 Subject: [PATCH 132/145] - Removed some condition check that some guy put in there --- reginaldCog/reginald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 3a1ca55..e53e63d 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -212,7 +212,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): 'content': response.choices[0].message.content, 'tool_calls': tool_calls }) - if isinstance(tool_calls, list) and tool_calls: + if tool_calls: for i_call in tool_calls: # Calling for necessary functions func_name = i_call.function.name -- 2.47.2 From 1a34585d33301319529a67cad82588521f7e16d0 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 16 Mar 2025 14:10:49 +0100 Subject: [PATCH 133/145] No max tokens, this is surely safe --- reginaldCog/reginald.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 3a1ca55..55a6b05 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -196,7 +196,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): completion_args = { 'model': model, 'messages': messages, - 'max_tokens': 2048, + #'max_tokens': 2048, 'temperature': 0.7, 'presence_penalty': 0.5, 'frequency_penalty': 0.5, @@ -237,6 +237,8 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): print("DEBUG: OpenAI response was empty or malformed:", response) response_text = "⚠️ No response received from AI." + completion_args["messages"] = messages + return response_text except OpenAIError as e: -- 2.47.2 From dc5bce9ca476a78476a32c26563ca2d64f0c1f52 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 16 Mar 2025 14:14:56 +0100 Subject: [PATCH 134/145] Re-adding max tokens --- reginaldCog/reginald.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 5906370..55aa82e 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -196,7 +196,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): completion_args = { 'model': model, 'messages': messages, - #'max_tokens': 2048, + 'max_tokens': 4096, 'temperature': 0.7, 'presence_penalty': 0.5, 'frequency_penalty': 0.5, @@ -237,8 +237,6 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): print("DEBUG: OpenAI response was empty or malformed:", response) response_text = "⚠️ No response received from AI." - completion_args["messages"] = messages - return response_text except OpenAIError as e: -- 2.47.2 From ae866894a341b08413f2939a0a16a728d9d98ee5 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 16 Mar 2025 14:25:10 +0100 Subject: [PATCH 135/145] Trying to update messages --- reginaldCog/reginald.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 55aa82e..904de8a 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -225,6 +225,8 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): 'content': func_result, 'tool_call_id': tool_call_id, }) + + completion_args["messages"] = messages # Second completion required if functions has been called to interpret the result into user-friendly # chat message. response = await client.chat.completions.create(**completion_args) -- 2.47.2 From d35110a83b750ea14b47f7757841cc83c50fe9d2 Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Mon, 17 Mar 2025 21:43:15 +0500 Subject: [PATCH 136/145] + Added openai_completion.py for later use as a external script for working with ChatGPT --- reginaldCog/openai_completion.py | 81 ++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 reginaldCog/openai_completion.py diff --git a/reginaldCog/openai_completion.py b/reginaldCog/openai_completion.py new file mode 100644 index 0000000..b19f720 --- /dev/null +++ b/reginaldCog/openai_completion.py @@ -0,0 +1,81 @@ +import random +import json +import openai +from openai import OpenAIError +from .weather import time_now, get_current_weather, get_weather_forecast +from .tools_description import TOOLS + +CALLABLE_FUNCTIONS = { + # Dictionary with functions to call. + # You can use globals()[func_name](**args) instead, but that's too implicit. + 'time_now': time_now, + 'get_current_weather': get_current_weather, + 'get_weather_forecast': get_weather_forecast, +} + + +class Completion: + def __init__(self, model: str, api_key: str): + self.__model = model + self.__api_key = api_key + self.__messages = [] + + async def create_completion(self, messages: list): + self.__messages = messages + model = self.__model + try: + client = openai.AsyncClient(api_key=self.__api_key) + completion_kwargs = { + "model": model, + "messages": messages, + "max_tokens": 4096, + "temperature": 0.7, + "presence_penalty": 0.5, + "frequency_penalty": 0.5, + "tools": TOOLS, + "tool_choice": "auto", + } + response = await client.chat.completions.create(**completion_kwargs) + response_content = response.choices[0].message.content + tool_calls = response.choices[0].message.tool_calls + self.append_message(role="assistant", content=response_content, tool_calls=tool_calls) + if tool_calls: + for i_call in tool_calls: + func_name = i_call.function.name + func_args = json.loads(i_call.function.arguments) + tool_call_id = i_call.id + self.function_manager(func_name, func_args, tool_call_id) + return self.create_completion(messages=self.__messages) + return response_content + except OpenAIError as e: + return self.get_error_message(error_message=str(e), error_type="OpenAIError") + + def append_message( + self, + role: str, + content: str, + tool_calls: list = None, + tool_call_id: str = None, + ): + self.__messages.append({ + "role": role, + "content": content, + "tool_calls": tool_calls, + "tool_call_id": tool_call_id, + }) + + @staticmethod + def get_error_message(error_message: str, error_type: str) -> str: + reginald_responses = [ + f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:", + f"It would seem that a most unfortunate technical hiccup has befallen my faculties:", + f"Ah, it appears I have received an urgent memorandum stating:", + f"I regret to inform you that my usual eloquence is presently obstructed by an unforeseen complication:", + ] + random_response = random.choice(reginald_responses) + return f"{random_response}\n\n{error_type}: {error_message}" + + def function_manager(self, func_name: str, func_kwargs: dict, tool_call_id: str): + result = CALLABLE_FUNCTIONS[func_name](**func_kwargs) + self.append_message(role="tool", content=result, tool_call_id=tool_call_id) + -- 2.47.2 From 881ef42f426ede41b6a5af57978454ae547be47a Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Tue, 18 Mar 2025 19:50:12 +0500 Subject: [PATCH 137/145] + Added debug decorator --- reginaldCog/debug_stuff.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 reginaldCog/debug_stuff.py diff --git a/reginaldCog/debug_stuff.py b/reginaldCog/debug_stuff.py new file mode 100644 index 0000000..92ccdf0 --- /dev/null +++ b/reginaldCog/debug_stuff.py @@ -0,0 +1,15 @@ +def debug(func): + def wrap(*args, **kwargs): + # Log the function name and arguments + print(f"DEBUG: Calling {func.__name__} with args: {args}, kwargs: {kwargs}") + + # Call the original function + result = func(*args, **kwargs) + + # Log the return value + print(f"DEBUG: {func.__name__} returned: {result}") + + # Return the result + return result + + return wrap -- 2.47.2 From b93ae09b69f7d9d5832c9f7b0dcf4a87385d72bb Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Tue, 18 Mar 2025 19:54:34 +0500 Subject: [PATCH 138/145] + Applied debug decorator to some functions and methods --- reginaldCog/openai_completion.py | 2 ++ reginaldCog/reginald.py | 2 ++ reginaldCog/weather.py | 6 ++++++ 3 files changed, 10 insertions(+) diff --git a/reginaldCog/openai_completion.py b/reginaldCog/openai_completion.py index b19f720..60aa384 100644 --- a/reginaldCog/openai_completion.py +++ b/reginaldCog/openai_completion.py @@ -4,6 +4,7 @@ import openai from openai import OpenAIError from .weather import time_now, get_current_weather, get_weather_forecast from .tools_description import TOOLS +from .debug_stuff import debug CALLABLE_FUNCTIONS = { # Dictionary with functions to call. @@ -20,6 +21,7 @@ class Completion: self.__api_key = api_key self.__messages = [] + @debug async def create_completion(self, messages: list): self.__messages = messages model = self.__model diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 904de8a..b821293 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -14,6 +14,7 @@ from .blacklist import BlacklistMixin from .memory import MemoryMixin from .weather import time_now, get_current_weather, get_weather_forecast from .tools_description import TOOLS +from .debug_stuff import debug CALLABLE_FUNCTIONS = { @@ -189,6 +190,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): return any(message_lower.startswith(invocation) for invocation in direct_invocation) + @debug async def generate_response(self, api_key, messages): model = await self.config.openai_model() try: diff --git a/reginaldCog/weather.py b/reginaldCog/weather.py index 86e6b79..580716d 100644 --- a/reginaldCog/weather.py +++ b/reginaldCog/weather.py @@ -2,20 +2,24 @@ from datetime import datetime, timezone from os import environ import requests import json +from .debug_stuff import debug #WEATHER_API_KEY = environ.get('WEATHER_API_KEY') URL = 'http://api.weatherapi.com/v1' +@debug def time_now() -> str: return str(datetime.now(timezone.utc)) +@debug def get_current_weather(location: str) -> str: weather = Weather(location=location) return json.dumps(weather.realtime()) +@debug def get_weather_forecast(location: str, days: int = 14, dt: str = '2025-03-24') -> str: weather = Weather(location=location) return json.dumps(weather.forecast(days=days, dt=dt)) @@ -35,6 +39,7 @@ class Weather: response = requests.get(url=f'{URL}{method}', params=params) return response.json() + @debug def realtime(self): method = '/current.json' params = { @@ -43,6 +48,7 @@ class Weather: } return self.make_request(method=method, params=params) + @debug def forecast(self, days: int = 14, dt: str = '2025-03-24'): method = '/forecast.json' params = { -- 2.47.2 From 38414027eca9813890314935a7d3c1dba80ba3e8 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 18 May 2025 12:51:52 +0200 Subject: [PATCH 139/145] Updated recruitment cog --- recruitmentCog/recruitment.py | 421 +++++++++++++++++++--------------- 1 file changed, 230 insertions(+), 191 deletions(-) diff --git a/recruitmentCog/recruitment.py b/recruitmentCog/recruitment.py index 59eabfc..6aba282 100644 --- a/recruitmentCog/recruitment.py +++ b/recruitmentCog/recruitment.py @@ -1,246 +1,285 @@ import asyncio import datetime import re -from typing import List -from datetime import timedelta +from typing import Dict, List, Optional import discord from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.utils.antispam import AntiSpam +# Define your application questions and field settings\ +QUESTIONS_LIST = [ + { + "key": "name", + "prompt": "First of all, what is your name?", + "field_name": "Name", + "inline": True, + }, + { + "key": "age", + "prompt": "What age are you?", + "field_name": "Age", + "inline": True, + }, + { + "key": "country", + "prompt": "Where are you from?", + "field_name": "Country", + "inline": True, + }, + { + "key": "hobbies", + "prompt": "Do you have any hobbies?", + "field_name": "Hobbies", + "inline": True, + }, + { + "key": "game", + "prompt": "Are you wishing to join because of a particular game? If so, which game?", + "field_name": "Specific game?", + "inline": True, + }, + { + "key": "motivation", + "prompt": "Write out, in a free-style way, what your motivation is for wanting to join us in particular and how you would be a good fit for Kanium", + "field_name": "Motivation for wanting to join:", + "inline": False, + }, +] + def sanitize_input(input_text: str) -> str: - """Sanitize input to remove mentions, links, and special characters.""" - sanitized_text = re.sub(r'<@!?&?(\d+)>', '', input_text) - sanitized_text = re.sub(r'http\S+', '', sanitized_text) - sanitized_text = re.sub(r'([^\w\s.,?!`~@#$%^&*()_+=-])', '', sanitized_text) - return sanitized_text + """Sanitize input to remove mentions, links, and unwanted special characters.""" + text = re.sub(r'<@!?(?:&)?\d+>', '', input_text) + text = re.sub(r'http\S+', '', text) + # Allow unicode letters and common punctuation + text = re.sub(r'[^\w\s\p{L}\.,\?!`~@#$%^&*()_+=-]', '', text) + return text -class Recruitment(commands.Cog): +class Recruitment(commands.Cog): # noqa """A cog that lets a user send a membership application.""" def __init__(self, bot: Red): self.bot = bot - self.message: str = '' - self.config = Config.get_conf(self, identifier=101101101101001110101) # Replace with your own unique identifier - default_guild = {"guild_id": 274657393936302080, "application_channel_id": None} + self.config = Config.get_conf(self, identifier=0xFAB123ABC456) + default_guild = {"application_channel_id": None} self.config.register_guild(**default_guild) - self.antispam = {} - self.cog_check_enabled = True # Attribute to track the state of cog_check + # antispam store per guild per user + self.antispam: Dict[int, Dict[int, AntiSpam]] = {} + self.cog_check_enabled = True - async def cog_check(self, ctx: commands.Context): - if await ctx.bot.is_admin(ctx.author): + async def cog_check(self, ctx: commands.Context) -> bool: + if ( + await ctx.bot.is_admin(ctx.author) + or not self.cog_check_enabled + or (ctx.guild and ctx.author.guild_permissions.manage_guild) + or (ctx.guild and ctx.author.guild_permissions.administrator) + ): return True - - if not self.cog_check_enabled: - return True # If disabled, always return True to allow all commands - guild_id = ctx.guild.id - author_id = ctx.author.id # Get the ID of the user who invoked the command - - # Check if the guild has an antispam entry, if not, create one - if guild_id not in self.antispam: - self.antispam[guild_id] = {} - - # Check if the user has an antispam entry in this guild, if not, create one - if author_id not in self.antispam[guild_id]: - self.antispam[guild_id][author_id] = AntiSpam([(datetime.timedelta(hours=1), 1)]) - - # Get the antispam object for this specific user in this guild - antispam = self.antispam[guild_id][author_id] - - if antispam.spammy: + gid = ctx.guild.id + uid = ctx.author.id + if gid not in self.antispam: + self.antispam[gid] = {} + if uid not in self.antispam[gid]: + self.antispam[gid][uid] = AntiSpam([(datetime.timedelta(hours=1), 1)]) + spam = self.antispam[gid][uid] + if spam.spammy: try: - await ctx.message.delete(delay=0) + await ctx.message.delete() + except discord.Forbidden: + pass + try: + await ctx.author.send("Please wait for an hour before sending another application.") except discord.Forbidden: pass - await ctx.author.send("Please wait for an hour before sending another application.") return False - - antispam.stamp() + spam.stamp() return True @commands.command(name="togglecogcheck") - @checks.is_owner() # Or use any other appropriate check - async def toggle_cog_check(self, ctx: commands.Context): + @checks.is_owner() + async def toggle_cog_check(self, ctx: commands.Context) -> None: """Toggle the cog_check functionality on or off.""" self.cog_check_enabled = not self.cog_check_enabled - status = "enabled" if self.cog_check_enabled else "disabled" - await ctx.send(f"Cog check has been {status}.") + await ctx.send(f"Cog checks are now {'enabled' if self.cog_check_enabled else 'disabled'}.") @commands.guild_only() @checks.admin_or_permissions(manage_guild=True) - @commands.group(name="setapplicationschannel", pass_context=True, no_pm=True) - async def setapplicationschannel(self, ctx: commands.Context): + @commands.group( + name="setapplicationschannel", + invoke_without_command=True, + ) + async def set_applications_channel(self, ctx: commands.Context) -> None: """Set the channel where applications will be sent.""" - if ctx.invoked_subcommand is None: - guild = ctx.guild - channel = ctx.channel - await self.config.guild(guild).guild_id.set(guild.id) - await self.config.guild(guild).application_channel_id.set(channel.id) - await ctx.send(f"Application channel set to {channel.mention}.") + await self.config.guild(ctx.guild).application_channel_id.set(ctx.channel.id) + await ctx.send(f"Application channel set to {ctx.channel.mention}.") - @setapplicationschannel.command(name="clear") - async def clear_application_channel(self, ctx: commands.Context): + @set_applications_channel.command(name="clear") + async def clear_applications_channel(self, ctx: commands.Context) -> None: """Clear the current application channel.""" - guild = ctx.guild - await self.config.guild(guild).clear_raw("application_channel_id") + await self.config.guild(ctx.guild).clear_raw("application_channel_id") await ctx.send("Application channel cleared.") - @commands.group(name="application", usage="[text]", invoke_without_command=True) - async def application(self, ctx: commands.Context, *, _application: str = ""): - # Input validation and sanitization for _application - _application = sanitize_input(_application) - if len(_application) > 2000: - await ctx.send("Your application is too long. Please limit it to 2000 characters.") - return - - guild_id = await self.get_guild_id(ctx) - guild = discord.utils.get(self.bot.guilds, id=guild_id) - if guild is None: - await ctx.send(f"The guild with ID {guild_id} could not be found.") - return - - author = ctx.author - if author.guild != guild: - await ctx.send(f"You need to be in the {guild.name} server to submit an application.") - return - - if await self.check_author_is_member_and_channel_is_dm(ctx): - await self.interactive_application(author) - - async def get_guild_id(self, ctx: commands.Context) -> int: - guild_id = await self.config.guild(ctx.author.guild).guild_id() - return guild_id - - async def check_author_is_member_and_channel_is_dm(self, ctx: commands.Context) -> bool: - if not isinstance(ctx.author, discord.Member): - await ctx.send("You need to join the server before your application can be processed.") - return False - if not isinstance(ctx.channel, discord.DMChannel): + @commands.group(name="application", invoke_without_command=True) + async def application(self, ctx: commands.Context, *, text: Optional[str] = None) -> None: + """Start an application process.""" + # Direct free-text application (optional) + if text: + text = sanitize_input(text) + if len(text) > 2000: + await ctx.send("Your application is too long (max 2000 chars).") + return + # Determine if in guild or DM + if isinstance(ctx.channel, discord.DMChannel): + await self._start_application(ctx.author) + else: try: await ctx.message.delete() - except: + except discord.Forbidden: pass - await self.interactive_application(ctx) - return False - return True + try: + await ctx.author.send( + "Let's move this to DM so we can process your application." + ) + except discord.Forbidden: + await ctx.send("I cannot DM you. Please enable DMs and try again.") + return + await self._start_application(ctx.author) - - async def interactive_application(self, ctx: commands.Context): - """Ask the user several questions to create an application.""" - author = ctx.author - embed = discord.Embed( + async def _start_application(self, member: discord.Member) -> None: + # Kick off interactive questions + if not await self._send_embed( + member, title="+++ KANIUM APPLICATION SYSTEM +++", - description="Ah, you wish to apply for Kanium membership. Very well, understand that This process is very important to us so we expect you to put effort in and be glorious about it. Let us begin!", + description=( + "Ah, you wish to apply for Kanium membership." + " Please take your time and answer thoughtfully." + ), color=discord.Color.green(), - ) - await author.send(embed=embed) - - answers = await self.ask_questions(author) + ): + return + answers = await self.ask_questions(member) if not answers: return + embed = self.format_application(answers, member) + await self.send_application(member, embed) - embeddedApplication = await self.format_application(answers, author) - - # Call the sendApplication to check if the author is a member of the guild and send the application if they are - await self.sendApplication(author, embeddedApplication) - - - async def sendApplication(self, author: discord.Member, embeddedApplication: discord.Embed): - # Check if the author is a member of the guild - guild = author.guild - member = guild.get_member(author.id) - if member is None: - await author.send("You need to join the server before your application can be processed.") - return - - # Send the embed to the application channel - application_channel_id = await self.config.guild(guild).application_channel_id() - if not application_channel_id: - await author.send("The application channel has not been set. Please use the `setapplicationschannel` command to set it.") - return - - application_channel = guild.get_channel(application_channel_id) - if application_channel is None: - await author.send(f"The application channel with ID {application_channel_id} could not be found.") - return - + async def _send_embed( + self, + member: discord.Member, + *, + title: str, + description: str, + color: discord.Color, + ) -> bool: + embed = discord.Embed(title=title, description=description, color=color) try: - message = await application_channel.send(embed=embeddedApplication) + await member.send(embed=embed) + return True except discord.Forbidden: - await author.send("I do not have permission to send messages in the application channel.") - return - - # Add reactions to the message - try: - await self.add_reactions(message) - except discord.Forbidden: - await author.send("I do not have permission to add reactions to messages in the application channel.") - return - - # Assign the Trial role to the author - role = guild.get_role(531181363420987423) - try: - await member.add_roles(role) - except discord.Forbidden: - await author.send("I do not have permission to assign roles.") - return - - await author.send("Thank you for submitting your application! You have been given the 'Trial' role.") - - - async def add_reactions(self, message: discord.Message): - reactions = ["✅", "❌", "❓"] - for reaction in reactions: - await message.add_reaction(reaction) - - async def format_application(self, answers: List[str], author: discord.Member) -> discord.Embed: - """Format the application answers into an embed.""" - application_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - trial_end_date = (datetime.datetime.now() + datetime.timedelta(days=30)).strftime("%Y-%m-%d") - - embed = discord.Embed(title=f"Application from {author.name}#{author.discriminator}", color=discord.Color.green()) - embed.set_thumbnail(url=author.avatar.url) - embed.add_field(name="Name", value=answers[0]) - embed.add_field(name="Age", value=answers[1]) - embed.add_field(name="Country", value=answers[2]) - embed.add_field(name="Hobbies", value=answers[3]) - embed.add_field(name="Specific game?", value=answers[4]) - embed.add_field(name="\u200b", value="\u200b") # Empty field for spacing - embed.add_field(name="Motivation for wanting to join:", value=answers[5], inline=False) - embed.set_footer(text=f"Application received: {application_date}, Trial ends: {trial_end_date}") - - return embed - - async def ask_questions(self, author: discord.Member) -> List[str]: - """Ask the user several questions and return the answers.""" - questions = [ - "First of all, what is your name?", - "What age are you?", - "Where are you from?", - "Do you have any hobbies?", - "Are you wishing to join because of a particular game? If so, which game?", - "Write out, in a free-style way, what your motivation is for wanting to join us in particular and how you would be a good fit for Kanium", - ] - - answers = [] - - for question in questions: - await author.send(question) + return False + async def ask_questions(self, member: discord.Member) -> Optional[Dict[str, str]]: + answers: Dict[str, str] = {} + for q in QUESTIONS_LIST: try: - answer = await asyncio.wait_for(self.get_answers(author), timeout=300.0) - answers.append(answer.content) + await member.send(q["prompt"]) + except discord.Forbidden: + return None + try: + msg = await asyncio.wait_for( + self.bot.wait_for( + "message", + check=lambda m: m.author == member and isinstance(m.channel, discord.DMChannel), + ), + timeout=300.0, + ) except asyncio.TimeoutError: - await author.send("You took too long to answer. Please start over by using the application command again.") - return [] - + try: + await member.send("You took too long. Please run the application command again.") + except discord.Forbidden: + pass + return None + answers[q["key"]] = sanitize_input(msg.content) return answers - async def get_answers(self, author: discord.Member) -> discord.Message: - """Wait for the user to send a message.""" - return await self.bot.wait_for("message", check=lambda m: m.author == author and isinstance(m.channel, discord.DMChannel)) \ No newline at end of file + def format_application( + self, answers: Dict[str, str], member: discord.Member + ) -> discord.Embed: + now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + trial_ends = ( + datetime.datetime.utcnow() + datetime.timedelta(days=30) + ).strftime("%Y-%m-%d UTC") + + embed = discord.Embed( + title=f"Application from {member}", color=discord.Color.green() + ) + embed.set_thumbnail(url=member.avatar.url) + for q in QUESTIONS_LIST: + embed.add_field( + name=q["field_name"], + value=answers[q["key"]], + inline=q["inline"], + ) + embed.set_footer(text=f"Received: {now} | Trial ends: {trial_ends}") + return embed + + async def send_application( + self, member: discord.Member, embed: discord.Embed + ) -> None: + guild = member.guild + if guild is None: + try: + await member.send("You need to be in the server to apply.") + except discord.Forbidden: + pass + return + + channel_id = await self.config.guild(guild).application_channel_id() + if not channel_id: + try: + await member.send( + "Application channel not set. Ask an admin to run `setapplicationschannel`." + ) + except discord.Forbidden: + pass + return + + channel = guild.get_channel(channel_id) + if not channel: + try: + await member.send("Application channel was not found. Please ask an admin to re-set it.") + except discord.Forbidden: + pass + return + + try: + sent = await channel.send(embed=embed) + await self.add_reactions(sent) + except discord.Forbidden: + try: + await member.send("I cannot post or react in the application channel.") + except discord.Forbidden: + pass + return + + # Assign Trial role + role = guild.get_role(531181363420987423) + if role: + try: + await member.add_roles(role) + await member.send("Thank you! You've been granted the Trial role.") + except discord.Forbidden: + try: + await member.send("I lack permissions to assign roles.") + except discord.Forbidden: + pass + + @staticmethod + async def add_reactions(message: discord.Message) -> None: + for emoji in ("✅", "❌", "❓"): + await message.add_reaction(emoji) -- 2.47.2 From 9a3e5860a7b307c9a82590e9ebc4a17a81548218 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 18 May 2025 12:58:41 +0200 Subject: [PATCH 140/145] Attempting to fix questions --- recruitmentCog/recruitment.py | 79 +++++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/recruitmentCog/recruitment.py b/recruitmentCog/recruitment.py index 6aba282..e605621 100644 --- a/recruitmentCog/recruitment.py +++ b/recruitmentCog/recruitment.py @@ -155,7 +155,7 @@ class Recruitment(commands.Cog): # noqa member, title="+++ KANIUM APPLICATION SYSTEM +++", description=( - "Ah, you wish to apply for Kanium membership." + "This is the process to join Kanium." " Please take your time and answer thoughtfully." ), color=discord.Color.green(), @@ -184,27 +184,70 @@ class Recruitment(commands.Cog): # noqa return False async def ask_questions(self, member: discord.Member) -> Optional[Dict[str, str]]: + """Ask each question, let the user confirm their answer, and return the final answers.""" answers: Dict[str, str] = {} + for q in QUESTIONS_LIST: - try: - await member.send(q["prompt"]) - except discord.Forbidden: - return None - try: - msg = await asyncio.wait_for( - self.bot.wait_for( - "message", - check=lambda m: m.author == member and isinstance(m.channel, discord.DMChannel), - ), - timeout=300.0, - ) - except asyncio.TimeoutError: + prompt = q["prompt"] + + while True: + # 1) Ask the question try: - await member.send("You took too long. Please run the application command again.") + await member.send(prompt) except discord.Forbidden: - pass - return None - answers[q["key"]] = sanitize_input(msg.content) + return None + + # 2) Wait for their reply (match by ID, not object equality) + try: + msg = await asyncio.wait_for( + self.bot.wait_for( + "message", + check=lambda m: ( + m.author.id == member.id + and isinstance(m.channel, discord.DMChannel) + ), + ), + timeout=300.0, + ) + except asyncio.TimeoutError: + await member.send( + "You took too long to answer. Please run the application command again." + ) + return None + + answer = sanitize_input(msg.content) + + # 3) Ask for confirmation + try: + await member.send(f"You answered:\n> {answer}\n\nIs that correct? (yes/no)") + except discord.Forbidden: + return None + + # 4) Wait for yes/no + try: + confirm = await asyncio.wait_for( + self.bot.wait_for( + "message", + check=lambda m: ( + m.author.id == member.id + and isinstance(m.channel, discord.DMChannel) + and m.content.lower() in ("y", "yes", "n", "no") + ), + ), + timeout=60.0, + ) + except asyncio.TimeoutError: + await member.send( + "Confirmation timed out. Please start your application again." + ) + return None + + if confirm.content.lower() in ("y", "yes"): + answers[q["key"]] = answer + break # move on to next question + else: + await member.send("Okay, let's try that again.") + return answers def format_application( -- 2.47.2 From a5ef741ad7166aa9ef772e495ab06f0e9a624e0c Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 18 May 2025 13:03:21 +0200 Subject: [PATCH 141/145] wtf questions --- recruitmentCog/recruitment.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/recruitmentCog/recruitment.py b/recruitmentCog/recruitment.py index e605621..70f72da 100644 --- a/recruitmentCog/recruitment.py +++ b/recruitmentCog/recruitment.py @@ -184,20 +184,23 @@ class Recruitment(commands.Cog): # noqa return False async def ask_questions(self, member: discord.Member) -> Optional[Dict[str, str]]: - """Ask each question, let the user confirm their answer, and return the final answers.""" + """ + Ask each question, let the user confirm their answer, + and return the final answers (or None on abort). + """ answers: Dict[str, str] = {} for q in QUESTIONS_LIST: prompt = q["prompt"] while True: - # 1) Ask the question + # 1) send the question try: await member.send(prompt) except discord.Forbidden: - return None + return None # can't DM - # 2) Wait for their reply (match by ID, not object equality) + # 2) wait for their reply (match on ID, not object) try: msg = await asyncio.wait_for( self.bot.wait_for( @@ -205,25 +208,25 @@ class Recruitment(commands.Cog): # noqa check=lambda m: ( m.author.id == member.id and isinstance(m.channel, discord.DMChannel) - ), + ) ), timeout=300.0, ) except asyncio.TimeoutError: await member.send( - "You took too long to answer. Please run the application command again." + "You took too long to answer. Please restart the application with `!application`." ) return None answer = sanitize_input(msg.content) - # 3) Ask for confirmation + # 3) echo back for confirmation try: await member.send(f"You answered:\n> {answer}\n\nIs that correct? (yes/no)") except discord.Forbidden: return None - # 4) Wait for yes/no + # 4) wait for a yes/no try: confirm = await asyncio.wait_for( self.bot.wait_for( @@ -232,24 +235,27 @@ class Recruitment(commands.Cog): # noqa m.author.id == member.id and isinstance(m.channel, discord.DMChannel) and m.content.lower() in ("y", "yes", "n", "no") - ), + ) ), timeout=60.0, ) except asyncio.TimeoutError: await member.send( - "Confirmation timed out. Please start your application again." + "Confirmation timed out. Please restart the application with `!application`." ) return None if confirm.content.lower() in ("y", "yes"): + # user confirmed, save and move on answers[q["key"]] = answer - break # move on to next question + break else: + # user said “no” → repeat this question await member.send("Okay, let's try that again.") return answers + def format_application( self, answers: Dict[str, str], member: discord.Member ) -> discord.Embed: -- 2.47.2 From 7a9313d5a5a07ac2eea70d895c3c00d566af4bfd Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 18 May 2025 13:07:11 +0200 Subject: [PATCH 142/145] Removing unsupported escape character --- recruitmentCog/recruitment.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/recruitmentCog/recruitment.py b/recruitmentCog/recruitment.py index 70f72da..6b16707 100644 --- a/recruitmentCog/recruitment.py +++ b/recruitmentCog/recruitment.py @@ -51,13 +51,16 @@ QUESTIONS_LIST = [ def sanitize_input(input_text: str) -> str: """Sanitize input to remove mentions, links, and unwanted special characters.""" + # Remove user/role/channel mentions text = re.sub(r'<@!?(?:&)?\d+>', '', input_text) + # Remove URLs text = re.sub(r'http\S+', '', text) - # Allow unicode letters and common punctuation - text = re.sub(r'[^\w\s\p{L}\.,\?!`~@#$%^&*()_+=-]', '', text) + # Keep only word characters (including Unicode letters/digits), whitespace, and your chosen punctuation + text = re.sub(r"[^\w\s\.,\?!`~@#$%^&*()_+=-]", "", text) return text + class Recruitment(commands.Cog): # noqa """A cog that lets a user send a membership application.""" -- 2.47.2 From 1a390c3d2588d812883ea375176887261f058a56 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 18 May 2025 13:12:25 +0200 Subject: [PATCH 143/145] Attempting to make recruitment channel behave --- recruitmentCog/recruitment.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/recruitmentCog/recruitment.py b/recruitmentCog/recruitment.py index 6b16707..1c5e831 100644 --- a/recruitmentCog/recruitment.py +++ b/recruitmentCog/recruitment.py @@ -115,10 +115,17 @@ class Recruitment(commands.Cog): # noqa name="setapplicationschannel", invoke_without_command=True, ) - async def set_applications_channel(self, ctx: commands.Context) -> None: + + @commands.command(name="setapplicationschannel") + @checks.admin_or_permissions(manage_guild=True) + async def set_applications_channel( + self, ctx: commands.Context, channel: discord.TextChannel = None + ) -> None: """Set the channel where applications will be sent.""" - await self.config.guild(ctx.guild).application_channel_id.set(ctx.channel.id) - await ctx.send(f"Application channel set to {ctx.channel.mention}.") + # if no channel was passed, default to the one you ran it in + channel = channel or ctx.channel + await self.config.guild(ctx.guild).application_channel_id.set(channel.id) + await ctx.send(f"Application channel set to {channel.mention}.") @set_applications_channel.command(name="clear") async def clear_applications_channel(self, ctx: commands.Context) -> None: -- 2.47.2 From 88ecf7911fd324a749460363e6aa0283bf89e402 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 18 May 2025 13:18:50 +0200 Subject: [PATCH 144/145] Attempting to make set channel behavior be nice --- recruitmentCog/recruitment.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/recruitmentCog/recruitment.py b/recruitmentCog/recruitment.py index 1c5e831..55cd308 100644 --- a/recruitmentCog/recruitment.py +++ b/recruitmentCog/recruitment.py @@ -109,27 +109,17 @@ class Recruitment(commands.Cog): # noqa self.cog_check_enabled = not self.cog_check_enabled await ctx.send(f"Cog checks are now {'enabled' if self.cog_check_enabled else 'disabled'}.") - @commands.guild_only() + @commands.group(name="setapplicationschannel", invoke_without_command=True) @checks.admin_or_permissions(manage_guild=True) - @commands.group( - name="setapplicationschannel", - invoke_without_command=True, - ) - - @commands.command(name="setapplicationschannel") - @checks.admin_or_permissions(manage_guild=True) - async def set_applications_channel( - self, ctx: commands.Context, channel: discord.TextChannel = None - ) -> None: + async def set_applications_channel(self, ctx: commands.Context, channel: discord.TextChannel = None): """Set the channel where applications will be sent.""" - # if no channel was passed, default to the one you ran it in channel = channel or ctx.channel await self.config.guild(ctx.guild).application_channel_id.set(channel.id) await ctx.send(f"Application channel set to {channel.mention}.") @set_applications_channel.command(name="clear") - async def clear_applications_channel(self, ctx: commands.Context) -> None: - """Clear the current application channel.""" + async def clear_applications_channel(self, ctx: commands.Context): + """Clear the application channel.""" await self.config.guild(ctx.guild).clear_raw("application_channel_id") await ctx.send("Application channel cleared.") -- 2.47.2 From 22767ad398e59115cb6e9f017494e48293c5b7a7 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Sun, 18 May 2025 13:24:16 +0200 Subject: [PATCH 145/145] Added command aliases --- recruitmentCog/recruitment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recruitmentCog/recruitment.py b/recruitmentCog/recruitment.py index 55cd308..e96a215 100644 --- a/recruitmentCog/recruitment.py +++ b/recruitmentCog/recruitment.py @@ -123,7 +123,7 @@ class Recruitment(commands.Cog): # noqa await self.config.guild(ctx.guild).clear_raw("application_channel_id") await ctx.send("Application channel cleared.") - @commands.group(name="application", invoke_without_command=True) + @commands.group(name="application", aliases=["joinus", "joinkanium", "applyformembership"], invoke_without_command=True) async def application(self, ctx: commands.Context, *, text: Optional[str] = None) -> None: """Start an application process.""" # Direct free-text application (optional) -- 2.47.2