# autonomous.py (V2) """ Enhanced autonomous system that uses the autonomous_engine for true autonomy. Integrates with legacy autonomous functions from autonomous_v1_legacy.py """ import asyncio import time from utils.autonomous_engine import autonomous_engine from server_manager import server_manager import globals # Rate limiting: Track last action time per server to prevent rapid-fire _last_action_execution = {} # guild_id -> timestamp _MIN_ACTION_INTERVAL = 30 # Minimum 30 seconds between autonomous actions async def autonomous_tick_v2(guild_id: int): """ New autonomous tick that uses context-aware decision making. Replaces the random 10% chance with intelligent decision. """ # Rate limiting check now = time.time() if guild_id in _last_action_execution: time_since_last = now - _last_action_execution[guild_id] if time_since_last < _MIN_ACTION_INTERVAL: print(f"โฑ๏ธ [V2] Rate limit: Only {time_since_last:.0f}s since last action (need {_MIN_ACTION_INTERVAL}s)") return # Ask the engine if Miku should act (with optional debug logging) action_type = autonomous_engine.should_take_action(guild_id, debug=globals.AUTONOMOUS_DEBUG) if action_type is None: # Engine decided not to act return print(f"๐Ÿค– [V2] Autonomous engine decided to: {action_type} for server {guild_id}") # Execute the action using legacy functions from utils.autonomous_v1_legacy import ( miku_say_something_general_for_server, miku_engage_random_user_for_server, share_miku_tweet_for_server, miku_detect_and_join_conversation_for_server ) from utils.profile_picture_manager import profile_picture_manager try: if action_type == "general": await miku_say_something_general_for_server(guild_id) elif action_type == "engage_user": await miku_engage_random_user_for_server(guild_id) elif action_type == "share_tweet": await share_miku_tweet_for_server(guild_id) elif action_type == "join_conversation": await miku_detect_and_join_conversation_for_server(guild_id) elif action_type == "change_profile_picture": # Get current mood for this server mood, _ = server_manager.get_server_mood(guild_id) print(f"๐ŸŽจ [V2] Changing profile picture (mood: {mood})") result = await profile_picture_manager.change_profile_picture(mood=mood, debug=True) if result["success"]: print(f"โœ… Profile picture changed successfully!") else: print(f"โš ๏ธ Profile picture change failed: {result.get('error')}") # Record that action was taken autonomous_engine.record_action(guild_id) # Update rate limiter _last_action_execution[guild_id] = time.time() except Exception as e: print(f"โš ๏ธ Error executing autonomous action: {e}") async def autonomous_reaction_tick_v2(guild_id: int): """ Scheduled check for reacting to older messages. This runs less frequently (e.g., every 20 minutes) and picks from recent messages. """ # Ask the engine if Miku should react (scheduled check) should_react = autonomous_engine.should_react_to_message(guild_id, message_age_seconds=600) # Check 10 min old msgs if not should_react: return print(f"๐Ÿค– [V2] Scheduled reaction check triggered for server {guild_id}") try: from utils.autonomous_v1_legacy import miku_autonomous_reaction_for_server # Don't pass force_message - let it pick a random recent message await miku_autonomous_reaction_for_server(guild_id, force_message=None) # Record action autonomous_engine.record_action(guild_id) except Exception as e: print(f"โš ๏ธ Error executing scheduled reaction: {e}") def on_message_event(message): """ Hook for bot.py to call on every message. Updates context without LLM calls. ONLY processes messages from the configured autonomous channel. """ if not message.guild: return # DMs don't use this system guild_id = message.guild.id # Get server config to check if this is the autonomous channel server_config = server_manager.get_server_config(guild_id) if not server_config: return # No config for this server # CRITICAL: Only process messages from the autonomous channel if message.channel.id != server_config.autonomous_channel_id: return # Ignore messages from other channels # Track the message autonomous_engine.track_message(guild_id, author_is_bot=message.author.bot) # Check if we should act (async, non-blocking) if not message.author.bot: # Only check for human messages asyncio.create_task(_check_and_act(guild_id)) # Also check if we should react to this specific message asyncio.create_task(_check_and_react(guild_id, message)) async def _check_and_react(guild_id: int, message): """ Check if Miku should react to a new message with an emoji. Called for each new message in real-time. """ # Calculate message age from datetime import datetime, timezone message_age = (datetime.now(timezone.utc) - message.created_at).total_seconds() # Ask engine if we should react should_react = autonomous_engine.should_react_to_message(guild_id, message_age) if should_react: print(f"๐ŸŽฏ [V2] Real-time reaction triggered for message from {message.author.display_name}") from utils.autonomous_v1_legacy import miku_autonomous_reaction_for_server await miku_autonomous_reaction_for_server(guild_id, force_message=message) # Record action (reactions count as actions for cooldown purposes) autonomous_engine.record_action(guild_id) async def _check_and_act(guild_id: int): """ Internal function to check if action should be taken. Called after each message, but engine makes smart decision. IMPORTANT: Pass triggered_by_message=True so the engine knows to respond to the message instead of saying something random/general. """ # Rate limiting check now = time.time() if guild_id in _last_action_execution: time_since_last = now - _last_action_execution[guild_id] if time_since_last < _MIN_ACTION_INTERVAL: return action_type = autonomous_engine.should_take_action(guild_id, triggered_by_message=True) if action_type: print(f"๐ŸŽฏ [V2] Message triggered autonomous action: {action_type}") # Execute the action directly (don't call autonomous_tick_v2 which would check again) from utils.autonomous_v1_legacy import ( miku_say_something_general_for_server, miku_engage_random_user_for_server, share_miku_tweet_for_server, miku_detect_and_join_conversation_for_server ) from utils.profile_picture_manager import profile_picture_manager try: if action_type == "general": await miku_say_something_general_for_server(guild_id) elif action_type == "engage_user": await miku_engage_random_user_for_server(guild_id) elif action_type == "share_tweet": await share_miku_tweet_for_server(guild_id) elif action_type == "join_conversation": await miku_detect_and_join_conversation_for_server(guild_id) elif action_type == "change_profile_picture": # Get current mood for this server mood, _ = server_manager.get_server_mood(guild_id) print(f"๐ŸŽจ [V2] Changing profile picture (mood: {mood})") result = await profile_picture_manager.change_profile_picture(mood=mood, debug=True) if result["success"]: print(f"โœ… Profile picture changed successfully!") else: print(f"โš ๏ธ Profile picture change failed: {result.get('error')}") # Record that action was taken autonomous_engine.record_action(guild_id) # Update rate limiter _last_action_execution[guild_id] = time.time() except Exception as e: print(f"โš ๏ธ Error executing message-triggered action: {e}") def on_presence_update(member, before, after): """ Hook for presence updates (status changes, activities). Args: member: The Member object (from 'after' in discord.py event) before: Member object with old state after: Member object with new state """ # Ignore bot users (including music bots that spam activity updates) if member.bot: return guild_id = member.guild.id # Track status changes if before.status != after.status: autonomous_engine.track_user_event(guild_id, "status_changed") print(f"๐Ÿ‘ค [V2] {member.display_name} status changed: {before.status} โ†’ {after.status}") # Track activity changes if before.activities != after.activities: # Check for new activities before_activity_names = {a.name for a in before.activities if hasattr(a, 'name')} after_activity_names = {a.name for a in after.activities if hasattr(a, 'name')} new_activities = after_activity_names - before_activity_names for activity_name in new_activities: autonomous_engine.track_user_event( guild_id, "activity_started", {"activity_name": activity_name} ) print(f"๐ŸŽฎ [V2] {member.display_name} started activity: {activity_name}") def on_member_join(member): """Hook for member join events""" # Ignore bot users if member.bot: return guild_id = member.guild.id autonomous_engine.track_user_event(guild_id, "user_joined") def on_mood_change(guild_id: int, new_mood: str): """Hook for mood changes""" autonomous_engine.update_mood(guild_id, new_mood) async def periodic_decay_task(): """ Background task that decays event counters and saves context. Run this every 15 minutes. """ task_start_time = time.time() iteration_count = 0 while True: await asyncio.sleep(900) # 15 minutes iteration_count += 1 # Use list() to safely iterate even if dict changes guild_ids = list(server_manager.servers.keys()) for guild_id in guild_ids: try: autonomous_engine.decay_events(guild_id) except Exception as e: print(f"โš ๏ธ Error decaying events for guild {guild_id}: {e}") # Save context to disk periodically try: autonomous_engine.save_context() except Exception as e: print(f"โš ๏ธ Error saving autonomous context: {e}") uptime_hours = (time.time() - task_start_time) / 3600 print(f"๐Ÿงน [V2] Decay task completed (iteration #{iteration_count}, uptime: {uptime_hours:.1f}h)") print(f" โ””โ”€ Processed {len(guild_ids)} servers") def initialize_v2_system(client): """ Initialize the V2 autonomous system. Call this from bot.py on startup. """ print("๐Ÿš€ Initializing Autonomous V2 System...") # Initialize mood states for all servers for guild_id, server_config in server_manager.servers.items(): autonomous_engine.update_mood(guild_id, server_config.current_mood_name) # Start decay task client.loop.create_task(periodic_decay_task()) print("โœ… Autonomous V2 System initialized") # ========== Legacy Function Wrappers ========== # These maintain compatibility with old code that imports from autonomous.py from utils.autonomous_v1_legacy import ( load_last_sent_tweets, save_last_sent_tweets, setup_autonomous_speaking, # Server-specific functions miku_autonomous_tick_for_server, miku_say_something_general_for_server, miku_engage_random_user_for_server, miku_detect_and_join_conversation_for_server, share_miku_tweet_for_server, miku_autonomous_reaction_for_server, miku_autonomous_reaction_for_dm, handle_custom_prompt_for_server, # Legacy global functions (for API compatibility) miku_autonomous_tick, miku_say_something_general, miku_engage_random_user, miku_detect_and_join_conversation, share_miku_tweet, handle_custom_prompt, miku_autonomous_reaction, ) # Alias the V2 tick as the main autonomous tick autonomous_tick = autonomous_tick_v2 autonomous_reaction_tick = autonomous_reaction_tick_v2