Compare commits
3 Commits
v4.19.2
...
perf/trace
| Author | SHA1 | Date | |
|---|---|---|---|
| 23f8d194ab | |||
| 65cceb2f21 | |||
| c4c356887b |
@@ -54,6 +54,14 @@ async def run_agent(
|
|||||||
return
|
return
|
||||||
if resp.type == "tool_call_result":
|
if resp.type == "tool_call_result":
|
||||||
msg_chain = resp.data["chain"]
|
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":
|
if msg_chain.type == "tool_direct_result":
|
||||||
# tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
|
# tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
|
||||||
await astr_event.send(msg_chain)
|
await astr_event.send(msg_chain)
|
||||||
@@ -67,12 +75,22 @@ async def run_agent(
|
|||||||
# 用来标记流式响应需要分节
|
# 用来标记流式响应需要分节
|
||||||
yield MessageChain(chain=[], type="break")
|
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":
|
if astr_event.get_platform_name() == "webchat":
|
||||||
await astr_event.send(resp.data["chain"])
|
await astr_event.send(resp.data["chain"])
|
||||||
elif show_tool_use:
|
elif show_tool_use:
|
||||||
json_comp = resp.data["chain"].chain[0]
|
if tool_info:
|
||||||
if isinstance(json_comp, Json):
|
m = f"🔨 调用工具: {tool_info.get('name', 'unknown')}"
|
||||||
m = f"🔨 调用工具: {json_comp.data.get('name')}"
|
|
||||||
else:
|
else:
|
||||||
m = "🔨 调用工具..."
|
m = "🔨 调用工具..."
|
||||||
chain = MessageChain(type="tool_call").message(m)
|
chain = MessageChain(type="tool_call").message(m)
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ DEFAULT_CONFIG = {
|
|||||||
"log_file_enable": False,
|
"log_file_enable": False,
|
||||||
"log_file_path": "logs/astrbot.log",
|
"log_file_path": "logs/astrbot.log",
|
||||||
"log_file_max_mb": 20,
|
"log_file_max_mb": 20,
|
||||||
|
"trace_enable": False,
|
||||||
"trace_log_enable": False,
|
"trace_log_enable": False,
|
||||||
"trace_log_path": "logs/astrbot.trace.log",
|
"trace_log_path": "logs/astrbot.trace.log",
|
||||||
"trace_log_max_mb": 20,
|
"trace_log_max_mb": 20,
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ class EventBus:
|
|||||||
event (AstrMessageEvent): 事件对象
|
event (AstrMessageEvent): 事件对象
|
||||||
|
|
||||||
"""
|
"""
|
||||||
event.trace.record("event_dispatch", config_name=conf_name)
|
|
||||||
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要
|
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要
|
||||||
if event.get_sender_name():
|
if event.get_sender_name():
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from astrbot.core.message.components import (
|
|||||||
AtAll,
|
AtAll,
|
||||||
BaseMessageComponent,
|
BaseMessageComponent,
|
||||||
Image,
|
Image,
|
||||||
|
Json,
|
||||||
Plain,
|
Plain,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,9 +118,26 @@ class MessageChain:
|
|||||||
self.use_t2i_ = use_t2i
|
self.use_t2i_ = use_t2i
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_plain_text(self) -> str:
|
def get_plain_text(self, with_other_comps_mark: bool = False) -> str:
|
||||||
"""获取纯文本消息。这个方法将获取 chain 中所有 Plain 组件的文本并拼接成一条消息。空格分隔。"""
|
"""获取纯文本消息。这个方法将获取 chain 中所有 Plain 组件的文本并拼接成一条消息。空格分隔。
|
||||||
return " ".join([comp.text for comp in self.chain if isinstance(comp, 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):
|
def squash_plain(self):
|
||||||
"""将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。"""
|
"""将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。"""
|
||||||
|
|||||||
@@ -85,6 +85,4 @@ class PipelineScheduler:
|
|||||||
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
|
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
|
||||||
await event.send(None)
|
await event.send(None)
|
||||||
|
|
||||||
event.trace.record("event_end")
|
|
||||||
|
|
||||||
logger.debug("pipeline 执行完毕。")
|
logger.debug("pipeline 执行完毕。")
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ class AstrMessageEvent(abc.ABC):
|
|||||||
self.span = self.trace
|
self.span = self.trace
|
||||||
"""事件级 TraceSpan(别名: span)"""
|
"""事件级 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._has_send_oper = False
|
||||||
"""在此次事件中是否有过至少一次发送消息的操作"""
|
"""在此次事件中是否有过至少一次发送消息的操作"""
|
||||||
self.call_llm = False
|
self.call_llm = False
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ class TraceSpan:
|
|||||||
self.started_at = time.time()
|
self.started_at = time.time()
|
||||||
|
|
||||||
def record(self, action: str, **fields: Any) -> None:
|
def record(self, action: str, **fields: Any) -> None:
|
||||||
|
# Check if trace recording is enabled
|
||||||
|
if not astrbot_config.get("trace_enable", True):
|
||||||
|
return
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"type": "trace",
|
"type": "trace",
|
||||||
"level": "TRACE",
|
"level": "TRACE",
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ class LogRoute(Route):
|
|||||||
view_func=self.log_history,
|
view_func=self.log_history,
|
||||||
methods=["GET"],
|
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(
|
async def _replay_cached_logs(
|
||||||
self, last_event_id: str
|
self, last_event_id: str
|
||||||
@@ -106,3 +116,29 @@ class LogRoute(Route):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取日志历史失败: {e}")
|
logger.error(f"获取日志历史失败: {e}")
|
||||||
return Response().error(f"获取日志历史失败: {e}").__dict__
|
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__
|
||||||
|
|||||||
@@ -3,5 +3,8 @@
|
|||||||
"autoScroll": {
|
"autoScroll": {
|
||||||
"enabled": "Auto-scroll: On",
|
"enabled": "Auto-scroll: On",
|
||||||
"disabled": "Auto-scroll: Off"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,8 @@
|
|||||||
"autoScroll": {
|
"autoScroll": {
|
||||||
"enabled": "自动滚动:开",
|
"enabled": "自动滚动:开",
|
||||||
"disabled": "自动滚动:关"
|
"disabled": "自动滚动:关"
|
||||||
}
|
},
|
||||||
|
"hint": "当前仅记录部分 AstrBot 主 Agent 的模型调用路径,后续会不断完善。",
|
||||||
|
"recording": "记录中",
|
||||||
|
"paused": "已暂停"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,72 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import TraceDisplayer from '@/components/shared/TraceDisplayer.vue';
|
import TraceDisplayer from '@/components/shared/TraceDisplayer.vue';
|
||||||
import { useModuleI18n } from '@/i18n/composables';
|
import { useModuleI18n } from '@/i18n/composables';
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
const { tm } = useModuleI18n('features/trace');
|
const { tm } = useModuleI18n('features/trace');
|
||||||
|
|
||||||
|
const traceEnabled = ref(true);
|
||||||
|
const loading = ref(false);
|
||||||
|
const traceDisplayerKey = ref(0);
|
||||||
|
|
||||||
|
const fetchTraceSettings = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/trace/settings');
|
||||||
|
if (res.data?.status === 'ok') {
|
||||||
|
traceEnabled.value = res.data.data?.trace_enable ?? true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch trace settings:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTraceSettings = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await axios.post('/api/trace/settings', {
|
||||||
|
trace_enable: traceEnabled.value
|
||||||
|
});
|
||||||
|
// Refresh the TraceDisplayer component to reconnect SSE
|
||||||
|
traceDisplayerKey.value += 1;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update trace settings:', err);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTraceSettings();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="height: 100%;">
|
<div style="height: 100%; display: flex; flex-direction: column;">
|
||||||
<TraceDisplayer />
|
<div class="trace-header">
|
||||||
|
<div class="trace-info">
|
||||||
|
<v-icon size="small" color="info" class="mr-2">mdi-information-outline</v-icon>
|
||||||
|
<span class="trace-hint">{{ tm('hint') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="trace-controls">
|
||||||
|
<v-switch
|
||||||
|
v-model="traceEnabled"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="loading"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
density="compact"
|
||||||
|
@update:model-value="updateTraceSettings"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="switch-label">{{ traceEnabled ? tm('recording') : tm('paused') }}</span>
|
||||||
|
</template>
|
||||||
|
</v-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; min-height: 0;">
|
||||||
|
<TraceDisplayer :key="traceDisplayerKey" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -19,3 +78,38 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trace-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(59, 130, 246, 0.05);
|
||||||
|
border-bottom: 1px solid rgba(59, 130, 246, 0.1);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user