""" Advanced analytics for LangCode Agent Provides usage analytics, insights, and reporting """ import os import sys import json import time from datetime import datetime, timedelta from typing import Dict, List, Any, Optional from pathlib import Path from collections import defaultdict, Counter # Add current directory to path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) class AnalyticsEngine: """Advanced analytics engine for LangCode Agent""" def __init__(self, data_dir: Optional[str] = None): """ Initialize analytics engine Args: data_dir: Directory for storing analytics data """ if data_dir is None: self.data_dir = Path.home() / ".langcode" / "analytics" else: self.data_dir = Path(data_dir) # Create data directory self.data_dir.mkdir(parents=True, exist_ok=True) # Analytics data self.usage_data = { "conversations": [], "tool_usage": defaultdict(int), "provider_usage": defaultdict(int), "command_usage": defaultdict(int), "error_patterns": defaultdict(int), "response_times": [], "user_sessions": [] } # Load existing data self._load_data() def _load_data(self): """Load analytics data from file""" data_file = self.data_dir / "analytics_data.json" if data_file.exists(): try: with open(data_file, 'r') as f: data = json.load(f) # Convert defaultdicts back self.usage_data["tool_usage"] = defaultdict(int, data.get("tool_usage", {})) self.usage_data["provider_usage"] = defaultdict(int, data.get("provider_usage", {})) self.usage_data["command_usage"] = defaultdict(int, data.get("command_usage", {})) self.usage_data["error_patterns"] = defaultdict(int, data.get("error_patterns", {})) self.usage_data["conversations"] = data.get("conversations", []) self.usage_data["response_times"] = data.get("response_times", []) self.usage_data["user_sessions"] = data.get("user_sessions", []) except Exception as e: print(f"Warning: Could not load analytics data: {e}") def _save_data(self): """Save analytics data to file""" data_file = self.data_dir / "analytics_data.json" try: # Convert defaultdicts to regular dicts for JSON serialization data_to_save = { "conversations": self.usage_data["conversations"], "tool_usage": dict(self.usage_data["tool_usage"]), "provider_usage": dict(self.usage_data["provider_usage"]), "command_usage": dict(self.usage_data["command_usage"]), "error_patterns": dict(self.usage_data["error_patterns"]), "response_times": self.usage_data["response_times"], "user_sessions": self.usage_data["user_sessions"] } with open(data_file, 'w') as f: json.dump(data_to_save, f, indent=2) except Exception as e: print(f"Warning: Could not save analytics data: {e}") def record_conversation(self, user_message: str, agent_response: str, provider: str, model: str, response_time: float): """Record a conversation for analytics""" conversation = { "timestamp": datetime.now().isoformat(), "user_message": user_message[:100], # Truncate for privacy "agent_response": agent_response[:100], # Truncate for privacy "provider": provider, "model": model, "response_time": response_time, "message_length": len(user_message), "response_length": len(agent_response) } self.usage_data["conversations"].append(conversation) self.usage_data["response_times"].append(response_time) # Keep only last 1000 conversations if len(self.usage_data["conversations"]) > 1000: self.usage_data["conversations"] = self.usage_data["conversations"][-1000:] # Keep only last 5000 response times if len(self.usage_data["response_times"]) > 5000: self.usage_data["response_times"] = self.usage_data["response_times"][-5000:] self._save_data() def record_tool_usage(self, tool_name: str, success: bool, duration: float): """Record tool usage""" self.usage_data["tool_usage"][tool_name] += 1 self._save_data() def record_provider_usage(self, provider: str, model: str): """Record provider usage""" key = f"{provider}:{model}" self.usage_data["provider_usage"][key] += 1 self._save_data() def record_command_usage(self, command: str): """Record command usage""" self.usage_data["command_usage"][command] += 1 self._save_data() def record_error(self, error_type: str, error_message: str): """Record error patterns""" # Simplify error message for pattern recognition simplified = error_message[:50].lower() self.usage_data["error_patterns"][simplified] += 1 self._save_data() def record_user_session(self, session_id: str, duration: float, messages_count: int, tools_used: List[str]): """Record user session""" session = { "session_id": session_id, "timestamp": datetime.now().isoformat(), "duration": duration, "messages_count": messages_count, "tools_used": tools_used, "tools_count": len(tools_used) } self.usage_data["user_sessions"].append(session) # Keep only last 100 sessions if len(self.usage_data["user_sessions"]) > 100: self.usage_data["user_sessions"] = self.usage_data["user_sessions"][-100:] self._save_data() def get_usage_summary(self, days: int = 30) -> Dict[str, Any]: """ Get usage summary for the last N days Args: days: Number of days to include Returns: Dictionary with usage summary """ cutoff_time = datetime.now() - timedelta(days=days) # Filter recent conversations recent_conversations = [ c for c in self.usage_data["conversations"] if datetime.fromisoformat(c["timestamp"]) > cutoff_time ] # Calculate statistics total_conversations = len(recent_conversations) total_messages = sum(c.get("message_length", 0) for c in recent_conversations) total_responses = sum(c.get("response_length", 0) for c in recent_conversations) # Provider usage provider_counts = Counter() for conv in recent_conversations: provider = conv.get("provider", "unknown") provider_counts[provider] += 1 # Model usage model_counts = Counter() for conv in recent_conversations: model = conv.get("model", "unknown") model_counts[model] += 1 # Response time statistics recent_response_times = [ c.get("response_time", 0) for c in recent_conversations if c.get("response_time") ] if recent_response_times: avg_response_time = sum(recent_response_times) / len(recent_response_times) min_response_time = min(recent_response_times) max_response_time = max(recent_response_times) else: avg_response_time = min_response_time = max_response_time = 0 # Tool usage tool_usage = dict(self.usage_data["tool_usage"]) # Command usage command_usage = dict(self.usage_data["command_usage"]) # Error patterns error_patterns = dict(self.usage_data["error_patterns"]) # Session statistics recent_sessions = [ s for s in self.usage_data["user_sessions"] if datetime.fromisoformat(s["timestamp"]) > cutoff_time ] total_sessions = len(recent_sessions) total_session_duration = sum(s.get("duration", 0) for s in recent_sessions) avg_session_duration = total_session_duration / total_sessions if total_sessions > 0 else 0 return { "period_days": days, "conversations": { "total": total_conversations, "total_messages_chars": total_messages, "total_responses_chars": total_responses, "avg_message_length": total_messages / total_conversations if total_conversations > 0 else 0, "avg_response_length": total_responses / total_conversations if total_conversations > 0 else 0 }, "providers": { "usage": dict(provider_counts), "most_used": provider_counts.most_common(1)[0][0] if provider_counts else "None" }, "models": { "usage": dict(model_counts), "most_used": model_counts.most_common(1)[0][0] if model_counts else "None" }, "performance": { "avg_response_time": avg_response_time, "min_response_time": min_response_time, "max_response_time": max_response_time, "total_response_time": sum(recent_response_times) }, "tools": { "usage": tool_usage, "most_used": max(tool_usage.items(), key=lambda x: x[1])[0] if tool_usage else "None" }, "commands": { "usage": command_usage, "most_used": max(command_usage.items(), key=lambda x: x[1])[0] if command_usage else "None" }, "errors": { "patterns": error_patterns, "most_common": max(error_patterns.items(), key=lambda x: x[1])[0] if error_patterns else "None" }, "sessions": { "total": total_sessions, "total_duration": total_session_duration, "avg_duration": avg_session_duration, "avg_messages_per_session": sum(s.get("messages_count", 0) for s in recent_sessions) / total_sessions if total_sessions > 0 else 0 }, "generated_at": datetime.now().isoformat() } def get_detailed_report(self, days: int = 30) -> str: """ Get detailed analytics report Args: days: Number of days to include Returns: Formatted report string """ summary = self.get_usage_summary(days) report = f"📊 Advanced Analytics Report (Last {days} days)\n" report += "=" * 60 + "\n\n" # Conversation statistics report += "💬 Conversation Statistics:\n" report += f" Total Conversations: {summary['conversations']['total']}\n" report += f" Total Message Characters: {summary['conversations']['total_messages_chars']:,}\n" report += f" Total Response Characters: {summary['conversations']['total_responses_chars']:,}\n" report += f" Average Message Length: {summary['conversations']['avg_message_length']:.0f} chars\n" report += f" Average Response Length: {summary['conversations']['avg_response_length']:.0f} chars\n\n" # Provider usage report += "🤖 Provider Usage:\n" for provider, count in summary['providers']['usage'].items(): percentage = (count / summary['conversations']['total'] * 100) if summary['conversations']['total'] > 0 else 0 report += f" {provider}: {count} ({percentage:.1f}%)\n" report += f" Most Used: {summary['providers']['most_used']}\n\n" # Model usage report += "🧠 Model Usage:\n" for model, count in summary['models']['usage'].items(): percentage = (count / summary['conversations']['total'] * 100) if summary['conversations']['total'] > 0 else 0 report += f" {model}: {count} ({percentage:.1f}%)\n" report += f" Most Used: {summary['models']['most_used']}\n\n" # Performance metrics report += "⚡ Performance Metrics:\n" report += f" Average Response Time: {summary['performance']['avg_response_time']:.3f}s\n" report += f" Minimum Response Time: {summary['performance']['min_response_time']:.3f}s\n" report += f" Maximum Response Time: {summary['performance']['max_response_time']:.3f}s\n" report += f" Total Response Time: {summary['performance']['total_response_time']:.1f}s\n\n" # Tool usage report += "🔧 Tool Usage:\n" if summary['tools']['usage']: for tool, count in sorted(summary['tools']['usage'].items(), key=lambda x: x[1], reverse=True)[:10]: report += f" {tool}: {count}\n" report += f" Most Used: {summary['tools']['most_used']}\n" else: report += " No tool usage recorded\n" report += "\n" # Command usage report += "⌨️ Command Usage:\n" if summary['commands']['usage']: for command, count in sorted(summary['commands']['usage'].items(), key=lambda x: x[1], reverse=True)[:10]: report += f" {command}: {count}\n" report += f" Most Used: {summary['commands']['most_used']}\n" else: report += " No command usage recorded\n" report += "\n" # Error patterns report += "❌ Error Patterns:\n" if summary['errors']['patterns']: for pattern, count in sorted(summary['errors']['patterns'].items(), key=lambda x: x[1], reverse=True)[:5]: report += f" {pattern}: {count}\n" report += f" Most Common: {summary['errors']['most_common']}\n" else: report += " No errors recorded\n" report += "\n" # Session statistics report += "👥 Session Statistics:\n" report += f" Total Sessions: {summary['sessions']['total']}\n" report += f" Total Session Duration: {summary['sessions']['total_duration']:.1f}s\n" report += f" Average Session Duration: {summary['sessions']['avg_duration']:.1f}s\n" report += f" Average Messages per Session: {summary['sessions']['avg_messages_per_session']:.1f}\n" return report def get_insights(self, days: int = 30) -> List[str]: """ Get insights and recommendations based on usage patterns Args: days: Number of days to analyze Returns: List of insights and recommendations """ summary = self.get_usage_summary(days) insights = [] # Provider insights if summary['providers']['usage']: most_used_provider = summary['providers']['most_used'] provider_count = summary['providers']['usage'].get(most_used_provider, 0) total_conversations = summary['conversations']['total'] if total_conversations > 0: provider_percentage = (provider_count / total_conversations) * 100 if provider_percentage > 80: insights.append(f"💡 You're heavily using {most_used_provider} ({provider_percentage:.0f}% of conversations). Consider diversifying with other providers for different use cases.") elif provider_percentage < 20: insights.append(f"💡 You're barely using {most_used_provider} ({provider_percentage:.0f}% of conversations). Consider if this provider is meeting your needs.") # Performance insights avg_response_time = summary['performance']['avg_response_time'] if avg_response_time > 5.0: insights.append(f"⚠️ Average response time is high ({avg_response_time:.1f}s). Consider using faster models or optimizing your queries.") elif avg_response_time < 1.0: insights.append(f"✅ Great response times! Average is {avg_response_time:.1f}s.") # Tool usage insights if summary['tools']['usage']: most_used_tool = summary['tools']['most_used'] tool_count = summary['tools']['usage'].get(most_used_tool, 0) insights.append(f"🔧 You frequently use the '{most_used_tool}' tool ({tool_count} times). Consider creating shortcuts or aliases for it.") # Error insights if summary['errors']['patterns']: most_common_error = summary['errors']['most_common'] error_count = summary['errors']['patterns'].get(most_common_error, 0) if error_count > 5: insights.append(f"❌ You're encountering a recurring error: '{most_common_error}' ({error_count} times). Consider investigating the root cause.") # Session insights avg_session_duration = summary['sessions']['avg_duration'] if avg_session_duration > 3600: # More than 1 hour insights.append(f"⏱️ Your average session is long ({avg_session_duration/60:.0f} minutes). Consider breaking work into shorter sessions for better focus.") # Usage pattern insights total_conversations = summary['conversations']['total'] if total_conversations > 100: insights.append(f"📈 You're a power user with {total_conversations} conversations in {days} days!") elif total_conversations < 10: insights.append(f"💡 You've only had {total_conversations} conversations in {days} days. Consider exploring more features.") # Default insight if none generated if not insights: insights.append("📊 Your usage patterns look normal. Keep using LangCode Agent!") return insights def clear_old_data(self, days: int = 90): """ Clear data older than specified days Args: days: Number of days to keep data """ cutoff_time = datetime.now() - timedelta(days=days) # Filter out old conversations self.usage_data["conversations"] = [ c for c in self.usage_data["conversations"] if datetime.fromisoformat(c["timestamp"]) > cutoff_time ] # Filter out old sessions self.usage_data["user_sessions"] = [ s for s in self.usage_data["user_sessions"] if datetime.fromisoformat(s["timestamp"]) > cutoff_time ] # Save cleaned data self._save_data() print(f"✅ Cleared analytics data older than {days} days") # Global analytics engine instance analytics_engine = AnalyticsEngine() def get_usage_summary(days: int = 30) -> Dict[str, Any]: """ Get usage summary Args: days: Number of days to include Returns: Usage summary dictionary """ return analytics_engine.get_usage_summary(days) def get_analytics_report(days: int = 30) -> str: """ Get formatted analytics report Args: days: Number of days to include Returns: Formatted report string """ return analytics_engine.get_detailed_report(days) def get_analytics_insights(days: int = 30) -> List[str]: """ Get analytics insights and recommendations Args: days: Number of days to analyze Returns: List of insights and recommendations """ return analytics_engine.get_insights(days) if __name__ == "__main__": # Print analytics report when run directly print(get_analytics_report()) print("\n" + "=" * 60) print("💡 Insights and Recommendations:") for insight in get_analytics_insights(): print(f" {insight}")