Skip to content

Commit ed72b99

Browse files
authored
✨ Agent copy feature
2 parents 8e7d83b + b2c9912 commit ed72b99

File tree

6 files changed

+181
-4
lines changed

6 files changed

+181
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ docker/openssh-server
2020
docker/volumes/db/data
2121
docker/.env
2222
docker/.run
23+
docker/deploy.options
2324

2425
frontend_standalone/
2526
.pnpm-store/

frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from "@/types/agentConfig";
2727
import AgentImportWizard from "@/components/agent/AgentImportWizard";
2828
import log from "@/lib/logger";
29+
import { useConfirmModal } from "@/hooks/useConfirmModal";
2930

3031
import SubAgentPool from "./agent/SubAgentPool";
3132
import CollaborativeAgentDisplay from "./agent/CollaborativeAgentDisplay";
@@ -150,7 +151,9 @@ export default function AgentSetupOrchestrator({
150151
detailReasons.length > 0 ? detailReasons : fallbackReasons;
151152

152153
const normalizedAvailability =
153-
typeof detail?.is_available === "boolean"
154+
normalizedReasons.length > 0
155+
? false
156+
: typeof detail?.is_available === "boolean"
154157
? detail.is_available
155158
: typeof fallback?.is_available === "boolean"
156159
? fallback.is_available
@@ -196,6 +199,7 @@ export default function AgentSetupOrchestrator({
196199

197200
const { t } = useTranslation("common");
198201
const { message } = App.useApp();
202+
const { confirm } = useConfirmModal();
199203

200204
// Common refresh agent list function, moved to the front to avoid hoisting issues
201205
const refreshAgentList = async (t: TFunction, clearTools: boolean = true) => {
@@ -1858,6 +1862,138 @@ export default function AgentSetupOrchestrator({
18581862
}
18591863
};
18601864

1865+
// Handle copy agent from list
1866+
const handleCopyAgentFromList = async (agent: Agent) => {
1867+
try {
1868+
// Fetch source agent detail before duplicating
1869+
const detailResult = await searchAgentInfo(Number(agent.id));
1870+
if (!detailResult.success || !detailResult.data) {
1871+
message.error(detailResult.message);
1872+
return;
1873+
}
1874+
const detail = detailResult.data;
1875+
1876+
// Prepare copy names
1877+
const copyName = `${detail.name || "agent"}_copy`;
1878+
const copyDisplayName = `${
1879+
detail.display_name || t("agentConfig.agents.defaultDisplayName")
1880+
}${t("agent.copySuffix")}`;
1881+
1882+
// Gather tool and sub-agent identifiers from the source agent
1883+
const tools = Array.isArray(detail.tools) ? detail.tools : [];
1884+
const unavailableTools = tools.filter(
1885+
(tool: any) => tool && tool.is_available === false
1886+
);
1887+
const unavailableToolNames = unavailableTools
1888+
.map(
1889+
(tool: any) =>
1890+
tool?.display_name || tool?.name || tool?.tool_name || ""
1891+
)
1892+
.filter((name: string) => Boolean(name));
1893+
1894+
const enabledToolIds = tools
1895+
.filter((tool: any) => tool && tool.is_available !== false)
1896+
.map((tool: any) => Number(tool.id))
1897+
.filter((id: number) => Number.isFinite(id));
1898+
const subAgentIds = (Array.isArray(detail.sub_agent_id_list)
1899+
? detail.sub_agent_id_list
1900+
: []
1901+
)
1902+
.map((id: any) => Number(id))
1903+
.filter((id: number) => Number.isFinite(id));
1904+
1905+
// Create a new agent using the source agent fields
1906+
const createResult = await updateAgent(
1907+
undefined,
1908+
copyName,
1909+
detail.description,
1910+
detail.model,
1911+
detail.max_step,
1912+
detail.provide_run_summary,
1913+
detail.enabled,
1914+
detail.business_description,
1915+
detail.duty_prompt,
1916+
detail.constraint_prompt,
1917+
detail.few_shots_prompt,
1918+
copyDisplayName,
1919+
detail.model_id ?? undefined,
1920+
detail.business_logic_model_name ?? undefined,
1921+
detail.business_logic_model_id ?? undefined,
1922+
enabledToolIds,
1923+
subAgentIds
1924+
);
1925+
if (!createResult.success || !createResult.data?.agent_id) {
1926+
message.error(
1927+
createResult.message ||
1928+
t("agentConfig.agents.copyFailed")
1929+
);
1930+
return;
1931+
}
1932+
const newAgentId = Number(createResult.data.agent_id);
1933+
const copiedAgentFallback: Agent = {
1934+
...detail,
1935+
id: String(newAgentId),
1936+
name: copyName,
1937+
display_name: copyDisplayName,
1938+
sub_agent_id_list: subAgentIds,
1939+
};
1940+
1941+
// Copy tool configuration to the new agent
1942+
for (const tool of tools) {
1943+
if (!tool || tool.is_available === false) {
1944+
continue;
1945+
}
1946+
const params =
1947+
tool.initParams?.reduce((acc: Record<string, any>, param: any) => {
1948+
acc[param.name] = param.value;
1949+
return acc;
1950+
}, {}) || {};
1951+
try {
1952+
await updateToolConfig(Number(tool.id), newAgentId, params, true);
1953+
} catch (error) {
1954+
log.error("Failed to copy tool configuration while duplicating agent:", error);
1955+
message.error(
1956+
t("agentConfig.agents.copyFailed")
1957+
);
1958+
return;
1959+
}
1960+
}
1961+
1962+
// Refresh UI state and notify user about copy result
1963+
await refreshAgentList(t, false);
1964+
message.success(t("agentConfig.agents.copySuccess"));
1965+
if (unavailableTools.length > 0) {
1966+
const names =
1967+
unavailableToolNames.join(", ") ||
1968+
unavailableTools
1969+
.map((tool: any) => Number(tool?.id))
1970+
.filter((id: number) => !Number.isNaN(id))
1971+
.join(", ");
1972+
message.warning(
1973+
t("agentConfig.agents.copyUnavailableTools", {
1974+
count: unavailableTools.length,
1975+
names,
1976+
})
1977+
);
1978+
}
1979+
// Auto select the newly copied agent for editing
1980+
await handleEditAgent(copiedAgentFallback, t);
1981+
} catch (error) {
1982+
log.error("Failed to copy agent:", error);
1983+
message.error(t("agentConfig.agents.copyFailed"));
1984+
}
1985+
};
1986+
1987+
const handleCopyAgentWithConfirm = (agent: Agent) => {
1988+
confirm({
1989+
title: t("agentConfig.agents.copyConfirmTitle"),
1990+
content: t("agentConfig.agents.copyConfirmContent", {
1991+
name: agent?.display_name || agent?.name || "",
1992+
}),
1993+
onConfirm: () => handleCopyAgentFromList(agent),
1994+
});
1995+
};
1996+
18611997
// Handle delete agent from list
18621998
const handleDeleteAgentFromList = (agent: Agent) => {
18631999
setAgentToDelete(agent);
@@ -1977,6 +2113,7 @@ export default function AgentSetupOrchestrator({
19772113
isGeneratingAgent={isGeneratingAgent}
19782114
editingAgent={editingAgent}
19792115
isCreatingNewAgent={isCreatingNewAgent}
2116+
onCopyAgent={handleCopyAgentWithConfirm}
19802117
onExportAgent={handleExportAgentFromList}
19812118
onDeleteAgent={handleDeleteAgentFromList}
19822119
unsavedAgentId={

frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
55

66
import { Button, Row, Col } from "antd";
77
import { ExclamationCircleOutlined } from "@ant-design/icons";
8-
import { FileOutput, Network, FileInput, Trash2, Plus, X } from "lucide-react";
8+
import { Copy, FileOutput, Network, FileInput, Trash2, Plus, X } from "lucide-react";
99

1010
import { ScrollArea } from "@/components/ui/scrollArea";
1111
import {
@@ -37,6 +37,7 @@ export default function SubAgentPool({
3737
isGeneratingAgent = false,
3838
editingAgent = null,
3939
isCreatingNewAgent = false,
40+
onCopyAgent,
4041
onExportAgent,
4142
onDeleteAgent,
4243
unsavedAgentId = null,
@@ -250,7 +251,7 @@ export default function SubAgentPool({
250251
isImporting ? "bg-gray-100" : "bg-green-100"
251252
}`}
252253
>
253-
<FileOutput
254+
<FileInput
254255
className={`w-4 h-4 ${
255256
isImporting ? "text-gray-400" : "text-green-600"
256257
}`}
@@ -357,6 +358,27 @@ export default function SubAgentPool({
357358

358359
{/* Operation button area */}
359360
<div className="flex items-center gap-1 ml-2 flex-shrink-0">
361+
{/* Copy agent button */}
362+
{onCopyAgent && (
363+
<Tooltip>
364+
<TooltipTrigger asChild>
365+
<Button
366+
type="text"
367+
size="small"
368+
icon={<Copy className="w-4 h-4" />}
369+
onClick={(e) => {
370+
e.preventDefault();
371+
e.stopPropagation();
372+
onCopyAgent(agent);
373+
}}
374+
className="agent-action-button agent-action-button-blue"
375+
/>
376+
</TooltipTrigger>
377+
<TooltipContent>
378+
{t("agent.contextMenu.copy")}
379+
</TooltipContent>
380+
</Tooltip>
381+
)}
360382
{/* View call relationship button */}
361383
<Tooltip>
362384
<TooltipTrigger asChild>
@@ -383,7 +405,7 @@ export default function SubAgentPool({
383405
<Button
384406
type="text"
385407
size="small"
386-
icon={<FileInput className="w-4 h-4" />}
408+
icon={<FileOutput className="w-4 h-4" />}
387409
onClick={(e) => {
388410
e.preventDefault();
389411
e.stopPropagation();

frontend/public/locales/en/common.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@
285285

286286
"agent.contextMenu.export": "Export",
287287
"agent.contextMenu.delete": "Delete",
288+
"agent.contextMenu.copy": "Copy",
289+
"agent.copySuffix": "Copy",
288290
"agent.info.title": "Agent Information",
289291
"agent.info.name.error.empty": "Name cannot be empty",
290292
"agent.info.name.error.format": "Name can only contain letters, numbers and underscores, and must start with a letter or underscore",
@@ -913,6 +915,12 @@
913915
"agentConfig.agents.createSubAgentIdFailed": "Failed to fetch creating sub agent ID, please try again later",
914916
"agentConfig.agents.detailsFetchFailed": "Failed to fetch agent details, please try again later",
915917
"agentConfig.agents.callRelationshipFetchFailed": "Failed to fetch agent call relationship, please try again later",
918+
"agentConfig.agents.defaultDisplayName": "Agent",
919+
"agentConfig.agents.copyConfirmTitle": "Confirm Copy",
920+
"agentConfig.agents.copyConfirmContent": "Create a duplicate of {{name}}?",
921+
"agentConfig.agents.copySuccess": "Agent copied successfully",
922+
"agentConfig.agents.copyUnavailableTools": "Ignored {{count}} unavailable tools: {{names}}",
923+
"agentConfig.agents.copyFailed": "Failed to copy Agent",
916924
"agentConfig.tools.refreshFailedDebug": "Failed to refresh tools list:",
917925
"agentConfig.agents.detailsLoadFailed": "Failed to load Agent details:",
918926
"agentConfig.agents.importFailed": "Failed to import Agent:",

frontend/public/locales/zh/common.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@
286286

287287
"agent.contextMenu.export": "导出",
288288
"agent.contextMenu.delete": "删除",
289+
"agent.contextMenu.copy": "复制",
290+
"agent.copySuffix": "副本",
289291
"agent.info.title": "Agent信息",
290292
"agent.info.name.error.empty": "名称不能为空",
291293
"agent.info.name.error.format": "名称只能包含字母、数字和下划线,且必须以字母或下划线开头",
@@ -913,6 +915,12 @@
913915
"agentConfig.agents.createSubAgentIdFailed": "获取创建子Agent ID失败,请稍后重试",
914916
"agentConfig.agents.detailsFetchFailed": "获取Agent详情失败,请稍后重试",
915917
"agentConfig.agents.callRelationshipFetchFailed": "获取Agent调用关系失败,请稍后重试",
918+
"agentConfig.agents.defaultDisplayName": "智能体",
919+
"agentConfig.agents.copyConfirmTitle": "确认复制",
920+
"agentConfig.agents.copyConfirmContent": "确定要复制 {{name}} 吗?",
921+
"agentConfig.agents.copySuccess": "Agent复制成功",
922+
"agentConfig.agents.copyUnavailableTools": "已忽略{{count}}个不可用工具:{{names}}",
923+
"agentConfig.agents.copyFailed": "Agent复制失败",
916924
"agentConfig.tools.refreshFailedDebug": "刷新工具列表失败:",
917925
"agentConfig.agents.detailsLoadFailed": "加载Agent详情失败:",
918926
"agentConfig.agents.importFailed": "导入Agent失败:",

frontend/types/agentConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export interface SubAgentPoolProps {
156156
isGeneratingAgent?: boolean;
157157
editingAgent?: Agent | null;
158158
isCreatingNewAgent?: boolean;
159+
onCopyAgent?: (agent: Agent) => void;
159160
onExportAgent?: (agent: Agent) => void;
160161
onDeleteAgent?: (agent: Agent) => void;
161162
}

0 commit comments

Comments
 (0)