import discord from discord import app_commands # Import app_commands from discord.ext import commands import random import os import time # Import time for timestamps import json # Import json for formatting import datetime # Import datetime for formatting from typing import TYPE_CHECKING, Optional, Dict, Any, List, Tuple # Add more types # Relative imports (assuming API functions are in api.py) # We need access to the cog instance for state and methods like get_ai_response # These commands will likely be added to the GurtCog instance dynamically in cog.py's setup try: from .config import AVAILABLE_AI_MODELS except (ImportError, AttributeError): AVAILABLE_AI_MODELS = { "google/gemini-2.5-flash-preview-05-20": "Gemini 2.5 Flash Preview", "google/gemini-2.5-pro-preview-06-05": "Gemini 2.5 Pro Preview", "claude-sonnet-4@20250514": "Claude Sonnet 4", "llama-4-maverick-17b-128e-instruct-maas": "Llama 4 Maverick Instruct", "google/gemini-2.0-flash-001": "Gemini 2.0 Flash", } if TYPE_CHECKING: from .cog import GurtCog # For type hinting from .config import ( MOOD_OPTIONS, IGNORED_CHANNEL_IDS, update_ignored_channels_file, TENOR_API_KEY, ) # Import for choices and ignored channels from .emojis import EmojiManager # Import EmojiManager # --- Helper Function for Embeds --- def create_gurt_embed( title: str, description: str = "", color=discord.Color.blue() ) -> discord.Embed: """Creates a standard Gurt-themed embed.""" embed = discord.Embed(title=title, description=description, color=color) # Placeholder icon URL, replace if Gurt has one # embed.set_footer(text="Gurt", icon_url="https://example.com/gurt_icon.png") embed.set_footer(text="Gurt") return embed # --- Helper Function for Stats Embeds --- def format_stats_embeds(stats: Dict[str, Any]) -> List[discord.Embed]: """Formats the collected stats into multiple embeds.""" embeds = [] main_embed = create_gurt_embed("Gurt Internal Stats", color=discord.Color.green()) ts_format = "" # Relative timestamp # Runtime Stats runtime = stats.get("runtime", {}) main_embed.add_field( name="Current Mood", value=f"{runtime.get('current_mood', 'N/A')} (Changed {ts_format.format(ts=int(runtime.get('last_mood_change_timestamp', 0)))})", inline=False, ) main_embed.add_field( name="Background Task", value="Running" if runtime.get("background_task_running") else "Stopped", inline=True, ) main_embed.add_field( name="Needs JSON Reminder", value=str(runtime.get("needs_json_reminder", "N/A")), inline=True, ) main_embed.add_field( name="Last Evolution", value=ts_format.format( ts=int(runtime.get("last_evolution_update_timestamp", 0)) ), inline=True, ) main_embed.add_field( name="Active Topics Channels", value=str(runtime.get("active_topics_channels", "N/A")), inline=True, ) main_embed.add_field( name="Conv History Channels", value=str(runtime.get("conversation_history_channels", "N/A")), inline=True, ) main_embed.add_field( name="Thread History Threads", value=str(runtime.get("thread_history_threads", "N/A")), inline=True, ) main_embed.add_field( name="User Relationships Pairs", value=str(runtime.get("user_relationships_pairs", "N/A")), inline=True, ) main_embed.add_field( name="Cached Summaries", value=str(runtime.get("conversation_summaries_cached", "N/A")), inline=True, ) main_embed.add_field( name="Cached Channel Topics", value=str(runtime.get("channel_topics_cached", "N/A")), inline=True, ) main_embed.add_field( name="Global Msg Cache", value=str(runtime.get("message_cache_global_count", "N/A")), inline=True, ) main_embed.add_field( name="Mention Msg Cache", value=str(runtime.get("message_cache_mentioned_count", "N/A")), inline=True, ) main_embed.add_field( name="Active Convos", value=str(runtime.get("active_conversations_count", "N/A")), inline=True, ) main_embed.add_field( name="Sentiment Channels", value=str(runtime.get("conversation_sentiment_channels", "N/A")), inline=True, ) main_embed.add_field( name="Gurt Participation Topics", value=str(runtime.get("gurt_participation_topics_count", "N/A")), inline=True, ) main_embed.add_field( name="Tracked Reactions", value=str(runtime.get("gurt_message_reactions_tracked", "N/A")), inline=True, ) embeds.append(main_embed) # Memory Stats memory_embed = create_gurt_embed("Gurt Memory Stats", color=discord.Color.orange()) memory = stats.get("memory", {}) if memory.get("error"): memory_embed.description = f"⚠️ Error retrieving memory stats: {memory['error']}" else: memory_embed.add_field( name="User Facts", value=str(memory.get("user_facts_count", "N/A")), inline=True, ) memory_embed.add_field( name="General Facts", value=str(memory.get("general_facts_count", "N/A")), inline=True, ) memory_embed.add_field( name="Chroma Messages", value=str(memory.get("chromadb_message_collection_count", "N/A")), inline=True, ) memory_embed.add_field( name="Chroma Facts", value=str(memory.get("chromadb_fact_collection_count", "N/A")), inline=True, ) personality = memory.get("personality_traits", {}) if personality: p_items = [f"`{k}`: {v}" for k, v in personality.items()] memory_embed.add_field( name="Personality Traits", value="\n".join(p_items) if p_items else "None", inline=False, ) interests = memory.get("top_interests", []) if interests: i_items = [f"`{t}`: {l:.2f}" for t, l in interests] memory_embed.add_field( name="Top Interests", value="\n".join(i_items) if i_items else "None", inline=False, ) embeds.append(memory_embed) # API Stats api_stats = stats.get("api_stats", {}) if api_stats: api_embed = create_gurt_embed("Gurt API Stats", color=discord.Color.red()) for model, data in api_stats.items(): avg_time = data.get("average_time_ms", 0) value = ( f"✅ Success: {data.get('success', 0)}\n" f"❌ Failure: {data.get('failure', 0)}\n" f"🔁 Retries: {data.get('retries', 0)}\n" f"⏱️ Avg Time: {avg_time} ms\n" f"📊 Count: {data.get('count', 0)}" ) api_embed.add_field(name=f"Model: `{model}`", value=value, inline=True) embeds.append(api_embed) # Tool Stats tool_stats = stats.get("tool_stats", {}) if tool_stats: tool_embed = create_gurt_embed("Gurt Tool Stats", color=discord.Color.purple()) for tool, data in tool_stats.items(): avg_time = data.get("average_time_ms", 0) value = ( f"✅ Success: {data.get('success', 0)}\n" f"❌ Failure: {data.get('failure', 0)}\n" f"⏱️ Avg Time: {avg_time} ms\n" f"📊 Count: {data.get('count', 0)}" ) tool_embed.add_field(name=f"Tool: `{tool}`", value=value, inline=True) embeds.append(tool_embed) # Config Stats (Less critical, maybe separate embed if needed) config_embed = create_gurt_embed( "Gurt Config Overview", color=discord.Color.greyple() ) config = stats.get("config", {}) config_embed.add_field( name="Default Model", value=f"`{config.get('default_model', 'N/A')}`", inline=True, ) config_embed.add_field( name="Fallback Model", value=f"`{config.get('fallback_model', 'N/A')}`", inline=True, ) config_embed.add_field( name="Semantic Model", value=f"`{config.get('semantic_model_name', 'N/A')}`", inline=True, ) config_embed.add_field( name="Max User Facts", value=str(config.get("max_user_facts", "N/A")), inline=True, ) config_embed.add_field( name="Max General Facts", value=str(config.get("max_general_facts", "N/A")), inline=True, ) config_embed.add_field( name="Context Window", value=str(config.get("context_window_size", "N/A")), inline=True, ) config_embed.add_field( name="API Key Set", value=str(config.get("api_key_set", "N/A")), inline=True ) config_embed.add_field( name="Tavily Key Set", value=str(config.get("tavily_api_key_set", "N/A")), inline=True, ) config_embed.add_field( name="Piston URL Set", value=str(config.get("piston_api_url_set", "N/A")), inline=True, ) config_embed.add_field( name="Tenor API Key Set", value=str(config.get("tenor_api_key_set", "N/A")), inline=True, ) # Added Tenor API Key embeds.append(config_embed) # Limit to 10 embeds max for Discord API return embeds[:10] # --- Command Setup Function --- # This function will be called from GurtCog's setup method def setup_commands(cog: "GurtCog"): """Adds Gurt-specific commands to the cog.""" # Create a list to store command functions for proper registration command_functions = [] # --- Gurt Mood Command --- @cog.bot.tree.command( name="gurtmood", description="Check or set Gurt's current mood." ) @app_commands.describe( mood="Optional: Set Gurt's mood to one of the available options." ) @app_commands.choices( mood=[ app_commands.Choice(name=m, value=m) for m in cog.MOOD_OPTIONS # Use cog's MOOD_OPTIONS ] ) async def gurtmood( interaction: discord.Interaction, mood: Optional[app_commands.Choice[str]] = None, ): """Handles the /gurtmood command.""" # Check if user is the bot owner for mood setting if mood and interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can change Gurt's mood.", ephemeral=True ) return if mood: cog.current_mood = mood.value cog.last_mood_change = time.time() await interaction.response.send_message( f"Gurt's mood set to: {mood.value}", ephemeral=True ) else: time_since_change = time.time() - cog.last_mood_change await interaction.response.send_message( f"Gurt's current mood is: {cog.current_mood} (Set {int(time_since_change // 60)} minutes ago)", ephemeral=True, ) command_functions.append(gurtmood) # --- Gurt Memory Command --- @cog.bot.tree.command(name="gurtmemory", description="Interact with Gurt's memory.") @app_commands.describe( action="Choose an action: add_user, add_general, get_user, get_general", user="The user for user-specific actions (mention or ID).", fact="The fact to add (for add actions).", query="A keyword to search for (for get_general).", ) @app_commands.choices( action=[ app_commands.Choice(name="Add User Fact", value="add_user"), app_commands.Choice(name="Add General Fact", value="add_general"), app_commands.Choice(name="Get User Facts", value="get_user"), app_commands.Choice(name="Get General Facts", value="get_general"), ] ) async def gurtmemory( interaction: discord.Interaction, action: app_commands.Choice[str], user: Optional[discord.User] = None, fact: Optional[str] = None, query: Optional[str] = None, ): """Handles the /gurtmemory command.""" await interaction.response.defer( ephemeral=True ) # Defer for potentially slow DB operations target_user_id = str(user.id) if user else None action_value = action.value # Check if user is the bot owner for modification actions if ( action_value in ["add_user", "add_general"] ) and interaction.user.id != cog.bot.owner_id: await interaction.followup.send( "⛔ Only the bot owner can add facts to Gurt's memory.", ephemeral=True ) return if action_value == "add_user": if not target_user_id or not fact: await interaction.followup.send( "Please provide both a user and a fact to add.", ephemeral=True ) return result = await cog.memory_manager.add_user_fact(target_user_id, fact) await interaction.followup.send( f"Add User Fact Result: `{json.dumps(result)}`", ephemeral=True ) elif action_value == "add_general": if not fact: await interaction.followup.send( "Please provide a fact to add.", ephemeral=True ) return result = await cog.memory_manager.add_general_fact(fact) await interaction.followup.send( f"Add General Fact Result: `{json.dumps(result)}`", ephemeral=True ) elif action_value == "get_user": if not target_user_id: await interaction.followup.send( "Please provide a user to get facts for.", ephemeral=True ) return facts = await cog.memory_manager.get_user_facts( target_user_id ) # Get newest by default if facts: facts_str = "\n- ".join(facts) await interaction.followup.send( f"**Facts for {user.display_name}:**\n- {facts_str}", ephemeral=True ) else: await interaction.followup.send( f"No facts found for {user.display_name}.", ephemeral=True ) elif action_value == "get_general": facts = await cog.memory_manager.get_general_facts( query=query, limit=10 ) # Get newest/filtered if facts: facts_str = "\n- ".join(facts) # Conditionally construct the title to avoid nested f-string issues if query: title = f'**General Facts matching "{query}":**' else: title = "**General Facts:**" await interaction.followup.send( f"{title}\n- {facts_str}", ephemeral=True ) else: # Conditionally construct the message for the same reason if query: message = f'No general facts found matching "{query}".' else: message = "No general facts found." await interaction.followup.send(message, ephemeral=True) else: await interaction.followup.send("Invalid action specified.", ephemeral=True) command_functions.append(gurtmemory) # --- Gurt Stats Command --- @cog.bot.tree.command( name="gurtstats", description="Display Gurt's internal statistics. (Owner only)" ) async def gurtstats(interaction: discord.Interaction): """Handles the /gurtstats command.""" await interaction.response.defer( ephemeral=True ) # Defer as stats collection might take time try: stats_data = await cog.get_gurt_stats() embeds = format_stats_embeds(stats_data) await interaction.followup.send(embeds=embeds, ephemeral=True) except Exception as e: print(f"Error in /gurtstats command: {e}") import traceback traceback.print_exc() await interaction.followup.send( "An error occurred while fetching Gurt's stats.", ephemeral=True ) command_functions.append(gurtstats) # --- Sync Gurt Commands (Owner Only) --- @cog.bot.tree.command( name="gurtsync", description="Sync Gurt commands with Discord (Owner only)" ) async def gurtsync(interaction: discord.Interaction): """Handles the /gurtsync command to force sync commands.""" # Check if user is the bot owner if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can sync commands.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) try: # Sync commands synced = await cog.bot.tree.sync() # Get list of commands after sync commands_after = [] for cmd_obj in cog.bot.tree.get_commands(): # Iterate over Command objects if cmd_obj.name.startswith("gurt"): commands_after.append(cmd_obj.name) await interaction.followup.send( f"✅ Successfully synced {len(synced)} commands!\nGurt commands: {', '.join(commands_after)}", ephemeral=True, ) except Exception as e: print(f"Error in /gurtsync command: {e}") import traceback traceback.print_exc() await interaction.followup.send( f"❌ Error syncing commands: {str(e)}", ephemeral=True ) command_functions.append(gurtsync) # --- Gurt Forget Command --- @cog.bot.tree.command( name="gurtforget", description="Make Gurt forget a specific fact." ) @app_commands.describe( scope="Choose the scope: user (for facts about a specific user) or general.", fact="The exact fact text Gurt should forget.", user="The user to forget a fact about (only if scope is 'user').", ) @app_commands.choices( scope=[ app_commands.Choice(name="User Fact", value="user"), app_commands.Choice(name="General Fact", value="general"), ] ) async def gurtforget( interaction: discord.Interaction, scope: app_commands.Choice[str], fact: str, user: Optional[discord.User] = None, ): """Handles the /gurtforget command.""" await interaction.response.defer(ephemeral=True) scope_value = scope.value target_user_id = str(user.id) if user else None # Permissions Check: Allow users to forget facts about themselves, owner can forget anything. can_forget = False if scope_value == "user": if target_user_id == str( interaction.user.id ): # User forgetting their own fact can_forget = True elif ( interaction.user.id == cog.bot.owner_id ): # Owner forgetting any user fact can_forget = True elif not target_user_id: await interaction.followup.send( "❌ Please specify a user when forgetting a user fact.", ephemeral=True, ) return elif scope_value == "general": if ( interaction.user.id == cog.bot.owner_id ): # Only owner can forget general facts can_forget = True if not can_forget: await interaction.followup.send( "⛔ You don't have permission to forget this fact.", ephemeral=True ) return if not fact: await interaction.followup.send( "❌ Please provide the exact fact text to forget.", ephemeral=True ) return result = None if scope_value == "user": if not target_user_id: # Should be caught above, but double-check await interaction.followup.send( "❌ User is required for scope 'user'.", ephemeral=True ) return result = await cog.memory_manager.delete_user_fact(target_user_id, fact) if result.get("status") == "deleted": await interaction.followup.send( f"✅ Okay, I've forgotten the fact '{fact}' about {user.display_name}.", ephemeral=True, ) elif result.get("status") == "not_found": await interaction.followup.send( f"❓ I couldn't find that exact fact ('{fact}') stored for {user.display_name}.", ephemeral=True, ) else: await interaction.followup.send( f"⚠️ Error forgetting user fact: {result.get('error', 'Unknown error')}", ephemeral=True, ) elif scope_value == "general": result = await cog.memory_manager.delete_general_fact(fact) if result.get("status") == "deleted": await interaction.followup.send( f"✅ Okay, I've forgotten the general fact: '{fact}'.", ephemeral=True, ) elif result.get("status") == "not_found": await interaction.followup.send( f"❓ I couldn't find that exact general fact: '{fact}'.", ephemeral=True, ) else: await interaction.followup.send( f"⚠️ Error forgetting general fact: {result.get('error', 'Unknown error')}", ephemeral=True, ) command_functions.append(gurtforget) # --- Gurt Force Autonomous Action Command (Owner Only) --- @cog.bot.tree.command( name="gurtforceauto", description="Force Gurt to execute an autonomous action immediately. (Owner only)", ) async def gurtforceauto(interaction: discord.Interaction): """Handles the /gurtforceauto command.""" if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can force autonomous actions.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) try: result = await cog.force_autonomous_action() summary = ( f"**Autonomous Action Forced:**\n" f"**Tool:** {result.get('tool')}\n" f"**Args:** `{result.get('args')}`\n" f"**Reasoning:** {result.get('reasoning')}\n" f"**Result:** {result.get('result')}" ) await interaction.followup.send(summary, ephemeral=True) except Exception as e: import traceback traceback.print_exc() await interaction.followup.send( f"❌ Error forcing autonomous action: {e}", ephemeral=True ) command_functions.append(gurtforceauto) # Add gurtforceauto to the list # --- Gurt Clear Action History Command (Owner Only) --- @cog.bot.tree.command( name="gurtclearhistory", description="Clear Gurt's internal autonomous action history. (Owner only)", ) async def gurtclearhistory(interaction: discord.Interaction): """Handles the /gurtclearhistory command.""" if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can clear the action history.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) try: result = await cog.memory_manager.clear_internal_action_logs() if "error" in result: await interaction.followup.send( f"⚠️ Error clearing action history: {result['error']}", ephemeral=True, ) else: await interaction.followup.send( "✅ Gurt's autonomous action history has been cleared.", ephemeral=True, ) except Exception as e: import traceback traceback.print_exc() await interaction.followup.send( f"❌ An unexpected error occurred while clearing history: {e}", ephemeral=True, ) command_functions.append(gurtclearhistory) # Add the new command # --- Gurt Goal Command Group --- gurtgoal_group = app_commands.Group( name="gurtgoal", description="Manage Gurt's long-term goals (Owner only)" ) @gurtgoal_group.command(name="add", description="Add a new goal for Gurt.") @app_commands.describe( description="The description of the goal.", priority="Priority (1=highest, 10=lowest, default=5).", details_json="Optional JSON string for goal details (e.g., sub-tasks).", ) async def gurtgoal_add( interaction: discord.Interaction, description: str, priority: Optional[int] = 5, details_json: Optional[str] = None, ): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can add goals.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) details = None if details_json: try: details = json.loads(details_json) except json.JSONDecodeError: await interaction.followup.send( "❌ Invalid JSON format for details.", ephemeral=True ) return # Capture context from interaction guild_id = str(interaction.guild_id) if interaction.guild_id else None channel_id = str(interaction.channel_id) if interaction.channel_id else None user_id = str(interaction.user.id) if interaction.user else None result = await cog.memory_manager.add_goal( description, priority, details, guild_id=guild_id, channel_id=channel_id, user_id=user_id, ) if result.get("status") == "added": await interaction.followup.send( f"✅ Goal added (ID: {result.get('goal_id')}): '{description}'", ephemeral=True, ) elif result.get("status") == "duplicate": await interaction.followup.send( f"⚠️ Goal '{description}' already exists (ID: {result.get('goal_id')}).", ephemeral=True, ) else: await interaction.followup.send( f"⚠️ Error adding goal: {result.get('error', 'Unknown error')}", ephemeral=True, ) @gurtgoal_group.command(name="list", description="List Gurt's current goals.") @app_commands.describe( status="Filter goals by status (e.g., pending, active).", limit="Maximum goals to show (default 10).", ) @app_commands.choices( status=[ app_commands.Choice(name="Pending", value="pending"), app_commands.Choice(name="Active", value="active"), app_commands.Choice(name="Completed", value="completed"), app_commands.Choice(name="Failed", value="failed"), ] ) async def gurtgoal_list( interaction: discord.Interaction, status: Optional[app_commands.Choice[str]] = None, limit: Optional[int] = 10, ): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can list goals.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) status_value = status.value if status else None limit_value = max(1, min(limit or 10, 25)) # Clamp limit goals = await cog.memory_manager.get_goals( status=status_value, limit=limit_value ) if not goals: await interaction.followup.send( f"No goals found matching the criteria (Status: {status_value or 'any'}).", ephemeral=True, ) return embed = create_gurt_embed( f"Gurt Goals (Status: {status_value or 'All'})", color=discord.Color.purple(), ) for goal in goals: details_str = ( f"\n Details: `{json.dumps(goal.get('details'))}`" if goal.get("details") else "" ) created_ts = int(goal.get("created_timestamp", 0)) updated_ts = int(goal.get("last_updated", 0)) embed.add_field( name=f"ID: {goal.get('goal_id')} | P: {goal.get('priority', '?')} | Status: {goal.get('status', '?')}", value=f"> {goal.get('description', 'N/A')}{details_str}\n" f"> Created: | Updated: ", inline=False, ) await interaction.followup.send(embed=embed, ephemeral=True) @gurtgoal_group.command( name="update", description="Update a goal's status, priority, or details." ) @app_commands.describe( goal_id="The ID of the goal to update.", status="New status for the goal.", priority="New priority (1=highest, 10=lowest).", details_json="Optional: New JSON string for goal details (replaces existing).", ) @app_commands.choices( status=[ app_commands.Choice(name="Pending", value="pending"), app_commands.Choice(name="Active", value="active"), app_commands.Choice(name="Completed", value="completed"), app_commands.Choice(name="Failed", value="failed"), ] ) async def gurtgoal_update( interaction: discord.Interaction, goal_id: int, status: Optional[app_commands.Choice[str]] = None, priority: Optional[int] = None, details_json: Optional[str] = None, ): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can update goals.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) status_value = status.value if status else None details = None if details_json: try: details = json.loads(details_json) except json.JSONDecodeError: await interaction.followup.send( "❌ Invalid JSON format for details.", ephemeral=True ) return if not any([status_value, priority is not None, details is not None]): await interaction.followup.send( "❌ You must provide at least one field to update (status, priority, or details_json).", ephemeral=True, ) return result = await cog.memory_manager.update_goal( goal_id, status=status_value, priority=priority, details=details ) if result.get("status") == "updated": await interaction.followup.send( f"✅ Goal ID {goal_id} updated.", ephemeral=True ) elif result.get("status") == "not_found": await interaction.followup.send( f"❓ Goal ID {goal_id} not found.", ephemeral=True ) else: await interaction.followup.send( f"⚠️ Error updating goal: {result.get('error', 'Unknown error')}", ephemeral=True, ) @gurtgoal_group.command(name="delete", description="Delete a goal.") @app_commands.describe(goal_id="The ID of the goal to delete.") async def gurtgoal_delete(interaction: discord.Interaction, goal_id: int): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can delete goals.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) result = await cog.memory_manager.delete_goal(goal_id) if result.get("status") == "deleted": await interaction.followup.send( f"✅ Goal ID {goal_id} deleted.", ephemeral=True ) elif result.get("status") == "not_found": await interaction.followup.send( f"❓ Goal ID {goal_id} not found.", ephemeral=True ) else: await interaction.followup.send( f"⚠️ Error deleting goal: {result.get('error', 'Unknown error')}", ephemeral=True, ) # Add the command group to the bot's tree cog.bot.tree.add_command(gurtgoal_group) # Add group command functions to the list for tracking (optional, but good practice) command_functions.extend( [gurtgoal_add, gurtgoal_list, gurtgoal_update, gurtgoal_delete] ) # --- Gurt Ignore Command Group (Owner Only) --- gurtignore_group = app_commands.Group( name="gurtignore", description="Manage channels Gurt should ignore. (Owner only)", ) @gurtignore_group.command( name="add", description="Add a channel to Gurt's ignore list." ) @app_commands.describe(channel="The channel or thread to ignore.") async def gurtignore_add( interaction: discord.Interaction, channel: discord.abc.GuildChannel ): # Use GuildChannel to accept TextChannel, Thread, etc. if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can modify the ignore list.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) current_ignored_ids = set(cog.IGNORED_CHANNEL_IDS) # Use cog's direct reference if channel.id in current_ignored_ids: await interaction.followup.send( f"⚠️ Channel {channel.mention} is already in the ignore list.", ephemeral=True, ) return current_ignored_ids.add(channel.id) if cog.update_ignored_channels_file( list(current_ignored_ids) ): # Use cog's direct reference, ensure it's a list await interaction.followup.send( f"✅ Channel {channel.mention} added to the ignore list.", ephemeral=True, ) else: await interaction.followup.send( f"❌ Failed to update the ignore list file. Check bot logs.", ephemeral=True, ) @gurtignore_group.command( name="remove", description="Remove a channel from Gurt's ignore list." ) @app_commands.describe(channel="The channel or thread to stop ignoring.") async def gurtignore_remove( interaction: discord.Interaction, channel: discord.abc.GuildChannel ): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can modify the ignore list.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) current_ignored_ids = set(cog.IGNORED_CHANNEL_IDS) # Use cog's direct reference if channel.id not in current_ignored_ids: await interaction.followup.send( f"⚠️ Channel {channel.mention} is not in the ignore list.", ephemeral=True, ) return current_ignored_ids.remove(channel.id) if cog.update_ignored_channels_file( list(current_ignored_ids) ): # Use cog's direct reference, ensure it's a list await interaction.followup.send( f"✅ Channel {channel.mention} removed from the ignore list.", ephemeral=True, ) else: await interaction.followup.send( f"❌ Failed to update the ignore list file. Check bot logs.", ephemeral=True, ) @gurtignore_group.command( name="list", description="List all channels Gurt is currently ignoring." ) async def gurtignore_list(interaction: discord.Interaction): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can view the ignore list.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) current_ignored_ids = cog.IGNORED_CHANNEL_IDS # Use cog's direct reference if not current_ignored_ids: await interaction.followup.send( "Gurt is not currently ignoring any channels.", ephemeral=True ) return embed = create_gurt_embed("Ignored Channels", color=discord.Color.orange()) description_lines = [] for channel_id in current_ignored_ids: ch = cog.bot.get_channel(channel_id) if ch: description_lines.append(f"- {ch.mention} (`{channel_id}`)") else: description_lines.append(f"- Unknown Channel (`{channel_id}`)") embed.description = "\n".join(description_lines) await interaction.followup.send(embed=embed, ephemeral=True) cog.bot.tree.add_command(gurtignore_group) command_functions.extend([gurtignore_add, gurtignore_remove, gurtignore_list]) # --- Gurt Emoji Command Group (Owner Only) --- gurtemoji_group = app_commands.Group( name="gurtemoji", description="Manage Gurt's custom emoji knowledge. (Owner only)", ) @gurtemoji_group.command( name="add", description="Add a custom emoji to Gurt's knowledge." ) @app_commands.describe( name="The name of the emoji (e.g., :custom_emoji:).", url="The URL of the emoji image.", ) async def gurtemoji_add(interaction: discord.Interaction, name: str, url: str): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can manage custom emojis.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) # Assuming cog.emoji_manager exists and has an add_emoji method if hasattr(cog, "emoji_manager") and hasattr(cog.emoji_manager, "add_emoji"): success = await cog.emoji_manager.add_emoji(name, url) if success: await interaction.followup.send( f"✅ Emoji '{name}' added.", ephemeral=True ) else: await interaction.followup.send( f"❌ Failed to add emoji '{name}'. It might already exist or there was an error.", ephemeral=True, ) else: await interaction.followup.send( "Emoji manager not available.", ephemeral=True ) @gurtemoji_group.command( name="remove", description="Remove a custom emoji from Gurt's knowledge." ) @app_commands.describe( name="The name of the emoji to remove (e.g., :custom_emoji:)." ) async def gurtemoji_remove(interaction: discord.Interaction, name: str): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can manage custom emojis.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) if hasattr(cog, "emoji_manager") and hasattr(cog.emoji_manager, "remove_emoji"): success = await cog.emoji_manager.remove_emoji(name) if success: await interaction.followup.send( f"✅ Emoji '{name}' removed.", ephemeral=True ) else: await interaction.followup.send( f"❌ Failed to remove emoji '{name}'. It might not exist or there was an error.", ephemeral=True, ) else: await interaction.followup.send( "Emoji manager not available.", ephemeral=True ) @gurtemoji_group.command( name="list", description="List all custom emojis Gurt knows." ) async def gurtemoji_list(interaction: discord.Interaction): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can manage custom emojis.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) if hasattr(cog, "emoji_manager") and hasattr(cog.emoji_manager, "list_emojis"): emojis = await cog.emoji_manager.list_emojis() if emojis: embed = create_gurt_embed( "Known Custom Emojis", color=discord.Color.gold() ) description = "\n".join( [f"- {name}: {url}" for name, url in emojis.items()] ) embed.description = description await interaction.followup.send(embed=embed, ephemeral=True) else: await interaction.followup.send( "Gurt doesn't know any custom emojis yet.", ephemeral=True ) else: await interaction.followup.send( "Emoji manager not available.", ephemeral=True ) cog.bot.tree.add_command(gurtemoji_group) command_functions.extend([gurtemoji_add, gurtemoji_remove, gurtemoji_list]) # --- Gurt Sticker Command Group (Owner Only) --- gurtsticker_group = app_commands.Group( name="gurtsticker", description="Manage Gurt's custom sticker knowledge. (Owner only)", ) @gurtsticker_group.command( name="add", description="Add a custom sticker to Gurt's knowledge." ) @app_commands.describe( name="The name of the sticker.", url="The URL of the sticker image." ) async def gurtsticker_add(interaction: discord.Interaction, name: str, url: str): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can manage custom stickers.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) if hasattr(cog, "emoji_manager") and hasattr(cog.emoji_manager, "add_sticker"): success = await cog.emoji_manager.add_sticker(name, url) if success: await interaction.followup.send( f"✅ Sticker '{name}' added.", ephemeral=True ) else: await interaction.followup.send( f"❌ Failed to add sticker '{name}'. It might already exist or there was an error.", ephemeral=True, ) else: await interaction.followup.send( "Sticker manager not available.", ephemeral=True ) @gurtsticker_group.command( name="remove", description="Remove a custom sticker from Gurt's knowledge." ) @app_commands.describe(name="The name of the sticker to remove.") async def gurtsticker_remove(interaction: discord.Interaction, name: str): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can manage custom stickers.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) if hasattr(cog, "emoji_manager") and hasattr( cog.emoji_manager, "remove_sticker" ): success = await cog.emoji_manager.remove_sticker(name) if success: await interaction.followup.send( f"✅ Sticker '{name}' removed.", ephemeral=True ) else: await interaction.followup.send( f"❌ Failed to remove sticker '{name}'. It might not exist or there was an error.", ephemeral=True, ) else: await interaction.followup.send( "Sticker manager not available.", ephemeral=True ) @gurtsticker_group.command( name="list", description="List all custom stickers Gurt knows." ) async def gurtsticker_list(interaction: discord.Interaction): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can manage custom stickers.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) if hasattr(cog, "emoji_manager") and hasattr( cog.emoji_manager, "list_stickers" ): stickers = await cog.emoji_manager.list_stickers() if stickers: embed = create_gurt_embed( "Known Custom Stickers", color=discord.Color.dark_gold() ) description = "\n".join( [f"- {name}: {url}" for name, url in stickers.items()] ) embed.description = description await interaction.followup.send(embed=embed, ephemeral=True) else: await interaction.followup.send( "Gurt doesn't know any custom stickers yet.", ephemeral=True ) else: await interaction.followup.send( "Sticker manager not available.", ephemeral=True ) cog.bot.tree.add_command(gurtsticker_group) command_functions.extend([gurtsticker_add, gurtsticker_remove, gurtsticker_list]) # --- Gurt Tenor API Key Command (Owner Only) --- @cog.bot.tree.command( name="gurttenorapikey", description="Set the Tenor API key for Gurt. (Owner only)", ) @app_commands.describe(api_key="The Tenor API key.") async def gurttenorapikey(interaction: discord.Interaction, api_key: str): if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can set the Tenor API key.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) # Assuming cog.config_manager or similar exists for updating config if hasattr(cog, "config_manager") and hasattr( cog.config_manager, "set_tenor_api_key" ): await cog.config_manager.set_tenor_api_key(api_key) # Update the cog's runtime TENOR_API_KEY if it's stored there directly or re-init relevant clients if hasattr(cog, "TENOR_API_KEY"): cog.TENOR_API_KEY = api_key # If cog holds it directly # Potentially re-initialize TavilyClient or other clients if they use Tenor key indirectly await interaction.followup.send( "✅ Tenor API key set. You may need to reload Gurt for changes to fully apply.", ephemeral=True, ) else: # Fallback: try to update config.py directly (less ideal) # This requires careful handling of file I/O and is generally not recommended for runtime changes. # For now, we'll assume a config_manager or direct cog attribute. # If direct modification of config.py is needed, it's a more complex operation. # We can also just store it in the cog instance and save it to a .env or db. # For simplicity, let's assume it's handled by a config manager or by updating cog.TENOR_API_KEY # and then saving that to a persistent store (e.g., in memory_manager or a dedicated config store) try: # This is a placeholder for a more robust config update mechanism # In a real scenario, you'd write this to a .env file or a database # For now, we'll just update the cog's attribute if it exists if hasattr(cog, "TENOR_API_KEY"): cog.TENOR_API_KEY = api_key # Here you would also save it persistently # e.g., await cog.memory_manager.save_setting("TENOR_API_KEY", api_key) await interaction.followup.send( "✅ Tenor API key updated in runtime. Save it persistently for it to survive restarts.", ephemeral=True, ) else: await interaction.followup.send( "⚠️ Tenor API key runtime attribute not found. Key not set.", ephemeral=True, ) except Exception as e: await interaction.followup.send( f"❌ Error setting Tenor API key: {e}", ephemeral=True ) command_functions.append(gurttenorapikey) # --- Gurt Reset Personality Command (Owner Only) --- @cog.bot.tree.command( name="gurtresetpersonality", description="Reset Gurt's personality and interests to baseline. (Owner only)", ) async def gurtresetpersonality(interaction: discord.Interaction): """Handles the /gurtresetpersonality command.""" if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can reset Gurt's personality.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) try: # Ensure the cog has access to baseline values, e.g., cog.BASELINE_PERSONALITY # These would typically be loaded from gurt.config into the GurtCog instance if not hasattr(cog, "BASELINE_PERSONALITY") or not hasattr( cog, "BASELINE_INTERESTS" ): await interaction.followup.send( "⚠️ Baseline personality or interests not found in cog configuration. Reset aborted.", ephemeral=True, ) return personality_result = await cog.memory_manager.reset_personality_to_baseline( cog.BASELINE_PERSONALITY ) interests_result = await cog.memory_manager.reset_interests_to_baseline( cog.BASELINE_INTERESTS ) messages = [] if personality_result.get("status") == "success": messages.append("✅ Personality traits reset to baseline.") else: messages.append( f"⚠️ Error resetting personality: {personality_result.get('error', 'Unknown error')}" ) if interests_result.get("status") == "success": messages.append("✅ Interests reset to baseline.") else: messages.append( f"⚠️ Error resetting interests: {interests_result.get('error', 'Unknown error')}" ) await interaction.followup.send("\n".join(messages), ephemeral=True) except Exception as e: import traceback traceback.print_exc() await interaction.followup.send( f"❌ An unexpected error occurred while resetting personality: {e}", ephemeral=True, ) command_functions.append(gurtresetpersonality) # --- Gurt Leave Others Command (Owner Only) --- @cog.bot.tree.command( name="gurtleaveothers", description="Leave all guilds except the designated one. (Owner only)", ) async def gurtleaveothers(interaction: discord.Interaction): """Leave all guilds except the target guild.""" if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can remove Gurt from guilds.", ephemeral=True, ) return await interaction.response.defer(ephemeral=True) target_id = 1360706427861209259 for guild in cog.bot.guilds: if guild.id != target_id: try: await guild.owner.send( "This bot was manually removed by the bot owner. If you have questions, contact the bot owner directly." ) except Exception: pass await guild.leave() await interaction.followup.send("✅ Done.", ephemeral=True) command_functions.append(gurtleaveothers) # --- Gurt Model Command (Owner Only) --- @cog.bot.tree.command( name="gurtmodel", description="Change Gurt's active AI model dynamically. (Owner only)", ) @app_commands.describe(model="The AI model to switch to.") @app_commands.choices( model=[ app_commands.Choice(name=friendly_name, value=model_id) for model_id, friendly_name in AVAILABLE_AI_MODELS.items() ] ) async def gurtmodel( interaction: discord.Interaction, model: app_commands.Choice[str] ): """Handles the /gurtmodel command.""" if interaction.user.id != cog.bot.owner_id: await interaction.response.send_message( "⛔ Only the bot owner can change Gurt's AI model.", ephemeral=True ) return await interaction.response.defer(ephemeral=False) try: new_model_id = model.value new_model_friendly_name = model.name # Update the cog's default model cog.default_model = new_model_id # Optionally, update the config file if you want this change to persist across restarts # This would require a function in config.py to update DEFAULT_MODEL in the .env or a separate config file # For now, we'll just update the runtime attribute. # If persistence is desired, you'd add something like: # await cog.config_manager.set_default_model(new_model_id) # Assuming a config_manager exists await interaction.followup.send( f"✅ Gurt's AI model has been changed to: **{new_model_friendly_name}** (`{new_model_id}`).", ephemeral=False, ) except Exception as e: print(f"Error in /gurtmodel command: {e}") import traceback traceback.print_exc() await interaction.followup.send( "❌ An error occurred while changing Gurt's AI model.", ephemeral=True ) command_functions.append(gurtmodel) # --- Gurt Get Model Command --- @cog.bot.tree.command( name="gurtgetmodel", description="Display Gurt's currently active AI model." ) async def gurtgetmodel(interaction: discord.Interaction): """Handles the /gurtgetmodel command.""" await interaction.response.defer(ephemeral=False) try: current_model_id = cog.default_model # Try to get the friendly name from AVAILABLE_AI_MODELS friendly_name = AVAILABLE_AI_MODELS.get( current_model_id, current_model_id ) # Fallback to ID if not found await interaction.followup.send( f"Gurt is currently using AI model: **{friendly_name}** (`{current_model_id}`).", ephemeral=False, ) except Exception as e: print(f"Error in /gurtgetmodel command: {e}") import traceback traceback.print_exc() await interaction.followup.send( "❌ An error occurred while fetching Gurt's current AI model.", ephemeral=True, ) command_functions.append(gurtgetmodel) # Get command names safely - Command objects don't have __name__ attribute command_names = [] for func in command_functions: # For app commands, use the name attribute directly if hasattr(func, "name"): command_names.append(func.name) # For regular functions, use __name__ elif hasattr(func, "__name__"): command_names.append(func.__name__) else: command_names.append(str(func)) print(f"Gurt commands setup in cog: {command_names}") # Return the command functions for proper registration return command_functions