feat: improve agent workflow observability
This commit is contained in:
@@ -23,6 +23,9 @@ coverage
|
|||||||
./web/package-lock.json
|
./web/package-lock.json
|
||||||
.VSCodeCounter
|
.VSCodeCounter
|
||||||
**/backend-mock/data
|
**/backend-mock/data
|
||||||
|
/.tmp/
|
||||||
|
/ai-agent-admin-dist-*.tgz
|
||||||
|
/ai-agent-admin-web-dist*.tgz
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
./web/.env.local
|
./web/.env.local
|
||||||
|
|||||||
@@ -422,8 +422,23 @@ async def publish_agent(agent_id: str, db: AsyncSession = Depends(get_db)):
|
|||||||
|
|
||||||
# 验证必要配置
|
# 验证必要配置
|
||||||
if agent.mode == "autonomous":
|
if agent.mode == "autonomous":
|
||||||
if not agent.model_id:
|
if agent.model_id:
|
||||||
raise HTTPException(status_code=400, detail="请先配置模型")
|
model_result = await db.execute(
|
||||||
|
select(LLMModel).where(
|
||||||
|
LLMModel.id == agent.model_id,
|
||||||
|
LLMModel.is_deleted == False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
model = model_result.scalar_one_or_none()
|
||||||
|
if not model:
|
||||||
|
raise HTTPException(status_code=400, detail="模型不存在")
|
||||||
|
if not model.is_active:
|
||||||
|
raise HTTPException(status_code=400, detail="模型已禁用")
|
||||||
|
elif not await _resolve_default_chat_model_id(db):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="请先在模型配置中启用一个 chat 模型,或为智能体指定模型",
|
||||||
|
)
|
||||||
elif agent.mode == "dialog_flow":
|
elif agent.mode == "dialog_flow":
|
||||||
if not agent.workflow_id:
|
if not agent.workflow_id:
|
||||||
raise HTTPException(status_code=400, detail="对话流模式需要配置工作流")
|
raise HTTPException(status_code=400, detail="对话流模式需要配置工作流")
|
||||||
|
|||||||
@@ -582,6 +582,9 @@ class AgentService:
|
|||||||
'type': 'complete',
|
'type': 'complete',
|
||||||
'elapsed_time': elapsed_time,
|
'elapsed_time': elapsed_time,
|
||||||
'tokens_used': total_tokens,
|
'tokens_used': total_tokens,
|
||||||
|
'total_tokens': total_tokens,
|
||||||
|
'conversation_id': str(conversation.id),
|
||||||
|
'message_id': str(assistant_msg.id),
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -150,6 +150,14 @@ const getStepLabel = (step: ReasoningStep) => {
|
|||||||
tool_result: '工具结果',
|
tool_result: '工具结果',
|
||||||
knowledge_retrieval: '知识库检索',
|
knowledge_retrieval: '知识库检索',
|
||||||
annotation_reply: '标注回复',
|
annotation_reply: '标注回复',
|
||||||
|
parallel_start: '并行中',
|
||||||
|
parallel_complete: '并行完成',
|
||||||
|
waiting_input: '等待输入',
|
||||||
|
error: '失败',
|
||||||
|
loop_iteration_start: '循环中',
|
||||||
|
loop_iteration_complete: '循环完成',
|
||||||
|
loop_iteration_error: '循环失败',
|
||||||
|
loop_complete: '循环完成',
|
||||||
};
|
};
|
||||||
return labels[step.type] || step.type;
|
return labels[step.type] || step.type;
|
||||||
};
|
};
|
||||||
@@ -172,6 +180,14 @@ const getStepColor = (step: ReasoningStep) => {
|
|||||||
tool_result: 'text-teal-500',
|
tool_result: 'text-teal-500',
|
||||||
knowledge_retrieval: 'text-cyan-500',
|
knowledge_retrieval: 'text-cyan-500',
|
||||||
annotation_reply: 'text-emerald-500',
|
annotation_reply: 'text-emerald-500',
|
||||||
|
parallel_start: 'text-indigo-500',
|
||||||
|
parallel_complete: 'text-emerald-500',
|
||||||
|
waiting_input: 'text-amber-500',
|
||||||
|
error: 'text-red-500',
|
||||||
|
loop_iteration_start: 'text-blue-500',
|
||||||
|
loop_iteration_complete: 'text-emerald-500',
|
||||||
|
loop_iteration_error: 'text-red-500',
|
||||||
|
loop_complete: 'text-emerald-500',
|
||||||
};
|
};
|
||||||
return colors[step.type] || 'text-gray-500';
|
return colors[step.type] || 'text-gray-500';
|
||||||
};
|
};
|
||||||
@@ -301,7 +317,9 @@ const formatVoiceDuration = (seconds: number) => {
|
|||||||
<span class="step-indicator" :class="getStepColor(step)">
|
<span class="step-indicator" :class="getStepColor(step)">
|
||||||
{{ getStepLabel(step) }}
|
{{ getStepLabel(step) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="step-text">{{ step.content }}</span>
|
<span class="step-text" :title="step.content">
|
||||||
|
{{ step.content }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,13 +41,21 @@ export interface ReasoningStep {
|
|||||||
type:
|
type:
|
||||||
| 'action'
|
| 'action'
|
||||||
| 'annotation_reply'
|
| 'annotation_reply'
|
||||||
|
| 'error'
|
||||||
| 'knowledge_retrieval'
|
| 'knowledge_retrieval'
|
||||||
|
| 'loop_complete'
|
||||||
|
| 'loop_iteration_complete'
|
||||||
|
| 'loop_iteration_error'
|
||||||
|
| 'loop_iteration_start'
|
||||||
| 'node_complete'
|
| 'node_complete'
|
||||||
| 'node_start'
|
| 'node_start'
|
||||||
| 'observation'
|
| 'observation'
|
||||||
|
| 'parallel_complete'
|
||||||
|
| 'parallel_start'
|
||||||
| 'thought'
|
| 'thought'
|
||||||
| 'tool_call'
|
| 'tool_call'
|
||||||
| 'tool_result';
|
| 'tool_result'
|
||||||
|
| 'waiting_input';
|
||||||
content: string;
|
content: string;
|
||||||
tool?: string;
|
tool?: string;
|
||||||
params?: Record<string, any>;
|
params?: Record<string, any>;
|
||||||
|
|||||||
@@ -164,6 +164,12 @@ export function useEventHandler(config: EventHandlerConfig) {
|
|||||||
return fallbackLabel || nodeType || '';
|
return fallbackLabel || nodeType || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getItemCount = (value: any): number => {
|
||||||
|
if (Array.isArray(value)) return value.length;
|
||||||
|
if (value && typeof value === 'object') return Object.keys(value).length;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
/** 处理流式事件 */
|
/** 处理流式事件 */
|
||||||
const handleStreamEvent = (event: StreamEvent, msgId: string) => {
|
const handleStreamEvent = (event: StreamEvent, msgId: string) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
@@ -272,9 +278,18 @@ export function useEventHandler(config: EventHandlerConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'error': {
|
case 'error': {
|
||||||
|
currentSteps.value.push({
|
||||||
|
type: 'error',
|
||||||
|
content: event.message || event.content || '执行失败',
|
||||||
|
node_id: event.node_id,
|
||||||
|
node_type: event.node_type,
|
||||||
|
status: 'completed',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
updateAssistantMessage(msgId, {
|
updateAssistantMessage(msgId, {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
error_message: event.message || event.content || '执行失败',
|
error_message: event.message || event.content || '执行失败',
|
||||||
|
reasoning_steps: [...currentSteps.value],
|
||||||
});
|
});
|
||||||
running.value = false;
|
running.value = false;
|
||||||
break;
|
break;
|
||||||
@@ -322,6 +337,19 @@ export function useEventHandler(config: EventHandlerConfig) {
|
|||||||
outputs[outputVar] = loopEvent.loop_results;
|
outputs[outputVar] = loopEvent.loop_results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentSteps.value.push({
|
||||||
|
type: 'loop_complete',
|
||||||
|
content: '循环执行完成',
|
||||||
|
node_id: loopEvent.node_id,
|
||||||
|
node_type: 'loop',
|
||||||
|
output: outputs,
|
||||||
|
status: 'completed',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
updateAssistantMessage(msgId, {
|
||||||
|
reasoning_steps: [...currentSteps.value],
|
||||||
|
});
|
||||||
|
|
||||||
if (props.enableNodeEvents) {
|
if (props.enableNodeEvents) {
|
||||||
emit('node-complete', {
|
emit('node-complete', {
|
||||||
node_id: loopEvent.node_id,
|
node_id: loopEvent.node_id,
|
||||||
@@ -336,6 +364,19 @@ export function useEventHandler(config: EventHandlerConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'loop_iteration_complete': {
|
case 'loop_iteration_complete': {
|
||||||
|
currentSteps.value.push({
|
||||||
|
type: 'loop_iteration_complete',
|
||||||
|
content: `循环第 ${event.iteration || '-'} 次完成`,
|
||||||
|
node_id: event.node_id,
|
||||||
|
node_type: 'loop',
|
||||||
|
output: event.output,
|
||||||
|
status: 'completed',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
updateAssistantMessage(msgId, {
|
||||||
|
reasoning_steps: [...currentSteps.value],
|
||||||
|
});
|
||||||
|
|
||||||
if (props.enableNodeEvents) {
|
if (props.enableNodeEvents) {
|
||||||
emit('loop-iteration', {
|
emit('loop-iteration', {
|
||||||
node_id: event.node_id,
|
node_id: event.node_id,
|
||||||
@@ -349,6 +390,18 @@ export function useEventHandler(config: EventHandlerConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'loop_iteration_error': {
|
case 'loop_iteration_error': {
|
||||||
|
currentSteps.value.push({
|
||||||
|
type: 'loop_iteration_error',
|
||||||
|
content: `循环第 ${event.iteration || '-'} 次失败:${event.error || '执行失败'}`,
|
||||||
|
node_id: event.node_id,
|
||||||
|
node_type: 'loop',
|
||||||
|
status: 'completed',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
updateAssistantMessage(msgId, {
|
||||||
|
reasoning_steps: [...currentSteps.value],
|
||||||
|
});
|
||||||
|
|
||||||
if (props.enableNodeEvents) {
|
if (props.enableNodeEvents) {
|
||||||
emit('loop-iteration', {
|
emit('loop-iteration', {
|
||||||
node_id: event.node_id,
|
node_id: event.node_id,
|
||||||
@@ -362,6 +415,19 @@ export function useEventHandler(config: EventHandlerConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'loop_iteration_start': {
|
case 'loop_iteration_start': {
|
||||||
|
currentSteps.value.push({
|
||||||
|
type: 'loop_iteration_start',
|
||||||
|
content: `循环第 ${event.iteration || '-'} 次开始`,
|
||||||
|
node_id: event.node_id,
|
||||||
|
node_type: 'loop',
|
||||||
|
params: { item: event.item, total: event.total },
|
||||||
|
status: 'running',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
updateAssistantMessage(msgId, {
|
||||||
|
reasoning_steps: [...currentSteps.value],
|
||||||
|
});
|
||||||
|
|
||||||
if (props.enableNodeEvents) {
|
if (props.enableNodeEvents) {
|
||||||
emit('loop-iteration', {
|
emit('loop-iteration', {
|
||||||
node_id: event.node_id,
|
node_id: event.node_id,
|
||||||
@@ -423,6 +489,60 @@ export function useEventHandler(config: EventHandlerConfig) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'parallel_complete': {
|
||||||
|
const existingIndex = currentSteps.value.findIndex(
|
||||||
|
(s) => s.node_id === event.node_id && s.type === 'parallel_start',
|
||||||
|
);
|
||||||
|
const branchResults = event.branch_results || {};
|
||||||
|
const content = `并行分支完成:${getItemCount(branchResults)} 个分支`;
|
||||||
|
if (existingIndex === -1) {
|
||||||
|
currentSteps.value.push({
|
||||||
|
type: 'parallel_complete',
|
||||||
|
content,
|
||||||
|
node_id: event.node_id,
|
||||||
|
node_type: 'parallel',
|
||||||
|
output: {
|
||||||
|
branch_results: branchResults,
|
||||||
|
total_tokens: event.total_tokens,
|
||||||
|
},
|
||||||
|
status: 'completed',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
currentSteps.value[existingIndex] = {
|
||||||
|
...currentSteps.value[existingIndex]!,
|
||||||
|
type: 'parallel_complete',
|
||||||
|
content,
|
||||||
|
output: {
|
||||||
|
branch_results: branchResults,
|
||||||
|
total_tokens: event.total_tokens,
|
||||||
|
},
|
||||||
|
status: 'completed',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
updateAssistantMessage(msgId, {
|
||||||
|
reasoning_steps: [...currentSteps.value],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'parallel_start': {
|
||||||
|
const branches = event.branches || [];
|
||||||
|
currentSteps.value.push({
|
||||||
|
type: 'parallel_start',
|
||||||
|
content: `并行分支开始:${getItemCount(branches)} 个分支`,
|
||||||
|
node_id: event.node_id,
|
||||||
|
node_type: 'parallel',
|
||||||
|
params: { branches },
|
||||||
|
status: 'running',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
updateAssistantMessage(msgId, {
|
||||||
|
reasoning_steps: [...currentSteps.value],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'node_event': {
|
case 'node_event': {
|
||||||
const nodeEvent = event.event;
|
const nodeEvent = event.event;
|
||||||
if (nodeEvent?.type === 'message') {
|
if (nodeEvent?.type === 'message') {
|
||||||
@@ -493,12 +613,28 @@ export function useEventHandler(config: EventHandlerConfig) {
|
|||||||
waitingConfig.value = event.waiting_config || event.config;
|
waitingConfig.value = event.waiting_config || event.config;
|
||||||
|
|
||||||
const configData = event.waiting_config || (event as any).config;
|
const configData = event.waiting_config || (event as any).config;
|
||||||
|
currentSteps.value.push({
|
||||||
|
type: 'waiting_input',
|
||||||
|
content:
|
||||||
|
configData?.title ||
|
||||||
|
configData?.question ||
|
||||||
|
event.node_label ||
|
||||||
|
'等待用户输入',
|
||||||
|
node_id: event.node_id,
|
||||||
|
node_type: event.node_type,
|
||||||
|
params: configData,
|
||||||
|
status: 'running',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
if (configData?.type === 'design_preview') {
|
if (configData?.type === 'design_preview') {
|
||||||
handleDesignPreview(event, msgId, configData);
|
handleDesignPreview(event, msgId, configData);
|
||||||
} else {
|
} else {
|
||||||
handleDialogFlowInteraction(msgId, configData);
|
handleDialogFlowInteraction(msgId, configData);
|
||||||
}
|
}
|
||||||
|
updateAssistantMessage(msgId, {
|
||||||
|
reasoning_steps: [...currentSteps.value],
|
||||||
|
});
|
||||||
|
|
||||||
running.value = false;
|
running.value = false;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -205,13 +205,6 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
|||||||
return await generateAccessible(preferences.app.accessMode, {
|
return await generateAccessible(preferences.app.accessMode, {
|
||||||
...options,
|
...options,
|
||||||
fetchMenuListAsync: async () => {
|
fetchMenuListAsync: async () => {
|
||||||
const { ElMessage } =
|
|
||||||
await import('element-plus/es/components/message/index');
|
|
||||||
ElMessage({
|
|
||||||
duration: 1500,
|
|
||||||
message: `${$t('common.loadingMenu')}...`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const appContextStore = useAppContextStore();
|
const appContextStore = useAppContextStore();
|
||||||
const menus = appContextStore.appCode
|
const menus = appContextStore.appCode
|
||||||
? await getAllMenusApi(appContextStore.appCode)
|
? await getAllMenusApi(appContextStore.appCode)
|
||||||
|
|||||||
Reference in New Issue
Block a user