2023-03-16 00:03:31 +01:00
|
|
|
import discord
|
2023-03-16 17:33:08 +01:00
|
|
|
import openai
|
2024-05-30 20:17:09 +02:00
|
|
|
import random
|
2025-02-20 16:52:54 +01:00
|
|
|
import asyncio
|
2025-02-20 20:00:01 +01:00
|
|
|
import traceback
|
2023-03-14 17:45:58 +01:00
|
|
|
from redbot.core import Config, commands
|
2025-02-20 19:28:45 +01:00
|
|
|
from openai import OpenAIError
|
2023-03-14 17:24:21 +01:00
|
|
|
|
|
|
|
|
class ReginaldCog(commands.Cog):
|
|
|
|
|
def __init__(self, bot):
|
|
|
|
|
self.bot = bot
|
2023-06-03 17:23:31 +02:00
|
|
|
self.config = Config.get_conf(self, identifier=71717171171717)
|
2025-02-20 20:20:53 +01:00
|
|
|
self.memory_locks = {} # ✅ Prevents race conditions per channel
|
2025-02-20 16:52:54 +01:00
|
|
|
|
2025-02-20 19:23:15 +01:00
|
|
|
# ✅ Properly Registered Configuration Keys
|
2025-02-20 16:04:44 +01:00
|
|
|
default_global = {"openai_model": "gpt-4o-mini"}
|
2025-02-20 16:52:54 +01:00
|
|
|
default_guild = {
|
|
|
|
|
"openai_api_key": None,
|
2025-02-20 21:34:57 +01:00
|
|
|
"short_term_memory": {}, # Tracks last 100 messages per channel
|
|
|
|
|
"mid_term_memory": {}, # Stores condensed summaries
|
|
|
|
|
"long_term_profiles": {}, # Stores persistent knowledge
|
2025-02-20 16:52:54 +01:00
|
|
|
"admin_role": None,
|
|
|
|
|
"allowed_role": None
|
|
|
|
|
}
|
2023-06-01 20:37:00 +02:00
|
|
|
self.config.register_global(**default_global)
|
|
|
|
|
self.config.register_guild(**default_guild)
|
2025-02-20 23:16:08 +01:00
|
|
|
|
2023-06-03 19:03:23 +02:00
|
|
|
async def is_admin(self, ctx):
|
2025-02-20 16:04:44 +01:00
|
|
|
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)
|
2024-05-30 21:20:09 +02:00
|
|
|
return ctx.author.guild_permissions.administrator
|
2023-03-15 23:18:55 +01:00
|
|
|
|
2023-06-03 19:03:23 +02:00
|
|
|
async def is_allowed(self, ctx):
|
2025-02-20 16:04:44 +01:00
|
|
|
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
|
2023-03-15 23:18:55 +01:00
|
|
|
|
2025-02-20 23:16:08 +01:00
|
|
|
|
2025-02-20 22:45:36 +01:00
|
|
|
@commands.command(name="reginald", aliases=["Reginald"], help="Ask Reginald a question in shared channels")
|
2025-02-20 16:04:44 +01:00
|
|
|
@commands.cooldown(1, 10, commands.BucketType.user)
|
2023-03-14 17:24:21 +01:00
|
|
|
async def reginald(self, ctx, *, prompt=None):
|
2023-06-03 19:20:01 +02:00
|
|
|
if not await self.is_admin(ctx) and not await self.is_allowed(ctx):
|
2025-02-20 16:52:54 +01:00
|
|
|
await ctx.send("You do not have the required role to use this command.")
|
|
|
|
|
return
|
2024-05-30 21:20:09 +02:00
|
|
|
|
2023-03-14 17:24:21 +01:00
|
|
|
if prompt is None:
|
2025-02-20 16:52:54 +01:00
|
|
|
await ctx.send(random.choice(["Yes?", "How may I assist?", "You rang?"]))
|
|
|
|
|
return
|
2023-03-14 20:03:30 +01:00
|
|
|
|
|
|
|
|
api_key = await self.config.guild(ctx.guild).openai_api_key()
|
2025-02-20 16:52:54 +01:00
|
|
|
if not api_key:
|
|
|
|
|
await ctx.send("OpenAI API key not set. Use `!setreginaldcogapi`.")
|
|
|
|
|
return
|
|
|
|
|
|
2025-02-20 20:27:26 +01:00
|
|
|
channel_id = str(ctx.channel.id)
|
2025-02-20 21:34:57 +01:00
|
|
|
user_id = str(ctx.author.id)
|
2025-02-20 23:16:08 +01:00
|
|
|
user_name = ctx.author.display_name
|
2025-02-20 20:27:26 +01:00
|
|
|
|
|
|
|
|
for mention in ctx.message.mentions:
|
|
|
|
|
prompt = prompt.replace(f"<@{mention.id}>", mention.display_name)
|
2025-02-20 16:52:54 +01:00
|
|
|
|
2025-02-20 20:20:53 +01:00
|
|
|
if channel_id not in self.memory_locks:
|
|
|
|
|
self.memory_locks[channel_id] = asyncio.Lock()
|
2025-02-20 16:52:54 +01:00
|
|
|
|
2025-02-20 23:16:08 +01:00
|
|
|
async with self.memory_locks[channel_id]:
|
2025-02-20 21:34:57 +01:00
|
|
|
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:
|
2024-05-30 21:20:09 +02:00
|
|
|
|
2025-02-20 21:34:57 +01:00
|
|
|
memory = short_memory.get(channel_id, [])
|
|
|
|
|
user_profile = long_memory.get(user_id, {})
|
2025-02-20 23:16:08 +01:00
|
|
|
mid_term_summary = mid_memory.get(channel_id, "")
|
2024-05-30 21:20:09 +02:00
|
|
|
|
2025-02-20 23:16:08 +01:00
|
|
|
formatted_messages = [{"role": "system", "content": "You are Reginald... (same full character prompt)"}]
|
|
|
|
|
|
2025-02-20 21:34:57 +01:00
|
|
|
if user_profile:
|
2025-02-20 23:16:08 +01:00
|
|
|
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}"})
|
|
|
|
|
|
2025-02-20 21:34:57 +01:00
|
|
|
formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory]
|
|
|
|
|
formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"})
|
|
|
|
|
|
2025-02-20 20:20:53 +01:00
|
|
|
response_text = await self.generate_response(api_key, formatted_messages)
|
2025-02-20 23:16:08 +01:00
|
|
|
|
|
|
|
|
memory.append({"user": user_name, "content": prompt})
|
|
|
|
|
memory.append({"user": "Reginald", "content": response_text})
|
2024-05-30 21:20:09 +02:00
|
|
|
|
2025-02-20 21:34:57 +01:00
|
|
|
if len(memory) > 100:
|
2025-02-20 23:16:08 +01:00
|
|
|
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
|
2025-02-20 21:34:57 +01:00
|
|
|
|
2025-02-20 23:16:08 +01:00
|
|
|
await ctx.send(response_text[:2000])
|
2024-05-30 21:20:09 +02:00
|
|
|
|
2023-03-14 17:24:21 +01:00
|
|
|
|
2025-02-20 21:34:57 +01:00
|
|
|
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."
|
|
|
|
|
|
|
|
|
|
|
2024-05-30 21:20:09 +02:00
|
|
|
async def generate_response(self, api_key, messages):
|
2023-03-14 20:03:30 +01:00
|
|
|
model = await self.config.openai_model()
|
2025-02-20 16:04:44 +01:00
|
|
|
try:
|
2025-02-20 23:16:08 +01:00
|
|
|
client = openai.AsyncClient(api_key=api_key)
|
2025-02-20 20:07:27 +01:00
|
|
|
response = await client.chat.completions.create(
|
2025-02-20 19:23:15 +01:00
|
|
|
model=model,
|
|
|
|
|
messages=messages,
|
|
|
|
|
max_tokens=1024,
|
|
|
|
|
temperature=0.7,
|
|
|
|
|
presence_penalty=0.5,
|
|
|
|
|
frequency_penalty=0.5
|
|
|
|
|
)
|
2025-02-20 21:55:25 +01:00
|
|
|
response_text = response.choices[0].message.content.strip()
|
|
|
|
|
if response_text.startswith("Reginald:"):
|
|
|
|
|
response_text = response_text[len("Reginald:"):].strip()
|
|
|
|
|
return response_text
|
2025-02-20 19:57:36 +01:00
|
|
|
|
2025-02-20 20:07:27 +01:00
|
|
|
except OpenAIError as e:
|
|
|
|
|
error_message = f"OpenAI Error: {e}"
|
2025-02-20 19:57:36 +01:00
|
|
|
reginald_responses = [
|
2025-02-20 20:07:27 +01:00
|
|
|
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}```"
|
2025-02-20 16:04:44 +01:00
|
|
|
]
|
2025-02-20 19:57:36 +01:00
|
|
|
return random.choice(reginald_responses)
|
2025-02-20 16:52:54 +01:00
|
|
|
|
2025-02-20 22:05:51 +01:00
|
|
|
@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.")
|
|
|
|
|
|
2025-02-20 16:17:36 +01:00
|
|
|
@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):
|
2025-02-20 17:00:43 +01:00
|
|
|
"""✅ Grants permission to a role to use Reginald."""
|
2025-02-20 16:17:36 +01:00
|
|
|
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):
|
2025-02-20 17:00:43 +01:00
|
|
|
"""✅ Removes a role's permission to use Reginald."""
|
2025-02-20 16:17:36 +01:00
|
|
|
await self.config.guild(ctx.guild).allowed_role.clear()
|
|
|
|
|
await ctx.send("The role's permission to use the Reginald command has been revoked.")
|
2025-02-20 19:38:49 +01:00
|
|
|
|
|
|
|
|
@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.")
|
|
|
|
|
|
2025-02-20 16:52:54 +01:00
|
|
|
async def setup(bot):
|
|
|
|
|
"""✅ Correct async cog setup for Redbot"""
|
|
|
|
|
await bot.add_cog(ReginaldCog(bot))
|