""" 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": , "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