diff --git a/llmage/init.py b/llmage/init.py index e0f3080..7dd86f3 100644 --- a/llmage/init.py +++ b/llmage/init.py @@ -32,6 +32,7 @@ from .accounting import ( get_failed_accounting_records, llm_accoung_failed ) +from .stats import get_llmage_stats from .asyncinference import ( get_asynctask_status, @@ -63,6 +64,7 @@ def load_llmage(): env.get_llms_by_catelog_to_customer = get_llms_by_catelog_to_customer env.backup_accounted_llmusage = backup_accounted_llmusage env.get_failed_accounting_records = get_failed_accounting_records + env.get_llmage_stats = get_llmage_stats rf = RegisterFunction() rf.register('jimeng_auth_headers', jimeng_auth_headers) diff --git a/llmage/stats.py b/llmage/stats.py new file mode 100644 index 0000000..8afe0f2 --- /dev/null +++ b/llmage/stats.py @@ -0,0 +1,69 @@ +from sqlor.dbpools import get_sor_context +from appPublic.timeUtils import curDateString, timestampstr +from datetime import datetime, timedelta +from appPublic.log import debug, exception + +async def get_llmage_stats(request): + """Get llmage module statistics""" + env = request._run_ns + userorgid = await env.get_userorgid() + today = curDateString() + tomorrow = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d') + + stats = { + 'total_models': 0, + 'today_usage_count': 0, + 'today_amount': 0, + 'catelog_count': 0 + } + + async with get_sor_context(env, 'llmage') as sor: + # Total enabled models + sql_models = """ + SELECT COUNT(DISTINCT id) as cnt FROM llm + WHERE enabled_date <= ${today}$ + AND expired_date > ${today}$ + """ + recs = await sor.sqlExe(sql_models, {'today': today}) + if recs: + stats['total_models'] = int(recs[0].cnt or 0) + + # Today's usage count + sql_usage = """ + SELECT COUNT(*) as cnt FROM llmusage + WHERE userorgid = ${userorgid}$ + AND use_date >= ${today}$ + AND use_date < ${tomorrow}$ + """ + recs = await sor.sqlExe(sql_usage, { + 'userorgid': userorgid, + 'today': today, + 'tomorrow': tomorrow + }) + if recs: + stats['today_usage_count'] = int(recs[0].cnt or 0) + + # Today's total amount + sql_amount = """ + SELECT COALESCE(SUM(amount), 0) as total FROM llmusage + WHERE userorgid = ${userorgid}$ + AND use_date >= ${today}$ + AND use_date < ${tomorrow}$ + """ + recs = await sor.sqlExe(sql_amount, { + 'userorgid': userorgid, + 'today': today, + 'tomorrow': tomorrow + }) + if recs: + stats['today_amount'] = float(recs[0].total or 0) + + # Catalog count + sql_catelog = """ + SELECT COUNT(*) as cnt FROM llmcatelog + """ + recs = await sor.sqlExe(sql_catelog, {}) + if recs: + stats['catelog_count'] = int(recs[0].cnt or 0) + + return stats diff --git a/wwwroot/index.ui b/wwwroot/index.ui index 86137d7..c9dcf6e 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -35,6 +35,40 @@ } ] }, + { + "widgettype": "ResponsableBox", + "options": { + "gap": "16px", + "minWidth": "200px", + "marginBottom": "24px" + }, + "subwidgets": [ + { + "widgettype": "urlwidget", + "options": { + "url": "{{entire_url('/llmage/stat_total_models.ui')}}" + } + }, + { + "widgettype": "urlwidget", + "options": { + "url": "{{entire_url('/llmage/stat_today_calls.ui')}}" + } + }, + { + "widgettype": "urlwidget", + "options": { + "url": "{{entire_url('/llmage/stat_today_amount.ui')}}" + } + }, + { + "widgettype": "urlwidget", + "options": { + "url": "{{entire_url('/llmage/stat_catelog_count.ui')}}" + } + } + ] + }, { "widgettype": "ResponsableBox", "options": { diff --git a/wwwroot/stat_catelog_count.ui b/wwwroot/stat_catelog_count.ui new file mode 100644 index 0000000..3bd121f --- /dev/null +++ b/wwwroot/stat_catelog_count.ui @@ -0,0 +1,53 @@ +{% set stats = get_llmage_stats(request) %} +{ + "widgettype": "VBox", + "options": { + "bgcolor": "#1E293B", + "padding": "20px", + "borderRadius": "12px", + "border": "1px solid #334155", + "flex": "1", + "minHeight": "110px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "text": "{{stats.catelog_count}}", + "fontSize": "32px", + "fontWeight": "700", + "color": "#F1F5F9", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "text": "模型分类", + "fontSize": "14px", + "color": "#94A3B8", + "marginTop": "4px" + } + } + ] +} diff --git a/wwwroot/stat_today_amount.ui b/wwwroot/stat_today_amount.ui new file mode 100644 index 0000000..2ac8469 --- /dev/null +++ b/wwwroot/stat_today_amount.ui @@ -0,0 +1,53 @@ +{% set stats = get_llmage_stats(request) %} +{ + "widgettype": "VBox", + "options": { + "bgcolor": "#1E293B", + "padding": "20px", + "borderRadius": "12px", + "border": "1px solid #334155", + "flex": "1", + "minHeight": "110px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "text": "¥{{'%.2f' % stats.today_amount}}", + "fontSize": "32px", + "fontWeight": "700", + "color": "#F1F5F9", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "text": "今日消费", + "fontSize": "14px", + "color": "#94A3B8", + "marginTop": "4px" + } + } + ] +} diff --git a/wwwroot/stat_today_calls.ui b/wwwroot/stat_today_calls.ui new file mode 100644 index 0000000..7edd862 --- /dev/null +++ b/wwwroot/stat_today_calls.ui @@ -0,0 +1,53 @@ +{% set stats = get_llmage_stats(request) %} +{ + "widgettype": "VBox", + "options": { + "bgcolor": "#1E293B", + "padding": "20px", + "borderRadius": "12px", + "border": "1px solid #334155", + "flex": "1", + "minHeight": "110px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "text": "{{stats.today_usage_count}}", + "fontSize": "32px", + "fontWeight": "700", + "color": "#F1F5F9", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "text": "今日调用", + "fontSize": "14px", + "color": "#94A3B8", + "marginTop": "4px" + } + } + ] +} diff --git a/wwwroot/stat_total_models.ui b/wwwroot/stat_total_models.ui new file mode 100644 index 0000000..3861e52 --- /dev/null +++ b/wwwroot/stat_total_models.ui @@ -0,0 +1,53 @@ +{% set stats = get_llmage_stats(request) %} +{ + "widgettype": "VBox", + "options": { + "bgcolor": "#1E293B", + "padding": "20px", + "borderRadius": "12px", + "border": "1px solid #334155", + "flex": "1", + "minHeight": "110px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "marginBottom": "12px" + }, + "subwidgets": [ + { + "widgettype": "Svg", + "options": { + "svg": "", + "width": "24px", + "height": "24px" + } + }, + { + "widgettype": "Filler" + } + ] + }, + { + "widgettype": "Text", + "options": { + "text": "{{stats.total_models}}", + "fontSize": "32px", + "fontWeight": "700", + "color": "#F1F5F9", + "lineHeight": "1.1" + } + }, + { + "widgettype": "Text", + "options": { + "text": "可用模型数", + "fontSize": "14px", + "color": "#94A3B8", + "marginTop": "4px" + } + } + ] +}