Files
miku-discord/backups/2025-12-07/autonomous_wip.py

349 lines
13 KiB
Python
Raw Normal View History

2025-12-07 17:15:09 +02:00
# autonomous.py
import random
import time
import json
import os
from datetime import datetime
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from discord import Status
from discord import TextChannel
from difflib import SequenceMatcher
import globals
from utils.llm import query_llama
2025-12-07 17:15:09 +02:00
from utils.moods import MOOD_EMOJIS
from utils.twitter_fetcher import fetch_miku_tweets
from utils.image_handling import analyze_image_with_qwen, download_and_encode_image
scheduler = AsyncIOScheduler()
_last_autonomous_messages = [] # rotating buffer of last general messages
MAX_HISTORY = 10
_last_user_engagements = {} # user_id -> timestamp
LAST_SENT_TWEETS_FILE = "memory/last_sent_tweets.json"
LAST_SENT_TWEETS = []
AUTONOMOUS_CONFIG_FILE = "memory/autonomous_config.json"
def load_autonomous_config():
if os.path.exists(AUTONOMOUS_CONFIG_FILE):
with open(AUTONOMOUS_CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def save_autonomous_config(config):
with open(AUTONOMOUS_CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2)
def setup_autonomous_speaking():
scheduler.add_job(run_autonomous_for_all_guilds, "interval", minutes=10)
scheduler.add_job(run_conversation_detection_all_guilds, "interval", minutes=3)
scheduler.start()
print("🤖 Autonomous Miku is active!")
async def run_autonomous_for_all_guilds():
config = load_autonomous_config()
for guild_id, settings in config.items():
await miku_autonomous_tick(guild_id, settings)
async def run_conversation_detection_all_guilds():
config = load_autonomous_config()
for guild_id, settings in config.items():
await miku_detect_and_join_conversation(guild_id, settings)
async def miku_autonomous_tick(guild_id, settings, action_type="general", force=False, force_action=None):
settings = globals.GUILD_SETTINGS.get(guild_id)
if not settings:
print(f"⚠️ No settings found for guild {guild_id}")
return
if not force and random.random() > 0.2: # 20% chance to act
return
# TODO edit this function as per ChatGPT's last reply and then go back to the long reply from step 5 onwards
if force_action:
action_type = force_action
else:
action_type = random.choice(["general", "engage_user", "share_tweet"])
if action_type == "general":
await miku_say_something_general(guild_id, settings)
elif action_type == "engage_user":
await miku_engage_random_user(guild_id, settings)
else:
await share_miku_tweet(guild_id, settings)
async def miku_say_something_general(guild_id, settings):
channel = globals.client.get_channel(int(settings["autonomous_channel_id"]))
if not channel:
print(f"⚠️ Autonomous channel not found for guild {guild_id}")
return
mood = settings.get("mood", "curious")
time_of_day = get_time_of_day()
emoji = MOOD_EMOJIS.get(mood, "")
history_summary = "\n".join(f"- {msg}" for msg in _last_autonomous_messages[-5:]) if _last_autonomous_messages else "None yet."
prompt = (
f"Miku is feeling {mood}. It's currently {time_of_day}. "
f"Write a short, natural message that Miku might say out of the blue in a chat. "
f"She might greet everyone, make a cute observation, ask a silly question, or say something funny. "
f"Make sure it feels casual and spontaneous, like a real person might say.\n\n"
f"Here are some things Miku recently said, do not repeat them or say anything too similar:\n{history_summary}"
)
for attempt in range(3): # retry up to 3 times if message is too similar
message = await query_llama(prompt, user_id=f"miku-general-{int(time.time())}", guild_id=guild_id, response_type="autonomous_general")
2025-12-07 17:15:09 +02:00
if not is_too_similar(message, _last_autonomous_messages):
break
print("🔁 Response was too similar to past messages, retrying...")
try:
await channel.send(message)
print(f"💬 Miku said something general in #{channel.name}")
except Exception as e:
print(f"⚠️ Failed to send autonomous message: {e}")
async def miku_engage_random_user(guild_id, settings):
guild = globals.client.get_guild(guild_id)
if not guild:
print(f"⚠️ Guild {guild_id} not found.")
return
channel = globals.client.get_channel(globals.AUTONOMOUS_CHANNEL_ID)
if not channel:
print("⚠️ Autonomous channel not found.")
return
members = [
m for m in guild.members
if m.status in {Status.online, Status.idle, Status.dnd} and not m.bot
]
time_of_day = get_time_of_day()
# Include the invisible user except during late night
specific_user_id = 214857593045254151 # Your invisible user's ID
specific_user = guild.get_member(specific_user_id)
if specific_user:
if specific_user.status != Status.offline or "late night" not in time_of_day:
if specific_user not in members:
members.append(specific_user)
if not members:
print("😴 No available members to talk to.")
return
target = random.choice(members)
now = time.time()
last_time = _last_user_engagements.get(target.id, 0)
if now - last_time < 43200: # 12 hours in seconds
print(f"⏱️ Recently engaged {target.display_name}, switching to general message.")
await miku_say_something_general()
return
activity_name = None
if target.activities:
for a in target.activities:
if hasattr(a, 'name') and a.name:
activity_name = a.name
break
mood = globals.CURRENT_MOOD_NAME
emoji = MOOD_EMOJIS.get(mood, "")
is_invisible = target.status == Status.offline
display_name = target.display_name
prompt = (
f"Miku is feeling {mood} {emoji} during the {time_of_day}. "
f"She notices {display_name}'s current status is {target.status.name}. "
)
if is_invisible:
prompt += (
f"Miku suspects that {display_name} is being sneaky and invisible 👻. "
f"She wants to playfully call them out in a fun, teasing, but still affectionate way. "
)
elif activity_name:
prompt += (
f"They appear to be playing or doing: {activity_name}. "
f"Miku wants to comment on this and start a friendly conversation."
)
else:
prompt += (
f"Miku wants to casually start a conversation with them, maybe ask how they're doing, what they're up to, or even talk about something random with them."
)
prompt += (
f"\nThe message should be short and reflect Mikus current mood."
)
try:
message = await query_llama(prompt, user_id=f"miku-engage-{int(time.time())}", guild_id=guild_id, response_type="autonomous_general")
2025-12-07 17:15:09 +02:00
await channel.send(f"{target.mention} {message}")
print(f"👤 Miku engaged {display_name}")
_last_user_engagements[target.id] = time.time()
except Exception as e:
print(f"⚠️ Failed to engage user: {e}")
async def miku_detect_and_join_conversation():
channel = globals.client.get_channel(globals.AUTONOMOUS_CHANNEL_ID)
if not isinstance(channel, TextChannel):
print("⚠️ Autonomous channel is invalid or not found.")
return
# Fetch last 20 messages (for filtering)
try:
messages = [msg async for msg in channel.history(limit=20)]
except Exception as e:
print(f"⚠️ Failed to fetch channel history: {e}")
return
# Filter to messages in last 10 minutes from real users (not bots)
recent_msgs = [
msg for msg in messages
if not msg.author.bot
and (datetime.now(msg.created_at.tzinfo) - msg.created_at).total_seconds() < 600
]
user_ids = set(msg.author.id for msg in recent_msgs)
if len(recent_msgs) < 5 or len(user_ids) < 2:
# Not enough activity
return
if random.random() > 0.5:
return # 50% chance to engage
# Use last 10 messages for context (oldest to newest)
convo_lines = reversed(recent_msgs[:10])
history_text = "\n".join(
f"{msg.author.display_name}: {msg.content}" for msg in convo_lines
)
mood = globals.CURRENT_MOOD_NAME
emoji = MOOD_EMOJIS.get(mood, "")
prompt = (
f"Miku is watching a conversation happen in the chat. Her current mood is {mood} {emoji}. "
f"She wants to say something relevant, playful, or insightful based on what people are talking about.\n\n"
f"Here's the conversation:\n{history_text}\n\n"
f"Write a short reply that feels natural and adds to the discussion. It should reflect Mikus mood and personality."
)
try:
reply = await query_llama(prompt, user_id=f"miku-chat-{int(time.time())}", guild_id=guild_id, response_type="conversation_join")
2025-12-07 17:15:09 +02:00
await channel.send(reply)
print(f"💬 Miku joined an ongoing conversation.")
except Exception as e:
print(f"⚠️ Failed to interject in conversation: {e}")
async def share_miku_tweet(guild_id, settings):
channel = globals.client.get_channel(globals.AUTONOMOUS_CHANNEL_ID)
tweets = await fetch_miku_tweets(limit=5)
if not tweets:
print("📭 No good tweets found.")
return
fresh_tweets = [t for t in tweets if t["url"] not in LAST_SENT_TWEETS]
if not fresh_tweets:
print("⚠️ All fetched tweets were recently sent. Reusing tweets.")
fresh_tweets = tweets
tweet = random.choice(fresh_tweets)
LAST_SENT_TWEETS.append(tweet["url"])
if len(LAST_SENT_TWEETS) > 50:
LAST_SENT_TWEETS.pop(0)
save_last_sent_tweets()
# Prepare prompt
mood = globals.CURRENT_MOOD_NAME
emoji = MOOD_EMOJIS.get(mood, "")
base_prompt = f"Here's a tweet from @{tweet['username']}:\n\n{tweet['text']}\n\nComment on it in a fun Miku style! Miku's current mood is {mood} {emoji}. Make sure the comment reflects Miku's mood and personality."
# Optionally analyze first image
first_img_url = tweet["media"][0]
base64_img = await download_and_encode_image(first_img_url)
if base64_img:
img_desc = await analyze_image_with_qwen(base64_img)
base_prompt += f"\n\nThe image looks like this: {img_desc}"
miku_comment = await query_llama(base_prompt, user_id="autonomous", guild_id=guild_id, response_type="autonomous_tweet")
2025-12-07 17:15:09 +02:00
# Post to Discord
# Convert to fxtwitter for better embeds
fx_tweet_url = tweet['url'].replace("twitter.com", "fxtwitter.com").replace("x.com", "fxtwitter.com")
await channel.send(f"{fx_tweet_url}")
await channel.send(miku_comment)
async def handle_custom_prompt(user_prompt: str):
channel = globals.client.get_channel(globals.AUTONOMOUS_CHANNEL_ID)
if not channel:
print("⚠️ Autonomous channel not found.")
return False
mood = globals.CURRENT_MOOD_NAME
emoji = MOOD_EMOJIS.get(mood, "")
time_of_day = get_time_of_day()
# Wrap users idea in Miku context
prompt = (
f"Miku is feeling {mood} {emoji} during the {time_of_day}. "
f"She has been instructed to: \"{user_prompt.strip()}\"\n\n"
f"Write a short, natural message as Miku that follows this instruction. "
f"Make it feel spontaneous, emotionally in character, and aligned with her mood and personality. Decide if the time of day is relevant to this request or not and if it is not, do not mention it."
)
try:
message = await query_llama(prompt, user_id=f"manual-{int(time.time())}", guild_id=None, response_type="autonomous_general")
2025-12-07 17:15:09 +02:00
await channel.send(message)
print("🎤 Miku responded to custom prompt.")
_last_autonomous_messages.append(message)
return True
except Exception as e:
print(f"❌ Failed to send custom autonomous message: {e}")
return False
def load_last_sent_tweets():
global LAST_SENT_TWEETS
if os.path.exists(LAST_SENT_TWEETS_FILE):
try:
with open(LAST_SENT_TWEETS_FILE, "r", encoding="utf-8") as f:
LAST_SENT_TWEETS = json.load(f)
except Exception as e:
print(f"⚠️ Failed to load last sent tweets: {e}")
LAST_SENT_TWEETS = []
else:
LAST_SENT_TWEETS = []
def save_last_sent_tweets():
try:
with open(LAST_SENT_TWEETS_FILE, "w", encoding="utf-8") as f:
json.dump(LAST_SENT_TWEETS, f)
except Exception as e:
print(f"⚠️ Failed to save last sent tweets: {e}")
def get_time_of_day():
hour = datetime.now().hour + 3
if 5 <= hour < 12:
return "morning"
elif 12 <= hour < 18:
return "afternoon"
elif 18 <= hour < 22:
return "evening"
return "late night. Miku wonders if anyone is still awake"
def is_too_similar(new_message, history, threshold=0.85):
for old in history:
ratio = SequenceMatcher(None, new_message.lower(), old.lower()).ratio()
if ratio > threshold:
return True
return False