diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..600a1b5 --- /dev/null +++ b/build.sh @@ -0,0 +1,42 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SAGE_ROOT="$(cd "$SCRIPT_DIR/../.." 2>/dev/null && pwd || echo "$SCRIPT_DIR")" +VENV_PYTHON="$SAGE_ROOT/py3/bin/python" +VENV_PIP="$SAGE_ROOT/py3/bin/pip" +WWWROOT="$SAGE_ROOT/wwwroot" + +MODULE_NAME="dashboard_for_sage" + +echo "=== Building $MODULE_NAME ===" + +# Install the module +cd "$SCRIPT_DIR" +$VENV_PIP install -e . + +# Link wwwroot files to Sage wwwroot +MODULE_WWWROOT="$SCRIPT_DIR/wwwroot" +SAGE_MODULE_WWWROOT="$WWWROOT/$MODULE_NAME" + +echo "Linking wwwroot..." +mkdir -p "$SAGE_MODULE_WWWROOT" + +# Link all .ui files +for f in "$MODULE_WWWROOT"/*.ui; do + [ -f "$f" ] && ln -sf "$f" "$SAGE_MODULE_WWWROOT/" +done + +# Link api/ directory +mkdir -p "$SAGE_MODULE_WWWROOT/api" +for f in "$MODULE_WWWROOT/api"/*.dspy; do + [ -f "$f" ] && ln -sf "$f" "$SAGE_MODULE_WWWROOT/api/" +done + +# Link scripts/ directory +mkdir -p "$SAGE_MODULE_WWWROOT/scripts" +for f in "$MODULE_WWWROOT/scripts"/*.js; do + [ -f "$f" ] && ln -sf "$f" "$SAGE_MODULE_WWWROOT/scripts/" +done + +echo "=== $MODULE_NAME build complete ===" diff --git a/dashboard_for_sage/__init__.py b/dashboard_for_sage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard_for_sage/init.py b/dashboard_for_sage/init.py new file mode 100644 index 0000000..96ab9da --- /dev/null +++ b/dashboard_for_sage/init.py @@ -0,0 +1,12 @@ +from ahserver.serverenv import ServerEnv + +MODULE_NAME = "dashboard_for_sage" +MODULE_VERSION = "1.0.0" + + +def load_dashboard_for_sage(): + """Register dashboard module with ServerEnv.""" + env = ServerEnv() + # Dashboard is pure display - all logic is in .dspy files + # which use globals() functions directly. + return True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5b83315 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "dashboard_for_sage" +version = "1.0.0" +description = "Dashboard module for Sage - LLM usage and user statistics" +requires-python = ">=3.8" +dependencies = [ + "sqlor", + "bricks_for_python", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["dashboard_for_sage*"] diff --git a/wwwroot/api/get_today_usage.dspy b/wwwroot/api/get_today_usage.dspy new file mode 100644 index 0000000..d54a3e1 --- /dev/null +++ b/wwwroot/api/get_today_usage.dspy @@ -0,0 +1,24 @@ +"""获取当天llmusage笔数和交易金额""" +import json +from datetime import date + +ns = params_kw.copy() + +sql = """ +SELECT + COUNT(*) as cnt, + COALESCE(SUM(amount), 0) as total_amount +FROM llmusage +WHERE use_date = ${today}$ +""" + +today = date.today().isoformat() +db = DBPools() +async with db.sqlorContext('sage') as sor: + recs = await sor.sqlExe(sql, {'today': today}) + if recs: + return { + 'cnt': int(recs[0].get('cnt', 0)), + 'total_amount': float(recs[0].get('total_amount', 0)) + } +return {'cnt': 0, 'total_amount': 0} diff --git a/wwwroot/api/get_top_models.dspy b/wwwroot/api/get_top_models.dspy new file mode 100644 index 0000000..06d502d --- /dev/null +++ b/wwwroot/api/get_top_models.dspy @@ -0,0 +1,32 @@ +"""获取当天排名前三的模型数量和金额""" +from datetime import date + +ns = params_kw.copy() +today = date.today().isoformat() + +sql = """ +SELECT + b.name as model_name, + COUNT(*) as cnt, + COALESCE(SUM(a.amount), 0) as total_amount +FROM llmusage a +LEFT JOIN llm b ON a.llmid = b.id +WHERE a.use_date = ${today}$ +GROUP BY a.llmid, b.name +ORDER BY cnt DESC +LIMIT 3 +""" + +db = DBPools() +async with db.sqlorContext('sage') as sor: + recs = await sor.sqlExe(sql, {'today': today}) + result = [] + for r in recs: + result.append({ + 'model_name': r.get('model_name', 'Unknown'), + 'cnt': int(r.get('cnt', 0)), + 'total_amount': float(r.get('total_amount', 0)) + }) + return result + +return [] diff --git a/wwwroot/api/get_user_stats.dspy b/wwwroot/api/get_user_stats.dspy new file mode 100644 index 0000000..fd5210e --- /dev/null +++ b/wwwroot/api/get_user_stats.dspy @@ -0,0 +1,35 @@ +"""获取当前用户总数和并发用户数(近5分钟有活跃记录的用户)""" +from datetime import datetime, timedelta + +ns = params_kw.copy() +db = DBPools() + +# 总用户数 +sql_users = "SELECT COUNT(*) as total_users FROM users" + +# 并发用户数:近5分钟内在llmusage中有记录的不同用户 +now = datetime.now() +five_min_ago = (now - timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S') +today = now.strftime('%Y-%m-%d') + +sql_concurrent = """ +SELECT COUNT(DISTINCT userid) as concurrent_users +FROM llmusage +WHERE use_date = ${today}$ + AND use_time >= ${five_min_ago}$ +""" + +async with db.sqlorContext('sage') as sor: + user_recs = await sor.sqlExe(sql_users, {}) + total_users = int(user_recs[0].get('total_users', 0)) if user_recs else 0 + + conc_recs = await sor.sqlExe(sql_concurrent, { + 'today': today, + 'five_min_ago': five_min_ago + }) + concurrent_users = int(conc_recs[0].get('concurrent_users', 0)) if conc_recs else 0 + +return { + 'total_users': total_users, + 'concurrent_users': concurrent_users +} diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..a88a00e --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,204 @@ +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "padding": "20px", + "bgcolor": "#f0f2f5" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "Dashboard", + "fontSize": "24px", + "fontWeight": "bold", + "color": "#333", + "marginBottom": "20px" + } + }, + { + "widgettype": "ResponsableBox", + "options": { + "gap": "16px", + "minWidth": "250px" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "id": "card_today_cnt", + "options": { + "bgcolor": "#FFFFFF", + "padding": "24px", + "borderRadius": "8px", + "flex": "1", + "minHeight": "120px", + "boxShadow": "0 2px 8px rgba(0,0,0,0.1)" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "今日调用笔数", + "fontSize": "14px", + "color": "#888", + "marginBottom": "8px" + } + }, + { + "widgettype": "Text", + "id": "today_cnt_value", + "options": { + "text": "--", + "fontSize": "32px", + "fontWeight": "bold", + "color": "#1890ff" + } + } + ] + }, + { + "widgettype": "VBox", + "id": "card_today_amount", + "options": { + "bgcolor": "#FFFFFF", + "padding": "24px", + "borderRadius": "8px", + "flex": "1", + "minHeight": "120px", + "boxShadow": "0 2px 8px rgba(0,0,0,0.1)" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "今日交易金额", + "fontSize": "14px", + "color": "#888", + "marginBottom": "8px" + } + }, + { + "widgettype": "Text", + "id": "today_amount_value", + "options": { + "text": "--", + "fontSize": "32px", + "fontWeight": "bold", + "color": "#52c41a" + } + } + ] + }, + { + "widgettype": "VBox", + "id": "card_total_users", + "options": { + "bgcolor": "#FFFFFF", + "padding": "24px", + "borderRadius": "8px", + "flex": "1", + "minHeight": "120px", + "boxShadow": "0 2px 8px rgba(0,0,0,0.1)" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "用户总数", + "fontSize": "14px", + "color": "#888", + "marginBottom": "8px" + } + }, + { + "widgettype": "Text", + "id": "total_users_value", + "options": { + "text": "--", + "fontSize": "32px", + "fontWeight": "bold", + "color": "#722ed1" + } + } + ] + }, + { + "widgettype": "VBox", + "id": "card_concurrent_users", + "options": { + "bgcolor": "#FFFFFF", + "padding": "24px", + "borderRadius": "8px", + "flex": "1", + "minHeight": "120px", + "boxShadow": "0 2px 8px rgba(0,0,0,0.1)" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "当前并发用户", + "fontSize": "14px", + "color": "#888", + "marginBottom": "8px" + } + }, + { + "widgettype": "Text", + "id": "concurrent_users_value", + "options": { + "text": "--", + "fontSize": "32px", + "fontWeight": "bold", + "color": "#fa8c16" + } + } + ] + } + ] + }, + { + "widgettype": "VBox", + "id": "chart_section", + "options": { + "bgcolor": "#FFFFFF", + "padding": "24px", + "borderRadius": "8px", + "marginTop": "20px", + "minHeight": "350px", + "boxShadow": "0 2px 8px rgba(0,0,0,0.1)" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "Top 3 模型(今日)", + "fontSize": "18px", + "fontWeight": "bold", + "color": "#333", + "marginBottom": "16px" + } + }, + { + "widgettype": "ChartBar", + "id": "top_models_chart", + "options": { + "height": "300px", + "width": "100%", + "data_url": "{{entire_url('api/get_top_models.dspy')}}", + "nameField": "model_name", + "valueFields": ["cnt", "total_amount"] + } + } + ] + }, + { + "widgettype": "Html", + "id": "auto_refresh", + "options": { + "html": "