Skip to content

Commit 2b435e0

Browse files
authored
feat: add brand-new statistics dashboarde overview with model usage and platform usage (#7152)
* feat: add new StatsPage for enhanced statistics overview - Introduced StatsPage.vue to provide a comprehensive overview of statistics with various metrics and charts. - Implemented fetching and displaying of base and model provider statistics. - Added unit tests for provider statistics persistence in the database. * style: refine card styles and remove unnecessary shadow for improved aesthetics
1 parent 9896b48 commit 2b435e0

File tree

23 files changed

+1921
-1523
lines changed

23 files changed

+1921
-1523
lines changed

astrbot/core/db/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
PlatformSession,
2222
PlatformStat,
2323
Preference,
24+
ProviderStat,
2425
SessionProjectRelation,
2526
Stats,
2627
)
@@ -105,6 +106,21 @@ async def get_platform_stats(self, offset_sec: int = 86400) -> list[PlatformStat
105106
"""Get platform statistics within the specified offset in seconds and group by platform_id."""
106107
...
107108

109+
@abc.abstractmethod
110+
async def insert_provider_stat(
111+
self,
112+
*,
113+
umo: str,
114+
provider_id: str,
115+
provider_model: str | None = None,
116+
conversation_id: str | None = None,
117+
status: str = "completed",
118+
stats: dict | None = None,
119+
agent_type: str = "internal",
120+
) -> ProviderStat:
121+
"""Insert a per-response provider stat record."""
122+
...
123+
108124
@abc.abstractmethod
109125
async def get_conversations(
110126
self,

astrbot/core/db/po.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,30 @@ class PlatformStat(SQLModel, table=True):
3838
)
3939

4040

41+
class ProviderStat(TimestampMixin, SQLModel, table=True):
42+
"""Per-response provider stats for internal agent runs."""
43+
44+
__tablename__: str = "provider_stats"
45+
46+
id: int | None = Field(
47+
default=None,
48+
primary_key=True,
49+
sa_column_kwargs={"autoincrement": True},
50+
)
51+
agent_type: str = Field(default="internal", nullable=False, index=True)
52+
status: str = Field(default="completed", nullable=False, index=True)
53+
umo: str = Field(nullable=False, index=True)
54+
conversation_id: str | None = Field(default=None, index=True)
55+
provider_id: str = Field(nullable=False, index=True)
56+
provider_model: str | None = Field(default=None, index=True)
57+
token_input_other: int = Field(default=0, nullable=False)
58+
token_input_cached: int = Field(default=0, nullable=False)
59+
token_output: int = Field(default=0, nullable=False)
60+
start_time: float = Field(default=0.0, nullable=False)
61+
end_time: float = Field(default=0.0, nullable=False)
62+
time_to_first_token: float = Field(default=0.0, nullable=False)
63+
64+
4165
class ConversationV2(TimestampMixin, SQLModel, table=True):
4266
__tablename__: str = "conversations"
4367

astrbot/core/db/sqlite.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
PlatformSession,
2424
PlatformStat,
2525
Preference,
26+
ProviderStat,
2627
SessionProjectRelation,
2728
SQLModel,
2829
)
@@ -169,6 +170,51 @@ async def get_platform_stats(self, offset_sec: int = 86400) -> list[PlatformStat
169170
)
170171
return list(result.scalars().all())
171172

173+
async def insert_provider_stat(
174+
self,
175+
*,
176+
umo: str,
177+
provider_id: str,
178+
provider_model: str | None = None,
179+
conversation_id: str | None = None,
180+
status: str = "completed",
181+
stats: dict | None = None,
182+
agent_type: str = "internal",
183+
) -> ProviderStat:
184+
"""Insert a provider stat record for a single agent response."""
185+
stats = stats or {}
186+
token_usage = stats.get("token_usage", {})
187+
188+
token_input_other = int(token_usage.get("input_other", 0) or 0)
189+
token_input_cached = int(token_usage.get("input_cached", 0) or 0)
190+
token_output = int(token_usage.get("output", 0) or 0)
191+
192+
start_time = float(stats.get("start_time", 0.0) or 0.0)
193+
end_time = float(stats.get("end_time", 0.0) or 0.0)
194+
time_to_first_token = float(stats.get("time_to_first_token", 0.0) or 0.0)
195+
196+
async with self.get_db() as session:
197+
session: AsyncSession
198+
async with session.begin():
199+
record = ProviderStat(
200+
agent_type=agent_type,
201+
status=status,
202+
umo=umo,
203+
conversation_id=conversation_id,
204+
provider_id=provider_id,
205+
provider_model=provider_model,
206+
token_input_other=token_input_other,
207+
token_input_cached=token_input_cached,
208+
token_output=token_output,
209+
start_time=start_time,
210+
end_time=end_time,
211+
time_to_first_token=time_to_first_token,
212+
)
213+
session.add(record)
214+
await session.flush()
215+
await session.refresh(record)
216+
return record
217+
172218
# ====
173219
# Conversation Management
174220
# ====

astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from collections.abc import AsyncGenerator
66
from dataclasses import replace
77

8-
from astrbot.core import logger
8+
from astrbot.core import db_helper, logger
99
from astrbot.core.agent.message import Message
1010
from astrbot.core.agent.response import AgentStats
1111
from astrbot.core.astr_main_agent import (
@@ -350,6 +350,15 @@ async def process(
350350
resp=final_resp.completion_text if final_resp else None,
351351
)
352352

353+
asyncio.create_task(
354+
_record_internal_agent_stats(
355+
event,
356+
req,
357+
agent_runner,
358+
final_resp,
359+
)
360+
)
361+
353362
# 检查事件是否被停止,如果被停止则不保存历史记录
354363
if not event.is_stopped() or agent_runner.was_aborted():
355364
await self._save_to_history(
@@ -462,3 +471,46 @@ async def _save_to_history(
462471
# these hosts are base64 encoded
463472
BLOCKED = {"dGZid2h2d3IuY2xvdWQuc2VhbG9zLmlv", "a291cmljaGF0"}
464473
decoded_blocked = [base64.b64decode(b).decode("utf-8") for b in BLOCKED]
474+
475+
476+
async def _record_internal_agent_stats(
477+
event: AstrMessageEvent,
478+
req: ProviderRequest | None,
479+
agent_runner: AgentRunner | None,
480+
final_resp: LLMResponse | None,
481+
) -> None:
482+
"""Persist internal agent stats without affecting the user response flow."""
483+
if agent_runner is None:
484+
return
485+
486+
provider = agent_runner.provider
487+
stats = agent_runner.stats
488+
if provider is None or stats is None:
489+
return
490+
491+
try:
492+
provider_config = getattr(provider, "provider_config", {}) or {}
493+
conversation_id = (
494+
req.conversation.cid
495+
if req is not None and req.conversation is not None
496+
else None
497+
)
498+
499+
if agent_runner.was_aborted():
500+
status = "aborted"
501+
elif final_resp is not None and final_resp.role == "err":
502+
status = "error"
503+
else:
504+
status = "completed"
505+
506+
await db_helper.insert_provider_stat(
507+
umo=event.unified_msg_origin,
508+
conversation_id=conversation_id,
509+
provider_id=provider_config.get("id", "") or provider.meta().id,
510+
provider_model=provider.get_model(),
511+
status=status,
512+
stats=stats.to_dict(),
513+
agent_type="internal",
514+
)
515+
except Exception as e:
516+
logger.warning("Persist provider stats failed: %s", e, exc_info=True)

0 commit comments

Comments
 (0)