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 )
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 20:20:53 +01:00
@commands.command ( name = " 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 ) :
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 21:48:44 +01:00
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. " } ]
2025-02-20 21:34:57 +01:00
# ✅ 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
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
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 ( )
2025-02-20 16:04:44 +01:00
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 :
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 21:55:25 +01:00
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
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 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 ) )