Files
miku-discord/bot/utils/dm_interaction_analyzer.py
koko210Serve d58be3b33e Remove all Ollama remnants and complete migration to llama.cpp
- Remove Ollama-specific files (Dockerfile.ollama, entrypoint.sh)
- Replace all query_ollama imports and calls with query_llama
- Remove langchain-ollama dependency from requirements.txt
- Update all utility files (autonomous, kindness, image_generation, etc.)
- Update README.md documentation references
- Maintain backward compatibility alias in llm.py
2025-12-07 17:50:28 +02:00

379 lines
14 KiB
Python

"""
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_llama
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_llama(
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