diff --git a/astrbot/core/astr_agent_run_util.py b/astrbot/core/astr_agent_run_util.py index 2267ae203..5556aa6b4 100644 --- a/astrbot/core/astr_agent_run_util.py +++ b/astrbot/core/astr_agent_run_util.py @@ -54,6 +54,14 @@ async def run_agent( return if resp.type == "tool_call_result": msg_chain = resp.data["chain"] + + astr_event.trace.record( + "agent_tool_result", + tool_result=msg_chain.get_plain_text( + with_other_comps_mark=True + ), + ) + if msg_chain.type == "tool_direct_result": # tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容 await astr_event.send(msg_chain) @@ -67,12 +75,22 @@ async def run_agent( # 用来标记流式响应需要分节 yield MessageChain(chain=[], type="break") + tool_info = None + + if resp.data["chain"].chain: + json_comp = resp.data["chain"].chain[0] + if isinstance(json_comp, Json): + tool_info = json_comp.data + astr_event.trace.record( + "agent_tool_call", + tool_name=tool_info if tool_info else "unknown", + ) + if astr_event.get_platform_name() == "webchat": await astr_event.send(resp.data["chain"]) elif show_tool_use: - json_comp = resp.data["chain"].chain[0] - if isinstance(json_comp, Json): - m = f"🔨 调用工具: {json_comp.data.get('name')}" + if tool_info: + m = f"🔨 调用工具: {tool_info.get('name', 'unknown')}" else: m = "🔨 调用工具..." chain = MessageChain(type="tool_call").message(m) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 7874892ea..1cc672004 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -202,6 +202,7 @@ "log_file_enable": False, "log_file_path": "logs/astrbot.log", "log_file_max_mb": 20, + "trace_enable": False, "trace_log_enable": False, "trace_log_path": "logs/astrbot.trace.log", "trace_log_max_mb": 20, diff --git a/astrbot/core/event_bus.py b/astrbot/core/event_bus.py index 82675585a..0017e65fa 100644 --- a/astrbot/core/event_bus.py +++ b/astrbot/core/event_bus.py @@ -54,7 +54,6 @@ def _print_event(self, event: AstrMessageEvent, conf_name: str): event (AstrMessageEvent): 事件对象 """ - event.trace.record("event_dispatch", config_name=conf_name) # 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要 if event.get_sender_name(): logger.info( diff --git a/astrbot/core/message/message_event_result.py b/astrbot/core/message/message_event_result.py index ed4e25f43..eba6a4fd6 100644 --- a/astrbot/core/message/message_event_result.py +++ b/astrbot/core/message/message_event_result.py @@ -9,6 +9,7 @@ AtAll, BaseMessageComponent, Image, + Json, Plain, ) @@ -117,9 +118,26 @@ def use_t2i(self, use_t2i: bool): self.use_t2i_ = use_t2i return self - def get_plain_text(self) -> str: - """获取纯文本消息。这个方法将获取 chain 中所有 Plain 组件的文本并拼接成一条消息。空格分隔。""" - return " ".join([comp.text for comp in self.chain if isinstance(comp, Plain)]) + def get_plain_text(self, with_other_comps_mark: bool = False) -> str: + """获取纯文本消息。这个方法将获取 chain 中所有 Plain 组件的文本并拼接成一条消息。空格分隔。 + + Args: + with_other_comps_mark (bool): 是否在纯文本中标记其他组件的位置 + """ + if not with_other_comps_mark: + return " ".join( + [comp.text for comp in self.chain if isinstance(comp, Plain)] + ) + else: + texts = [] + for comp in self.chain: + if isinstance(comp, Plain): + texts.append(comp.text) + elif isinstance(comp, Json): + texts.append(f"{comp.data}") + else: + texts.append(f"[{comp.__class__.__name__}]") + return " ".join(texts) def squash_plain(self): """将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。""" diff --git a/astrbot/core/pipeline/scheduler.py b/astrbot/core/pipeline/scheduler.py index 6d2d72b80..8569f945a 100644 --- a/astrbot/core/pipeline/scheduler.py +++ b/astrbot/core/pipeline/scheduler.py @@ -85,6 +85,4 @@ async def execute(self, event: AstrMessageEvent): if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent): await event.send(None) - event.trace.record("event_end") - logger.debug("pipeline 执行完毕。") diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index a1bc393c7..a7d633abb 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -73,9 +73,6 @@ def __init__( self.span = self.trace """事件级 TraceSpan(别名: span)""" - self.trace.record("umo", umo=self.unified_msg_origin) - self.trace.record("event_created", created_at=self.created_at) - self._has_send_oper = False """在此次事件中是否有过至少一次发送消息的操作""" self.call_llm = False diff --git a/astrbot/core/utils/trace.py b/astrbot/core/utils/trace.py index 74c533ef4..7b095dbc0 100644 --- a/astrbot/core/utils/trace.py +++ b/astrbot/core/utils/trace.py @@ -50,6 +50,10 @@ def __init__( self.started_at = time.time() def record(self, action: str, **fields: Any) -> None: + # Check if trace recording is enabled + if not astrbot_config.get("trace_enable", True): + return + payload = { "type": "trace", "level": "TRACE", diff --git a/astrbot/dashboard/routes/log.py b/astrbot/dashboard/routes/log.py index d5aa7c1de..e7eebef6e 100644 --- a/astrbot/dashboard/routes/log.py +++ b/astrbot/dashboard/routes/log.py @@ -31,6 +31,16 @@ def __init__(self, context: RouteContext, log_broker: LogBroker) -> None: view_func=self.log_history, methods=["GET"], ) + self.app.add_url_rule( + "/api/trace/settings", + view_func=self.get_trace_settings, + methods=["GET"], + ) + self.app.add_url_rule( + "/api/trace/settings", + view_func=self.update_trace_settings, + methods=["POST"], + ) async def _replay_cached_logs( self, last_event_id: str @@ -106,3 +116,29 @@ async def log_history(self): except Exception as e: logger.error(f"获取日志历史失败: {e}") return Response().error(f"获取日志历史失败: {e}").__dict__ + + async def get_trace_settings(self): + """获取 Trace 设置""" + try: + trace_enable = self.config.get("trace_enable", True) + return Response().ok(data={"trace_enable": trace_enable}).__dict__ + except Exception as e: + logger.error(f"获取 Trace 设置失败: {e}") + return Response().error(f"获取 Trace 设置失败: {e}").__dict__ + + async def update_trace_settings(self): + """更新 Trace 设置""" + try: + data = await request.json + if data is None: + return Response().error("请求数据为空").__dict__ + + trace_enable = data.get("trace_enable") + if trace_enable is not None: + self.config["trace_enable"] = bool(trace_enable) + self.config.save_config() + + return Response().ok(message="Trace 设置已更新").__dict__ + except Exception as e: + logger.error(f"更新 Trace 设置失败: {e}") + return Response().error(f"更新 Trace 设置失败: {e}").__dict__ diff --git a/dashboard/src/i18n/locales/en-US/features/trace.json b/dashboard/src/i18n/locales/en-US/features/trace.json index 777dbe946..00b8ab44e 100644 --- a/dashboard/src/i18n/locales/en-US/features/trace.json +++ b/dashboard/src/i18n/locales/en-US/features/trace.json @@ -3,5 +3,8 @@ "autoScroll": { "enabled": "Auto-scroll: On", "disabled": "Auto-scroll: Off" - } + }, + "hint": "Currently only recording partial model call paths from AstrBot main Agent. More coverage will be added.", + "recording": "Recording", + "paused": "Paused" } diff --git a/dashboard/src/i18n/locales/zh-CN/features/trace.json b/dashboard/src/i18n/locales/zh-CN/features/trace.json index 5e36ac5d5..a8923d3bc 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/trace.json +++ b/dashboard/src/i18n/locales/zh-CN/features/trace.json @@ -3,5 +3,8 @@ "autoScroll": { "enabled": "自动滚动:开", "disabled": "自动滚动:关" - } + }, + "hint": "当前仅记录部分 AstrBot 主 Agent 的模型调用路径,后续会不断完善。", + "recording": "记录中", + "paused": "已暂停" } diff --git a/dashboard/src/views/TracePage.vue b/dashboard/src/views/TracePage.vue index 501ebc4dd..9bc048575 100644 --- a/dashboard/src/views/TracePage.vue +++ b/dashboard/src/views/TracePage.vue @@ -1,13 +1,72 @@ - - + + + + mdi-information-outline + {{ tm('hint') }} + + + + + {{ traceEnabled ? tm('recording') : tm('paused') }} + + + + + + + @@ -19,3 +78,38 @@ export default { } }; + +