LiamKhoaLe commited on
Commit
5a24119
·
1 Parent(s): b2520cd

Upd analytis

Browse files
app.py CHANGED
@@ -11,6 +11,7 @@ import routes.reports as _routes_report
11
  import routes.chats as _routes_chat
12
  import routes.sessions as _routes_sessions
13
  import routes.health as _routes_health
 
14
 
15
  # Local dev
16
  # if __name__ == "__main__":
 
11
  import routes.chats as _routes_chat
12
  import routes.sessions as _routes_sessions
13
  import routes.health as _routes_health
14
+ import routes.analytics as _routes_analytics
15
 
16
  # Local dev
17
  # if __name__ == "__main__":
helpers/setup.py CHANGED
@@ -11,6 +11,7 @@ from utils.api.rotator import APIKeyRotator
11
  from utils.ingestion.caption import BlipCaptioner
12
  from utils.rag.embeddings import EmbeddingClient
13
  from utils.rag.rag import RAGStore, ensure_indexes
 
14
 
15
 
16
  # ────────────────────────────── App Setup ──────────────────────────────
@@ -49,6 +50,10 @@ try:
49
  logger.info("[APP] MongoDB connection successful")
50
  ensure_indexes(rag)
51
  logger.info("[APP] MongoDB indexes ensured")
 
 
 
 
52
  except Exception as e:
53
  logger.error(f"[APP] Failed to initialize MongoDB/RAG store: {str(e)}")
54
  logger.error(f"[APP] MONGO_URI: {os.getenv('MONGO_URI', 'Not set')}")
 
11
  from utils.ingestion.caption import BlipCaptioner
12
  from utils.rag.embeddings import EmbeddingClient
13
  from utils.rag.rag import RAGStore, ensure_indexes
14
+ from utils.analytics import init_analytics
15
 
16
 
17
  # ────────────────────────────── App Setup ──────────────────────────────
 
50
  logger.info("[APP] MongoDB connection successful")
51
  ensure_indexes(rag)
52
  logger.info("[APP] MongoDB indexes ensured")
53
+
54
+ # Initialize analytics tracker
55
+ init_analytics(rag.client, os.getenv("MONGO_DB", "studybuddy"))
56
+ logger.info("[APP] Analytics tracker initialized")
57
  except Exception as e:
58
  logger.error(f"[APP] Failed to initialize MongoDB/RAG store: {str(e)}")
59
  logger.error(f"[APP] MONGO_URI: {os.getenv('MONGO_URI', 'Not set')}")
routes/analytics.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ────────────────────────────── routes/analytics.py ──────────────────────────────
2
+ """
3
+ Analytics API Routes
4
+
5
+ Provides endpoints for retrieving user and global analytics data.
6
+ """
7
+
8
+ from fastapi import HTTPException, Query
9
+ from typing import Optional
10
+ from helpers.setup import app, logger
11
+ from utils.analytics import get_analytics_tracker
12
+ from helpers.models import BaseResponse
13
+
14
+
15
+ class AnalyticsResponse(BaseResponse):
16
+ """Response model for analytics data."""
17
+ pass
18
+
19
+
20
+ @app.get("/analytics/user", response_model=AnalyticsResponse)
21
+ async def get_user_analytics(
22
+ user_id: str = Query(..., description="User ID to get analytics for"),
23
+ days: int = Query(30, description="Number of days to include in analytics", ge=1, le=365)
24
+ ):
25
+ """Get analytics data for a specific user."""
26
+ try:
27
+ tracker = get_analytics_tracker()
28
+ if not tracker:
29
+ raise HTTPException(500, detail="Analytics tracker not initialized")
30
+
31
+ analytics_data = await tracker.get_user_analytics(user_id, days)
32
+
33
+ return AnalyticsResponse(
34
+ success=True,
35
+ data=analytics_data,
36
+ message=f"Analytics data retrieved for user {user_id}"
37
+ )
38
+
39
+ except Exception as e:
40
+ logger.error(f"[ANALYTICS] Failed to get user analytics: {e}")
41
+ raise HTTPException(500, detail=f"Failed to retrieve analytics: {str(e)}")
42
+
43
+
44
+ @app.get("/analytics/global", response_model=AnalyticsResponse)
45
+ async def get_global_analytics(
46
+ days: int = Query(30, description="Number of days to include in analytics", ge=1, le=365)
47
+ ):
48
+ """Get global analytics data across all users."""
49
+ try:
50
+ tracker = get_analytics_tracker()
51
+ if not tracker:
52
+ raise HTTPException(500, detail="Analytics tracker not initialized")
53
+
54
+ analytics_data = await tracker.get_global_analytics(days)
55
+
56
+ return AnalyticsResponse(
57
+ success=True,
58
+ data=analytics_data,
59
+ message="Global analytics data retrieved"
60
+ )
61
+
62
+ except Exception as e:
63
+ logger.error(f"[ANALYTICS] Failed to get global analytics: {e}")
64
+ raise HTTPException(500, detail=f"Failed to retrieve global analytics: {str(e)}")
65
+
66
+
67
+ @app.post("/analytics/cleanup", response_model=AnalyticsResponse)
68
+ async def cleanup_analytics(
69
+ days_to_keep: int = Query(90, description="Number of days of data to keep", ge=30, le=365)
70
+ ):
71
+ """Clean up old analytics data."""
72
+ try:
73
+ tracker = get_analytics_tracker()
74
+ if not tracker:
75
+ raise HTTPException(500, detail="Analytics tracker not initialized")
76
+
77
+ deleted_count = await tracker.cleanup_old_data(days_to_keep)
78
+
79
+ return AnalyticsResponse(
80
+ success=True,
81
+ data={"deleted_records": deleted_count},
82
+ message=f"Cleaned up {deleted_count} old analytics records"
83
+ )
84
+
85
+ except Exception as e:
86
+ logger.error(f"[ANALYTICS] Failed to cleanup analytics: {e}")
87
+ raise HTTPException(500, detail=f"Failed to cleanup analytics: {str(e)}")
routes/chats.py CHANGED
@@ -10,6 +10,7 @@ from utils.service.common import trim_text
10
  from .search import build_web_context
11
  # Removed: enhance_question_with_memory - now handled by conversation manager
12
  from utils.api.router import select_model, generate_answer_with_model
 
13
 
14
 
15
  @app.post("/chat/save", response_model=MessageResponse)
@@ -708,6 +709,24 @@ async def _chat_impl(
708
  update_chat_status(session_id, "generating", "Generating answer...", 80)
709
 
710
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
  answer = await generate_answer_with_model(
712
  selection=selection,
713
  system_prompt=system_prompt,
@@ -888,6 +907,24 @@ async def chat_with_search(
888
  selection = select_model(question=question, context=doc_context)
889
  logger.info(f"[CHAT] Generating web-augmented answer with {selection['provider']} {selection['model']}")
890
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
891
  answer = await generate_answer_with_model(
892
  selection=selection,
893
  system_prompt=system_prompt,
 
10
  from .search import build_web_context
11
  # Removed: enhance_question_with_memory - now handled by conversation manager
12
  from utils.api.router import select_model, generate_answer_with_model
13
+ from utils.analytics import get_analytics_tracker
14
 
15
 
16
  @app.post("/chat/save", response_model=MessageResponse)
 
709
  update_chat_status(session_id, "generating", "Generating answer...", 80)
710
 
711
  try:
712
+ # Track model usage for analytics
713
+ tracker = get_analytics_tracker()
714
+ if tracker:
715
+ await tracker.track_model_usage(
716
+ user_id=user_id,
717
+ model_name=selection["model"],
718
+ provider=selection["provider"],
719
+ context="chat",
720
+ metadata={"project_id": project_id, "session_id": session_id}
721
+ )
722
+ await tracker.track_agent_usage(
723
+ user_id=user_id,
724
+ agent_name="chat",
725
+ action="generate_answer",
726
+ context="chat",
727
+ metadata={"project_id": project_id, "session_id": session_id}
728
+ )
729
+
730
  answer = await generate_answer_with_model(
731
  selection=selection,
732
  system_prompt=system_prompt,
 
907
  selection = select_model(question=question, context=doc_context)
908
  logger.info(f"[CHAT] Generating web-augmented answer with {selection['provider']} {selection['model']}")
909
  try:
910
+ # Track model usage for analytics
911
+ tracker = get_analytics_tracker()
912
+ if tracker:
913
+ await tracker.track_model_usage(
914
+ user_id=user_id,
915
+ model_name=selection["model"],
916
+ provider=selection["provider"],
917
+ context="chat_web_augmented",
918
+ metadata={"project_id": project_id, "session_id": session_id}
919
+ )
920
+ await tracker.track_agent_usage(
921
+ user_id=user_id,
922
+ agent_name="chat",
923
+ action="generate_web_augmented_answer",
924
+ context="chat_web_augmented",
925
+ metadata={"project_id": project_id, "session_id": session_id}
926
+ )
927
+
928
  answer = await generate_answer_with_model(
929
  selection=selection,
930
  system_prompt=system_prompt,
routes/reports.py CHANGED
@@ -10,6 +10,7 @@ from .search import build_web_context
10
  from helpers.models import ReportResponse, StatusUpdateResponse
11
  from utils.service.common import trim_text
12
  from utils.api.router import select_model, generate_answer_with_model
 
13
  from helpers.coder import generate_code_artifacts, extract_structured_code
14
  from helpers.diagram import should_generate_mermaid, generate_mermaid_diagram
15
 
@@ -123,6 +124,18 @@ async def generate_report(
123
  # Step 1: Chain of Thought Planning with NVIDIA
124
  logger.info("[REPORT] Starting CoT planning phase")
125
  update_report_status(session_id, "planning", "Planning action...", 25)
 
 
 
 
 
 
 
 
 
 
 
 
126
  # Use enhanced instructions for better CoT planning
127
  cot_plan = await generate_cot_plan(enhanced_instructions, file_summary, context_text, web_context_block, nvidia_rotator, gemini_rotator)
128
 
 
10
  from helpers.models import ReportResponse, StatusUpdateResponse
11
  from utils.service.common import trim_text
12
  from utils.api.router import select_model, generate_answer_with_model
13
+ from utils.analytics import get_analytics_tracker
14
  from helpers.coder import generate_code_artifacts, extract_structured_code
15
  from helpers.diagram import should_generate_mermaid, generate_mermaid_diagram
16
 
 
124
  # Step 1: Chain of Thought Planning with NVIDIA
125
  logger.info("[REPORT] Starting CoT planning phase")
126
  update_report_status(session_id, "planning", "Planning action...", 25)
127
+
128
+ # Track report agent usage
129
+ tracker = get_analytics_tracker()
130
+ if tracker:
131
+ await tracker.track_agent_usage(
132
+ user_id=user_id,
133
+ agent_name="report",
134
+ action="generate_report",
135
+ context="report_generation",
136
+ metadata={"project_id": project_id, "session_id": session_id, "filename": filename}
137
+ )
138
+
139
  # Use enhanced instructions for better CoT planning
140
  cot_plan = await generate_cot_plan(enhanced_instructions, file_summary, context_text, web_context_block, nvidia_rotator, gemini_rotator)
141
 
routes/search.py CHANGED
@@ -4,6 +4,7 @@ from typing import List, Dict, Any, Tuple
4
  from helpers.setup import logger, embedder, gemini_rotator, nvidia_rotator
5
  from utils.api.router import select_model, generate_answer_with_model, qwen_chat_completion, nvidia_large_chat_completion
6
  from utils.service.summarizer import llama_summarize
 
7
 
8
 
9
  async def extract_search_keywords(user_query: str, nvidia_rotator) -> List[str]:
@@ -28,6 +29,17 @@ Return only the keywords, separated by spaces, no other text."""
28
 
29
  user_prompt = f"User query: {user_query}\n\nExtract search keywords:"
30
 
 
 
 
 
 
 
 
 
 
 
 
31
  # Use NVIDIA Large for better keyword extraction
32
  response = await nvidia_large_chat_completion(sys_prompt, user_prompt, nvidia_rotator)
33
 
 
4
  from helpers.setup import logger, embedder, gemini_rotator, nvidia_rotator
5
  from utils.api.router import select_model, generate_answer_with_model, qwen_chat_completion, nvidia_large_chat_completion
6
  from utils.service.summarizer import llama_summarize
7
+ from utils.analytics import get_analytics_tracker
8
 
9
 
10
  async def extract_search_keywords(user_query: str, nvidia_rotator) -> List[str]:
 
29
 
30
  user_prompt = f"User query: {user_query}\n\nExtract search keywords:"
31
 
32
+ # Track search agent usage
33
+ tracker = get_analytics_tracker()
34
+ if tracker:
35
+ await tracker.track_agent_usage(
36
+ user_id="system", # Search is system-level
37
+ agent_name="search",
38
+ action="extract_keywords",
39
+ context="web_search",
40
+ metadata={"query": user_query}
41
+ )
42
+
43
  # Use NVIDIA Large for better keyword extraction
44
  response = await nvidia_large_chat_completion(sys_prompt, user_prompt, nvidia_rotator)
45
 
static/analytics.js ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ────────────────────────────── static/analytics.js ──────────────────────────────
2
+ (function() {
3
+ // DOM elements
4
+ const analyticsSection = document.getElementById('analytics-section');
5
+ const analyticsPeriod = document.getElementById('analytics-period');
6
+ const refreshAnalytics = document.getElementById('refresh-analytics');
7
+ const modelUsageChart = document.getElementById('model-usage-chart');
8
+ const agentUsageChart = document.getElementById('agent-usage-chart');
9
+ const dailyTrendsChart = document.getElementById('daily-trends-chart');
10
+ const usageSummary = document.getElementById('usage-summary');
11
+
12
+ // State
13
+ let currentAnalyticsData = null;
14
+ let isAnalyticsVisible = false;
15
+
16
+ // Initialize
17
+ init();
18
+
19
+ function init() {
20
+ setupEventListeners();
21
+ // Load analytics when section becomes visible
22
+ document.addEventListener('sectionChanged', (event) => {
23
+ if (event.detail.section === 'analytics') {
24
+ isAnalyticsVisible = true;
25
+ loadAnalytics();
26
+ } else {
27
+ isAnalyticsVisible = false;
28
+ }
29
+ });
30
+ }
31
+
32
+ function setupEventListeners() {
33
+ // Period change
34
+ if (analyticsPeriod) {
35
+ analyticsPeriod.addEventListener('change', () => {
36
+ if (isAnalyticsVisible) {
37
+ loadAnalytics();
38
+ }
39
+ });
40
+ }
41
+
42
+ // Refresh button
43
+ if (refreshAnalytics) {
44
+ refreshAnalytics.addEventListener('click', () => {
45
+ loadAnalytics();
46
+ });
47
+ }
48
+ }
49
+
50
+ async function loadAnalytics() {
51
+ if (!isAnalyticsVisible) return;
52
+
53
+ const user = window.__sb_get_user();
54
+ if (!user) {
55
+ showAnalyticsError('Please sign in to view analytics');
56
+ return;
57
+ }
58
+
59
+ const period = analyticsPeriod ? analyticsPeriod.value : '30';
60
+
61
+ try {
62
+ showAnalyticsLoading();
63
+
64
+ const response = await fetch(`/analytics/user?user_id=${encodeURIComponent(user.user_id)}&days=${period}`);
65
+ if (!response.ok) {
66
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
67
+ }
68
+
69
+ const data = await response.json();
70
+ if (data.success) {
71
+ currentAnalyticsData = data.data;
72
+ renderAnalytics(data.data);
73
+ } else {
74
+ throw new Error(data.message || 'Failed to load analytics');
75
+ }
76
+
77
+ } catch (error) {
78
+ console.error('Analytics loading error:', error);
79
+ showAnalyticsError(`Failed to load analytics: ${error.message}`);
80
+ }
81
+ }
82
+
83
+ function showAnalyticsLoading() {
84
+ const loadingHtml = '<div class="loading-spinner">Loading analytics...</div>';
85
+ if (modelUsageChart) modelUsageChart.innerHTML = loadingHtml;
86
+ if (agentUsageChart) agentUsageChart.innerHTML = loadingHtml;
87
+ if (dailyTrendsChart) dailyTrendsChart.innerHTML = loadingHtml;
88
+ if (usageSummary) usageSummary.innerHTML = loadingHtml;
89
+ }
90
+
91
+ function showAnalyticsError(message) {
92
+ const errorHtml = `<div class="analytics-error">${message}</div>`;
93
+ if (modelUsageChart) modelUsageChart.innerHTML = errorHtml;
94
+ if (agentUsageChart) agentUsageChart.innerHTML = errorHtml;
95
+ if (dailyTrendsChart) dailyTrendsChart.innerHTML = errorHtml;
96
+ if (usageSummary) usageSummary.innerHTML = errorHtml;
97
+ }
98
+
99
+ function renderAnalytics(data) {
100
+ renderModelUsage(data.model_usage);
101
+ renderAgentUsage(data.agent_usage);
102
+ renderDailyTrends(data.daily_usage);
103
+ renderUsageSummary(data);
104
+ }
105
+
106
+ function renderModelUsage(modelUsage) {
107
+ if (!modelUsageChart) return;
108
+
109
+ if (!modelUsage || modelUsage.length === 0) {
110
+ modelUsageChart.innerHTML = '<div class="analytics-empty">No model usage data available</div>';
111
+ return;
112
+ }
113
+
114
+ // Sort by usage count
115
+ const sortedModels = modelUsage.sort((a, b) => b.count - a.count);
116
+ const totalUsage = sortedModels.reduce((sum, model) => sum + model.count, 0);
117
+
118
+ let html = '<div class="model-usage-list">';
119
+ sortedModels.forEach(model => {
120
+ const percentage = totalUsage > 0 ? Math.round((model.count / totalUsage) * 100) : 0;
121
+ const lastUsed = new Date(model.last_used * 1000).toLocaleDateString();
122
+
123
+ html += `
124
+ <div class="model-usage-item">
125
+ <div class="model-info">
126
+ <div class="model-name">${model._id}</div>
127
+ <div class="model-provider">${model.provider}</div>
128
+ </div>
129
+ <div class="model-stats">
130
+ <div class="model-count">${model.count} requests</div>
131
+ <div class="model-percentage">${percentage}%</div>
132
+ <div class="model-last-used">Last used: ${lastUsed}</div>
133
+ </div>
134
+ <div class="model-bar">
135
+ <div class="model-bar-fill" style="width: ${percentage}%"></div>
136
+ </div>
137
+ </div>
138
+ `;
139
+ });
140
+ html += '</div>';
141
+
142
+ modelUsageChart.innerHTML = html;
143
+ }
144
+
145
+ function renderAgentUsage(agentUsage) {
146
+ if (!agentUsageChart) return;
147
+
148
+ if (!agentUsage || agentUsage.length === 0) {
149
+ agentUsageChart.innerHTML = '<div class="analytics-empty">No agent usage data available</div>';
150
+ return;
151
+ }
152
+
153
+ // Sort by usage count
154
+ const sortedAgents = agentUsage.sort((a, b) => b.count - a.count);
155
+ const totalUsage = sortedAgents.reduce((sum, agent) => sum + agent.count, 0);
156
+
157
+ let html = '<div class="agent-usage-list">';
158
+ sortedAgents.forEach(agent => {
159
+ const percentage = totalUsage > 0 ? Math.round((agent.count / totalUsage) * 100) : 0;
160
+ const lastUsed = new Date(agent.last_used * 1000).toLocaleDateString();
161
+ const actions = agent.actions ? agent.actions.join(', ') : 'N/A';
162
+
163
+ html += `
164
+ <div class="agent-usage-item">
165
+ <div class="agent-info">
166
+ <div class="agent-name">${agent._id}</div>
167
+ <div class="agent-actions">Actions: ${actions}</div>
168
+ </div>
169
+ <div class="agent-stats">
170
+ <div class="agent-count">${agent.count} requests</div>
171
+ <div class="agent-percentage">${percentage}%</div>
172
+ <div class="agent-last-used">Last used: ${lastUsed}</div>
173
+ </div>
174
+ <div class="agent-bar">
175
+ <div class="agent-bar-fill" style="width: ${percentage}%"></div>
176
+ </div>
177
+ </div>
178
+ `;
179
+ });
180
+ html += '</div>';
181
+
182
+ agentUsageChart.innerHTML = html;
183
+ }
184
+
185
+ function renderDailyTrends(dailyUsage) {
186
+ if (!dailyTrendsChart) return;
187
+
188
+ if (!dailyUsage || dailyUsage.length === 0) {
189
+ dailyTrendsChart.innerHTML = '<div class="analytics-empty">No daily usage data available</div>';
190
+ return;
191
+ }
192
+
193
+ // Sort by date
194
+ const sortedDaily = dailyUsage.sort((a, b) => {
195
+ const dateA = new Date(a._id.year, a._id.month - 1, a._id.day);
196
+ const dateB = new Date(b._id.year, b._id.month - 1, b._id.day);
197
+ return dateA - dateB;
198
+ });
199
+
200
+ const maxUsage = Math.max(...sortedDaily.map(d => d.total_requests));
201
+
202
+ let html = '<div class="daily-trends-chart">';
203
+ sortedDaily.forEach(day => {
204
+ const date = new Date(day._id.year, day._id.month - 1, day._id.day);
205
+ const dateStr = date.toLocaleDateString();
206
+ const height = maxUsage > 0 ? (day.total_requests / maxUsage) * 100 : 0;
207
+
208
+ html += `
209
+ <div class="daily-bar">
210
+ <div class="daily-bar-fill" style="height: ${height}%"></div>
211
+ <div class="daily-label">${dateStr}</div>
212
+ <div class="daily-count">${day.total_requests}</div>
213
+ </div>
214
+ `;
215
+ });
216
+ html += '</div>';
217
+
218
+ dailyTrendsChart.innerHTML = html;
219
+ }
220
+
221
+ function renderUsageSummary(data) {
222
+ if (!usageSummary) return;
223
+
224
+ const totalRequests = data.total_requests || 0;
225
+ const periodDays = data.period_days || 30;
226
+ const avgPerDay = Math.round(totalRequests / periodDays * 10) / 10;
227
+
228
+ const modelCount = data.model_usage ? data.model_usage.length : 0;
229
+ const agentCount = data.agent_usage ? data.agent_usage.length : 0;
230
+
231
+ const mostUsedModel = data.model_usage && data.model_usage.length > 0
232
+ ? data.model_usage[0]
233
+ : null;
234
+ const mostUsedAgent = data.agent_usage && data.agent_usage.length > 0
235
+ ? data.agent_usage[0]
236
+ : null;
237
+
238
+ let html = `
239
+ <div class="usage-summary-content">
240
+ <div class="summary-stat">
241
+ <div class="summary-value">${totalRequests}</div>
242
+ <div class="summary-label">Total Requests</div>
243
+ </div>
244
+ <div class="summary-stat">
245
+ <div class="summary-value">${avgPerDay}</div>
246
+ <div class="summary-label">Avg per Day</div>
247
+ </div>
248
+ <div class="summary-stat">
249
+ <div class="summary-value">${modelCount}</div>
250
+ <div class="summary-label">Models Used</div>
251
+ </div>
252
+ <div class="summary-stat">
253
+ <div class="summary-value">${agentCount}</div>
254
+ <div class="summary-label">Agents Used</div>
255
+ </div>
256
+ `;
257
+
258
+ if (mostUsedModel) {
259
+ html += `
260
+ <div class="summary-highlight">
261
+ <div class="highlight-label">Most Used Model:</div>
262
+ <div class="highlight-value">${mostUsedModel._id} (${mostUsedModel.count} times)</div>
263
+ </div>
264
+ `;
265
+ }
266
+
267
+ if (mostUsedAgent) {
268
+ html += `
269
+ <div class="summary-highlight">
270
+ <div class="highlight-label">Most Used Agent:</div>
271
+ <div class="highlight-value">${mostUsedAgent._id} (${mostUsedAgent.count} times)</div>
272
+ </div>
273
+ `;
274
+ }
275
+
276
+ html += '</div>';
277
+ usageSummary.innerHTML = html;
278
+ }
279
+
280
+ // Expose functions for external use
281
+ window.__sb_load_analytics = loadAnalytics;
282
+ window.__sb_show_analytics_section = () => {
283
+ if (analyticsSection) {
284
+ analyticsSection.style.display = 'block';
285
+ }
286
+ };
287
+ window.__sb_hide_analytics_section = () => {
288
+ if (analyticsSection) {
289
+ analyticsSection.style.display = 'none';
290
+ }
291
+ };
292
+ })();
static/index.html CHANGED
@@ -193,6 +193,61 @@
193
  </div>
194
  </section>
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  <!-- Chat Section -->
197
  <section class="card reveal" id="chat-section">
198
  <div class="card-header">
@@ -380,5 +435,6 @@
380
  <script src="/static/projects.js"></script>
381
  <script src="/static/script.js"></script>
382
  <script src="/static/sessions.js"></script>
 
383
  </body>
384
  </html>
 
193
  </div>
194
  </section>
195
 
196
+ <!-- Analytics Section -->
197
+ <section class="card reveal" id="analytics-section" style="display:none;">
198
+ <div class="card-header">
199
+ <h2>📊 Analytics Dashboard</h2>
200
+ <p>Track your usage of AI models and agents</p>
201
+ </div>
202
+ <div class="analytics-content">
203
+ <div class="analytics-controls">
204
+ <div class="analytics-period">
205
+ <label for="analytics-period">Time Period:</label>
206
+ <select id="analytics-period" class="analytics-select">
207
+ <option value="7">Last 7 days</option>
208
+ <option value="30" selected>Last 30 days</option>
209
+ <option value="90">Last 90 days</option>
210
+ </select>
211
+ </div>
212
+ <button id="refresh-analytics" class="btn-primary">Refresh Data</button>
213
+ </div>
214
+
215
+ <div class="analytics-grid">
216
+ <!-- Model Usage -->
217
+ <div class="analytics-card">
218
+ <h3>🤖 Model Usage</h3>
219
+ <div class="analytics-chart" id="model-usage-chart">
220
+ <div class="loading-spinner">Loading...</div>
221
+ </div>
222
+ </div>
223
+
224
+ <!-- Agent Usage -->
225
+ <div class="analytics-card">
226
+ <h3>🔧 Agent Usage</h3>
227
+ <div class="analytics-chart" id="agent-usage-chart">
228
+ <div class="loading-spinner">Loading...</div>
229
+ </div>
230
+ </div>
231
+
232
+ <!-- Daily Trends -->
233
+ <div class="analytics-card analytics-wide">
234
+ <h3>📈 Daily Usage Trends</h3>
235
+ <div class="analytics-chart" id="daily-trends-chart">
236
+ <div class="loading-spinner">Loading...</div>
237
+ </div>
238
+ </div>
239
+
240
+ <!-- Usage Summary -->
241
+ <div class="analytics-card">
242
+ <h3>📋 Usage Summary</h3>
243
+ <div class="analytics-summary" id="usage-summary">
244
+ <div class="loading-spinner">Loading...</div>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ </section>
250
+
251
  <!-- Chat Section -->
252
  <section class="card reveal" id="chat-section">
253
  <div class="card-header">
 
435
  <script src="/static/projects.js"></script>
436
  <script src="/static/script.js"></script>
437
  <script src="/static/sessions.js"></script>
438
+ <script src="/static/analytics.js"></script>
439
  </body>
440
  </html>
static/sidebar.js CHANGED
@@ -149,7 +149,10 @@
149
  showSection('chat');
150
  break;
151
  case 'analytics':
152
- // Could show usage analytics or insights
 
 
 
153
  break;
154
  case 'settings':
155
  // Could show user settings or preferences
@@ -166,14 +169,33 @@
166
  const upload = document.getElementById('upload-section');
167
  const chat = document.getElementById('chat-section');
168
  const files = document.getElementById('files-section');
 
 
169
  if (!upload || !chat || !files) return;
170
- // Show upload section for projects, files section for files, chat section for chat
171
- upload.style.display = name === 'upload' ? 'block' : 'none';
172
- chat.style.display = name === 'chat' ? 'block' : 'none';
173
- files.style.display = name === 'files' ? 'block' : 'none';
174
- // Enable chat functionality when chat section is visible
175
- if (name === 'chat' && window.__sb_enable_chat) {
176
- window.__sb_enable_chat();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  }
178
  }
179
 
 
149
  showSection('chat');
150
  break;
151
  case 'analytics':
152
+ showSection('analytics');
153
+ if (window.__sb_load_analytics) {
154
+ window.__sb_load_analytics();
155
+ }
156
  break;
157
  case 'settings':
158
  // Could show user settings or preferences
 
169
  const upload = document.getElementById('upload-section');
170
  const chat = document.getElementById('chat-section');
171
  const files = document.getElementById('files-section');
172
+ const analytics = document.getElementById('analytics-section');
173
+
174
  if (!upload || !chat || !files) return;
175
+
176
+ // Hide all sections first
177
+ upload.style.display = 'none';
178
+ chat.style.display = 'none';
179
+ files.style.display = 'none';
180
+ if (analytics) analytics.style.display = 'none';
181
+
182
+ // Show selected section
183
+ switch (name) {
184
+ case 'upload':
185
+ upload.style.display = 'block';
186
+ break;
187
+ case 'chat':
188
+ chat.style.display = 'block';
189
+ if (window.__sb_enable_chat) {
190
+ window.__sb_enable_chat();
191
+ }
192
+ break;
193
+ case 'files':
194
+ files.style.display = 'block';
195
+ break;
196
+ case 'analytics':
197
+ if (analytics) analytics.style.display = 'block';
198
+ break;
199
  }
200
  }
201
 
static/styles.css CHANGED
@@ -1805,4 +1805,347 @@
1805
  .session-actions {
1806
  justify-content: center;
1807
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1808
  }
 
1805
  .session-actions {
1806
  justify-content: center;
1807
  }
1808
+ }
1809
+
1810
+ /* Analytics Styles */
1811
+ .analytics-content {
1812
+ padding: 1.5rem;
1813
+ }
1814
+
1815
+ .analytics-controls {
1816
+ display: flex;
1817
+ justify-content: space-between;
1818
+ align-items: center;
1819
+ margin-bottom: 2rem;
1820
+ padding: 1rem;
1821
+ background: var(--card);
1822
+ border-radius: var(--radius);
1823
+ border: 1px solid var(--border);
1824
+ }
1825
+
1826
+ .analytics-period {
1827
+ display: flex;
1828
+ align-items: center;
1829
+ gap: 0.75rem;
1830
+ }
1831
+
1832
+ .analytics-period label {
1833
+ font-weight: 500;
1834
+ color: var(--text);
1835
+ }
1836
+
1837
+ .analytics-select {
1838
+ padding: 0.5rem 0.75rem;
1839
+ border: 1px solid var(--border);
1840
+ border-radius: var(--radius);
1841
+ background: var(--bg);
1842
+ color: var(--text);
1843
+ font-size: 0.875rem;
1844
+ }
1845
+
1846
+ .analytics-grid {
1847
+ display: grid;
1848
+ grid-template-columns: 1fr 1fr;
1849
+ gap: 1.5rem;
1850
+ margin-bottom: 2rem;
1851
+ }
1852
+
1853
+ .analytics-card {
1854
+ background: var(--card);
1855
+ border-radius: var(--radius);
1856
+ border: 1px solid var(--border);
1857
+ padding: 1.5rem;
1858
+ }
1859
+
1860
+ .analytics-card h3 {
1861
+ margin: 0 0 1rem 0;
1862
+ font-size: 1.125rem;
1863
+ font-weight: 600;
1864
+ color: var(--text);
1865
+ }
1866
+
1867
+ .analytics-wide {
1868
+ grid-column: 1 / -1;
1869
+ }
1870
+
1871
+ .analytics-chart {
1872
+ min-height: 200px;
1873
+ display: flex;
1874
+ align-items: center;
1875
+ justify-content: center;
1876
+ }
1877
+
1878
+ .analytics-empty {
1879
+ text-align: center;
1880
+ color: var(--muted);
1881
+ font-style: italic;
1882
+ }
1883
+
1884
+ .analytics-error {
1885
+ text-align: center;
1886
+ color: var(--error);
1887
+ font-weight: 500;
1888
+ }
1889
+
1890
+ .loading-spinner {
1891
+ display: flex;
1892
+ align-items: center;
1893
+ justify-content: center;
1894
+ color: var(--muted);
1895
+ font-size: 0.875rem;
1896
+ }
1897
+
1898
+ /* Model Usage Styles */
1899
+ .model-usage-list {
1900
+ display: flex;
1901
+ flex-direction: column;
1902
+ gap: 1rem;
1903
+ }
1904
+
1905
+ .model-usage-item {
1906
+ display: flex;
1907
+ flex-direction: column;
1908
+ gap: 0.5rem;
1909
+ padding: 1rem;
1910
+ background: var(--bg-secondary);
1911
+ border-radius: var(--radius);
1912
+ border: 1px solid var(--border);
1913
+ }
1914
+
1915
+ .model-info {
1916
+ display: flex;
1917
+ justify-content: space-between;
1918
+ align-items: center;
1919
+ }
1920
+
1921
+ .model-name {
1922
+ font-weight: 600;
1923
+ color: var(--text);
1924
+ font-size: 0.875rem;
1925
+ }
1926
+
1927
+ .model-provider {
1928
+ font-size: 0.75rem;
1929
+ color: var(--muted);
1930
+ text-transform: uppercase;
1931
+ letter-spacing: 0.05em;
1932
+ }
1933
+
1934
+ .model-stats {
1935
+ display: flex;
1936
+ justify-content: space-between;
1937
+ align-items: center;
1938
+ font-size: 0.875rem;
1939
+ }
1940
+
1941
+ .model-count {
1942
+ font-weight: 600;
1943
+ color: var(--accent);
1944
+ }
1945
+
1946
+ .model-percentage {
1947
+ color: var(--text-secondary);
1948
+ }
1949
+
1950
+ .model-last-used {
1951
+ color: var(--muted);
1952
+ font-size: 0.75rem;
1953
+ }
1954
+
1955
+ .model-bar {
1956
+ height: 4px;
1957
+ background: var(--border);
1958
+ border-radius: 2px;
1959
+ overflow: hidden;
1960
+ }
1961
+
1962
+ .model-bar-fill {
1963
+ height: 100%;
1964
+ background: var(--gradient-accent);
1965
+ transition: width 0.3s ease;
1966
+ }
1967
+
1968
+ /* Agent Usage Styles */
1969
+ .agent-usage-list {
1970
+ display: flex;
1971
+ flex-direction: column;
1972
+ gap: 1rem;
1973
+ }
1974
+
1975
+ .agent-usage-item {
1976
+ display: flex;
1977
+ flex-direction: column;
1978
+ gap: 0.5rem;
1979
+ padding: 1rem;
1980
+ background: var(--bg-secondary);
1981
+ border-radius: var(--radius);
1982
+ border: 1px solid var(--border);
1983
+ }
1984
+
1985
+ .agent-info {
1986
+ display: flex;
1987
+ justify-content: space-between;
1988
+ align-items: center;
1989
+ }
1990
+
1991
+ .agent-name {
1992
+ font-weight: 600;
1993
+ color: var(--text);
1994
+ font-size: 0.875rem;
1995
+ }
1996
+
1997
+ .agent-actions {
1998
+ font-size: 0.75rem;
1999
+ color: var(--muted);
2000
+ }
2001
+
2002
+ .agent-stats {
2003
+ display: flex;
2004
+ justify-content: space-between;
2005
+ align-items: center;
2006
+ font-size: 0.875rem;
2007
+ }
2008
+
2009
+ .agent-count {
2010
+ font-weight: 600;
2011
+ color: var(--success);
2012
+ }
2013
+
2014
+ .agent-percentage {
2015
+ color: var(--text-secondary);
2016
+ }
2017
+
2018
+ .agent-last-used {
2019
+ color: var(--muted);
2020
+ font-size: 0.75rem;
2021
+ }
2022
+
2023
+ .agent-bar {
2024
+ height: 4px;
2025
+ background: var(--border);
2026
+ border-radius: 2px;
2027
+ overflow: hidden;
2028
+ }
2029
+
2030
+ .agent-bar-fill {
2031
+ height: 100%;
2032
+ background: var(--gradient-success);
2033
+ transition: width 0.3s ease;
2034
+ }
2035
+
2036
+ /* Daily Trends Styles */
2037
+ .daily-trends-chart {
2038
+ display: flex;
2039
+ align-items: end;
2040
+ gap: 0.5rem;
2041
+ height: 200px;
2042
+ padding: 1rem 0;
2043
+ }
2044
+
2045
+ .daily-bar {
2046
+ flex: 1;
2047
+ display: flex;
2048
+ flex-direction: column;
2049
+ align-items: center;
2050
+ gap: 0.5rem;
2051
+ min-height: 100px;
2052
+ }
2053
+
2054
+ .daily-bar-fill {
2055
+ width: 100%;
2056
+ background: var(--gradient-accent);
2057
+ border-radius: 2px 2px 0 0;
2058
+ min-height: 4px;
2059
+ transition: height 0.3s ease;
2060
+ }
2061
+
2062
+ .daily-label {
2063
+ font-size: 0.75rem;
2064
+ color: var(--muted);
2065
+ text-align: center;
2066
+ writing-mode: vertical-rl;
2067
+ text-orientation: mixed;
2068
+ }
2069
+
2070
+ .daily-count {
2071
+ font-size: 0.75rem;
2072
+ color: var(--text-secondary);
2073
+ font-weight: 500;
2074
+ }
2075
+
2076
+ /* Usage Summary Styles */
2077
+ .usage-summary-content {
2078
+ display: grid;
2079
+ grid-template-columns: repeat(2, 1fr);
2080
+ gap: 1rem;
2081
+ }
2082
+
2083
+ .summary-stat {
2084
+ text-align: center;
2085
+ padding: 1rem;
2086
+ background: var(--bg-secondary);
2087
+ border-radius: var(--radius);
2088
+ border: 1px solid var(--border);
2089
+ }
2090
+
2091
+ .summary-value {
2092
+ font-size: 1.5rem;
2093
+ font-weight: 700;
2094
+ color: var(--accent);
2095
+ margin-bottom: 0.25rem;
2096
+ }
2097
+
2098
+ .summary-label {
2099
+ font-size: 0.75rem;
2100
+ color: var(--muted);
2101
+ text-transform: uppercase;
2102
+ letter-spacing: 0.05em;
2103
+ }
2104
+
2105
+ .summary-highlight {
2106
+ grid-column: 1 / -1;
2107
+ padding: 1rem;
2108
+ background: var(--bg-secondary);
2109
+ border-radius: var(--radius);
2110
+ border: 1px solid var(--border);
2111
+ margin-top: 1rem;
2112
+ }
2113
+
2114
+ .highlight-label {
2115
+ font-size: 0.75rem;
2116
+ color: var(--muted);
2117
+ text-transform: uppercase;
2118
+ letter-spacing: 0.05em;
2119
+ margin-bottom: 0.25rem;
2120
+ }
2121
+
2122
+ .highlight-value {
2123
+ font-size: 0.875rem;
2124
+ color: var(--text);
2125
+ font-weight: 500;
2126
+ }
2127
+
2128
+ /* Analytics responsive */
2129
+ @media (max-width: 1024px) {
2130
+ .analytics-grid {
2131
+ grid-template-columns: 1fr;
2132
+ }
2133
+
2134
+ .analytics-controls {
2135
+ flex-direction: column;
2136
+ gap: 1rem;
2137
+ align-items: stretch;
2138
+ }
2139
+
2140
+ .usage-summary-content {
2141
+ grid-template-columns: 1fr;
2142
+ }
2143
+
2144
+ .daily-trends-chart {
2145
+ gap: 0.25rem;
2146
+ }
2147
+
2148
+ .daily-label {
2149
+ font-size: 0.625rem;
2150
+ }
2151
  }
utils/analytics.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ────────────────────────────── utils/analytics.py ──────────────────────────────
2
+ """
3
+ Analytics and Usage Tracking System
4
+
5
+ Tracks user-specific usage of models and agents for analytics dashboard.
6
+ """
7
+
8
+ import time
9
+ from datetime import datetime, timezone
10
+ from typing import Dict, Any, List, Optional
11
+ from pymongo.collection import Collection
12
+ from pymongo.database import Database
13
+ from pymongo import MongoClient
14
+ from utils.logger import get_logger
15
+
16
+ logger = get_logger("ANALYTICS", __name__)
17
+
18
+ class AnalyticsTracker:
19
+ """Tracks user usage analytics for models and agents."""
20
+
21
+ def __init__(self, mongo_client: MongoClient, db_name: str = "studybuddy"):
22
+ self.client = mongo_client
23
+ self.db = mongo_client[db_name]
24
+ self.usage_collection = self.db["usage_analytics"]
25
+ self._ensure_indexes()
26
+
27
+ def _ensure_indexes(self):
28
+ """Create necessary indexes for efficient queries."""
29
+ try:
30
+ # Compound index for user_id + timestamp
31
+ self.usage_collection.create_index([("user_id", 1), ("timestamp", -1)])
32
+ # Index for aggregation queries
33
+ self.usage_collection.create_index([("user_id", 1), ("type", 1), ("timestamp", -1)])
34
+ logger.info("[ANALYTICS] Indexes created successfully")
35
+ except Exception as e:
36
+ logger.warning(f"[ANALYTICS] Failed to create indexes: {e}")
37
+
38
+ async def track_model_usage(self, user_id: str, model_name: str, provider: str,
39
+ context: str = "", metadata: Optional[Dict] = None):
40
+ """Track model usage for analytics."""
41
+ try:
42
+ usage_record = {
43
+ "user_id": user_id,
44
+ "type": "model",
45
+ "model_name": model_name,
46
+ "provider": provider,
47
+ "context": context,
48
+ "timestamp": time.time(),
49
+ "created_at": datetime.now(timezone.utc),
50
+ "metadata": metadata or {}
51
+ }
52
+
53
+ await self.usage_collection.insert_one(usage_record)
54
+ logger.debug(f"[ANALYTICS] Tracked model usage: {model_name} for user {user_id}")
55
+
56
+ except Exception as e:
57
+ logger.error(f"[ANALYTICS] Failed to track model usage: {e}")
58
+
59
+ async def track_agent_usage(self, user_id: str, agent_name: str, action: str,
60
+ context: str = "", metadata: Optional[Dict] = None):
61
+ """Track agent usage for analytics."""
62
+ try:
63
+ usage_record = {
64
+ "user_id": user_id,
65
+ "type": "agent",
66
+ "agent_name": agent_name,
67
+ "action": action,
68
+ "context": context,
69
+ "timestamp": time.time(),
70
+ "created_at": datetime.now(timezone.utc),
71
+ "metadata": metadata or {}
72
+ }
73
+
74
+ await self.usage_collection.insert_one(usage_record)
75
+ logger.debug(f"[ANALYTICS] Tracked agent usage: {agent_name} for user {user_id}")
76
+
77
+ except Exception as e:
78
+ logger.error(f"[ANALYTICS] Failed to track agent usage: {e}")
79
+
80
+ async def get_user_analytics(self, user_id: str, days: int = 30) -> Dict[str, Any]:
81
+ """Get comprehensive analytics for a user."""
82
+ try:
83
+ # Calculate time range
84
+ cutoff_time = time.time() - (days * 24 * 60 * 60)
85
+
86
+ # Model usage analytics
87
+ model_pipeline = [
88
+ {"$match": {"user_id": user_id, "type": "model", "timestamp": {"$gte": cutoff_time}}},
89
+ {"$group": {
90
+ "_id": "$model_name",
91
+ "count": {"$sum": 1},
92
+ "provider": {"$first": "$provider"},
93
+ "last_used": {"$max": "$timestamp"}
94
+ }},
95
+ {"$sort": {"count": -1}}
96
+ ]
97
+
98
+ model_usage = list(self.usage_collection.aggregate(model_pipeline))
99
+
100
+ # Agent usage analytics
101
+ agent_pipeline = [
102
+ {"$match": {"user_id": user_id, "type": "agent", "timestamp": {"$gte": cutoff_time}}},
103
+ {"$group": {
104
+ "_id": "$agent_name",
105
+ "count": {"$sum": 1},
106
+ "actions": {"$addToSet": "$action"},
107
+ "last_used": {"$max": "$timestamp"}
108
+ }},
109
+ {"$sort": {"count": -1}}
110
+ ]
111
+
112
+ agent_usage = list(self.usage_collection.aggregate(agent_pipeline))
113
+
114
+ # Daily usage trends
115
+ daily_pipeline = [
116
+ {"$match": {"user_id": user_id, "timestamp": {"$gte": cutoff_time}}},
117
+ {"$group": {
118
+ "_id": {
119
+ "year": {"$year": {"$dateFromTimestamp": {"$multiply": ["$timestamp", 1000]}}},
120
+ "month": {"$month": {"$dateFromTimestamp": {"$multiply": ["$timestamp", 1000]}}},
121
+ "day": {"$dayOfMonth": {"$dateFromTimestamp": {"$multiply": ["$timestamp", 1000]}}}
122
+ },
123
+ "total_requests": {"$sum": 1},
124
+ "model_requests": {"$sum": {"$cond": [{"$eq": ["$type", "model"]}, 1, 0]}},
125
+ "agent_requests": {"$sum": {"$cond": [{"$eq": ["$type", "agent"]}, 1, 0]}}
126
+ }},
127
+ {"$sort": {"_id.year": 1, "_id.month": 1, "_id.day": 1}}
128
+ ]
129
+
130
+ daily_usage = list(self.usage_collection.aggregate(daily_pipeline))
131
+
132
+ return {
133
+ "user_id": user_id,
134
+ "period_days": days,
135
+ "model_usage": model_usage,
136
+ "agent_usage": agent_usage,
137
+ "daily_usage": daily_usage,
138
+ "total_requests": sum(item["count"] for item in model_usage + agent_usage),
139
+ "generated_at": datetime.now(timezone.utc).isoformat()
140
+ }
141
+
142
+ except Exception as e:
143
+ logger.error(f"[ANALYTICS] Failed to get user analytics: {e}")
144
+ return {
145
+ "user_id": user_id,
146
+ "period_days": days,
147
+ "model_usage": [],
148
+ "agent_usage": [],
149
+ "daily_usage": [],
150
+ "total_requests": 0,
151
+ "error": str(e),
152
+ "generated_at": datetime.now(timezone.utc).isoformat()
153
+ }
154
+
155
+ async def get_global_analytics(self, days: int = 30) -> Dict[str, Any]:
156
+ """Get global analytics across all users."""
157
+ try:
158
+ cutoff_time = time.time() - (days * 24 * 60 * 60)
159
+
160
+ # Global model usage
161
+ model_pipeline = [
162
+ {"$match": {"type": "model", "timestamp": {"$gte": cutoff_time}}},
163
+ {"$group": {
164
+ "_id": "$model_name",
165
+ "count": {"$sum": 1},
166
+ "unique_users": {"$addToSet": "$user_id"},
167
+ "provider": {"$first": "$provider"}
168
+ }},
169
+ {"$addFields": {"unique_user_count": {"$size": "$unique_users"}}},
170
+ {"$sort": {"count": -1}}
171
+ ]
172
+
173
+ global_model_usage = list(self.usage_collection.aggregate(model_pipeline))
174
+
175
+ # Global agent usage
176
+ agent_pipeline = [
177
+ {"$match": {"type": "agent", "timestamp": {"$gte": cutoff_time}}},
178
+ {"$group": {
179
+ "_id": "$agent_name",
180
+ "count": {"$sum": 1},
181
+ "unique_users": {"$addToSet": "$user_id"},
182
+ "actions": {"$addToSet": "$action"}
183
+ }},
184
+ {"$addFields": {"unique_user_count": {"$size": "$unique_users"}}},
185
+ {"$sort": {"count": -1}}
186
+ ]
187
+
188
+ global_agent_usage = list(self.usage_collection.aggregate(agent_pipeline))
189
+
190
+ return {
191
+ "period_days": days,
192
+ "global_model_usage": global_model_usage,
193
+ "global_agent_usage": global_agent_usage,
194
+ "total_requests": sum(item["count"] for item in global_model_usage + global_agent_usage),
195
+ "generated_at": datetime.now(timezone.utc).isoformat()
196
+ }
197
+
198
+ except Exception as e:
199
+ logger.error(f"[ANALYTICS] Failed to get global analytics: {e}")
200
+ return {
201
+ "period_days": days,
202
+ "global_model_usage": [],
203
+ "global_agent_usage": [],
204
+ "total_requests": 0,
205
+ "error": str(e),
206
+ "generated_at": datetime.now(timezone.utc).isoformat()
207
+ }
208
+
209
+ async def cleanup_old_data(self, days_to_keep: int = 90):
210
+ """Clean up old analytics data to prevent database bloat."""
211
+ try:
212
+ cutoff_time = time.time() - (days_to_keep * 24 * 60 * 60)
213
+ result = await self.usage_collection.delete_many({"timestamp": {"$lt": cutoff_time}})
214
+ logger.info(f"[ANALYTICS] Cleaned up {result.deleted_count} old records")
215
+ return result.deleted_count
216
+ except Exception as e:
217
+ logger.error(f"[ANALYTICS] Failed to cleanup old data: {e}")
218
+ return 0
219
+
220
+
221
+ # Global analytics tracker instance
222
+ analytics_tracker: Optional[AnalyticsTracker] = None
223
+
224
+ def init_analytics(mongo_client, db_name: str = "studybuddy"):
225
+ """Initialize the global analytics tracker."""
226
+ global analytics_tracker
227
+ analytics_tracker = AnalyticsTracker(mongo_client, db_name)
228
+ logger.info("[ANALYTICS] Analytics tracker initialized")
229
+
230
+ def get_analytics_tracker() -> Optional[AnalyticsTracker]:
231
+ """Get the global analytics tracker instance."""
232
+ return analytics_tracker