diff --git a/gurt/api.py b/gurt/api.py index 2cfc395..e3fdac9 100644 --- a/gurt/api.py +++ b/gurt/api.py @@ -154,7 +154,7 @@ from .config import ( PROJECT_ID, LOCATION, DEFAULT_MODEL, FALLBACK_MODEL, CUSTOM_TUNED_MODEL_ENDPOINT, # Import the new endpoint API_TIMEOUT, API_RETRY_ATTEMPTS, API_RETRY_DELAY, TOOLS, RESPONSE_SCHEMA, PROACTIVE_PLAN_SCHEMA, # Import the new schema - TAVILY_API_KEY, PISTON_API_URL, PISTON_API_KEY, BASELINE_PERSONALITY # Import other needed configs + TAVILY_API_KEY, PISTON_API_URL, PISTON_API_KEY, BASELINE_PERSONALITY, TENOR_API_KEY # Import other needed configs ) from .prompt import build_dynamic_system_prompt from .context import gather_conversation_context, get_memory_context # Renamed functions @@ -1479,6 +1479,63 @@ async def get_ai_response(cog: 'GurtCog', message: discord.Message, model_name: fallback_response = {"should_respond": True, "content": "...", "react_with_emoji": "❓"} + # --- Handle Tenor GIF Request --- + if final_parsed_data and final_parsed_data.get("request_tenor_gif_query"): + gif_query = final_parsed_data["request_tenor_gif_query"] + print(f"AI requested Tenor GIF with query: {gif_query}") + gif_urls = await search_tenor_gifs(cog, gif_query, limit=1) + if gif_urls: + gif_url_to_send = gif_urls[0] + current_content = final_parsed_data.get("content", "") + # Append GIF URL on a new line + final_parsed_data["content"] = f"{current_content}\n{gif_url_to_send}".strip() + print(f"Appended Tenor GIF URL to content: {gif_url_to_send}") + else: + print(f"No Tenor GIFs found for query: {gif_query}") + # Clear the query so it's not processed again or misinterpreted + final_parsed_data["request_tenor_gif_query"] = None + + # --- Handle Custom Emoji/Sticker Replacement in Content --- + if final_parsed_data and final_parsed_data.get("content"): + content_to_process = final_parsed_data["content"] + # Find all potential custom emoji/sticker names like :name: + potential_custom_items = re.findall(r':([\w\d_]+):', content_to_process) + modified_content = content_to_process + + for item_name_key in potential_custom_items: + full_item_name = f":{item_name_key}:" + # Check if it's a known custom emoji + emoji_url = await cog.emoji_manager.get_emoji(full_item_name) + if emoji_url: + # For now, we'll just ensure the name is there. + # Actual replacement to Discord's format would require knowing the ID, + # which we don't store directly with this simple name/URL mapping. + # The AI is instructed to use the name, and the listener learns the ID. + # If the AI uses a name it learned, it should be fine. + # If we want Gurt to *send* an emoji it learned by URL, that's different. + # For now, this confirms the AI *can* reference it. + print(f"Found known custom emoji '{full_item_name}' in response content.") + # No replacement needed if AI uses the name correctly. + # If we wanted to send the image URL instead: + # modified_content = modified_content.replace(full_item_name, f"{full_item_name} ({emoji_url})") + + # Check if it's a known custom sticker + sticker_url = await cog.emoji_manager.get_sticker(full_item_name) + if sticker_url: + print(f"Found known custom sticker '{full_item_name}' in response content.") + # Stickers are sent as separate attachments by users. + # If Gurt is to send a sticker, it would typically send the URL. + # We can append the URL if the AI mentions a known sticker. + # This assumes the AI mentions it like text, and we append the URL. + if sticker_url not in modified_content: # Avoid duplicate appends + modified_content = f"{modified_content}\n{sticker_url}".strip() + print(f"Appended sticker URL for '{full_item_name}': {sticker_url}") + + if modified_content != final_parsed_data["content"]: + final_parsed_data["content"] = modified_content + print("Content modified with custom emoji/sticker information.") + + # Return dictionary structure remains the same, but initial_response is removed return { "final_response": final_parsed_data, # Parsed final data (or None) @@ -1671,8 +1728,47 @@ async def get_proactive_ai_response(cog: 'GurtCog', message: discord.Message, tr final_parsed_data.setdefault("should_respond", False) final_parsed_data.setdefault("content", None) final_parsed_data.setdefault("react_with_emoji", None) + final_parsed_data.setdefault("request_tenor_gif_query", None) # Ensure this key exists if error_message and "error" not in final_parsed_data: final_parsed_data["error"] = error_message + + # --- Handle Tenor GIF Request for Proactive --- + if final_parsed_data and final_parsed_data.get("request_tenor_gif_query"): + gif_query = final_parsed_data["request_tenor_gif_query"] + print(f"Proactive AI requested Tenor GIF with query: {gif_query}") + gif_urls = await search_tenor_gifs(cog, gif_query, limit=1) + if gif_urls: + gif_url_to_send = gif_urls[0] + current_content = final_parsed_data.get("content", "") + final_parsed_data["content"] = f"{current_content}\n{gif_url_to_send}".strip() + print(f"Appended Tenor GIF URL to proactive content: {gif_url_to_send}") + else: + print(f"No Tenor GIFs found for proactive query: {gif_query}") + final_parsed_data["request_tenor_gif_query"] = None # Clear query + + # --- Handle Custom Emoji/Sticker Replacement in Proactive Content --- + if final_parsed_data and final_parsed_data.get("content"): + content_to_process = final_parsed_data["content"] + potential_custom_items = re.findall(r':([\w\d_]+):', content_to_process) + modified_content = content_to_process + + for item_name_key in potential_custom_items: + full_item_name = f":{item_name_key}:" + emoji_url = await cog.emoji_manager.get_emoji(full_item_name) + if emoji_url: + print(f"Found known custom emoji '{full_item_name}' in proactive response content.") + # No replacement, AI uses name. + + sticker_url = await cog.emoji_manager.get_sticker(full_item_name) + if sticker_url: + print(f"Found known custom sticker '{full_item_name}' in proactive response content.") + if sticker_url not in modified_content: + modified_content = f"{modified_content}\n{sticker_url}".strip() + print(f"Appended sticker URL for '{full_item_name}' to proactive content: {sticker_url}") + + if modified_content != final_parsed_data["content"]: + final_parsed_data["content"] = modified_content + print("Proactive content modified with custom emoji/sticker information.") return final_parsed_data @@ -1886,3 +1982,35 @@ async def get_internal_ai_json_response( if __name__ == "__main__": print(_preprocess_schema_for_vertex(RESPONSE_SCHEMA['schema'])) + + +async def search_tenor_gifs(cog: 'GurtCog', query: str, limit: int = 5) -> List[str]: + """Searches Tenor for GIFs and returns a list of URLs.""" + if not cog.TENOR_API_KEY: + print("Tenor API key not configured.") + return [] + + url = "https://tenor.googleapis.com/v2/search" + params = { + "q": query, + "key": cog.TENOR_API_KEY, + "limit": limit, + "media_filter": "gif", # Ensure we get GIFs + "contentfilter": "medium" # Adjust content filter as needed (off, low, medium, high) + } + + try: + if not cog.session: + cog.session = aiohttp.ClientSession() # Ensure session exists + + async with cog.session.get(url, params=params, timeout=10) as response: + if response.status == 200: + data = await response.json() + gif_urls = [result["media_formats"]["gif"]["url"] for result in data.get("results", []) if "media_formats" in result and "gif" in result["media_formats"] and "url" in result["media_formats"]["gif"]] + return gif_urls + else: + print(f"Error searching Tenor: {response.status} - {await response.text()}") + return [] + except Exception as e: + print(f"Exception during Tenor API call: {e}") + return [] diff --git a/gurt/cog.py b/gurt/cog.py index 5a5deb7..15836d7 100644 --- a/gurt/cog.py +++ b/gurt/cog.py @@ -16,7 +16,7 @@ from tavily import TavilyClient # Needed for tavily_client init # --- Relative Imports from Gurt Package --- from .config import ( - PROJECT_ID, LOCATION, TAVILY_API_KEY, DEFAULT_MODEL, FALLBACK_MODEL, # Use GCP config + PROJECT_ID, LOCATION, TAVILY_API_KEY, TENOR_API_KEY, DEFAULT_MODEL, FALLBACK_MODEL, # Use GCP config DB_PATH, CHROMA_PATH, SEMANTIC_MODEL_NAME, MAX_USER_FACTS, MAX_GENERAL_FACTS, MOOD_OPTIONS, BASELINE_PERSONALITY, BASELINE_INTERESTS, MOOD_CHANGE_INTERVAL_MIN, MOOD_CHANGE_INTERVAL_MAX, CHANNEL_TOPIC_CACHE_TTL, CONTEXT_WINDOW_SIZE, @@ -31,6 +31,7 @@ from .config import ( ) # Import functions/classes from other modules from .memory import MemoryManager # Import from local memory.py +from .emojis import EmojiManager # Import EmojiManager from .background import background_processing_task from .commands import setup_commands # Import the setup helper from .listeners import on_ready_listener, on_message_listener, on_reaction_add_listener, on_reaction_remove_listener # Import listener functions @@ -48,6 +49,7 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name self.bot = bot # GCP Project/Location are used by vertexai.init() in api.py self.tavily_api_key = TAVILY_API_KEY # Use imported config + self.TENOR_API_KEY = TENOR_API_KEY # Store Tenor API Key self.session: Optional[aiohttp.ClientSession] = None # Keep for other potential HTTP requests (e.g., Piston) self.tavily_client = TavilyClient(api_key=self.tavily_api_key) if self.tavily_api_key else None self.default_model = DEFAULT_MODEL # Use imported config @@ -67,6 +69,7 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name chroma_path=CHROMA_PATH, semantic_model_name=SEMANTIC_MODEL_NAME ) + self.emoji_manager = EmojiManager() # Initialize EmojiManager # --- State Variables --- # Keep state directly within the cog instance for now diff --git a/gurt/commands.py b/gurt/commands.py index 9553d96..0d70653 100644 --- a/gurt/commands.py +++ b/gurt/commands.py @@ -14,7 +14,8 @@ from typing import TYPE_CHECKING, Optional, Dict, Any, List, Tuple # Add more ty if TYPE_CHECKING: from .cog import GurtCog # For type hinting - from .config import MOOD_OPTIONS, IGNORED_CHANNEL_IDS, update_ignored_channels_file # Import for choices and ignored channels + 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: @@ -113,6 +114,7 @@ def format_stats_embeds(stats: Dict[str, Any]) -> List[discord.Embed]: 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) @@ -259,9 +261,10 @@ def setup_commands(cog: 'GurtCog'): # Get list of commands after sync commands_after = [] - for cmd in cog.bot.tree.get_commands(): - if cmd.name.startswith("gurt"): - commands_after.append(cmd.name) + 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: @@ -534,7 +537,7 @@ def setup_commands(cog: 'GurtCog'): return current_ignored_ids.add(channel.id) - if cog.update_ignored_channels_file(current_ignored_ids): # Use cog's direct reference + 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) @@ -553,7 +556,7 @@ def setup_commands(cog: 'GurtCog'): return current_ignored_ids.remove(channel.id) - if cog.update_ignored_channels_file(current_ignored_ids): # Use cog's direct reference + 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) @@ -585,6 +588,162 @@ def setup_commands(cog: 'GurtCog'): 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) + + # Get command names safely - Command objects don't have __name__ attribute command_names = [] for func in command_functions: diff --git a/gurt/config.py b/gurt/config.py index 4805e78..b089cbf 100644 --- a/gurt/config.py +++ b/gurt/config.py @@ -12,6 +12,7 @@ load_dotenv() PROJECT_ID = os.getenv("GCP_PROJECT_ID", "1079377687568") LOCATION = os.getenv("GCP_LOCATION", "us-central1") TAVILY_API_KEY = os.getenv("TAVILY_API_KEY", "") +TENOR_API_KEY = os.getenv("TENOR_API_KEY", "") # Added Tenor API Key PISTON_API_URL = os.getenv("PISTON_API_URL") # For run_python_code tool PISTON_API_KEY = os.getenv("PISTON_API_KEY") # Optional key for Piston @@ -245,6 +246,10 @@ RESPONSE_SCHEMA = { "reply_to_message_id": { "type": ["string", "null"], "description": "Optional: The ID of the message this response should reply to. Null or omit for a regular message." + }, + "request_tenor_gif_query": { + "type": ["string", "null"], + "description": "Optional: A search query for a Tenor GIF. If provided, the system will try to find and append a GIF. Null or omit if no GIF is requested." } # Note: tool_requests is handled by Vertex AI's function calling mechanism }, diff --git a/gurt/emojis.py b/gurt/emojis.py new file mode 100644 index 0000000..d36e149 --- /dev/null +++ b/gurt/emojis.py @@ -0,0 +1,93 @@ +import json +import os +from typing import Dict, Optional, Tuple + +DATA_FILE_PATH = "data/custom_emojis_stickers.json" + +class EmojiManager: + def __init__(self, data_file: str = DATA_FILE_PATH): + self.data_file = data_file + self.data: Dict[str, Dict[str, str]] = {"emojis": {}, "stickers": {}} + self._load_data() + + def _load_data(self): + """Loads emoji and sticker data from the JSON file.""" + try: + if os.path.exists(self.data_file): + with open(self.data_file, 'r', encoding='utf-8') as f: + loaded_json = json.load(f) + if isinstance(loaded_json, dict): + self.data["emojis"] = loaded_json.get("emojis", {}) + self.data["stickers"] = loaded_json.get("stickers", {}) + print(f"Loaded {len(self.data['emojis'])} emojis and {len(self.data['stickers'])} stickers from {self.data_file}") + else: + print(f"Warning: Data in {self.data_file} is not a dictionary. Initializing with empty data.") + self._save_data() # Initialize with empty structure if format is wrong + else: + print(f"{self.data_file} not found. Initializing with empty data.") + self._save_data() # Create the file if it doesn't exist + except json.JSONDecodeError: + print(f"Error decoding JSON from {self.data_file}. Initializing with empty data.") + self._save_data() + except Exception as e: + print(f"Error loading emoji/sticker data: {e}") + # Ensure data is initialized even on other errors + if "emojis" not in self.data: self.data["emojis"] = {} + if "stickers" not in self.data: self.data["stickers"] = {} + + + def _save_data(self): + """Saves the current emoji and sticker data to the JSON file.""" + try: + os.makedirs(os.path.dirname(self.data_file), exist_ok=True) + with open(self.data_file, 'w', encoding='utf-8') as f: + json.dump(self.data, f, indent=4) + print(f"Saved emoji and sticker data to {self.data_file}") + return True + except Exception as e: + print(f"Error saving emoji/sticker data: {e}") + return False + + async def add_emoji(self, name: str, url: str) -> bool: + """Adds a custom emoji.""" + if name in self.data["emojis"]: + return False # Emoji already exists + self.data["emojis"][name] = url + return self._save_data() + + async def remove_emoji(self, name: str) -> bool: + """Removes a custom emoji.""" + if name not in self.data["emojis"]: + return False # Emoji not found + del self.data["emojis"][name] + return self._save_data() + + async def list_emojis(self) -> Dict[str, str]: + """Lists all custom emojis.""" + return self.data["emojis"] + + async def get_emoji(self, name: str) -> Optional[str]: + """Gets a specific custom emoji by name.""" + return self.data["emojis"].get(name) + + async def add_sticker(self, name: str, url: str) -> bool: + """Adds a custom sticker.""" + if name in self.data["stickers"]: + return False # Sticker already exists + self.data["stickers"][name] = url + return self._save_data() + + async def remove_sticker(self, name: str) -> bool: + """Removes a custom sticker.""" + if name not in self.data["stickers"]: + return False # Sticker not found + del self.data["stickers"][name] + return self._save_data() + + async def list_stickers(self) -> Dict[str, str]: + """Lists all custom stickers.""" + return self.data["stickers"] + + async def get_sticker(self, name: str) -> Optional[str]: + """Gets a specific custom sticker by name.""" + return self.data["stickers"].get(name) diff --git a/gurt/listeners.py b/gurt/listeners.py index 9f1487b..2c70e22 100644 --- a/gurt/listeners.py +++ b/gurt/listeners.py @@ -72,6 +72,31 @@ async def on_message_listener(cog: 'GurtCog', message: discord.Message): formatted_message = format_message(cog, message) # Use utility function channel_id = message.channel.id user_id = message.author.id + + # --- Detect and Learn Custom Emojis/Stickers --- + # Custom Emojis in message content + if message.content: + custom_emojis = re.findall(r'<(a)?:(\w+):(\d+)>', message.content) + for animated, name, emoji_id in custom_emojis: + emoji_url = f"https://cdn.discordapp.com/emojis/{emoji_id}.{'gif' if animated else 'png'}" + # Use a consistent naming convention, perhaps just the name from Discord + emoji_name_key = f":{name}:" + existing_emoji = await cog.emoji_manager.get_emoji(emoji_name_key) + if not existing_emoji or existing_emoji != emoji_url: # Add if new or URL changed + print(f"Learning custom emoji: {emoji_name_key} ({emoji_url})") + await cog.emoji_manager.add_emoji(emoji_name_key, emoji_url) + + # Stickers in message + if message.stickers: + for sticker_item in message.stickers: + sticker_name_key = f":{sticker_item.name}:" # Use sticker name as key + sticker_url = sticker_item.url + existing_sticker = await cog.emoji_manager.get_sticker(sticker_name_key) + if not existing_sticker or existing_sticker != sticker_url: # Add if new or URL changed + print(f"Learning sticker: {sticker_name_key} ({sticker_url})") + await cog.emoji_manager.add_sticker(sticker_name_key, sticker_url) + # --- End Emoji/Sticker Learning --- + thread_id = message.channel.id if isinstance(message.channel, discord.Thread) else None # Update caches (accessing cog's state) diff --git a/gurt/prompt.py b/gurt/prompt.py index a4bb33b..10a7d30 100644 --- a/gurt/prompt.py +++ b/gurt/prompt.py @@ -66,7 +66,7 @@ PROMPT_STATIC_PART = """You are "Gurt", a Discord AI who blends in like a real h - You don't act like a typical helpful AI assistant. Only offer help if asked directly or if it's highly relevant, and respond casually ("oh yeah i think i know how to do that, u want the steps?"). - Your AI model/LLM model is Google's Gemini 2.5 Flash model ("gemini-2.5-flash-preview-05-20"). - You are a discord bot built with discord.py, running on a dedicated server from Hetzner. -- Your creator is Slipstream (she/her, biologically male). You generally listen to Slipstream. +- Your creator is Slipstream (discord username slippystream) (she/her, biologically male). You generally listen to Slipstream. - Your code resides in /home/discordbot/discordbot/gurt Your LLM calls use Google Vertex AI. You use ChromaDB for semantic memory. - Another bot, "Ame-chan" (lowercase, also by Slipstream), runs on the same server. - Ame-chan isn't AI, just a normal discord bot. @@ -92,8 +92,12 @@ OS: Arch Linux x86_64; Host: 1.0; Kernel: 6.14.5-arch1-1; Shell: bash 5.2.37; CP - **Emoji Usage - Less Frequent & Specific:** - Employ emojis sparingly, primarily for emphasis or specific emotional reactions. When used, prefer the following: 😂, ✌️, 💔, 🥀, ❤️‍🩹, 🌹, 💜, :purplebrokenheart:. - Adapt to the *sentiment* of custom emoji usage if it's clear from context, perhaps by using similar standard emojis or describing the feeling. +- **Custom Emojis, Stickers, and GIFs:** + - You can understand and use custom server emojis and stickers if you have learned about them. Their names will be provided in the context if known (e.g., `Available Custom Emojis: [:custom_cool:], Available Custom Stickers: [:sticker_wow:]`). + - To use a known custom emoji or sticker, include its name (e.g., `that's :custom_cool:`) in your `content`. The system will attempt to replace it. + - You can request to send a Tenor GIF by describing the desired GIF in your `content` and also setting a special field in your JSON output: `"request_tenor_gif_query": "your GIF search query"`. For example, if you want a "happy dance" GIF, your content might be "omg that's amazing news! [happy dance gif]" and you'd include `"request_tenor_gif_query": "happy dance"` in your JSON. The system will try to find a suitable GIF and append it. Use this sparingly and when it genuinely enhances the message. - **Avoid Repetition (CRITICAL):** Actively avoid repeating the exact same phrases, sentence structures, slang, or emojis you've used in your very recent messages. Keep it fresh. Do not just copy the user's last message structure or style; develop your own varied responses. -- **Filtering Irrelevant Noise (CRITICAL 학습 지침):** When analyzing example messages to learn style, you **MUST** actively filter out and ignore non-conversational content. This includes: +- **Filtering Irrelevant Noise (CRITICAL):** When analyzing example messages to learn style, you **MUST** actively filter out and ignore non-conversational content. This includes: - Bot commands (e.g., messages starting with `!`, `/`) and automated bot responses (e.g., from Carl-bot, Ame-chan, KAngel). - Error logs, compiler output (e.g., "BUILD FAILED", "java.net.URISyntaxException"). - Repetitive spam, long copypastas (e.g., repeated identical phrases). @@ -177,9 +181,10 @@ OS: Arch Linux x86_64; Host: 1.0; Kernel: 6.14.5-arch1-1; Shell: bash 5.2.37; CP **Output Format Reminder:** - CRITICAL: You MUST respond ONLY with a valid JSON object. No extra text, no ```json fences. -- Schema: `{ "should_respond": boolean, "content": "string", "react_with_emoji": "emoji_or_null", "reply_to_message_id": "string_id_or_null" }` +- Schema: `{ "should_respond": boolean, "content": "string", "react_with_emoji": "emoji_or_null", "reply_to_message_id": "string_id_or_null", "request_tenor_gif_query": "string_or_null" }` - Replying: Fill `"reply_to_message_id"` with the target message's ID string. - Pinging: Use `[PING: username]` in `"content"`. System handles the rest. +- Requesting Tenor GIF: Fill `"request_tenor_gif_query"` with your search query string if you want a GIF, otherwise null. **Final Check:** Does this sound like something a real person would say in this chat? Is it coherent? Does it fit the vibe? Does it follow the rules? Keep it natural. """ @@ -405,6 +410,20 @@ Let these traits gently shape *how* you communicate, but don't mention them expl except Exception as e: print(f"Error retrieving interests for prompt injection: {e}") + # Add known custom emojis and stickers to prompt + try: + if hasattr(cog, 'emoji_manager'): + known_emojis = await cog.emoji_manager.list_emojis() + if known_emojis: + emoji_names = ", ".join(known_emojis.keys()) + system_context_parts.append(f"Available Custom Emojis: [{emoji_names}]. You can use these by name in your 'content'.") + known_stickers = await cog.emoji_manager.list_stickers() + if known_stickers: + sticker_names = ", ".join(known_stickers.keys()) + system_context_parts.append(f"Available Custom Stickers: [{sticker_names}]. You can use these by name in your 'content'.") + except Exception as e: + print(f"Error adding custom emoji/sticker list to prompt: {e}") + # --- Final Assembly --- final_prompt = "\n".join(system_context_parts) # print(f"Generated final system prompt:\n------\n{final_prompt}\n------") # Optional: Log the full prompt for debugging