595 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""sage_datamart 模块初始化 — 统一 Dashboard API支持客户/分销商角色"""
from ahserver.serverenv import ServerEnv
from appPublic.log import debug
from sqlor.dbpools import get_sor_context
from .etl import sync_call_fact, aggregate_daily_perf, aggregate_provider_cost
from .dashboards import (
CUSTOMER, DISTRIBUTOR,
get_today_usage, get_today_amount, get_success_rate, get_fail_count,
get_active_users, get_top_models, get_top_providers, get_top_users,
get_customer_daily_models, get_customer_monthly_models,
get_customer_user_today, get_daily_trend, get_hourly_concurrency,
get_avg_ttft, get_currency_breakdown,
get_realtime_calls, get_realtime_amount, get_total_users_count,
get_realtime_active_users, get_online_users, get_realtime_stats,
get_distributor_customers_count, get_distributor_active_customers,
get_distributor_top_customers,
get_scoped_today_usage, get_scoped_today_amount,
get_scoped_success_rate, get_scoped_fail_count,
)
MODULE_NAME = "sage_datamart"
async def _resolve_view(request):
"""解析当前用户角色和机构过滤条件
返回 (scope, org_filter) — scope: 'customer'|'distributor'|None, org_filter: org_id|None
org_filter=None 表示不按机构过滤(平台管理员看全部数据)
"""
env = request._run_ns
userorgid = await env.get_userorgid()
scope = CUSTOMER
org_filter = userorgid
# 平台根机构('0')无过滤,看全部数据
if not userorgid or userorgid == '0':
return None, None
try:
async with get_sor_context(env, MODULE_NAME) as sor:
recs = await sor.sqlExe(
"SELECT org_type FROM organization WHERE id = ${id}$",
{'id': userorgid})
if recs:
org_type = (recs[0].org_type if hasattr(recs[0], 'org_type')
else recs[0].get('org_type', ''))
if org_type == 'reseller':
scope = DISTRIBUTOR
except Exception as e:
debug("sage_datamart _resolve_view error: " + str(e))
pass
return scope, org_filter
# ── 性能指标 API ──
async def api_model_perf(sor, params_kw=None):
try:
stat_date = (params_kw or {}).get('date')
sql = """SELECT COALESCE(l.name, f.model) as model,
CASE WHEN SUM(total_calls)>0 THEN ROUND(SUM(total_calls*COALESCE(avg_ttft_ms,avg_ttot_ms))/SUM(total_calls),1) ELSE NULL END as avg_latency_ms,
CASE WHEN SUM(total_calls)>0 THEN ROUND(SUM(total_calls*avg_ttot_ms)/SUM(total_calls),1) ELSE NULL END as avg_ttot_ms,
ROUND(SUM(success_calls)*100.0/GREATEST(SUM(total_calls),1),1) as success_rate
FROM dm_model_perf_daily f
LEFT JOIN llm l ON l.id = f.llmid
WHERE 1=1"""
ns = {}
if stat_date:
sql += " AND f.stat_date=${stat_date}$"
ns['stat_date'] = stat_date
sql += " GROUP BY f.model, f.catelogid, f.providerid"
sql += " ORDER BY avg_latency_ms ASC LIMIT 5"
rows = await sor.sqlExe(sql, ns)
return [dict(r) for r in rows]
except Exception as e:
debug("sage_datamart API error: " + str(e))
return []
async def api_provider_roi(sor, params_kw=None):
try:
model = (params_kw or {}).get('model')
stat_date = (params_kw or {}).get('date')
sql = """SELECT p.*, COALESCE(o.orgname, p.providerid) as provider_name
FROM dm_provider_cost_daily p
LEFT JOIN organization o ON o.id = p.providerid
WHERE 1=1"""
ns = {}
if stat_date:
sql += " AND stat_date=${stat_date}$"
ns['stat_date'] = stat_date
if model:
sql += " AND model=${model}$"
ns['model'] = model
sql += " ORDER BY total_amount DESC LIMIT 50"
rows = await sor.sqlExe(sql, ns)
return [dict(r) for r in rows]
except Exception as e:
debug("sage_datamart API error: " + str(e))
return []
# ── 运营指标 API ──
async def api_stats(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return {
'today_usage': await get_today_usage(env, sor),
'today_amount': await get_today_amount(env, sor),
'success_rate': await get_success_rate(env, sor),
'fail_count': await get_fail_count(env, sor),
'active_users': await get_active_users(env, sor),
}
except Exception as e:
debug("sage_datamart API error: " + str(e))
return {}
async def api_top_models(request):
try:
env = request._run_ns
scope, org_filter = await _resolve_view(request)
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_top_models(env, sor, org_filter=org_filter)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return []
async def api_top_providers(request):
try:
env = request._run_ns
scope, org_filter = await _resolve_view(request)
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_top_providers(env, sor, org_filter=org_filter)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return []
async def api_top_users(request):
try:
env = request._run_ns
scope, org_filter = await _resolve_view(request)
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_top_users(env, sor, org_filter=org_filter)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return []
async def api_customer_models(request):
try:
env = request._run_ns
userorgid = await env.get_userorgid()
async with get_sor_context(env, MODULE_NAME) as sor:
return {
'daily': await get_customer_daily_models(env, sor, userorgid),
'monthly': await get_customer_monthly_models(env, sor, userorgid),
}
except Exception as e:
debug("sage_datamart API error: " + str(e))
return {'daily': [], 'monthly': []}
async def api_daily_trend(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_daily_trend(env, sor)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return []
async def api_hourly_concurrency(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_hourly_concurrency(env, sor)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return []
async def api_currency_stats(request):
try:
env = request._run_ns
scope, org_filter = await _resolve_view(request)
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_currency_breakdown(env, sor, org_filter=org_filter)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return []
# ── 实时指标 API角色感知──
async def api_realtime_cards(request):
"""实时统计卡片 — /sage_datamart/api/realtime_cards.dspy"""
try:
env = request._run_ns
scope, org_filter = await _resolve_view(request)
today = env.curDateString()
async with get_sor_context(env, MODULE_NAME) as sor:
if scope == DISTRIBUTOR:
return {
'scope': scope,
'calls': await get_realtime_calls(sor, today, scope, org_filter),
'amount': await get_realtime_amount(sor, today, scope, org_filter),
'total_users': await get_distributor_customers_count(sor, org_filter),
'active_customers': await get_distributor_active_customers(sor, today, org_filter),
'online_users': await get_online_users(env),
'success_rate': await get_scoped_success_rate(sor, today, scope, org_filter),
'fail_count': await get_scoped_fail_count(sor, today, scope, org_filter),
}
else:
return {
'scope': scope,
'calls': await get_realtime_calls(sor, today, scope, org_filter),
'amount': await get_realtime_amount(sor, today, scope, org_filter),
'total_users': await get_total_users_count(sor, scope, org_filter),
'active_users': await get_realtime_active_users(sor, today, scope, org_filter),
'online_users': await get_online_users(env),
'success_rate': await get_scoped_success_rate(sor, today, scope, org_filter),
'fail_count': await get_scoped_fail_count(sor, today, scope, org_filter),
}
except Exception as e:
debug("sage_datamart API error: " + str(e))
return {}
# ── 分销商专用 API ──
async def api_distributor_customers(request):
"""分销商客户排行 — /sage_datamart/api/distributor_customers.dspy"""
try:
env = request._run_ns
scope, org_filter = await _resolve_view(request)
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_distributor_top_customers(sor, org_filter)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return []
# ── Jinja2 模板函数(实时,角色感知)──
async def _get_realtime_snapshot(request):
"""一次查询返回全部实时指标(含角色),同一请求内缓存复用"""
cache_key = '_sage_datamart_rt_cache'
if cache_key in request:
return request[cache_key]
env = request._run_ns
scope, org_filter = await _resolve_view(request)
today = env.curDateString()
async with get_sor_context(env, MODULE_NAME) as sor:
result = await get_realtime_stats(sor, today, scope, org_filter)
request[cache_key] = result
return result
async def j2_realtime_calls(request):
try:
s = await _get_realtime_snapshot(request)
return s.get('calls', 0)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def j2_realtime_amount(request):
try:
s = await _get_realtime_snapshot(request)
return s.get('amount', 0)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def j2_total_users(request):
try:
env = request._run_ns
scope, org_filter = await _resolve_view(request)
async with get_sor_context(env, MODULE_NAME) as sor:
if scope == DISTRIBUTOR:
return await get_distributor_customers_count(sor, org_filter)
return await get_total_users_count(sor, scope, org_filter)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def j2_active_today(request):
try:
s = await _get_realtime_snapshot(request)
return s.get('active_users', 0)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def j2_online_users(request):
try:
env = request._run_ns
return await get_online_users(env)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def j2_scoped_success_rate(request):
try:
s = await _get_realtime_snapshot(request)
return s.get('success_rate', 0)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def j2_scoped_fail_count(request):
try:
s = await _get_realtime_snapshot(request)
return s.get('fail_count', 0)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
# ── 向后兼容 Jinja2 函数 ──
async def j2_today_usage(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_today_usage(env, sor)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def j2_today_amount(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_today_amount(env, sor)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def j2_active_users(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_active_users(env, sor)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def j2_fail_count(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_fail_count(env, sor)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def j2_success_rate(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_success_rate(env, sor)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def j2_avg_ttft(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_avg_ttft(env, sor)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return 0
async def api_user_apikeys(request):
"""当前用户每个 API Key 的使用排行"""
try:
env = request._run_ns
userid = await env.get_userid()
async with get_sor_context(env, MODULE_NAME) as sor:
sql = """SELECT uak.name as apikey_name, COUNT(*) as cnt, COALESCE(SUM(lu.amount), 0) as total_amount
FROM llmusage lu
JOIN userapikey uak ON uak.apikey = lu.apikey
WHERE lu.userid = ${userid}$
GROUP BY uak.name ORDER BY cnt DESC LIMIT 10"""
recs = await sor.sqlExe(sql, {'userid': userid})
return [{'apikey_name': r.apikey_name or 'Unknown',
'cnt': int(r.cnt or 0),
'total_amount': round(float(r.total_amount or 0), 2)} for r in recs]
except Exception as e:
debug("sage_datamart API error: " + str(e))
return []
async def api_distributor_financial(request):
"""分销商财务统计 API"""
try:
env = request._run_ns
scope, org_filter = await _resolve_view(request)
period = (request._run_ns.params_kw or {}).get('period', 'today')
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_distributor_financial(sor, org_filter, period)
except Exception as e:
debug("sage_datamart API error: " + str(e))
return {}
async def j2_distributor_financial(request, period='today'):
"""Jinja2: 分销商财务数据"""
try:
env = request._run_ns
scope, org_filter = await _resolve_view(request)
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_distributor_financial(sor, org_filter, period)
except Exception:
return {'total_revenue_cny': 0, 'total_cost_cny': 0, 'gross_profit_cny': 0,
'revenue_by_currency': [], 'total_calls': 0, 'period': period}
async def j2_is_distributor(request):
"""Jinja2: 是否分销商角色"""
try:
scope, _ = await _resolve_view(request)
return scope == DISTRIBUTOR
except Exception:
return False
# ── 监控组件 j2 函数,每段独立 .ui 通过 RefreshWidget 加载 ──
async def j2_model_perf_data(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await api_model_perf(sor)
except Exception:
return []
async def j2_currency_data(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_currency_breakdown(env, sor)
except Exception:
return []
async def j2_provider_roi_data(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await api_provider_roi(sor)
except Exception:
return []
async def j2_top_models_data(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_top_models(env, sor)
except Exception:
return []
async def j2_top_users_data(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_top_users(env, sor)
except Exception:
return []
async def j2_top_providers_data(request):
try:
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
return await get_top_providers(env, sor)
except Exception:
return []
# ── Cron ETL ──
async def cron_etl_sync(request):
ip = request['client_ip']
if ip not in ['127.0.0.1']:
return {'error': f'IP {ip} not allowed'}
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
count = await sync_call_fact(sor)
return {'status': 'ok', 'synced': count}
async def cron_etl_aggregate(request):
ip = request['client_ip']
if ip not in ['127.0.0.1']:
return {'error': f'IP {ip} not allowed'}
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
count = await aggregate_daily_perf(sor)
return {'status': 'ok', 'aggregated': count}
async def cron_etl_provider_cost(request):
ip = request['client_ip']
if ip not in ['127.0.0.1']:
return {'error': f'IP {ip} not allowed'}
env = request._run_ns
async with get_sor_context(env, MODULE_NAME) as sor:
count = await aggregate_provider_cost(sor)
return {'status': 'ok', 'aggregated': count}
def load_sage_datamart():
env = ServerEnv()
env.api_model_perf = api_model_perf
env.api_provider_roi = api_provider_roi
env.api_stats = api_stats
env.api_top_models = api_top_models
env.api_top_providers = api_top_providers
env.api_top_users = api_top_users
env.api_customer_models = api_customer_models
env.api_daily_trend = api_daily_trend
env.api_hourly_concurrency = api_hourly_concurrency
env.api_currency_stats = api_currency_stats
env.api_realtime_cards = api_realtime_cards
env.api_distributor_customers = api_distributor_customers
env.j2_today_usage = j2_today_usage
env.j2_today_amount = j2_today_amount
env.j2_active_users = j2_active_users
env.j2_fail_count = j2_fail_count
env.j2_success_rate = j2_success_rate
env.j2_avg_ttft = j2_avg_ttft
env.j2_realtime_calls = j2_realtime_calls
env.j2_realtime_amount = j2_realtime_amount
env.j2_total_users = j2_total_users
env.j2_active_today = j2_active_today
env.j2_online_users = j2_online_users
env.j2_scoped_success_rate = j2_scoped_success_rate
env.j2_scoped_fail_count = j2_scoped_fail_count
env.api_user_apikeys = api_user_apikeys
env.api_distributor_financial = api_distributor_financial
env.j2_distributor_financial = j2_distributor_financial
env.j2_is_distributor = j2_is_distributor
env.j2_model_perf_data = j2_model_perf_data
env.j2_currency_data = j2_currency_data
env.j2_provider_roi_data = j2_provider_roi_data
env.j2_top_models_data = j2_top_models_data
env.j2_top_users_data = j2_top_users_data
env.j2_top_providers_data = j2_top_providers_data
env.cron_etl_sync = cron_etl_sync
env.cron_etl_aggregate = cron_etl_aggregate
env.cron_etl_provider_cost = cron_etl_provider_cost
debug("[sage_datamart] registered all API + realtime + role-aware endpoints")