Initial commit: Miku Discord Bot
This commit is contained in:
344
bot/utils/autonomous.py
Normal file
344
bot/utils/autonomous.py
Normal file
@@ -0,0 +1,344 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user