This commit is contained in:
Slipstream 2025-04-27 00:58:21 -06:00
parent 9c60dd70fa
commit 33e15df5be
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
4 changed files with 171 additions and 51 deletions

View File

@ -498,7 +498,7 @@ async def get_ai_response(cog: 'GurtCog', message: discord.Message, model: Optio
messages_for_follow_up.extend(tool_results_for_api)
messages_for_follow_up.append({
"role": "user",
"content": f"Okay, the requested tools have been executed. Here are the results. Now, generate the final user-facing response based on these results and the previous conversation context. **CRITICAL: Your response MUST be ONLY the raw JSON object matching the standard schema (should_respond, content, react_with_emoji). Do NOT include the 'tool_requests' field this time.**\n\n**Ensure nothing precedes or follows the JSON.**{message_length_guidance}"
"content": f"Okay, the requested tools have been executed. Here are the results. Now, generate the final user-facing response based on these results and the previous conversation context. **CRITICAL: Your response MUST be ONLY the raw JSON object matching the standard schema (should_respond, content, react_with_emoji). Do NOT include the 'tool_requests' field this time.**\n\n**IMPORTANT: If you set 'should_respond' to true, you MUST provide a non-empty string for the 'content' field.**\n\n**Ensure nothing precedes or follows the JSON.**{message_length_guidance}"
})
follow_up_payload = {

View File

@ -131,12 +131,14 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
# --- Setup Commands and Listeners ---
# Add commands defined in commands.py
setup_commands(self)
self.command_functions = setup_commands(self)
# Store command functions for reference
self.registered_commands = [func.__name__ for func in self.command_functions]
# Add listeners defined in listeners.py
# Note: Listeners need to be added to the bot instance, not the cog directly in this pattern.
# We'll add them in cog_load or the main setup function.
print("GurtCog initialized.")
print(f"GurtCog initialized with commands: {self.registered_commands}")
async def cog_load(self):
"""Create aiohttp session, initialize DB, load baselines, start background task"""
@ -177,6 +179,20 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
print("GurtCog: Listeners added.")
# Sync commands with Discord
try:
print("GurtCog: Syncing commands with Discord...")
synced = await self.bot.tree.sync()
print(f"GurtCog: Synced {len(synced)} command(s)")
# List the synced commands
gurt_commands = [cmd.name for cmd in self.bot.tree.get_commands() if cmd.name.startswith("gurt")]
print(f"GurtCog: Available Gurt commands: {', '.join(gurt_commands)}")
except Exception as e:
print(f"GurtCog: Failed to sync commands: {e}")
import traceback
traceback.print_exc()
# Start background task
if self.background_task is None or self.background_task.done():
self.background_task = asyncio.create_task(background_processing_task(self))
@ -314,6 +330,24 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
return stats
async def sync_commands(self):
"""Manually sync commands with Discord."""
try:
print("GurtCog: Manually syncing commands with Discord...")
synced = await self.bot.tree.sync()
print(f"GurtCog: Synced {len(synced)} command(s)")
# List the synced commands
gurt_commands = [cmd.name for cmd in self.bot.tree.get_commands() if cmd.name.startswith("gurt")]
print(f"GurtCog: Available Gurt commands: {', '.join(gurt_commands)}")
return synced, gurt_commands
except Exception as e:
print(f"GurtCog: Failed to sync commands: {e}")
import traceback
traceback.print_exc()
return [], []
# Setup function for loading the cog
async def setup(bot):

View File

@ -125,7 +125,10 @@ def format_stats_embeds(stats: Dict[str, Any]) -> List[discord.Embed]:
def setup_commands(cog: 'GurtCog'):
"""Adds Gurt-specific commands to the cog."""
# Example using app_commands - adapt existing commands if needed
# 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=[
@ -133,6 +136,11 @@ def setup_commands(cog: 'GurtCog'):
])
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()
@ -141,6 +149,9 @@ def setup_commands(cog: 'GurtCog'):
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",
@ -161,6 +172,11 @@ def setup_commands(cog: 'GurtCog'):
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)
@ -207,10 +223,17 @@ def setup_commands(cog: 'GurtCog'):
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.")
@cog.bot.tree.command(name="gurtstats", description="Display Gurt's internal statistics. (Owner only)")
async def gurtstats(interaction: discord.Interaction):
"""Handles the /gurtstats command."""
# 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 view detailed stats.", ephemeral=True)
return
await interaction.response.defer(ephemeral=True) # Defer as stats collection might take time
try:
stats_data = await cog.get_gurt_stats()
@ -222,5 +245,38 @@ def setup_commands(cog: 'GurtCog'):
traceback.print_exc()
await interaction.followup.send("An error occurred while fetching Gurt's stats.", ephemeral=True)
command_functions.append(gurtstats)
print("Gurt commands setup in cog.")
# --- 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 in cog.bot.tree.get_commands():
if cmd.name.startswith("gurt"):
commands_after.append(cmd.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)
print(f"Gurt commands setup in cog: {[func.__name__ for func in command_functions]}")
# Return the command functions for proper registration
return command_functions

View File

@ -240,62 +240,92 @@ async def on_message_listener(cog: 'GurtCog', message: discord.Message):
await message.channel.send(random.choice(["...", "*confused gurting*", "brain broke sorry"]))
return
# Determine which response to use (prefer final if available)
response_to_use = final_response if final_response else initial_response
# --- Process and Send Responses ---
sent_any_message = False
reacted = False
if response_to_use and isinstance(response_to_use, dict):
sent_message = False
reacted = False
# Handle Reaction
emoji_to_react = response_to_use.get("react_with_emoji")
if emoji_to_react and isinstance(emoji_to_react, str):
try:
# Basic validation for standard emoji
if 1 <= len(emoji_to_react) <= 4 and not re.match(r'<a?:.+?:\d+>', emoji_to_react):
await message.add_reaction(emoji_to_react)
reacted = True
print(f"Bot reacted to message {message.id} with {emoji_to_react}")
else: print(f"Invalid emoji format: {emoji_to_react}")
except Exception as e: print(f"Error adding reaction '{emoji_to_react}': {e}")
# Handle Text Response
if response_to_use.get("should_respond") and response_to_use.get("content"):
response_text = response_to_use["content"]
# Helper function to handle sending a single response text and caching
async def send_response_content(response_data: Optional[Dict[str, Any]], response_label: str) -> bool:
nonlocal sent_any_message # Allow modification of the outer scope variable
if response_data and isinstance(response_data, dict) and \
response_data.get("should_respond") and response_data.get("content"):
response_text = response_data["content"]
print(f"Attempting to send {response_label} content...")
if len(response_text) > 1900:
filepath = f'gurt_response_{message.id}.txt'
filepath = f'gurt_{response_label}_{message.id}.txt'
try:
with open(filepath, 'w', encoding='utf-8') as f: f.write(response_text)
await message.channel.send("Response too long:", file=discord.File(filepath))
sent_message = True
except Exception as file_e: print(f"Error writing/sending long response file: {file_e}")
await message.channel.send(f"{response_label.capitalize()} response too long:", file=discord.File(filepath))
sent_any_message = True
print(f"Sent {response_label} content as file.")
return True
except Exception as file_e: print(f"Error writing/sending long {response_label} response file: {file_e}")
finally:
try: os.remove(filepath)
except OSError as os_e: print(f"Error removing temp file {filepath}: {os_e}")
else:
async with message.channel.typing():
await simulate_human_typing(cog, message.channel, response_text) # Use simulation
sent_msg = await message.channel.send(response_text)
sent_message = True
# Cache this bot response
bot_response_cache_entry = format_message(cog, sent_msg)
cog.message_cache['by_channel'][channel_id].append(bot_response_cache_entry)
cog.message_cache['global_recent'].append(bot_response_cache_entry)
cog.message_cache['replied_to'][channel_id].append(bot_response_cache_entry) # Track replies
cog.bot_last_spoke[channel_id] = time.time()
# Track participation topic
identified_topics = identify_conversation_topics(cog, [bot_response_cache_entry])
if identified_topics:
topic = identified_topics[0]['topic'].lower().strip()
cog.gurt_participation_topics[topic] += 1
print(f"Tracked Gurt participation in topic: '{topic}'")
try:
async with message.channel.typing():
await simulate_human_typing(cog, message.channel, response_text) # Use simulation
sent_msg = await message.channel.send(response_text)
sent_any_message = True
# Cache this bot response
bot_response_cache_entry = format_message(cog, sent_msg)
cog.message_cache['by_channel'][channel_id].append(bot_response_cache_entry)
cog.message_cache['global_recent'].append(bot_response_cache_entry)
# cog.message_cache['replied_to'][channel_id].append(bot_response_cache_entry) # Maybe track replies differently?
cog.bot_last_spoke[channel_id] = time.time()
# Track participation topic
identified_topics = identify_conversation_topics(cog, [bot_response_cache_entry])
if identified_topics:
topic = identified_topics[0]['topic'].lower().strip()
cog.gurt_participation_topics[topic] += 1
print(f"Tracked Gurt participation ({response_label}) in topic: '{topic}'")
print(f"Sent {response_label} content.")
return True
except Exception as send_e:
print(f"Error sending {response_label} content: {send_e}")
return False
# Send initial response content if valid
sent_initial_message = await send_response_content(initial_response, "initial")
if response_to_use.get("should_respond") and not sent_message and not reacted:
print(f"Warning: AI response intended but no valid content/reaction. Data: {response_to_use}")
# Send final response content if valid (and different from initial, if initial was sent)
sent_final_message = False
# Ensure initial_response exists before accessing its content for comparison
initial_content = initial_response.get("content") if initial_response else None
if final_response and (not sent_initial_message or initial_content != final_response.get("content")):
sent_final_message = await send_response_content(final_response, "final")
elif not error_msg: # No valid response and no critical error
print(f"Warning: No valid response generated for message {message.id}, and no critical error reported.")
# Handle Reaction (prefer final response for reaction if it exists)
reaction_source = final_response if final_response else initial_response
if reaction_source and isinstance(reaction_source, dict):
emoji_to_react = reaction_source.get("react_with_emoji")
if emoji_to_react and isinstance(emoji_to_react, str):
try:
# Basic validation for standard emoji
if 1 <= len(emoji_to_react) <= 4 and not re.match(r'<a?:.+?:\d+>', emoji_to_react):
# Only react if we haven't sent any message content (avoid double interaction)
if not sent_any_message:
await message.add_reaction(emoji_to_react)
reacted = True
print(f"Bot reacted to message {message.id} with {emoji_to_react}")
else:
print(f"Skipping reaction {emoji_to_react} because a message was already sent.")
else: print(f"Invalid emoji format: {emoji_to_react}")
except Exception as e: print(f"Error adding reaction '{emoji_to_react}': {e}")
# Log if response was intended but nothing was sent/reacted
# Check if initial response intended action but nothing happened
initial_intended_action = initial_response and initial_response.get("should_respond")
initial_action_taken = sent_initial_message or (reacted and reaction_source == initial_response)
# Check if final response intended action but nothing happened
final_intended_action = final_response and final_response.get("should_respond")
final_action_taken = sent_final_message or (reacted and reaction_source == final_response)
if (initial_intended_action and not initial_action_taken) or \
(final_intended_action and not final_action_taken):
print(f"Warning: AI response intended action but nothing sent/reacted. Initial: {initial_response}, Final: {final_response}")
except Exception as e:
print(f"Exception in on_message listener main block: {str(e)}")