Spaces:
Sleeping
Sleeping
Commit
·
5a24119
1
Parent(s):
b2520cd
Upd analytis
Browse files- app.py +1 -0
- helpers/setup.py +5 -0
- routes/analytics.py +87 -0
- routes/chats.py +37 -0
- routes/reports.py +13 -0
- routes/search.py +12 -0
- static/analytics.js +292 -0
- static/index.html +56 -0
- static/sidebar.js +30 -8
- static/styles.css +343 -0
- utils/analytics.py +232 -0
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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
if (
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|