mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
Compare commits
8 Commits
master
...
fix/agent-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
577b1de835 | ||
|
|
e47e04c1ac | ||
|
|
0887c7a858 | ||
|
|
5c72ee8225 | ||
|
|
a85925782b | ||
|
|
5feee01450 | ||
|
|
3223cc1ed8 | ||
|
|
f5ef508334 |
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { serializeGraphForChat } from "../helpers";
|
||||
import { getNodeDisplayName, serializeGraphForChat } from "../helpers";
|
||||
import type { CustomNode } from "../../FlowEditor/nodes/CustomNode/CustomNode";
|
||||
|
||||
describe("serializeGraphForChat – XML injection prevention", () => {
|
||||
@@ -53,3 +53,53 @@ describe("serializeGraphForChat – XML injection prevention", () => {
|
||||
expect(result).toContain("<injection>");
|
||||
});
|
||||
});
|
||||
|
||||
function makeNode(overrides: Partial<CustomNode["data"]> = {}): CustomNode {
|
||||
return {
|
||||
id: "node-1",
|
||||
data: {
|
||||
title: "AgentExecutorBlock",
|
||||
description: "",
|
||||
hardcodedValues: {},
|
||||
inputSchema: {},
|
||||
outputSchema: {},
|
||||
uiType: "agent",
|
||||
block_id: "b1",
|
||||
costs: [],
|
||||
categories: [],
|
||||
...overrides,
|
||||
},
|
||||
type: "custom" as const,
|
||||
position: { x: 0, y: 0 },
|
||||
} as unknown as CustomNode;
|
||||
}
|
||||
|
||||
describe("getNodeDisplayName", () => {
|
||||
it("returns fallback when node is undefined", () => {
|
||||
expect(getNodeDisplayName(undefined, "fallback-id")).toBe("fallback-id");
|
||||
});
|
||||
|
||||
it("returns customized_name when set", () => {
|
||||
const node = makeNode({
|
||||
metadata: { customized_name: "My Agent" } as any,
|
||||
});
|
||||
expect(getNodeDisplayName(node, "fallback")).toBe("My Agent");
|
||||
});
|
||||
|
||||
it("returns agent_name with version via getNodeDisplayTitle delegation", () => {
|
||||
const node = makeNode({
|
||||
hardcodedValues: { agent_name: "Researcher", graph_version: 3 },
|
||||
});
|
||||
expect(getNodeDisplayName(node, "fallback")).toBe("Researcher v3");
|
||||
});
|
||||
|
||||
it("returns block title when no custom or agent name", () => {
|
||||
const node = makeNode({ title: "SomeBlock" });
|
||||
expect(getNodeDisplayName(node, "fallback")).toBe("SomeBlock");
|
||||
});
|
||||
|
||||
it("returns fallback when title is empty", () => {
|
||||
const node = makeNode({ title: "" });
|
||||
expect(getNodeDisplayName(node, "fallback")).toBe("fallback");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CustomNode } from "../FlowEditor/nodes/CustomNode/CustomNode";
|
||||
import type { CustomEdge } from "../FlowEditor/edges/CustomEdge";
|
||||
import { getNodeDisplayTitle } from "../FlowEditor/nodes/CustomNode/helpers";
|
||||
|
||||
/** Maximum nodes serialized into the AI context to prevent token overruns. */
|
||||
const MAX_NODES = 100;
|
||||
@@ -144,18 +145,16 @@ export function getActionKey(action: GraphAction): string {
|
||||
|
||||
/**
|
||||
* Resolves the display name for a node: prefers the user-customized name,
|
||||
* falls back to the block title, then to the raw ID.
|
||||
* then agent name from hardcodedValues, then block title, then fallback ID.
|
||||
* Delegates to `getNodeDisplayTitle` for the 3-tier resolution logic.
|
||||
* Shared between `serializeGraphForChat` and `ActionItem` to avoid duplication.
|
||||
*/
|
||||
export function getNodeDisplayName(
|
||||
node: CustomNode | undefined,
|
||||
fallback: string,
|
||||
): string {
|
||||
return (
|
||||
(node?.data.metadata?.customized_name as string | undefined) ||
|
||||
node?.data.title ||
|
||||
fallback
|
||||
);
|
||||
if (!node) return fallback;
|
||||
return getNodeDisplayTitle(node.data) || fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getNodeDisplayTitle, formatNodeDisplayTitle } from "../helpers";
|
||||
import { CustomNodeData } from "../CustomNode";
|
||||
|
||||
function makeNodeData(overrides: Partial<CustomNodeData> = {}): CustomNodeData {
|
||||
return {
|
||||
title: "AgentExecutorBlock",
|
||||
description: "",
|
||||
hardcodedValues: {},
|
||||
inputSchema: {},
|
||||
outputSchema: {},
|
||||
uiType: "agent",
|
||||
block_id: "block-1",
|
||||
costs: [],
|
||||
categories: [],
|
||||
...overrides,
|
||||
} as CustomNodeData;
|
||||
}
|
||||
|
||||
describe("getNodeDisplayTitle", () => {
|
||||
it("returns customized_name when set (tier 1)", () => {
|
||||
const data = makeNodeData({
|
||||
metadata: { customized_name: "My Custom Agent" } as any,
|
||||
hardcodedValues: { agent_name: "Researcher", graph_version: 2 },
|
||||
});
|
||||
expect(getNodeDisplayTitle(data)).toBe("My Custom Agent");
|
||||
});
|
||||
|
||||
it("returns agent_name with version when no customized_name (tier 2)", () => {
|
||||
const data = makeNodeData({
|
||||
hardcodedValues: { agent_name: "Researcher", graph_version: 2 },
|
||||
});
|
||||
expect(getNodeDisplayTitle(data)).toBe("Researcher v2");
|
||||
});
|
||||
|
||||
it("returns agent_name without version when graph_version is undefined (tier 2)", () => {
|
||||
const data = makeNodeData({
|
||||
hardcodedValues: { agent_name: "Researcher" },
|
||||
});
|
||||
expect(getNodeDisplayTitle(data)).toBe("Researcher");
|
||||
});
|
||||
|
||||
it("returns agent_name with version 0 (tier 2)", () => {
|
||||
const data = makeNodeData({
|
||||
hardcodedValues: { agent_name: "Researcher", graph_version: 0 },
|
||||
});
|
||||
expect(getNodeDisplayTitle(data)).toBe("Researcher v0");
|
||||
});
|
||||
|
||||
it("returns generic block title when no custom or agent name (tier 3)", () => {
|
||||
const data = makeNodeData({ title: "AgentExecutorBlock" });
|
||||
expect(getNodeDisplayTitle(data)).toBe("AgentExecutorBlock");
|
||||
});
|
||||
|
||||
it("prioritizes customized_name over agent_name", () => {
|
||||
const data = makeNodeData({
|
||||
metadata: { customized_name: "Renamed" } as any,
|
||||
hardcodedValues: { agent_name: "Original Agent", graph_version: 1 },
|
||||
});
|
||||
expect(getNodeDisplayTitle(data)).toBe("Renamed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatNodeDisplayTitle", () => {
|
||||
it("returns custom name as-is without beautifying", () => {
|
||||
const data = makeNodeData({
|
||||
metadata: { customized_name: "my_custom_name" } as any,
|
||||
});
|
||||
expect(formatNodeDisplayTitle(data)).toBe("my_custom_name");
|
||||
});
|
||||
|
||||
it("returns agent name as-is without beautifying", () => {
|
||||
const data = makeNodeData({
|
||||
hardcodedValues: { agent_name: "Blockchain Agent", graph_version: 1 },
|
||||
});
|
||||
expect(formatNodeDisplayTitle(data)).toBe("Blockchain Agent v1");
|
||||
});
|
||||
|
||||
it("beautifies generic block title and strips Block suffix", () => {
|
||||
const data = makeNodeData({ title: "AgentExecutorBlock" });
|
||||
const result = formatNodeDisplayTitle(data);
|
||||
expect(result).not.toContain("Block");
|
||||
expect(result).toBe("Agent Executor");
|
||||
});
|
||||
|
||||
it("does not corrupt agent names containing 'Block'", () => {
|
||||
const data = makeNodeData({
|
||||
hardcodedValues: { agent_name: "Blockchain Agent", graph_version: 2 },
|
||||
});
|
||||
expect(formatNodeDisplayTitle(data)).toBe("Blockchain Agent v2");
|
||||
});
|
||||
});
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { beautifyString, cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CustomNodeData } from "../CustomNode";
|
||||
import { formatNodeDisplayTitle, getNodeDisplayTitle } from "../helpers";
|
||||
import { NodeBadges } from "./NodeBadges";
|
||||
import { NodeContextMenu } from "./NodeContextMenu";
|
||||
import { NodeCost } from "./NodeCost";
|
||||
@@ -21,15 +22,24 @@ type Props = {
|
||||
export const NodeHeader = ({ data, nodeId }: Props) => {
|
||||
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
||||
|
||||
const title = (data.metadata?.customized_name as string) || data.title;
|
||||
const title = getNodeDisplayTitle(data);
|
||||
const displayTitle = formatNodeDisplayTitle(data);
|
||||
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
const [editedTitle, setEditedTitle] = useState(title);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditingTitle) {
|
||||
setEditedTitle(title);
|
||||
}
|
||||
}, [title, isEditingTitle]);
|
||||
|
||||
const handleTitleEdit = () => {
|
||||
updateNodeData(nodeId, {
|
||||
metadata: { ...data.metadata, customized_name: editedTitle },
|
||||
});
|
||||
if (editedTitle !== title) {
|
||||
updateNodeData(nodeId, {
|
||||
metadata: { ...data.metadata, customized_name: editedTitle },
|
||||
});
|
||||
}
|
||||
setIsEditingTitle(false);
|
||||
};
|
||||
|
||||
@@ -72,12 +82,12 @@ export const NodeHeader = ({ data, nodeId }: Props) => {
|
||||
variant="large-semibold"
|
||||
className="line-clamp-1 hover:cursor-text"
|
||||
>
|
||||
{beautifyString(title).replace("Block", "").trim()}
|
||||
{displayTitle}
|
||||
</Text>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{beautifyString(title).replace("Block", "").trim()}</p>
|
||||
<p>{displayTitle}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent } from "@/tests/integrations/test-utils";
|
||||
import { NodeHeader } from "../NodeHeader";
|
||||
import { CustomNodeData } from "../../CustomNode";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
|
||||
vi.mock("../NodeCost", () => ({
|
||||
NodeCost: () => <div data-testid="node-cost" />,
|
||||
}));
|
||||
|
||||
vi.mock("../NodeContextMenu", () => ({
|
||||
NodeContextMenu: () => <div data-testid="node-context-menu" />,
|
||||
}));
|
||||
|
||||
vi.mock("../NodeBadges", () => ({
|
||||
NodeBadges: () => <div data-testid="node-badges" />,
|
||||
}));
|
||||
|
||||
function makeData(overrides: Partial<CustomNodeData> = {}): CustomNodeData {
|
||||
return {
|
||||
title: "AgentExecutorBlock",
|
||||
description: "",
|
||||
hardcodedValues: {},
|
||||
inputSchema: {},
|
||||
outputSchema: {},
|
||||
uiType: "agent",
|
||||
block_id: "block-1",
|
||||
costs: [],
|
||||
categories: [],
|
||||
...overrides,
|
||||
} as CustomNodeData;
|
||||
}
|
||||
|
||||
describe("NodeHeader", () => {
|
||||
const mockUpdateNodeData = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useNodeStore.setState({ updateNodeData: mockUpdateNodeData } as any);
|
||||
});
|
||||
|
||||
it("renders beautified generic block title", () => {
|
||||
render(<NodeHeader data={makeData()} nodeId="abc-123" />);
|
||||
expect(screen.getByText("Agent Executor")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders agent name with version from hardcodedValues", () => {
|
||||
const data = makeData({
|
||||
hardcodedValues: { agent_name: "Researcher", graph_version: 2 },
|
||||
});
|
||||
render(<NodeHeader data={data} nodeId="abc-123" />);
|
||||
expect(screen.getByText("Researcher v2")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders customized_name over agent name", () => {
|
||||
const data = makeData({
|
||||
metadata: { customized_name: "My Custom Node" } as any,
|
||||
hardcodedValues: { agent_name: "Researcher", graph_version: 1 },
|
||||
});
|
||||
render(<NodeHeader data={data} nodeId="abc-123" />);
|
||||
expect(screen.getByText("My Custom Node")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows node ID prefix", () => {
|
||||
render(<NodeHeader data={makeData()} nodeId="abc-123" />);
|
||||
expect(screen.getByText("#abc")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("enters edit mode on double-click and saves on blur", () => {
|
||||
render(<NodeHeader data={makeData()} nodeId="node-1" />);
|
||||
const titleEl = screen.getByText("Agent Executor");
|
||||
fireEvent.doubleClick(titleEl);
|
||||
|
||||
const input = screen.getByDisplayValue("AgentExecutorBlock");
|
||||
fireEvent.change(input, { target: { value: "New Name" } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(mockUpdateNodeData).toHaveBeenCalledWith("node-1", {
|
||||
metadata: { customized_name: "New Name" },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not save when title is unchanged on blur", () => {
|
||||
const data = makeData({
|
||||
hardcodedValues: { agent_name: "Researcher", graph_version: 2 },
|
||||
});
|
||||
render(<NodeHeader data={data} nodeId="node-1" />);
|
||||
const titleEl = screen.getByText("Researcher v2");
|
||||
fireEvent.doubleClick(titleEl);
|
||||
|
||||
const input = screen.getByDisplayValue("Researcher v2");
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(mockUpdateNodeData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("saves on Enter key", () => {
|
||||
render(<NodeHeader data={makeData()} nodeId="node-1" />);
|
||||
fireEvent.doubleClick(screen.getByText("Agent Executor"));
|
||||
|
||||
const input = screen.getByDisplayValue("AgentExecutorBlock");
|
||||
fireEvent.change(input, { target: { value: "Renamed" } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
|
||||
expect(mockUpdateNodeData).toHaveBeenCalledWith("node-1", {
|
||||
metadata: { customized_name: "Renamed" },
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels edit on Escape key", () => {
|
||||
render(<NodeHeader data={makeData()} nodeId="node-1" />);
|
||||
fireEvent.doubleClick(screen.getByText("Agent Executor"));
|
||||
|
||||
const input = screen.getByDisplayValue("AgentExecutorBlock");
|
||||
fireEvent.change(input, { target: { value: "Changed" } });
|
||||
fireEvent.keyDown(input, { key: "Escape" });
|
||||
|
||||
expect(mockUpdateNodeData).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("Agent Executor")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,55 @@
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { NodeResolutionData } from "@/app/(platform)/build/stores/types";
|
||||
import { beautifyString } from "@/lib/utils";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { CustomNodeData } from "./CustomNode";
|
||||
|
||||
/**
|
||||
* Resolves the display title for a node using a 3-tier fallback:
|
||||
*
|
||||
* 1. `customized_name` — the user's manual rename (highest priority)
|
||||
* 2. `agent_name` (+ version) from `hardcodedValues` — the selected agent's
|
||||
* display name, persisted by blocks like AgentExecutorBlock
|
||||
* 3. `data.title` — the generic block name (e.g. "Agent Executor")
|
||||
*
|
||||
* `customized_name` is the user's explicit rename via double-click; it lives in
|
||||
* node metadata. `agent_name` is the programmatic name of the agent graph
|
||||
* selected in the block's input form; it lives in `hardcodedValues` alongside
|
||||
* `graph_version`. These are distinct sources of truth — customized_name always
|
||||
* wins because it reflects deliberate user intent.
|
||||
*/
|
||||
export function getNodeDisplayTitle(data: CustomNodeData): string {
|
||||
if (data.metadata?.customized_name) {
|
||||
return data.metadata.customized_name as string;
|
||||
}
|
||||
|
||||
const agentName = data.hardcodedValues?.agent_name as string | undefined;
|
||||
const graphVersion = data.hardcodedValues?.graph_version as
|
||||
| number
|
||||
| undefined;
|
||||
if (agentName) {
|
||||
return graphVersion != null ? `${agentName} v${graphVersion}` : agentName;
|
||||
}
|
||||
|
||||
return data.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the formatted display title for rendering.
|
||||
* Agent names and custom names are shown as-is; generic block names get
|
||||
* beautified and have the trailing " Block" suffix stripped.
|
||||
*/
|
||||
export function formatNodeDisplayTitle(data: CustomNodeData): string {
|
||||
const title = getNodeDisplayTitle(data);
|
||||
const isAgentOrCustom = !!(
|
||||
data.metadata?.customized_name || data.hardcodedValues?.agent_name
|
||||
);
|
||||
return isAgentOrCustom
|
||||
? title
|
||||
: beautifyString(title)
|
||||
.replace(/ Block$/, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
|
||||
INCOMPLETE: "ring-slate-300 bg-slate-300",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatNodeDisplayTitle } from "@/app/(platform)/build/components/FlowEditor/nodes/CustomNode/helpers";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { beautifyString, cn } from "@/lib/utils";
|
||||
@@ -58,9 +59,7 @@ export function GraphSearchContent({
|
||||
filteredNodes.map((node, index) => {
|
||||
if (!node?.data) return null;
|
||||
|
||||
const nodeTitle =
|
||||
(node.data.metadata?.customized_name as string) ||
|
||||
beautifyString(node.data.title || "").replace(/ Block$/, "");
|
||||
const nodeTitle = formatNodeDisplayTitle(node.data);
|
||||
const nodeType = beautifyString(node.data.title || "").replace(
|
||||
/ Block$/,
|
||||
"",
|
||||
@@ -70,7 +69,10 @@ export function GraphSearchContent({
|
||||
node.data.description ||
|
||||
"";
|
||||
|
||||
const hasCustomName = !!node.data.metadata?.customized_name;
|
||||
const hasCustomName = !!(
|
||||
node.data.metadata?.customized_name ||
|
||||
node.data.hardcodedValues?.agent_name
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -69,6 +69,9 @@ function calculateNodeScore(
|
||||
const customizedName = String(
|
||||
node.data?.metadata?.customized_name || "",
|
||||
).toLowerCase();
|
||||
const agentName = String(
|
||||
node.data?.hardcodedValues?.agent_name || "",
|
||||
).toLowerCase();
|
||||
|
||||
// Get input and output names with defensive checks
|
||||
const inputNames = Object.keys(node.data?.inputSchema?.properties || {}).map(
|
||||
@@ -81,6 +84,7 @@ function calculateNodeScore(
|
||||
// 1. Check exact match in customized name, title (includes ID), node ID, or block type (highest priority)
|
||||
if (
|
||||
customizedName.includes(query) ||
|
||||
agentName.includes(query) ||
|
||||
nodeTitle.includes(query) ||
|
||||
nodeID.includes(query) ||
|
||||
blockType.includes(query) ||
|
||||
@@ -95,6 +99,7 @@ function calculateNodeScore(
|
||||
queryWords.every(
|
||||
(word) =>
|
||||
customizedName.includes(word) ||
|
||||
agentName.includes(word) ||
|
||||
nodeTitle.includes(word) ||
|
||||
beautifiedBlockType.includes(word),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user