Files
miku-discord/backups/2025-12-07/old-bot-bak-80825/utils/autonomous.py

318 lines
11 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_ollama
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 = []
def setup_autonomous_speaking():
scheduler.add_job(miku_autonomous_tick, "interval", minutes=10)
scheduler.add_job(miku_detect_and_join_conversation, "interval", minutes=3)
scheduler.start()
print("🤖 Autonomous Miku is active!")
async def miku_autonomous_tick(action_type="general", force=False, force_action=None):
if not force and random.random() > 0.2: # 20% chance to act
return
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()
elif action_type == "engage_user":
await miku_engage_random_user()
else:
await share_miku_tweet()
async def miku_say_something_general():
channel = globals.client.get_channel(globals.AUTONOMOUS_CHANNEL_ID)
if not channel:
print("⚠️ Autonomous channel not found.")
return
mood = globals.CURRENT_MOOD_NAME
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_ollama(prompt, user_id=f"miku-general-{int(time.time())}")
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 = globals.client.get_guild(globals.TARGET_GUILD_ID)
if not guild:
print("⚠️ Target guild 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_ollama(prompt, user_id=f"miku-engage-{int(time.time())}")
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_ollama(prompt, user_id=f"miku-chat-{int(time.time())}")
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():
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_ollama(base_prompt, user_id="autonomous")
# Post to Discord
await channel.send(f"{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_ollama(prompt, user_id=f"manual-{int(time.time())}")
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