diff --git a/api/controllers/console/app/statistic.py b/api/controllers/console/app/statistic.py index 8e4ac51c7f..f13721350f 100644 --- a/api/controllers/console/app/statistic.py +++ b/api/controllers/console/app/statistic.py @@ -398,9 +398,74 @@ class AverageResponseTimeStatistic(Resource): }) +class TokensPerSecondStatistic(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self, app_id): + account = current_user + app_id = str(app_id) + app_model = _get_app(app_id) + + parser = reqparse.RequestParser() + parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args') + parser.add_argument('end', type=datetime_string('%Y-%m-%d %H:%M'), location='args') + args = parser.parse_args() + + sql_query = '''SELECT date(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date, + CASE + WHEN SUM(provider_response_latency) = 0 THEN 0 + ELSE (SUM(answer_tokens) / SUM(provider_response_latency)) + END as tokens_per_second +FROM messages +WHERE app_id = :app_id''' + arg_dict = {'tz': account.timezone, 'app_id': app_model.id} + + timezone = pytz.timezone(account.timezone) + utc_timezone = pytz.utc + + if args['start']: + start_datetime = datetime.strptime(args['start'], '%Y-%m-%d %H:%M') + start_datetime = start_datetime.replace(second=0) + + start_datetime_timezone = timezone.localize(start_datetime) + start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone) + + sql_query += ' and created_at >= :start' + arg_dict['start'] = start_datetime_utc + + if args['end']: + end_datetime = datetime.strptime(args['end'], '%Y-%m-%d %H:%M') + end_datetime = end_datetime.replace(second=0) + + end_datetime_timezone = timezone.localize(end_datetime) + end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone) + + sql_query += ' and created_at < :end' + arg_dict['end'] = end_datetime_utc + + sql_query += ' GROUP BY date order by date' + + with db.engine.begin() as conn: + rs = conn.execute(db.text(sql_query), arg_dict) + + response_data = [] + + for i in rs: + response_data.append({ + 'date': str(i.date), + 'tps': round(i.tokens_per_second, 4) + }) + + return jsonify({ + 'data': response_data + }) + + api.add_resource(DailyConversationStatistic, '/apps//statistics/daily-conversations') api.add_resource(DailyTerminalsStatistic, '/apps//statistics/daily-end-users') api.add_resource(DailyTokenCostStatistic, '/apps//statistics/token-costs') api.add_resource(AverageSessionInteractionStatistic, '/apps//statistics/average-session-interactions') api.add_resource(UserSatisfactionRateStatistic, '/apps//statistics/user-satisfaction-rate') api.add_resource(AverageResponseTimeStatistic, '/apps//statistics/average-response-time') +api.add_resource(TokensPerSecondStatistic, '/apps//statistics/tokens-per-second') diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx index dcf6923520..6570c23a95 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import useSWR from 'swr' import { fetchAppDetail } from '@/service/apps' import type { PeriodParams } from '@/app/components/app/overview/appChart' -import { AvgResponseTime, AvgSessionInteractions, ConversationsChart, CostChart, EndUsersChart, UserSatisfactionRate } from '@/app/components/app/overview/appChart' +import { AvgResponseTime, AvgSessionInteractions, ConversationsChart, CostChart, EndUsersChart, TokenPerSecond, UserSatisfactionRate } from '@/app/components/app/overview/appChart' import type { Item } from '@/app/components/base/select' import { SimpleSelect } from '@/app/components/base/select' import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter' @@ -65,10 +65,17 @@ export default function ChartView({ appId }: IChartViewProps) { )}
- + +
+ +
+
+ +
+
+
- ) } diff --git a/web/app/components/app-sidebar/basic.tsx b/web/app/components/app-sidebar/basic.tsx index b354e6ba15..3e467d6dcf 100644 --- a/web/app/components/app-sidebar/basic.tsx +++ b/web/app/components/app-sidebar/basic.tsx @@ -4,7 +4,6 @@ import { } from '@heroicons/react/24/outline' import Tooltip from '../base/tooltip' import AppIcon from '../base/app-icon' - const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_' export function randomString(length: number) { @@ -21,6 +20,7 @@ export type IAppBasicProps = { type: string | React.ReactNode hoverTip?: string textStyle?: { main?: string; extra?: string } + isExtraInLine?: boolean } const ApiSvg = @@ -61,7 +61,7 @@ const ICON_MAP = { notion: , } -export default function AppBasic({ icon, icon_background, name, type, hoverTip, textStyle, iconType = 'app' }: IAppBasicProps) { +export default function AppBasic({ icon, icon_background, name, type, hoverTip, textStyle, iconType = 'app', isExtraInLine }: IAppBasicProps) { return (
{icon && icon_background && iconType === 'app' && ( diff --git a/web/app/components/app/overview/appChart.tsx b/web/app/components/app/overview/appChart.tsx index 39ed250a79..2f0986f426 100644 --- a/web/app/components/app/overview/appChart.tsx +++ b/web/app/components/app/overview/appChart.tsx @@ -231,6 +231,7 @@ const Chart: React.FC = ({
= ({ id, period }) => { /> } +export const TokenPerSecond: FC = ({ id, period }) => { + const { t } = useTranslation() + const { data: response } = useSWR({ url: `/apps/${id}/statistics/tokens-per-second`, params: period.query }, getAppStatistics) + if (!response) + return + const noDataFlag = !response.data || response.data.length === 0 + return +} + export const UserSatisfactionRate: FC = ({ id, period }) => { const { t } = useTranslation() const { data: response } = useSWR({ url: `/apps/${id}/statistics/user-satisfaction-rate`, params: period.query }, getAppStatistics) diff --git a/web/i18n/lang/app-overview.en.ts b/web/i18n/lang/app-overview.en.ts index e31a165495..b7d9caaca6 100644 --- a/web/i18n/lang/app-overview.en.ts +++ b/web/i18n/lang/app-overview.en.ts @@ -81,6 +81,7 @@ const translation = { analysis: { title: 'Analysis', ms: 'ms', + tokenPS: 'Token/s', totalMessages: { title: 'Total Messages', explanation: 'Daily AI interactions count; prompt engineering/debugging excluded.', @@ -106,6 +107,10 @@ const translation = { title: 'Avg. Response Time', explanation: 'Time (ms) for AI to process/respond; for text-based apps.', }, + tps: { + title: 'Token Output Speed', + explanation: 'Measure the performance of the LLM. Count the Tokens output speed of LLM from the beginning of the request to the completion of the output.', + }, }, } diff --git a/web/i18n/lang/app-overview.zh.ts b/web/i18n/lang/app-overview.zh.ts index e21b824d97..bf5a234c32 100644 --- a/web/i18n/lang/app-overview.zh.ts +++ b/web/i18n/lang/app-overview.zh.ts @@ -81,6 +81,7 @@ const translation = { analysis: { title: '分析', ms: '毫秒', + tokenPS: 'Token/秒', totalMessages: { title: '全部消息数', explanation: '反映 AI 每天的互动总次数,每回答用户一个问题算一条 Message。提示词编排和调试的消息不计入。', @@ -106,6 +107,10 @@ const translation = { title: '平均响应时间', explanation: '衡量 AI 应用处理和回复用户请求所花费的平均时间,单位为毫秒,反映性能和用户体验。仅在文本型应用提供。', }, + tps: { + title: 'Token 输出速度', + explanation: '衡量 LLM 的性能。统计 LLM 从请求开始到输出完毕这段期间的 Tokens 输出速度。', + }, }, }