Feat: Integrate Tenor GIF search and custom emoji/sticker support

This commit introduces two main enhancements to Gurt's capabilities:

1.  **Tenor GIF Integration:**
    *   The AI can now request Tenor GIFs via a `request_tenor_gif_query` field in its response schema.
    *   `api.py` handles these requests by searching Tenor (using the new `TENOR_API_KEY` from config) and appending the GIF URL to the AI's message content.
    *   System prompts have been updated to instruct the AI on how to request GIFs.

2.  **Custom Emoji & Sticker Learning and Usage:**
    *   Introduces an `EmojiManager` (in the new `gurt/emojis.py`) to store and manage mappings of custom emoji/sticker names to their URLs. This data is persisted to `EMOJI_STORAGE_FILE`.
    *   Gurt now automatically learns custom emojis and stickers (and their URLs) from user messages via the `on_message` listener in `listeners.py`.
    *   The AI can be prompted to use these learned emojis/stickers by referencing their `:name:`.
    *   `api.py` processes AI responses to identify known custom emojis/stickers. For stickers, their URL is appended to the content if mentioned.
    *   A `learnemoji` command has been added for manually teaching Gurt emojis/stickers, although automatic learning is the primary mechanism.
    *   System prompts are updated to guide the AI on using custom emojis/stickers.
This commit is contained in:
Slipstream 2025-05-28 14:30:26 -06:00
parent aa2cdcd3d3
commit 6868b2e95e
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
7 changed files with 443 additions and 11 deletions

View File

@ -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 <name:id> 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 []

View File

@ -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

View File

@ -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:

View File

@ -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
},

93
gurt/emojis.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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