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
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 19:23:15 +01:00
self . memory_locks = { }
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 ,
" memory " : { } ,
" 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 )
2023-03-14 17:24:21 +01:00
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). """
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 17:00:43 +01:00
""" ✅ Checks if the user is allowed to use Reginald based on role settings. """
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 16:04:44 +01:00
@commands.command ( name = " reginald " , help = " Ask Reginald a question " )
@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 17:00:43 +01:00
""" Handles direct interactions with Reginald, ensuring per-user memory tracking. """
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
user_id = str ( ctx . author . id )
# ✅ Ensure only one update per user at a time
if user_id not in self . memory_locks :
self . memory_locks [ user_id ] = asyncio . Lock ( )
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 , [ ] )
2024-05-30 21:20:09 +02:00
2025-02-20 16:52:54 +01:00
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 } ]
2024-05-30 21:20:09 +02:00
2025-02-20 16:52:54 +01:00
response_text = await self . generate_response ( api_key , messages )
2024-05-30 21:20:09 +02:00
2025-02-20 16:52:54 +01:00
# ✅ Store conversation history correctly (while lock is held)
memory . append ( { " role " : " user " , " content " : prompt } )
memory . append ( { " role " : " assistant " , " content " : response_text } )
2025-02-20 19:23:15 +01:00
memory = memory [ - 25 : ]
2024-05-30 21:20:09 +02:00
2025-02-20 16:52:54 +01:00
guild_memory [ user_id ] = memory # ✅ Atomic update inside async context
del self . memory_locks [ user_id ] # ✅ Clean up lock to prevent memory leaks
2024-05-30 21:20:09 +02:00
2025-02-20 16:04:44 +01:00
await ctx . send ( response_text [ : 2000 ] ) # Discord character limit safeguard
2023-03-14 17:24:21 +01:00
2024-05-30 21:20:09 +02:00
async def generate_response ( self , api_key , messages ) :
2025-02-20 17:00:43 +01:00
""" ✅ Generates a response using OpenAI ' s async API client (corrected version). """
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 19:23:15 +01:00
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
)
2025-02-20 16:52:54 +01:00
if not response . choices :
2025-02-20 16:04:44 +01:00
return " I fear I have no words to offer at this time. "
2025-02-20 16:52:54 +01:00
2025-02-20 19:23:15 +01:00
return response . choices [ 0 ] . message [ " content " ] . strip ( )
2025-02-20 17:00:43 +01:00
except OpenAIError :
2025-02-20 16:04:44 +01:00
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 )
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:17:36 +01:00
2025-02-20 16:52:54 +01:00
async def setup ( bot ) :
""" ✅ Correct async cog setup for Redbot """
await bot . add_cog ( ReginaldCog ( bot ) )