|
@@ -1,7 +1,7 @@
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from "react";
|
|
import { useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from "react";
|
|
|
import type { ForwardRefRenderFunction } from "react";
|
|
import type { ForwardRefRenderFunction } from "react";
|
|
|
import type { Goal } from "../../types/goal";
|
|
import type { Goal } from "../../types/goal";
|
|
|
-import type { Edge as EdgeType, Message } from "../../types/message";
|
|
|
|
|
|
|
+import type { Edge as EdgeType, Message, MessageContent } from "../../types/message";
|
|
|
import { ArrowMarkers } from "./components/ArrowMarkers";
|
|
import { ArrowMarkers } from "./components/ArrowMarkers";
|
|
|
import styles from "./styles/FlowChart.module.css";
|
|
import styles from "./styles/FlowChart.module.css";
|
|
|
import { ImagePreviewModal } from "../ImagePreview/ImagePreviewModal";
|
|
import { ImagePreviewModal } from "../ImagePreview/ImagePreviewModal";
|
|
@@ -23,6 +23,56 @@ export interface FlowChartRef {
|
|
|
|
|
|
|
|
export type SubTraceEntry = { id: string; mission?: string };
|
|
export type SubTraceEntry = { id: string; mission?: string };
|
|
|
type FlowMessage = Message & { _injectedGoalId?: string };
|
|
type FlowMessage = Message & { _injectedGoalId?: string };
|
|
|
|
|
+type NodeMarker = "agent_call" | "agent_return" | "agent_exit" | "agent_resume";
|
|
|
|
|
+type FoldedNodeData = { goalId: string; count: number };
|
|
|
|
|
+
|
|
|
|
|
+const AGENT_TOOLS = new Set(["agent", "evaluate"]);
|
|
|
|
|
+
|
|
|
|
|
+const getContentParts = (content: Message["content"]): MessageContent[] => {
|
|
|
|
|
+ if (Array.isArray(content)) return content;
|
|
|
|
|
+ return content && typeof content === "object" ? [content] : [];
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const getMessageText = (message: Message): string => {
|
|
|
|
|
+ if (typeof message.content === "string") return message.content;
|
|
|
|
|
+ return getContentParts(message.content).map((part) => part.text || "").join("");
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const getMessageMarker = (message: Message, previous?: NodeMarker): NodeMarker | undefined => {
|
|
|
|
|
+ const parts = getContentParts(message.content);
|
|
|
|
|
+ const text = getMessageText(message);
|
|
|
|
|
+ const isTerminate = text.includes("TERMINATE");
|
|
|
|
|
+
|
|
|
|
|
+ if (previous === "agent_exit" && !isTerminate) return "agent_resume";
|
|
|
|
|
+
|
|
|
|
|
+ const hasAgentCall = parts.some((part) =>
|
|
|
|
|
+ part.tool_calls?.some((call) =>
|
|
|
|
|
+ AGENT_TOOLS.has(call.function?.name || call.name || ""),
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ if (hasAgentCall) return "agent_call";
|
|
|
|
|
+
|
|
|
|
|
+ const hasAgentReturn = parts.some((part) =>
|
|
|
|
|
+ !!part.tool_name && AGENT_TOOLS.has(part.tool_name),
|
|
|
|
|
+ );
|
|
|
|
|
+ if (hasAgentReturn) return "agent_return";
|
|
|
|
|
+
|
|
|
|
|
+ const hasStructuredContent = !!message.content && typeof message.content === "object";
|
|
|
|
|
+ const hasToolCalls = parts.some((part) => (part.tool_calls?.length || 0) > 0);
|
|
|
|
|
+ if (message.role === "assistant" && hasStructuredContent && text && !isTerminate && !hasToolCalls) {
|
|
|
|
|
+ return "agent_exit";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return undefined;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const isContinueMessage = (message: Message): boolean =>
|
|
|
|
|
+ getContentParts(message.content).some((part) =>
|
|
|
|
|
+ part.tool_calls?.some((call) => {
|
|
|
|
|
+ const args = call.function?.arguments || call.arguments;
|
|
|
|
|
+ return typeof args === "string" && args.includes("continue_from");
|
|
|
|
|
+ }),
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
const normalizeSubTraceEntries = (goal: Goal): SubTraceEntry[] =>
|
|
const normalizeSubTraceEntries = (goal: Goal): SubTraceEntry[] =>
|
|
|
(goal.sub_trace_ids ?? [])
|
|
(goal.sub_trace_ids ?? [])
|
|
@@ -34,18 +84,21 @@ const normalizeSubTraceEntries = (goal: Goal): SubTraceEntry[] =>
|
|
|
.filter((entry): entry is SubTraceEntry => entry !== null && entry.id.length > 0)
|
|
.filter((entry): entry is SubTraceEntry => entry !== null && entry.id.length > 0)
|
|
|
.slice(0, 6);
|
|
.slice(0, 6);
|
|
|
|
|
|
|
|
-interface LayoutNode {
|
|
|
|
|
|
|
+interface LayoutNodeBase {
|
|
|
id: string;
|
|
id: string;
|
|
|
x: number;
|
|
x: number;
|
|
|
y: number;
|
|
y: number;
|
|
|
- data: Goal | Message | any;
|
|
|
|
|
- type: "goal" | "message" | "folded";
|
|
|
|
|
level: number;
|
|
level: number;
|
|
|
parentId?: string;
|
|
parentId?: string;
|
|
|
isInvalid?: boolean;
|
|
isInvalid?: boolean;
|
|
|
- marker?: "agent_call" | "agent_return" | "agent_exit" | "agent_resume";
|
|
|
|
|
|
|
+ marker?: NodeMarker;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+type LayoutNode =
|
|
|
|
|
+ | (LayoutNodeBase & { type: "goal"; data: Goal })
|
|
|
|
|
+ | (LayoutNodeBase & { type: "message"; data: FlowMessage })
|
|
|
|
|
+ | (LayoutNodeBase & { type: "folded"; data: FoldedNodeData });
|
|
|
|
|
+
|
|
|
interface LayoutEdge {
|
|
interface LayoutEdge {
|
|
|
id: string;
|
|
id: string;
|
|
|
source: LayoutNode;
|
|
source: LayoutNode;
|
|
@@ -258,36 +311,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
const msg = item as FlowMessage;
|
|
const msg = item as FlowMessage;
|
|
|
- let marker: LayoutNode["marker"] = undefined;
|
|
|
|
|
- const mc = typeof msg.content === "object" && msg.content ? msg.content : null;
|
|
|
|
|
-
|
|
|
|
|
- const textStr = Array.isArray(msg.content) ? msg.content.map((c: any) => c.text || "").join("") : ((msg.content as any)?.text || "");
|
|
|
|
|
- const isTerminate = textStr.includes("TERMINATE");
|
|
|
|
|
-
|
|
|
|
|
- if (previousMarker === "agent_exit" && !isTerminate) {
|
|
|
|
|
- marker = "agent_resume";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!marker && mc) {
|
|
|
|
|
- const AGENT_TOOLS = ["agent", "evaluate"];
|
|
|
|
|
- const contentArr = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
|
|
|
- const hasAgentCall = contentArr.some((c: any) => c && c.tool_calls?.some((tc: any) => AGENT_TOOLS.includes(tc.function?.name || tc.name || "")));
|
|
|
|
|
- const hasAgentReturn = contentArr.some((c: any) => c && c.tool_name && AGENT_TOOLS.includes(c.tool_name));
|
|
|
|
|
-
|
|
|
|
|
- if (hasAgentCall) {
|
|
|
|
|
- marker = "agent_call";
|
|
|
|
|
- } else if (hasAgentReturn) {
|
|
|
|
|
- marker = "agent_return";
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if (!marker && msg.role === "assistant" && mc && textStr && !isTerminate) {
|
|
|
|
|
- // If there are no tool calls, it's an exit (or natural pause)
|
|
|
|
|
- const contentArr = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
|
|
|
- const hasAnyTools = contentArr.some((c: any) => c && c.tool_calls?.length > 0);
|
|
|
|
|
- if (!hasAnyTools) {
|
|
|
|
|
- marker = "agent_exit";
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const marker = getMessageMarker(msg, previousMarker);
|
|
|
|
|
|
|
|
previousMarker = marker || previousMarker;
|
|
previousMarker = marker || previousMarker;
|
|
|
|
|
|
|
@@ -315,8 +339,8 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
|
|
|
|
|
let shouldConnect = true;
|
|
let shouldConnect = true;
|
|
|
if (target.type === "message") {
|
|
if (target.type === "message") {
|
|
|
- const pSeq = (target.data as any).parent_sequence;
|
|
|
|
|
- const sourceSeq = source.type === "message" ? (source.data as any).sequence : undefined;
|
|
|
|
|
|
|
+ const pSeq = target.data.parent_sequence;
|
|
|
|
|
+ const sourceSeq = source.type === "message" ? source.data.sequence : undefined;
|
|
|
if (source.type !== "folded" && typeof pSeq === "number" && sourceSeq !== pSeq) {
|
|
if (source.type !== "folded" && typeof pSeq === "number" && sourceSeq !== pSeq) {
|
|
|
shouldConnect = false;
|
|
shouldConnect = false;
|
|
|
}
|
|
}
|
|
@@ -340,14 +364,14 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
const seqNodeMap = new Map<number, LayoutNode>();
|
|
const seqNodeMap = new Map<number, LayoutNode>();
|
|
|
timelineNodes.forEach((node) => {
|
|
timelineNodes.forEach((node) => {
|
|
|
if (node.type === "message") {
|
|
if (node.type === "message") {
|
|
|
- const seq = (node.data as any).sequence;
|
|
|
|
|
|
|
+ const seq = node.data.sequence;
|
|
|
if (typeof seq === "number") seqNodeMap.set(seq, node);
|
|
if (typeof seq === "number") seqNodeMap.set(seq, node);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
timelineNodes.forEach((node, i) => {
|
|
timelineNodes.forEach((node, i) => {
|
|
|
if (node.type === "message") {
|
|
if (node.type === "message") {
|
|
|
- const pSeq = (node.data as any).parent_sequence;
|
|
|
|
|
|
|
+ const pSeq = node.data.parent_sequence;
|
|
|
if (typeof pSeq === "number") {
|
|
if (typeof pSeq === "number") {
|
|
|
const parentNode = seqNodeMap.get(pSeq);
|
|
const parentNode = seqNodeMap.get(pSeq);
|
|
|
if (parentNode) {
|
|
if (parentNode) {
|
|
@@ -377,7 +401,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
nodes.push(startNode);
|
|
nodes.push(startNode);
|
|
|
edges.push({ id: "sys-start", source: startNode, target: firstMsg, type: "line", level: 0, collapsible: false, collapsed: false });
|
|
edges.push({ id: "sys-start", source: startNode, target: firstMsg, type: "line", level: 0, collapsible: false, collapsed: false });
|
|
|
|
|
|
|
|
- const hasEndGoal = goals.some((g) => g.id === "END");
|
|
|
|
|
|
|
+ const hasEndGoal = displayGoals.some((goal) => goal.id === "END");
|
|
|
const endY = lastMsg.y + MESSAGE_Y_STEP;
|
|
const endY = lastMsg.y + MESSAGE_Y_STEP;
|
|
|
const endNode: LayoutNode = { id: "SYSTEM_END", x: MESSAGE_X, y: endY, data: { description: hasEndGoal ? "END" : "TERMINATED" } as Goal, type: "goal", level: 0 };
|
|
const endNode: LayoutNode = { id: "SYSTEM_END", x: MESSAGE_X, y: endY, data: { description: hasEndGoal ? "END" : "TERMINATED" } as Goal, type: "goal", level: 0 };
|
|
|
nodes.push(endNode);
|
|
nodes.push(endNode);
|
|
@@ -448,7 +472,9 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
allGoalNodes.push(node);
|
|
allGoalNodes.push(node);
|
|
|
|
|
|
|
|
if (isSelfCollapsed) {
|
|
if (isSelfCollapsed) {
|
|
|
- const myFoldedChunks = timelineNodes.filter(n => n.type === "folded" && (n.data as any).goalId === goal.id);
|
|
|
|
|
|
|
+ const myFoldedChunks = timelineNodes.filter(
|
|
|
|
|
+ (timelineNode) => timelineNode.type === "folded" && timelineNode.data.goalId === goal.id,
|
|
|
|
|
+ );
|
|
|
myFoldedChunks.forEach((chunk, idx) => {
|
|
myFoldedChunks.forEach((chunk, idx) => {
|
|
|
edges.push({
|
|
edges.push({
|
|
|
id: `fold-link-${goal.id}-${idx}`,
|
|
id: `fold-link-${goal.id}-${idx}`,
|
|
@@ -483,10 +509,10 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
|
|
|
|
|
timelineNodes.forEach((node) => {
|
|
timelineNodes.forEach((node) => {
|
|
|
if (node.type === "message") {
|
|
if (node.type === "message") {
|
|
|
- const textStr = Array.isArray((node.data as any).content) ? (node.data as any).content.map((c: any) => c.text || "").join("") : (((node.data as any).content as any)?.text || "");
|
|
|
|
|
|
|
+ const textStr = getMessageText(node.data);
|
|
|
const isCompression = textStr.includes("摘要") || textStr.includes("压缩");
|
|
const isCompression = textStr.includes("摘要") || textStr.includes("压缩");
|
|
|
if (isCompression) {
|
|
if (isCompression) {
|
|
|
- const goalId = (node.data as any)._injectedGoalId || (node.data as any).goal_id;
|
|
|
|
|
|
|
+ const goalId = node.data._injectedGoalId || node.data.goal_id;
|
|
|
if (goalId) {
|
|
if (goalId) {
|
|
|
const goalNode = allGoalNodes.find(g => g.id === goalId);
|
|
const goalNode = allGoalNodes.find(g => g.id === goalId);
|
|
|
if (goalNode) {
|
|
if (goalNode) {
|
|
@@ -569,8 +595,8 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
if (invalidBranches && invalidBranches.length > 0) {
|
|
if (invalidBranches && invalidBranches.length > 0) {
|
|
|
const validMsgMap = new Map<number, LayoutNode>();
|
|
const validMsgMap = new Map<number, LayoutNode>();
|
|
|
nodes.forEach((n) => {
|
|
nodes.forEach((n) => {
|
|
|
- if (n.type === "message" || n.type === "folded") {
|
|
|
|
|
- const seq = n.data?.sequence;
|
|
|
|
|
|
|
+ if (n.type === "message") {
|
|
|
|
|
+ const seq = n.data.sequence;
|
|
|
if (typeof seq === "number") {
|
|
if (typeof seq === "number") {
|
|
|
validMsgMap.set(seq, n);
|
|
validMsgMap.set(seq, n);
|
|
|
}
|
|
}
|
|
@@ -582,7 +608,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
invalidBranches.forEach((branch) => {
|
|
invalidBranches.forEach((branch) => {
|
|
|
if (branch.length === 0) return;
|
|
if (branch.length === 0) return;
|
|
|
const firstMsg = branch[0];
|
|
const firstMsg = branch[0];
|
|
|
- const pSeq = (firstMsg as any).parent_sequence;
|
|
|
|
|
|
|
+ const pSeq = firstMsg.parent_sequence;
|
|
|
|
|
|
|
|
if (typeof pSeq === "number") {
|
|
if (typeof pSeq === "number") {
|
|
|
const parentNode = validMsgMap.get(pSeq);
|
|
const parentNode = validMsgMap.get(pSeq);
|
|
@@ -606,27 +632,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
const stableId = msg.id || `${parentNode.id}-branch-${branchIdx}-${idx}`;
|
|
const stableId = msg.id || `${parentNode.id}-branch-${branchIdx}-${idx}`;
|
|
|
const nodeId = `invalid-${stableId}`;
|
|
const nodeId = `invalid-${stableId}`;
|
|
|
|
|
|
|
|
- let marker: LayoutNode["marker"] = undefined;
|
|
|
|
|
- const mc = typeof msg.content === "object" && msg.content ? msg.content : null;
|
|
|
|
|
- const textStr = Array.isArray(msg.content) ? msg.content.map((c: any) => c.text || "").join("") : ((msg.content as any)?.text || "");
|
|
|
|
|
- const isTerminate = textStr.includes("TERMINATE");
|
|
|
|
|
-
|
|
|
|
|
- if (branchPrevMarker === "agent_exit" && !isTerminate) {
|
|
|
|
|
- marker = "agent_resume";
|
|
|
|
|
- }
|
|
|
|
|
- if (!marker && mc) {
|
|
|
|
|
- const AGENT_TOOLS = ["agent", "evaluate"];
|
|
|
|
|
- const contentArr = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
|
|
|
- const hasAgentCall = contentArr.some((c: any) => c && c.tool_calls?.some((tc: any) => AGENT_TOOLS.includes(tc.function?.name || tc.name || "")));
|
|
|
|
|
- const hasAgentReturn = contentArr.some((c: any) => c && c.tool_name && AGENT_TOOLS.includes(c.tool_name));
|
|
|
|
|
- if (hasAgentCall) marker = "agent_call";
|
|
|
|
|
- else if (hasAgentReturn) marker = "agent_return";
|
|
|
|
|
- }
|
|
|
|
|
- if (!marker && msg.role === "assistant" && mc && textStr && !isTerminate) {
|
|
|
|
|
- const contentArr = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
|
|
|
- const hasAnyTools = contentArr.some((c: any) => c && c.tool_calls?.length > 0);
|
|
|
|
|
- if (!hasAnyTools) marker = "agent_exit";
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const marker = getMessageMarker(msg, branchPrevMarker);
|
|
|
branchPrevMarker = marker || branchPrevMarker;
|
|
branchPrevMarker = marker || branchPrevMarker;
|
|
|
|
|
|
|
|
const node: LayoutNode = {
|
|
const node: LayoutNode = {
|
|
@@ -817,7 +823,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
|
|
|
|
|
const jumpToNode = (nodeId: string, e: React.MouseEvent) => {
|
|
const jumpToNode = (nodeId: string, e: React.MouseEvent) => {
|
|
|
e.stopPropagation();
|
|
e.stopPropagation();
|
|
|
- const targetNode = layoutData?.nodes.find((n: any) => n.id === nodeId);
|
|
|
|
|
|
|
+ const targetNode = layoutData?.nodes.find((node) => node.id === nodeId);
|
|
|
if (!targetNode) return;
|
|
if (!targetNode) return;
|
|
|
|
|
|
|
|
setSelectedNodeId(targetNode.id);
|
|
setSelectedNodeId(targetNode.id);
|
|
@@ -1083,8 +1089,8 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
<g className={styles.nodes}>
|
|
<g className={styles.nodes}>
|
|
|
{visibleData.nodes.map((node) => {
|
|
{visibleData.nodes.map((node) => {
|
|
|
if (node.type === "folded") {
|
|
if (node.type === "folded") {
|
|
|
- const count = (node.data as any).count;
|
|
|
|
|
- const goalId = (node.data as any).goalId;
|
|
|
|
|
|
|
+ const count = node.data.count;
|
|
|
|
|
+ const goalId = node.data.goalId;
|
|
|
return (
|
|
return (
|
|
|
<g
|
|
<g
|
|
|
key={node.id}
|
|
key={node.id}
|
|
@@ -1127,22 +1133,8 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
if (node.isInvalid) textColor = "#94a3b8";
|
|
if (node.isInvalid) textColor = "#94a3b8";
|
|
|
|
|
|
|
|
let emoji = "";
|
|
let emoji = "";
|
|
|
- if (!isGoal && node.id !== "SYSTEM_START" && node.id !== "SYSTEM_END") {
|
|
|
|
|
- const msg = node.data as any;
|
|
|
|
|
- let isContinue = false;
|
|
|
|
|
- const contentArr = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
|
|
|
- contentArr.forEach((c: any) => {
|
|
|
|
|
- if (c && c.tool_calls) {
|
|
|
|
|
- c.tool_calls.forEach((tc: any) => {
|
|
|
|
|
- const argsStr = tc.function?.arguments || tc.arguments;
|
|
|
|
|
- if (argsStr && typeof argsStr === "string" && argsStr.includes("continue_from")) {
|
|
|
|
|
- isContinue = true;
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (isContinue) {
|
|
|
|
|
|
|
+ if (node.type === "message" && node.id !== "SYSTEM_START" && node.id !== "SYSTEM_END") {
|
|
|
|
|
+ if (isContinueMessage(node.data)) {
|
|
|
emoji = "🔄🤖 ";
|
|
emoji = "🔄🤖 ";
|
|
|
text = "Agent 重调";
|
|
text = "Agent 重调";
|
|
|
} else if (node.marker === "agent_call") {
|
|
} else if (node.marker === "agent_call") {
|
|
@@ -1228,7 +1220,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
<g transform="translate(-70, -25)">
|
|
<g transform="translate(-70, -25)">
|
|
|
<circle r={10} cx={0} cy={0} fill="#f1f5f9" stroke={nodeStroke} strokeWidth={1} />
|
|
<circle r={10} cx={0} cy={0} fill="#f1f5f9" stroke={nodeStroke} strokeWidth={1} />
|
|
|
<text x={0} y={3} fontSize={10} fontWeight="bold" fill={textColor} textAnchor="middle">
|
|
<text x={0} y={3} fontSize={10} fontWeight="bold" fill={textColor} textAnchor="middle">
|
|
|
- {(node.data as any).sequence ?? ""}
|
|
|
|
|
|
|
+ {node.data.sequence ?? ""}
|
|
|
</text>
|
|
</text>
|
|
|
</g>
|
|
</g>
|
|
|
)}
|
|
)}
|