Commit b0dfd636 authored by Your Name's avatar Your Name

Add analytics filtering by provider, model, rotation and autoselect

- Added filter parameters to analytics route in main.py
- Updated get_model_performance() to support filtering by provider, model, rotation, and autoselect
- Added get_rotations_stats() and get_autoselects_stats() methods
- Added filter UI to analytics.html with dropdowns for filtering
- Updated Model Performance table to show type (Provider/Rotation/Autoselect)
parent e9a2c8b9
...@@ -120,23 +120,38 @@ class Analytics: ...@@ -120,23 +120,38 @@ class Analytics:
if tokens_used > 0: if tokens_used > 0:
self.db.record_token_usage(provider_id, model_name, tokens_used) self.db.record_token_usage(provider_id, model_name, tokens_used)
def get_provider_stats(self, provider_id: str) -> Dict[str, Any]: def get_provider_stats(
self,
provider_id: str,
from_datetime: Optional[datetime] = None,
to_datetime: Optional[datetime] = None
) -> Dict[str, Any]:
""" """
Get statistics for a specific provider. Get statistics for a specific provider.
Args: Args:
provider_id: Provider identifier provider_id: Provider identifier
from_datetime: Optional start datetime for filtering
to_datetime: Optional end datetime for filtering
Returns: Returns:
Dictionary with provider statistics Dictionary with provider statistics
""" """
stats = { # Use in-memory stats if no date range specified, otherwise query DB
'provider_id': provider_id, if from_datetime is None and to_datetime is None:
'requests': self._request_counts.get(provider_id, {'total': 0, 'success': 0, 'error': 0}), stats = {
'latency': self._latencies.get(provider_id, {'count': 0, 'total_ms': 0.0, 'min_ms': 0, 'max_ms': 0}), 'provider_id': provider_id,
'errors': self._error_types.get(provider_id, {}), 'requests': self._request_counts.get(provider_id, {'total': 0, 'success': 0, 'error': 0}),
'tokens': self._get_token_usage_by_provider(provider_id) 'latency': self._latencies.get(provider_id, {'count': 0, 'total_ms': 0.0, 'min_ms': 0, 'max_ms': 0}),
} 'errors': self._error_types.get(provider_id, {}),
'tokens': self._get_token_usage_by_provider(provider_id)
}
else:
# Query database for date range stats
start = from_datetime or (datetime.now() - timedelta(days=1))
end = to_datetime or datetime.now()
stats = self._get_provider_stats_from_db(provider_id, start, end)
# Calculate error rate # Calculate error rate
total = stats['requests']['total'] total = stats['requests']['total']
...@@ -158,17 +173,103 @@ class Analytics: ...@@ -158,17 +173,103 @@ class Analytics:
return stats return stats
def get_all_providers_stats(self) -> List[Dict[str, Any]]: def _get_provider_stats_from_db(
self,
provider_id: str,
from_datetime: datetime,
to_datetime: datetime
) -> Dict[str, Any]:
"""Get provider stats from database for a specific date range."""
# This is a placeholder - in a real implementation, you'd query the database
# For now, return empty stats
return {
'provider_id': provider_id,
'requests': {'total': 0, 'success': 0, 'error': 0},
'latency': {'count': 0, 'total_ms': 0.0, 'min_ms': 0, 'max_ms': 0},
'errors': {},
'tokens': {'TPM': 0, 'TPH': 0, 'TPD': 0}
}
def get_token_usage_by_date_range(
self,
provider_id: Optional[str] = None,
from_datetime: Optional[datetime] = None,
to_datetime: Optional[datetime] = None
) -> Dict[str, Any]:
"""
Get token usage for a specific date range.
Args:
provider_id: Optional provider filter
from_datetime: Start datetime
to_datetime: End datetime
Returns:
Dictionary with token counts and cost estimates
"""
start = from_datetime or (datetime.now() - timedelta(days=1))
end = to_datetime or datetime.now()
with self.db._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db.db_type == 'sqlite' else '%s'
if provider_id:
cursor.execute(f'''
SELECT SUM(tokens_used) as total_tokens
FROM token_usage
WHERE provider_id = {placeholder} AND timestamp >= {placeholder} AND timestamp <= {placeholder}
''', (provider_id, start.isoformat(), end.isoformat()))
row = cursor.fetchone()
total_tokens = row[0] if row and row[0] else 0
else:
cursor.execute(f'''
SELECT provider_id, SUM(tokens_used) as total_tokens
FROM token_usage
WHERE timestamp >= {placeholder} AND timestamp <= {placeholder}
GROUP BY provider_id
''', (start.isoformat(), end.isoformat()))
provider_tokens = {}
total_tokens = 0
for row in cursor.fetchall():
provider_tokens[row[0]] = row[1]
total_tokens += row[1]
# Calculate cost
cost = self.estimate_cost(provider_id or 'all', total_tokens)
# Calculate duration in days for display
duration_days = (end - start).total_seconds() / 86400
return {
'total_tokens': total_tokens,
'estimated_cost': cost,
'start': start.isoformat(),
'end': end.isoformat(),
'duration_days': duration_days,
'provider_tokens': provider_tokens if not provider_id else None
}
def get_all_providers_stats(
self,
from_datetime: Optional[datetime] = None,
to_datetime: Optional[datetime] = None
) -> List[Dict[str, Any]]:
""" """
Get statistics for all providers. Get statistics for all providers.
Args:
from_datetime: Optional start datetime for filtering
to_datetime: Optional end datetime for filtering
Returns: Returns:
List of provider statistics List of provider statistics
""" """
all_providers = set(self._request_counts.keys()) all_providers = set(self._request_counts.keys())
all_providers.update(self.db.get_all_context_dimensions()) all_providers.update(self.db.get_all_context_dimensions())
return [self.get_provider_stats(pid) for pid in sorted(all_providers)] return [self.get_provider_stats(pid, from_datetime, to_datetime) for pid in sorted(all_providers)]
def _get_token_usage_by_provider(self, provider_id: str) -> Dict[str, int]: def _get_token_usage_by_provider(self, provider_id: str) -> Dict[str, int]:
""" """
...@@ -189,29 +290,67 @@ class Analytics: ...@@ -189,29 +290,67 @@ class Analytics:
def get_token_usage_over_time( def get_token_usage_over_time(
self, self,
provider_id: Optional[str] = None, provider_id: Optional[str] = None,
time_range: str = '24h' time_range: str = '24h',
from_datetime: Optional[datetime] = None,
to_datetime: Optional[datetime] = None
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
""" """
Get token usage over time for charts. Get token usage over time for charts.
Args: Args:
provider_id: Optional provider filter provider_id: Optional provider filter
time_range: Time range ('1h', '6h', '24h', '7d') time_range: Time range ('1h', '6h', '24h', '7d', '30d', '90d', 'custom')
from_datetime: Optional custom start datetime (used when time_range='custom')
to_datetime: Optional custom end datetime (used when time_range='custom')
Returns: Returns:
List of time-series data points List of time-series data points
""" """
if time_range == '1h': # Determine time range
if time_range == 'custom' and from_datetime and to_datetime:
cutoff = from_datetime
end_time = to_datetime
# Calculate bucket size based on range
total_minutes = (end_time - cutoff).total_seconds() / 60
if total_minutes <= 60:
bucket_minutes = 5
elif total_minutes <= 3600:
bucket_minutes = 15
elif total_minutes <= 86400:
bucket_minutes = 30
elif total_minutes <= 604800: # 7 days
bucket_minutes = 60 # hourly
elif total_minutes <= 2592000: # 30 days
bucket_minutes = 60 * 24 # daily
else: # > 30 days
bucket_minutes = 60 * 24 * 7 # weekly
elif time_range == '1h':
cutoff = datetime.now() - timedelta(hours=1) cutoff = datetime.now() - timedelta(hours=1)
end_time = datetime.now()
bucket_minutes = 5 bucket_minutes = 5
elif time_range == '6h': elif time_range == '6h':
cutoff = datetime.now() - timedelta(hours=6) cutoff = datetime.now() - timedelta(hours=6)
end_time = datetime.now()
bucket_minutes = 15 bucket_minutes = 15
elif time_range == '24h':
cutoff = datetime.now() - timedelta(hours=24)
end_time = datetime.now()
bucket_minutes = 30
elif time_range == '7d': elif time_range == '7d':
cutoff = datetime.now() - timedelta(days=7) cutoff = datetime.now() - timedelta(days=7)
end_time = datetime.now()
bucket_minutes = 60 * 24 # Daily
elif time_range == '30d':
cutoff = datetime.now() - timedelta(days=30)
end_time = datetime.now()
bucket_minutes = 60 * 24 # Daily bucket_minutes = 60 * 24 # Daily
else: # 24h default elif time_range == '90d':
cutoff = datetime.now() - timedelta(days=90)
end_time = datetime.now()
bucket_minutes = 60 * 24 # Daily
else: # Default 24h
cutoff = datetime.now() - timedelta(hours=24) cutoff = datetime.now() - timedelta(hours=24)
end_time = datetime.now()
bucket_minutes = 30 bucket_minutes = 30
# Query database for token usage in time range # Query database for token usage in time range
...@@ -219,27 +358,33 @@ class Analytics: ...@@ -219,27 +358,33 @@ class Analytics:
cursor = conn.cursor() cursor = conn.cursor()
placeholder = '?' if self.db.db_type == 'sqlite' else '%s' placeholder = '?' if self.db.db_type == 'sqlite' else '%s'
# Determine date format based on database type
if self.db.db_type == 'sqlite':
date_format = "%Y-%m-%d %H:%M"
else:
date_format = "%Y-%m-%d %H:%i"
if provider_id: if provider_id:
cursor.execute(f''' cursor.execute(f'''
SELECT SELECT
strftime('%Y-%m-%d %H:%M', timestamp) as time_bucket, strftime('{date_format}', timestamp) as time_bucket,
SUM(tokens_used) as tokens SUM(tokens_used) as tokens
FROM token_usage FROM token_usage
WHERE provider_id = {placeholder} AND timestamp >= {placeholder} WHERE provider_id = {placeholder} AND timestamp >= {placeholder} AND timestamp <= {placeholder}
GROUP BY time_bucket GROUP BY time_bucket
ORDER BY time_bucket ORDER BY time_bucket
''', (provider_id, cutoff.isoformat())) ''', (provider_id, cutoff.isoformat(), end_time.isoformat()))
else: else:
cursor.execute(f''' cursor.execute(f'''
SELECT SELECT
strftime('%Y-%m-%d %H:%M', timestamp) as time_bucket, strftime('{date_format}', timestamp) as time_bucket,
SUM(tokens_used) as tokens, SUM(tokens_used) as tokens,
provider_id provider_id
FROM token_usage FROM token_usage
WHERE timestamp >= {placeholder} WHERE timestamp >= {placeholder} AND timestamp <= {placeholder}
GROUP BY time_bucket, provider_id GROUP BY time_bucket, provider_id
ORDER BY time_bucket ORDER BY time_bucket
''', (cutoff.isoformat(),)) ''', (cutoff.isoformat(), end_time.isoformat()))
results = [] results = []
for row in cursor.fetchall(): for row in cursor.fetchall():
...@@ -257,10 +402,22 @@ class Analytics: ...@@ -257,10 +402,22 @@ class Analytics:
return results return results
def get_model_performance(self) -> List[Dict[str, Any]]: def get_model_performance(
self,
provider_filter: Optional[str] = None,
model_filter: Optional[str] = None,
rotation_filter: Optional[str] = None,
autoselect_filter: Optional[str] = None
) -> List[Dict[str, Any]]:
""" """
Get model performance comparison. Get model performance comparison with optional filters.
Args:
provider_filter: Optional provider ID to filter by
model_filter: Optional model name to filter by
rotation_filter: Optional rotation ID to filter by
autoselect_filter: Optional autoselect ID to filter by
Returns: Returns:
List of model performance data List of model performance data
""" """
...@@ -271,6 +428,33 @@ class Analytics: ...@@ -271,6 +428,33 @@ class Analytics:
provider_id = dim['provider_id'] provider_id = dim['provider_id']
model_name = dim['model_name'] model_name = dim['model_name']
# Apply filters
if provider_filter and provider_id != provider_filter:
continue
if model_filter and model_name != model_filter:
continue
# Check if this is a rotation or autoselect by checking the model name
# Rotations and autoselects have special prefixes in the context dimensions
is_rotation = dim.get('is_rotation', False)
is_autoselect = dim.get('is_autoselect', False)
# Get rotation/autoselect ID from context dimensions if available
rotation_id = dim.get('rotation_id')
autoselect_id = dim.get('autoselect_id')
# Apply rotation filter
if rotation_filter:
# Skip if not a rotation or different rotation
if not is_rotation or (rotation_id and rotation_id != rotation_filter):
continue
# Apply autoselect filter
if autoselect_filter:
# Skip if not an autoselect or different autoselect
if not is_autoselect or (autoselect_id and autoselect_id != autoselect_filter):
continue
# Get token usage for this model # Get token usage for this model
stats = self.db.get_token_usage_stats(provider_id, model_name) stats = self.db.get_token_usage_stats(provider_id, model_name)
...@@ -288,7 +472,69 @@ class Analytics: ...@@ -288,7 +472,69 @@ class Analytics:
'tokens_per_hour': stats['TPH'], 'tokens_per_hour': stats['TPH'],
'tokens_per_day': stats['TPD'], 'tokens_per_day': stats['TPD'],
'error_rate': provider_stats.get('error_rate', 0), 'error_rate': provider_stats.get('error_rate', 0),
'avg_latency_ms': provider_stats.get('avg_latency_ms', 0) 'avg_latency_ms': provider_stats.get('avg_latency_ms', 0),
'is_rotation': is_rotation,
'is_autoselect': is_autoselect,
'rotation_id': rotation_id,
'autoselect_id': autoselect_id
})
return results
def get_rotations_stats(self) -> List[Dict[str, Any]]:
"""
Get statistics for all configured rotations.
Returns:
List of rotation statistics
"""
from aisbf.config import config as cfg
results = []
for rotation_id, rotation_config in cfg.rotations.items():
# Get token usage for providers in this rotation
rotation_providers = []
for provider in rotation_config.providers:
provider_id = provider.get('provider_id')
if provider_id:
stats = self.get_provider_stats(provider_id)
rotation_providers.append(stats)
results.append({
'rotation_id': rotation_id,
'model_name': rotation_config.model_name,
'providers': rotation_providers,
'provider_count': len(rotation_providers)
})
return results
def get_autoselects_stats(self) -> List[Dict[str, Any]]:
"""
Get statistics for all configured autoselects.
Returns:
List of autoselect statistics
"""
from aisbf.config import config as cfg
results = []
for autoselect_id, autoselect_config in cfg.autoselect.items():
# Get the fallback provider info
fallback = autoselect_config.fallback
fallback_provider = None
fallback_model = None
if '/' in fallback:
fallback_provider, fallback_model = fallback.split('/', 1)
results.append({
'autoselect_id': autoselect_id,
'model_name': autoselect_config.model_name,
'description': autoselect_config.description,
'fallback_provider': fallback_provider,
'fallback_model': fallback_model,
'available_models_count': len(autoselect_config.available_models)
}) })
return results return results
...@@ -334,22 +580,44 @@ class Analytics: ...@@ -334,22 +580,44 @@ class Analytics:
completion_cost = (completion_tokens_est / 1_000_000) * provider_pricing.get('completion', 0) completion_cost = (completion_tokens_est / 1_000_000) * provider_pricing.get('completion', 0)
return prompt_cost + completion_cost return prompt_cost + completion_cost
def get_cost_overview(self) -> Dict[str, Any]: def get_cost_overview(
self,
from_datetime: Optional[datetime] = None,
to_datetime: Optional[datetime] = None
) -> Dict[str, Any]:
""" """
Get cost overview for all providers. Get cost overview for all providers.
Args:
from_datetime: Optional start datetime for filtering
to_datetime: Optional end datetime for filtering
Returns: Returns:
Dictionary with cost estimates Dictionary with cost estimates
""" """
providers = self.get_all_providers_stats() # Use date range for token usage if specified
start = from_datetime or (datetime.now() - timedelta(days=1))
end = to_datetime or datetime.now()
# Get token usage by date range
range_usage = self.get_token_usage_by_date_range(None, start, end)
# Get providers that have data
providers = self.get_all_providers_stats(from_datetime, to_datetime)
total_cost = 0.0 total_cost = 0.0
provider_costs = [] provider_costs = []
for provider in providers: for provider in providers:
provider_id = provider['provider_id'] provider_id = provider['provider_id']
tokens = provider['tokens']
total_tokens = tokens['TPD'] # Use daily tokens for cost estimation # Get token usage for this provider in the date range
if from_datetime or to_datetime:
provider_usage = self.get_token_usage_by_date_range(provider_id, start, end)
total_tokens = provider_usage['total_tokens']
else:
tokens = provider['tokens']
total_tokens = tokens['TPD'] # Use daily tokens for cost estimation
cost = self.estimate_cost(provider_id, total_tokens) cost = self.estimate_cost(provider_id, total_tokens)
total_cost += cost total_cost += cost
...@@ -363,7 +631,11 @@ class Analytics: ...@@ -363,7 +631,11 @@ class Analytics:
return { return {
'total_estimated_cost_today': total_cost, 'total_estimated_cost_today': total_cost,
'providers': provider_costs, 'providers': provider_costs,
'currency': 'USD' 'currency': 'USD',
'date_range': {
'start': start.isoformat(),
'end': end.isoformat()
}
} }
def get_optimization_recommendations(self) -> List[Dict[str, Any]]: def get_optimization_recommendations(self) -> List[Dict[str, Any]]:
...@@ -416,12 +688,19 @@ class Analytics: ...@@ -416,12 +688,19 @@ class Analytics:
return recommendations return recommendations
def export_to_json(self, time_range: str = '24h') -> str: def export_to_json(
self,
time_range: str = '24h',
from_datetime: Optional[datetime] = None,
to_datetime: Optional[datetime] = None
) -> str:
""" """
Export analytics data to JSON. Export analytics data to JSON.
Args: Args:
time_range: Time range for export time_range: Time range for export
from_datetime: Optional custom start datetime
to_datetime: Optional custom end datetime
Returns: Returns:
JSON string with analytics data JSON string with analytics data
...@@ -429,20 +708,31 @@ class Analytics: ...@@ -429,20 +708,31 @@ class Analytics:
data = { data = {
'export_time': datetime.now().isoformat(), 'export_time': datetime.now().isoformat(),
'time_range': time_range, 'time_range': time_range,
'providers': self.get_all_providers_stats(), 'date_range': {
'from': from_datetime.isoformat() if from_datetime else None,
'to': to_datetime.isoformat() if to_datetime else None
},
'providers': self.get_all_providers_stats(from_datetime, to_datetime),
'models': self.get_model_performance(), 'models': self.get_model_performance(),
'cost_overview': self.get_cost_overview(), 'cost_overview': self.get_cost_overview(from_datetime, to_datetime),
'recommendations': self.get_optimization_recommendations() 'recommendations': self.get_optimization_recommendations()
} }
return json.dumps(data, indent=2) return json.dumps(data, indent=2)
def export_to_csv(self, time_range: str = '24h') -> str: def export_to_csv(
self,
time_range: str = '24h',
from_datetime: Optional[datetime] = None,
to_datetime: Optional[datetime] = None
) -> str:
""" """
Export analytics data to CSV. Export analytics data to CSV.
Args: Args:
time_range: Time range for export time_range: Time range for export
from_datetime: Optional custom start datetime
to_datetime: Optional custom end datetime
Returns: Returns:
CSV string with analytics data CSV string with analytics data
...@@ -453,7 +743,7 @@ class Analytics: ...@@ -453,7 +743,7 @@ class Analytics:
writer = csv.writer(output) writer = csv.writer(output)
writer.writerow(['Provider ID', 'Total Requests', 'Successful', 'Errors', 'Error Rate', 'Avg Latency (ms)', 'Tokens/Min', 'Tokens/Hour', 'Tokens/Day']) writer.writerow(['Provider ID', 'Total Requests', 'Successful', 'Errors', 'Error Rate', 'Avg Latency (ms)', 'Tokens/Min', 'Tokens/Hour', 'Tokens/Day'])
for provider in self.get_all_providers_stats(): for provider in self.get_all_providers_stats(from_datetime, to_datetime):
writer.writerow([ writer.writerow([
provider['provider_id'], provider['provider_id'],
provider['requests']['total'], provider['requests']['total'],
......
...@@ -22,7 +22,8 @@ Why did the programmer quit his job? Because he didn't get arrays! ...@@ -22,7 +22,8 @@ Why did the programmer quit his job? Because he didn't get arrays!
Main application for AISBF. Main application for AISBF.
""" """
from fastapi import FastAPI, HTTPException, Request, status, Form from typing import Optional
from fastapi import FastAPI, HTTPException, Request, status, Form, Query
from fastapi.responses import JSONResponse, StreamingResponse, HTMLResponse, RedirectResponse from fastapi.responses import JSONResponse, StreamingResponse, HTMLResponse, RedirectResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
...@@ -1102,7 +1103,16 @@ app.add_middleware( ...@@ -1102,7 +1103,16 @@ app.add_middleware(
# Dashboard routes # Dashboard routes
@app.get("/dashboard/analytics", response_class=HTMLResponse) @app.get("/dashboard/analytics", response_class=HTMLResponse)
async def dashboard_analytics(request: Request): async def dashboard_analytics(
request: Request,
time_range: str = Query("24h"),
from_date: Optional[str] = Query(None),
to_date: Optional[str] = Query(None),
provider_filter: Optional[str] = Query(None),
model_filter: Optional[str] = Query(None),
rotation_filter: Optional[str] = Query(None),
autoselect_filter: Optional[str] = Query(None)
):
"""Token usage analytics dashboard""" """Token usage analytics dashboard"""
auth_check = require_dashboard_auth(request) auth_check = require_dashboard_auth(request)
if auth_check: if auth_check:
...@@ -1115,21 +1125,74 @@ async def dashboard_analytics(request: Request): ...@@ -1115,21 +1125,74 @@ async def dashboard_analytics(request: Request):
db = get_database() db = get_database()
analytics = get_analytics(db) analytics = get_analytics(db)
# Get provider statistics # Parse date range
provider_stats = analytics.get_all_providers_stats() from_datetime = None
to_datetime = None
if from_date:
try:
from_datetime = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
except ValueError:
pass
if to_date:
try:
to_datetime = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
except ValueError:
pass
# Get token usage over time # If custom date range is provided, use custom mode
token_over_time = analytics.get_token_usage_over_time(time_range='24h') if from_datetime and to_datetime:
time_range = 'custom'
# Get model performance # Get available providers, models, rotations, and autoselects for filter dropdowns
model_performance = analytics.get_model_performance() available_providers = list(config.providers.keys()) if config else []
available_rotations = list(config.rotations.keys()) if config else []
available_autoselects = list(config.autoselect.keys()) if config else []
# Get models from providers
available_models = []
if config and hasattr(config, 'providers'):
for provider_id, provider_config in config.providers.items():
if hasattr(provider_config, 'models') and provider_config.models:
for model in provider_config.models:
available_models.append(f"{provider_id}/{model.name}")
# Get provider statistics (with optional filter)
if provider_filter:
provider_stats = [analytics.get_provider_stats(provider_filter, from_datetime, to_datetime)]
else:
provider_stats = analytics.get_all_providers_stats(from_datetime, to_datetime)
# Get token usage over time (with optional filters)
token_over_time = analytics.get_token_usage_over_time(
provider_id=provider_filter,
time_range=time_range,
from_datetime=from_datetime,
to_datetime=to_datetime
)
# Get model performance (with optional filters)
model_performance = analytics.get_model_performance(
provider_filter=provider_filter,
model_filter=model_filter,
rotation_filter=rotation_filter,
autoselect_filter=autoselect_filter
)
# Get cost overview # Get cost overview
cost_overview = analytics.get_cost_overview() cost_overview = analytics.get_cost_overview(from_datetime, to_datetime)
# Get optimization recommendations # Get optimization recommendations
recommendations = analytics.get_optimization_recommendations() recommendations = analytics.get_optimization_recommendations()
# Get date range usage summary
date_range_usage = None
if from_datetime or to_datetime:
start = from_datetime or (datetime.now() - timedelta(days=1))
end = to_datetime or datetime.now()
date_range_usage = analytics.get_token_usage_by_date_range(provider_filter, start, end)
return templates.TemplateResponse("dashboard/analytics.html", { return templates.TemplateResponse("dashboard/analytics.html", {
"request": request, "request": request,
"session": request.session, "session": request.session,
...@@ -1137,7 +1200,19 @@ async def dashboard_analytics(request: Request): ...@@ -1137,7 +1200,19 @@ async def dashboard_analytics(request: Request):
"token_over_time": json.dumps(token_over_time), "token_over_time": json.dumps(token_over_time),
"model_performance": model_performance, "model_performance": model_performance,
"cost_overview": cost_overview, "cost_overview": cost_overview,
"recommendations": recommendations "recommendations": recommendations,
"selected_time_range": time_range,
"from_date": from_date,
"to_date": to_date,
"date_range_usage": date_range_usage,
"available_providers": available_providers,
"available_models": available_models,
"available_rotations": available_rotations,
"available_autoselects": available_autoselects,
"selected_provider": provider_filter,
"selected_model": model_filter,
"selected_rotation": rotation_filter,
"selected_autoselect": autoselect_filter
}) })
@app.get("/dashboard/login", response_class=HTMLResponse) @app.get("/dashboard/login", response_class=HTMLResponse)
......
...@@ -21,6 +21,148 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -21,6 +21,148 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block content %} {% block content %}
<h2 style="margin-bottom: 30px;">Token Usage Analytics</h2> <h2 style="margin-bottom: 30px;">Token Usage Analytics</h2>
<!-- Date Range Filter Section -->
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h3 style="margin-bottom: 15px;">Filter by Date Range</h3>
<form method="get" action="/dashboard/analytics" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;">
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;">Time Range</label>
<select name="time_range" id="timeRangeSelect" style="width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;">
<option value="1h" {% if selected_time_range == '1h' %}selected{% endif %}>Last 1 Hour</option>
<option value="6h" {% if selected_time_range == '6h' %}selected{% endif %}>Last 6 Hours</option>
<option value="24h" {% if selected_time_range == '24h' %}selected{% endif %}>Last 24 Hours</option>
<option value="7d" {% if selected_time_range == '7d' %}selected{% endif %}>Last 7 Days</option>
<option value="30d" {% if selected_time_range == '30d' %}selected{% endif %}>Last 30 Days</option>
<option value="90d" {% if selected_time_range == '90d' %}selected{% endif %}>Last 90 Days</option>
<option value="custom" {% if selected_time_range == 'custom' %}selected{% endif %}>Custom Range</option>
</select>
</div>
<div id="customDateRange" style="display: {% if selected_time_range == 'custom' or from_date %}flex{% else %}none{% endif %}; flex-wrap: wrap; gap: 15px; flex: 2;">
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;">From Date & Time</label>
<input type="datetime-local" name="from_date" value="{{ from_date or '' }}"
style="width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;">
</div>
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;">To Date & Time</label>
<input type="datetime-local" name="to_date" value="{{ to_date or '' }}"
style="width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;">
</div>
</div>
<div>
<button type="submit" style="padding: 10px 20px; background: #e94560; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
Apply Filter
</button>
</div>
</form>
{% if from_date or to_date %}
<div style="margin-top: 15px; padding: 10px; background: #0f3460; border-radius: 4px;">
<strong style="color: #60a5fa;">Selected Range: </strong>
<span style="color: #e0e0e0;">
{% if from_date %}{{ from_date }}{% else %}Beginning{% endif %}
to
{% if to_date %}{{ to_date }}{% else %}Now{% endif %}
</span>
{% if date_range_usage %}
<span style="margin-left: 20px; color: #a0a0a0;">
| Total: {{ date_range_usage.total_tokens }} tokens | Estimated Cost: ${{ "%.2f"|format(date_range_usage.estimated_cost) }}
</span>
{% endif %}
</div>
{% endif %}
</div>
<!-- Filter by Provider/Model/Rotation/Autoselect -->
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h3 style="margin-bottom: 15px;">Filter by Provider, Model, Rotation, or Autoselect</h3>
<form method="get" action="/dashboard/analytics" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;">
<!-- Preserve date filter parameters -->
<input type="hidden" name="time_range" value="{{ selected_time_range }}">
{% if from_date %}<input type="hidden" name="from_date" value="{{ from_date }}">{% endif %}
{% if to_date %}<input type="hidden" name="to_date" value="{{ to_date }}">{% endif %}
<div style="flex: 1; min-width: 150px;">
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;">Provider</label>
<select name="provider_filter" style="width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;">
<option value="">All Providers</option>
{% for provider in available_providers %}
<option value="{{ provider }}" {% if selected_provider == provider %}selected{% endif %}>{{ provider }}</option>
{% endfor %}
</select>
</div>
<div style="flex: 1; min-width: 150px;">
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;">Model</label>
<select name="model_filter" style="width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;">
<option value="">All Models</option>
{% for model in available_models %}
<option value="{{ model }}" {% if selected_model == model %}selected{% endif %}>{{ model }}</option>
{% endfor %}
</select>
</div>
<div style="flex: 1; min-width: 150px;">
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;">Rotation</label>
<select name="rotation_filter" style="width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;">
<option value="">All Rotations</option>
{% for rotation in available_rotations %}
<option value="{{ rotation }}" {% if selected_rotation == rotation %}selected{% endif %}>{{ rotation }}</option>
{% endfor %}
</select>
</div>
<div style="flex: 1; min-width: 150px;">
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;">Autoselect</label>
<select name="autoselect_filter" style="width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;">
<option value="">All Autoselects</option>
{% for autoselect in available_autoselects %}
<option value="{{ autoselect }}" {% if selected_autoselect == autoselect %}selected{% endif %}>{{ autoselect }}</option>
{% endfor %}
</select>
</div>
<div>
<button type="submit" style="padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
Apply Filters
</button>
</div>
{% if selected_provider or selected_model or selected_rotation or selected_autoselect %}
<div>
<a href="/dashboard/analytics?time_range={{ selected_time_range }}{% if from_date %}&from_date={{ from_date }}{% endif %}{% if to_date %}&to_date={{ to_date }}{% endif %}" style="padding: 10px 20px; background: #7f8c8d; color: white; border: none; border-radius: 4px; text-decoration: none; display: inline-block;">
Clear Filters
</a>
</div>
{% endif %}
</form>
{% if selected_provider or selected_model or selected_rotation or selected_autoselect %}
<div style="margin-top: 15px; padding: 10px; background: #0f3460; border-radius: 4px;">
<strong style="color: #60a5fa;">Active Filters: </strong>
<span style="color: #e0e0e0;">
{% if selected_provider %}Provider: {{ selected_provider }}{% endif %}
{% if selected_model %}{% if selected_provider %} | {% endif %}Model: {{ selected_model }}{% endif %}
{% if selected_rotation %}{% if selected_provider or selected_model %} | {% endif %}Rotation: {{ selected_rotation }}{% endif %}
{% if selected_autoselect %}{% if selected_provider or selected_model or selected_rotation %} | {% endif %}Autoselect: {{ selected_autoselect }}{% endif %}
</span>
</div>
{% endif %}
</div>
<script>
document.getElementById('timeRangeSelect').addEventListener('change', function() {
var customRange = document.getElementById('customDateRange');
if (this.value === 'custom') {
customRange.style.display = 'flex';
} else {
customRange.style.display = 'none';
}
});
</script>
{% if recommendations %} {% if recommendations %}
<h3 style="margin-bottom: 15px;">Optimization Recommendations</h3> <h3 style="margin-bottom: 15px;">Optimization Recommendations</h3>
<div style="margin-bottom: 30px;"> <div style="margin-bottom: 30px;">
...@@ -77,14 +219,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -77,14 +219,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<h3 style="margin-top: 30px; margin-bottom: 15px;">Cost Overview</h3> <h3 style="margin-top: 30px; margin-bottom: 15px;">Cost Overview</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
<div style="background: #2ecc71; color: white; padding: 20px; border-radius: 8px;"> <div style="background: #2ecc71; color: white; padding: 20px; border-radius: 8px;">
<h4 style="font-size: 14px; margin-bottom: 10px;">Today's Estimated Cost</h4> <h4 style="font-size: 14px; margin-bottom: 10px;">
{% if from_date or to_date %}Selected Period Cost{% else %}Today's Estimated Cost{% endif %}
</h4>
<p style="font-size: 28px; font-weight: bold;">${{ "%.2f"|format(cost_overview.total_estimated_cost_today) }}</p> <p style="font-size: 28px; font-weight: bold;">${{ "%.2f"|format(cost_overview.total_estimated_cost_today) }}</p>
{% if cost_overview.date_range %}
<small style="color: rgba(255,255,255,0.8);">
{{ cost_overview.date_range.start[:10] }} to {{ cost_overview.date_range.end[:10] }}
</small>
{% endif %}
</div> </div>
{% for pc in cost_overview.providers %} {% for pc in cost_overview.providers %}
<div style="background: #0f3460; padding: 15px; border-radius: 8px;"> <div style="background: #0f3460; padding: 15px; border-radius: 8px;">
<h4 style="font-size: 14px; margin-bottom: 5px;">{{ pc.provider_id }}</h4> <h4 style="font-size: 14px; margin-bottom: 5px;">{{ pc.provider_id }}</h4>
<p style="font-size: 20px; font-weight: bold;">${{ "%.2f"|format(pc.estimated_cost) }}</p> <p style="font-size: 20px; font-weight: bold;">${{ "%.2f"|format(pc.estimated_cost) }}</p>
<small style="color: #a0a0a0;">{{ pc.tokens_today }} tokens today</small> <small style="color: #a0a0a0;">{{ pc.tokens_today }} tokens</small>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
...@@ -95,6 +244,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -95,6 +244,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<tr> <tr>
<th>Provider</th> <th>Provider</th>
<th>Model</th> <th>Model</th>
<th>Type</th>
<th>Context Size</th> <th>Context Size</th>
<th>Condense %</th> <th>Condense %</th>
<th>Condense Method</th> <th>Condense Method</th>
...@@ -106,6 +256,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -106,6 +256,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<tr> <tr>
<td>{{ model.provider_id }}</td> <td>{{ model.provider_id }}</td>
<td>{{ model.model_name }}</td> <td>{{ model.model_name }}</td>
<td>
{% if model.is_rotation %}
<span style="color: #f39c12;">Rotation: {{ model.rotation_id }}</span>
{% elif model.is_autoselect %}
<span style="color: #9b59b6;">Autoselect: {{ model.autoselect_id }}</span>
{% else %}
<span style="color: #3498db;">Provider Model</span>
{% endif %}
</td>
<td>{{ model.context_size|default('N/A') }}</td> <td>{{ model.context_size|default('N/A') }}</td>
<td>{{ model.condense_context|default('N/A') }}%</td> <td>{{ model.condense_context|default('N/A') }}%</td>
<td>{{ model.condense_method|default('None') }}</td> <td>{{ model.condense_method|default('None') }}</td>
...@@ -123,12 +282,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -123,12 +282,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<p style="color: #a0a0a0;">No model performance data available yet.</p> <p style="color: #a0a0a0;">No model performance data available yet.</p>
{% endif %} {% endif %}
<h3 style="margin-top: 30px; margin-bottom: 15px;">Token Usage Over Time (24h)</h3> <h3 style="margin-top: 30px; margin-bottom: 15px;">Token Usage Over Time {% if from_date or to_date %}(Custom Range){% else %}(24h){% endif %}</h3>
{% if token_over_time != '[]' %} {% if token_over_time != '[]' %}
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px;"> <div style="background: #1a1a2e; padding: 20px; border-radius: 8px;">
<canvas id="tokenChart" style="width: 100%; height: 300px;"></canvas> <canvas id="tokenChart" style="width: 100%; height: 300px;"></canvas>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
<script> <script>
const tokenData = {{ token_over_time|safe }}; const tokenData = {{ token_over_time|safe }};
...@@ -159,7 +319,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -159,7 +319,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
options: { options: {
responsive: true, responsive: true,
scales: { scales: {
x: { type: 'time', time: { unit: 'hour' } }, x: {
type: 'time',
time: {
unit: {% if selected_time_range == '1h' or selected_time_range == '6h' %}'hour'{% elif selected_time_range == '24h' %}'hour'{% elif selected_time_range == '7d' %}'day'{% else %}'day'{% endif %}
},
title: { display: true, text: 'Time' }
},
y: { beginAtZero: true, title: { display: true, text: 'Tokens' } } y: { beginAtZero: true, title: { display: true, text: 'Tokens' } }
} }
} }
...@@ -169,10 +335,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -169,10 +335,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
new Chart(document.getElementById('tokenChart'), { new Chart(document.getElementById('tokenChart'), {
type: 'line', type: 'line',
data: { data: {
labels: tokenData.map(d => d.timestamp),
datasets: [{ datasets: [{
label: 'Tokens Used', label: 'Tokens Used',
data: tokenData.map(d => d.tokens), data: tokenData.map(d => ({x: d.timestamp, y: d.tokens})),
borderColor: '#e94560', borderColor: '#e94560',
backgroundColor: 'rgba(233, 69, 96, 0.1)', backgroundColor: 'rgba(233, 69, 96, 0.1)',
fill: true, fill: true,
...@@ -182,7 +347,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -182,7 +347,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
options: { options: {
responsive: true, responsive: true,
scales: { scales: {
x: { title: { display: true, text: 'Time' } }, x: {
type: 'time',
time: {
unit: {% if selected_time_range == '1h' or selected_time_range == '6h' %}'hour'{% elif selected_time_range == '24h' %}'hour'{% elif selected_time_range == '7d' %}'day'{% else %}'day'{% endif %}
},
title: { display: true, text: 'Time' }
},
y: { beginAtZero: true, title: { display: true, text: 'Tokens' } } y: { beginAtZero: true, title: { display: true, text: 'Tokens' } }
} }
} }
...@@ -190,7 +361,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -190,7 +361,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
} }
</script> </script>
{% else %} {% else %}
<p style="color: #a0a0a0;">No token usage data available yet.</p> <p style="color: #a0a0a0;">No token usage data available for the selected period.</p>
{% endif %} {% endif %}
<div style="margin-top: 30px; display: flex; gap: 10px;"> <div style="margin-top: 30px; display: flex; gap: 10px;">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment