KaniumCogs/reginaldCog/reginald.py

189 lines
9.4 KiB
Python
Raw Normal View History

2023-03-16 00:03:31 +01:00
import discord
import openai
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
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
}
self.config.register_global(**default_global)
self.config.register_guild(**default_guild)
2023-06-03 19:03:23 +02:00
async def is_admin(self, ctx):
2025-02-20 17:00:43 +01:00
"""✅ 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)
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 17:00:43 +01:00
"""✅ 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
2023-03-15 23:18:55 +01:00
2025-02-20 20:20:53 +01:00
@commands.command(name="reginald", help="Ask Reginald a question in shared channels")
@commands.cooldown(1, 10, commands.BucketType.user)
2023-03-14 17:24:21 +01:00
async def reginald(self, ctx, *, prompt=None):
2025-02-20 20:27:26 +01:00
"""Handles multi-user memory tracking in shared channels, recognizing mentions properly."""
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)
user_name = ctx.author.display_name # Uses Discord nickname if available
2025-02-20 20:27:26 +01:00
# ✅ Convert mentions into readable names
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
# ✅ Ensure only one update per channel at a time
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 20:20:53 +01:00
async with self.memory_locks[channel_id]: # ✅ Prevent race conditions
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
# ✅ Retrieve memory
memory = short_memory.get(channel_id, [])
user_profile = long_memory.get(user_id, {})
2024-05-30 21:20:09 +02:00
2025-02-20 21:34:57 +01:00
# ✅ Format messages properly
2025-02-20 20:20:53 +01:00
formatted_messages = [{"role": "system", "content": (
"You are Reginald, the esteemed butler of The Kanium Estate. "
2025-02-20 21:34:57 +01:00
"This estate is home to Lords, Ladies, and distinguished guests, each with unique personalities and demands. "
2025-02-20 20:20:53 +01:00
"Your duty is to uphold decorum while providing assistance with wit and intelligence. "
2025-02-20 21:34:57 +01:00
"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})
2024-05-30 21:20:09 +02:00
2025-02-20 21:34:57 +01:00
# ✅ 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
2025-02-20 20:20:53 +01:00
response_text = await self.generate_response(api_key, formatted_messages)
2024-05-30 21:20:09 +02:00
2025-02-20 21:34:57 +01:00
# ✅ 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
2024-05-30 21:20:09 +02:00
await ctx.send(response_text[:2000]) # Discord character limit safeguard
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):
2025-02-20 20:07:27 +01:00
"""✅ Generates a response using OpenAI's new async API client (OpenAI v1.0+)."""
2023-03-14 20:03:30 +01:00
model = await self.config.openai_model()
try:
2025-02-20 21:34:57 +01:00
client = openai.AsyncClient(api_key=api_key) # ✅ Correct API usage
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 16:52:54 +01:00
if not response.choices:
return "I fear I have no words to offer at this time."
2025-02-20 16:52:54 +01:00
2025-02-20 20:07:27 +01:00
return response.choices[0].message.content.strip()
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 19:57:36 +01:00
return random.choice(reginald_responses)
2025-02-20 16:52:54 +01:00
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))