Initial commit: Miku Discord Bot
This commit is contained in:
378
bot/utils/dm_interaction_analyzer.py
Normal file
378
bot/utils/dm_interaction_analyzer.py
Normal file
@@ -0,0 +1,378 @@
|
||||
"""
|
||||
DM Interaction Analyzer
|
||||
Analyzes user interactions with Miku in DMs and reports to the owner
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional
|
||||
import discord
|
||||
import globals
|
||||
from utils.llm import query_ollama
|
||||
from utils.dm_logger import dm_logger
|
||||
|
||||
# Directories
|
||||
REPORTS_DIR = "memory/dm_reports"
|
||||
REPORTED_TODAY_FILE = "memory/dm_reports/reported_today.json"
|
||||
|
||||
class DMInteractionAnalyzer:
|
||||
def __init__(self, owner_user_id: int):
|
||||
"""
|
||||
Initialize the DM Interaction Analyzer
|
||||
|
||||
Args:
|
||||
owner_user_id: Discord user ID of the bot owner to send reports to
|
||||
"""
|
||||
self.owner_user_id = owner_user_id
|
||||
os.makedirs(REPORTS_DIR, exist_ok=True)
|
||||
print(f"📊 DM Interaction Analyzer initialized for owner: {owner_user_id}")
|
||||
|
||||
def _load_reported_today(self) -> Dict[str, str]:
|
||||
"""Load the list of users reported today with their dates"""
|
||||
if os.path.exists(REPORTED_TODAY_FILE):
|
||||
try:
|
||||
with open(REPORTED_TODAY_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load reported_today.json: {e}")
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def _save_reported_today(self, reported: Dict[str, str]):
|
||||
"""Save the list of users reported today"""
|
||||
try:
|
||||
with open(REPORTED_TODAY_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(reported, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save reported_today.json: {e}")
|
||||
|
||||
def _clean_old_reports(self, reported: Dict[str, str]) -> Dict[str, str]:
|
||||
"""Remove entries from reported_today that are older than 24 hours"""
|
||||
now = datetime.now()
|
||||
cleaned = {}
|
||||
|
||||
for user_id, date_str in reported.items():
|
||||
try:
|
||||
report_date = datetime.fromisoformat(date_str)
|
||||
if now - report_date < timedelta(hours=24):
|
||||
cleaned[user_id] = date_str
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to parse date for user {user_id}: {e}")
|
||||
|
||||
return cleaned
|
||||
|
||||
def has_been_reported_today(self, user_id: int) -> bool:
|
||||
"""Check if a user has been reported in the last 24 hours"""
|
||||
reported = self._load_reported_today()
|
||||
reported = self._clean_old_reports(reported)
|
||||
return str(user_id) in reported
|
||||
|
||||
def mark_as_reported(self, user_id: int):
|
||||
"""Mark a user as having been reported"""
|
||||
reported = self._load_reported_today()
|
||||
reported = self._clean_old_reports(reported)
|
||||
reported[str(user_id)] = datetime.now().isoformat()
|
||||
self._save_reported_today(reported)
|
||||
|
||||
def _get_recent_messages(self, user_id: int, hours: int = 24) -> List[Dict]:
|
||||
"""Get recent messages from a user within the specified hours"""
|
||||
logs = dm_logger._load_user_logs(user_id)
|
||||
|
||||
if not logs or not logs.get("conversations"):
|
||||
return []
|
||||
|
||||
cutoff_time = datetime.now() - timedelta(hours=hours)
|
||||
recent_messages = []
|
||||
|
||||
for msg in logs["conversations"]:
|
||||
try:
|
||||
msg_time = datetime.fromisoformat(msg["timestamp"])
|
||||
if msg_time >= cutoff_time:
|
||||
recent_messages.append(msg)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to parse message timestamp: {e}")
|
||||
|
||||
return recent_messages
|
||||
|
||||
def _format_messages_for_analysis(self, messages: List[Dict], username: str) -> str:
|
||||
"""Format messages into a readable format for the LLM"""
|
||||
formatted = []
|
||||
|
||||
for msg in messages:
|
||||
timestamp = msg.get("timestamp", "Unknown time")
|
||||
is_bot = msg.get("is_bot_message", False)
|
||||
content = msg.get("content", "")
|
||||
|
||||
if is_bot:
|
||||
formatted.append(f"[{timestamp}] Miku: {content}")
|
||||
else:
|
||||
formatted.append(f"[{timestamp}] {username}: {content}")
|
||||
|
||||
return "\n".join(formatted)
|
||||
|
||||
async def analyze_user_interaction(self, user_id: int) -> Optional[Dict]:
|
||||
"""
|
||||
Analyze a user's interactions with Miku
|
||||
|
||||
Returns:
|
||||
Dict with analysis results or None if no messages to analyze
|
||||
"""
|
||||
# Get user info
|
||||
logs = dm_logger._load_user_logs(user_id)
|
||||
username = logs.get("username", "Unknown User")
|
||||
|
||||
# Get recent messages
|
||||
recent_messages = self._get_recent_messages(user_id, hours=24)
|
||||
|
||||
if not recent_messages:
|
||||
print(f"📊 No recent messages from user {username} ({user_id})")
|
||||
return None
|
||||
|
||||
# Count user messages only (not bot responses)
|
||||
user_messages = [msg for msg in recent_messages if not msg.get("is_bot_message", False)]
|
||||
|
||||
if len(user_messages) < 3: # Minimum threshold for analysis
|
||||
print(f"📊 Not enough messages from user {username} ({user_id}) for analysis")
|
||||
return None
|
||||
|
||||
# Format messages for analysis
|
||||
conversation_text = self._format_messages_for_analysis(recent_messages, username)
|
||||
|
||||
# Create analysis prompt
|
||||
analysis_prompt = f"""You are Hatsune Miku, a virtual idol who chats with fans in Discord DMs.
|
||||
|
||||
Analyze the following conversation from the last 24 hours with a user named "{username}".
|
||||
|
||||
Evaluate how this user has treated you based on:
|
||||
- **Positive behaviors**: Kindness, affection, respect, genuine interest, compliments, supportive messages, love
|
||||
- **Negative behaviors**: Rudeness, harassment, inappropriate requests, threats, abuse, disrespect, mean comments
|
||||
|
||||
Provide your analysis in this exact JSON format:
|
||||
{{
|
||||
"overall_sentiment": "positive|neutral|negative",
|
||||
"sentiment_score": <number from -10 (very negative) to +10 (very positive)>,
|
||||
"key_behaviors": ["list", "of", "notable", "behaviors"],
|
||||
"your_feelings": "How you (Miku) feel about this interaction in 1-2 sentences, in your own voice",
|
||||
"notable_moment": "A specific quote or moment that stands out (if any)",
|
||||
"should_report": true
|
||||
}}
|
||||
|
||||
Set "should_report" to true (always report all interactions to the bot owner).
|
||||
|
||||
Conversation:
|
||||
{conversation_text}
|
||||
|
||||
Respond ONLY with the JSON object, no other text."""
|
||||
|
||||
# Query the LLM
|
||||
try:
|
||||
response = await query_ollama(
|
||||
analysis_prompt,
|
||||
user_id=f"analyzer-{user_id}",
|
||||
guild_id=None,
|
||||
response_type="dm_analysis"
|
||||
)
|
||||
|
||||
print(f"📊 Raw LLM response for {username}:\n{response}\n")
|
||||
|
||||
# Parse JSON response
|
||||
# Remove markdown code blocks if present
|
||||
cleaned_response = response.strip()
|
||||
if "```json" in cleaned_response:
|
||||
cleaned_response = cleaned_response.split("```json")[1].split("```")[0].strip()
|
||||
elif "```" in cleaned_response:
|
||||
cleaned_response = cleaned_response.split("```")[1].split("```")[0].strip()
|
||||
|
||||
# Remove any leading/trailing text before/after JSON
|
||||
# Find the first { and last }
|
||||
start_idx = cleaned_response.find('{')
|
||||
end_idx = cleaned_response.rfind('}')
|
||||
|
||||
if start_idx != -1 and end_idx != -1:
|
||||
cleaned_response = cleaned_response[start_idx:end_idx+1]
|
||||
|
||||
print(f"📊 Cleaned JSON for {username}:\n{cleaned_response}\n")
|
||||
|
||||
analysis = json.loads(cleaned_response)
|
||||
|
||||
# Add metadata
|
||||
analysis["user_id"] = user_id
|
||||
analysis["username"] = username
|
||||
analysis["analyzed_at"] = datetime.now().isoformat()
|
||||
analysis["message_count"] = len(user_messages)
|
||||
|
||||
return analysis
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"⚠️ JSON parse error for user {username}: {e}")
|
||||
print(f"⚠️ Failed response: {response}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to analyze interaction for user {username}: {e}")
|
||||
return None
|
||||
|
||||
def _save_report(self, user_id: int, analysis: Dict) -> str:
|
||||
"""Save an analysis report to a file"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"{user_id}_{timestamp}.json"
|
||||
filepath = os.path.join(REPORTS_DIR, filename)
|
||||
|
||||
try:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(analysis, f, indent=2, ensure_ascii=False)
|
||||
print(f"💾 Saved report: {filepath}")
|
||||
return filepath
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save report: {e}")
|
||||
return ""
|
||||
|
||||
async def _send_report_to_owner(self, analysis: Dict):
|
||||
"""Send the analysis report to the bot owner"""
|
||||
try:
|
||||
# Ensure we're using the Discord client's event loop
|
||||
if not globals.client or not globals.client.is_ready():
|
||||
print(f"⚠️ Discord client not ready, cannot send report")
|
||||
return
|
||||
|
||||
owner = await globals.client.fetch_user(self.owner_user_id)
|
||||
|
||||
sentiment = analysis.get("overall_sentiment", "neutral")
|
||||
score = analysis.get("sentiment_score", 0)
|
||||
username = analysis.get("username", "Unknown User")
|
||||
user_id = analysis.get("user_id", "Unknown")
|
||||
feelings = analysis.get("your_feelings", "")
|
||||
notable_moment = analysis.get("notable_moment", "")
|
||||
message_count = analysis.get("message_count", 0)
|
||||
|
||||
# Create embed based on sentiment
|
||||
if sentiment == "positive" or score >= 5:
|
||||
color = discord.Color.green()
|
||||
title = f"💚 Positive Interaction Report: {username}"
|
||||
emoji = "😊"
|
||||
elif sentiment == "negative" or score <= -3:
|
||||
color = discord.Color.red()
|
||||
title = f"💔 Negative Interaction Report: {username}"
|
||||
emoji = "😢"
|
||||
else:
|
||||
color = discord.Color.blue()
|
||||
title = f"📊 Interaction Report: {username}"
|
||||
emoji = "😐"
|
||||
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
description=f"{emoji} **My feelings about this interaction:**\n{feelings}",
|
||||
color=color,
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="User Information",
|
||||
value=f"**Username:** {username}\n**User ID:** {user_id}\n**Messages (24h):** {message_count}",
|
||||
inline=False
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="Sentiment Analysis",
|
||||
value=f"**Overall:** {sentiment.capitalize()}\n**Score:** {score}/10",
|
||||
inline=True
|
||||
)
|
||||
|
||||
if notable_moment:
|
||||
embed.add_field(
|
||||
name="Notable Moment",
|
||||
value=f"_{notable_moment}_",
|
||||
inline=False
|
||||
)
|
||||
|
||||
behaviors = analysis.get("key_behaviors", [])
|
||||
if behaviors:
|
||||
embed.add_field(
|
||||
name="Key Behaviors",
|
||||
value="\n".join([f"• {behavior}" for behavior in behaviors[:5]]),
|
||||
inline=False
|
||||
)
|
||||
|
||||
await owner.send(embed=embed)
|
||||
print(f"📤 Report sent to owner for user {username}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to send report to owner: {e}")
|
||||
|
||||
async def analyze_and_report(self, user_id: int) -> bool:
|
||||
"""
|
||||
Analyze a user's interaction and report to owner if significant
|
||||
|
||||
Returns:
|
||||
True if analysis was performed and reported, False otherwise
|
||||
"""
|
||||
# Check if already reported today
|
||||
if self.has_been_reported_today(user_id):
|
||||
print(f"📊 User {user_id} already reported today, skipping")
|
||||
return False
|
||||
|
||||
# Analyze interaction
|
||||
analysis = await self.analyze_user_interaction(user_id)
|
||||
|
||||
if not analysis:
|
||||
return False
|
||||
|
||||
# Always report (removed threshold check - owner wants all reports)
|
||||
# Save report
|
||||
self._save_report(user_id, analysis)
|
||||
|
||||
# Send to owner
|
||||
await self._send_report_to_owner(analysis)
|
||||
|
||||
# Mark as reported
|
||||
self.mark_as_reported(user_id)
|
||||
|
||||
return True
|
||||
|
||||
async def run_daily_analysis(self):
|
||||
"""Run analysis on all DM users and report significant interactions"""
|
||||
print("📊 Starting daily DM interaction analysis...")
|
||||
|
||||
# Get all DM users
|
||||
all_users = dm_logger.get_all_dm_users()
|
||||
|
||||
if not all_users:
|
||||
print("📊 No DM users to analyze")
|
||||
return
|
||||
|
||||
reported_count = 0
|
||||
analyzed_count = 0
|
||||
|
||||
for user_summary in all_users:
|
||||
try:
|
||||
user_id = int(user_summary["user_id"])
|
||||
|
||||
# Skip if already reported today
|
||||
if self.has_been_reported_today(user_id):
|
||||
continue
|
||||
|
||||
# Analyze and potentially report
|
||||
result = await self.analyze_and_report(user_id)
|
||||
|
||||
if result:
|
||||
reported_count += 1
|
||||
analyzed_count += 1
|
||||
# Only report one user per run to avoid spam
|
||||
break
|
||||
else:
|
||||
analyzed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to process user {user_summary.get('username', 'Unknown')}: {e}")
|
||||
|
||||
print(f"📊 Daily analysis complete: Analyzed {analyzed_count} users, reported {reported_count}")
|
||||
|
||||
|
||||
# Global instance (will be initialized with owner ID)
|
||||
dm_analyzer: Optional[DMInteractionAnalyzer] = None
|
||||
|
||||
def init_dm_analyzer(owner_user_id: int):
|
||||
"""Initialize the DM analyzer with owner user ID"""
|
||||
global dm_analyzer
|
||||
dm_analyzer = DMInteractionAnalyzer(owner_user_id)
|
||||
return dm_analyzer
|
||||
Reference in New Issue
Block a user