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