Bläddra i källkod

完善流程图消息与节点类型

SamLee 1 dag sedan
förälder
incheckning
bc067a0012

+ 11 - 0
frontend/react-template/src/components/DetailPanel/DetailPanel.tsx

@@ -69,6 +69,17 @@ export const DetailPanel = ({ node, edge, messages = [], onClose, traceId }: Det
   const renderMessageContent = (content: Message["content"], msgKey?: string) => {
     if (!content) return "";
     if (typeof content === "string") return <ReactMarkdown>{content}</ReactMarkdown>;
+    if (Array.isArray(content)) {
+      return (
+        <>
+          {content.map((part, index) => (
+            <div key={`${msgKey || "content"}-${index}`}>
+              {renderMessageContent(part, `${msgKey || "content"}-${index}`)}
+            </div>
+          ))}
+        </>
+      );
+    }
 
     const hasText = !!content.text;
     const hasReasoning = !!content.reasoning_content;

+ 79 - 87
frontend/react-template/src/components/FlowChart/FlowChart.tsx

@@ -1,7 +1,7 @@
 import { useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from "react";
 import type { ForwardRefRenderFunction } from "react";
 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 styles from "./styles/FlowChart.module.css";
 import { ImagePreviewModal } from "../ImagePreview/ImagePreviewModal";
@@ -23,6 +23,56 @@ export interface FlowChartRef {
 
 export type SubTraceEntry = { id: string; mission?: 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[] =>
   (goal.sub_trace_ids ?? [])
@@ -34,18 +84,21 @@ const normalizeSubTraceEntries = (goal: Goal): SubTraceEntry[] =>
     .filter((entry): entry is SubTraceEntry => entry !== null && entry.id.length > 0)
     .slice(0, 6);
 
-interface LayoutNode {
+interface LayoutNodeBase {
   id: string;
   x: number;
   y: number;
-  data: Goal | Message | any;
-  type: "goal" | "message" | "folded";
   level: number;
   parentId?: string;
   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 {
   id: string;
   source: LayoutNode;
@@ -258,36 +311,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
         }
       } else {
         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;
 
@@ -315,8 +339,8 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
 
       let shouldConnect = true;
       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) {
           shouldConnect = false;
         }
@@ -340,14 +364,14 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
     const seqNodeMap = new Map<number, LayoutNode>();
     timelineNodes.forEach((node) => {
       if (node.type === "message") {
-        const seq = (node.data as any).sequence;
+        const seq = node.data.sequence;
         if (typeof seq === "number") seqNodeMap.set(seq, node);
       }
     });
 
     timelineNodes.forEach((node, i) => {
       if (node.type === "message") {
-        const pSeq = (node.data as any).parent_sequence;
+        const pSeq = node.data.parent_sequence;
         if (typeof pSeq === "number") {
           const parentNode = seqNodeMap.get(pSeq);
           if (parentNode) {
@@ -377,7 +401,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
       nodes.push(startNode);
       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 endNode: LayoutNode = { id: "SYSTEM_END", x: MESSAGE_X, y: endY, data: { description: hasEndGoal ? "END" : "TERMINATED" } as Goal, type: "goal", level: 0 };
       nodes.push(endNode);
@@ -448,7 +472,9 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
         allGoalNodes.push(node);
 
         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) => {
             edges.push({
               id: `fold-link-${goal.id}-${idx}`,
@@ -483,10 +509,10 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
 
     timelineNodes.forEach((node) => {
       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("压缩");
         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) {
             const goalNode = allGoalNodes.find(g => g.id === goalId);
             if (goalNode) {
@@ -569,8 +595,8 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
     if (invalidBranches && invalidBranches.length > 0) {
       const validMsgMap = new Map<number, LayoutNode>();
       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") {
             validMsgMap.set(seq, n);
           }
@@ -582,7 +608,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
       invalidBranches.forEach((branch) => {
         if (branch.length === 0) return;
         const firstMsg = branch[0];
-        const pSeq = (firstMsg as any).parent_sequence;
+        const pSeq = firstMsg.parent_sequence;
 
         if (typeof pSeq === "number") {
           const parentNode = validMsgMap.get(pSeq);
@@ -606,27 +632,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
             const stableId = msg.id || `${parentNode.id}-branch-${branchIdx}-${idx}`;
             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;
 
             const node: LayoutNode = {
@@ -817,7 +823,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
 
   const jumpToNode = (nodeId: string, e: React.MouseEvent) => {
     e.stopPropagation();
-    const targetNode = layoutData?.nodes.find((n: any) => n.id === nodeId);
+    const targetNode = layoutData?.nodes.find((node) => node.id === nodeId);
     if (!targetNode) return;
 
     setSelectedNodeId(targetNode.id);
@@ -1083,8 +1089,8 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
             <g className={styles.nodes}>
               {visibleData.nodes.map((node) => {
                 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 (
                     <g
                       key={node.id}
@@ -1127,22 +1133,8 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
                 if (node.isInvalid) textColor = "#94a3b8";
 
                 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 = "🔄🤖 ";
                     text = "Agent 重调";
                   } else if (node.marker === "agent_call") {
@@ -1228,7 +1220,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
                       <g transform="translate(-70, -25)">
                         <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">
-                          {(node.data as any).sequence ?? ""}
+                          {node.data.sequence ?? ""}
                         </text>
                       </g>
                     )}

+ 10 - 5
frontend/react-template/src/types/message.ts

@@ -1,11 +1,12 @@
 export interface ToolCall {
-  id: string;
+  id?: string;
   type?: string;
   function?: {
-    name: string;
-    arguments: string;
+    name?: string;
+    arguments?: string;
   };
-  name: string;
+  name?: string;
+  arguments?: string;
 }
 export interface MsgResult {
   type?: string;
@@ -17,19 +18,23 @@ export interface MsgResultDict {
 }
 
 export interface MessageContent {
+  type?: string;
   text?: string;
   reasoning_content?: string;
   tool_calls?: ToolCall[];
   tool_name?: string;
+  arguments?: string;
   result?: string | MsgResult[];
 }
 
+export type MessageContentValue = string | MessageContent | MessageContent[];
+
 export interface Message {
   id?: string;
   message_id?: string;
   goal_id?: string;
   role?: string;
-  content?: string | MessageContent;
+  content?: MessageContentValue;
   description?: string;
   tokens?: number | null;
   parent_sequence?: number | null;