Skip to content

Commit c39699e

Browse files
committed
Agent copy feature
1 parent fa1f5f4 commit c39699e

File tree

6 files changed

+155
-4
lines changed

6 files changed

+155
-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: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ export default function AgentSetupOrchestrator({
150150
detailReasons.length > 0 ? detailReasons : fallbackReasons;
151151

152152
const normalizedAvailability =
153-
typeof detail?.is_available === "boolean"
153+
normalizedReasons.length > 0
154+
? false
155+
: typeof detail?.is_available === "boolean"
154156
? detail.is_available
155157
: typeof fallback?.is_available === "boolean"
156158
? fallback.is_available
@@ -1858,6 +1860,118 @@ export default function AgentSetupOrchestrator({
18581860
}
18591861
};
18601862

1863+
const handleCopyAgentFromList = async (agent: Agent) => {
1864+
try {
1865+
// Fetch source agent detail before duplicating
1866+
const detailResult = await searchAgentInfo(Number(agent.id));
1867+
if (!detailResult.success || !detailResult.data) {
1868+
message.error(detailResult.message);
1869+
return;
1870+
}
1871+
const detail = detailResult.data;
1872+
1873+
// Prepare copy names
1874+
const copyName = `${detail.name || "agent"}_copy`;
1875+
const copyDisplayName = `${
1876+
detail.display_name || t("agentConfig.agents.defaultDisplayName")
1877+
}${t("agent.copySuffix")}`;
1878+
1879+
// Gather tool and sub-agent identifiers from the source agent
1880+
const tools = Array.isArray(detail.tools) ? detail.tools : [];
1881+
const unavailableTools = tools.filter(
1882+
(tool: any) => tool && tool.is_available === false
1883+
);
1884+
const unavailableToolNames = unavailableTools
1885+
.map(
1886+
(tool: any) =>
1887+
tool?.display_name || tool?.name || tool?.tool_name || ""
1888+
)
1889+
.filter((name: string) => Boolean(name));
1890+
1891+
const enabledToolIds = tools
1892+
.filter((tool: any) => tool && tool.is_available !== false)
1893+
.map((tool: any) => Number(tool.id))
1894+
.filter((id: number) => Number.isFinite(id));
1895+
const subAgentIds = (Array.isArray(detail.sub_agent_id_list)
1896+
? detail.sub_agent_id_list
1897+
: []
1898+
)
1899+
.map((id: any) => Number(id))
1900+
.filter((id: number) => Number.isFinite(id));
1901+
1902+
// Create a new agent using the source agent fields
1903+
const createResult = await updateAgent(
1904+
undefined,
1905+
copyName,
1906+
detail.description,
1907+
detail.model,
1908+
detail.max_step,
1909+
detail.provide_run_summary,
1910+
detail.enabled,
1911+
detail.business_description,
1912+
detail.duty_prompt,
1913+
detail.constraint_prompt,
1914+
detail.few_shots_prompt,
1915+
copyDisplayName,
1916+
detail.model_id ?? undefined,
1917+
detail.business_logic_model_name ?? undefined,
1918+
detail.business_logic_model_id ?? undefined,
1919+
enabledToolIds,
1920+
subAgentIds
1921+
);
1922+
if (!createResult.success || !createResult.data?.agent_id) {
1923+
message.error(
1924+
createResult.message ||
1925+
t("agentConfig.agents.copyFailed")
1926+
);
1927+
return;
1928+
}
1929+
const newAgentId = Number(createResult.data.agent_id);
1930+
1931+
// Copy tool configuration to the new agent
1932+
for (const tool of tools) {
1933+
if (!tool || tool.is_available === false) {
1934+
continue;
1935+
}
1936+
const params =
1937+
tool.initParams?.reduce((acc: Record<string, any>, param: any) => {
1938+
acc[param.name] = param.value;
1939+
return acc;
1940+
}, {}) || {};
1941+
try {
1942+
await updateToolConfig(Number(tool.id), newAgentId, params, true);
1943+
} catch (error) {
1944+
log.error("Failed to copy tool configuration while duplicating agent:", error);
1945+
message.error(
1946+
t("agentConfig.agents.copyFailed")
1947+
);
1948+
return;
1949+
}
1950+
}
1951+
1952+
// Refresh UI state and notify user about copy result
1953+
await refreshAgentList(t, false);
1954+
message.success(t("agentConfig.agents.copySuccess"));
1955+
if (unavailableTools.length > 0) {
1956+
const names =
1957+
unavailableToolNames.join(", ") ||
1958+
unavailableTools
1959+
.map((tool: any) => Number(tool?.id))
1960+
.filter((id: number) => !Number.isNaN(id))
1961+
.join(", ");
1962+
message.warning(
1963+
t("agentConfig.agents.copyUnavailableTools", {
1964+
count: unavailableTools.length,
1965+
names,
1966+
})
1967+
);
1968+
}
1969+
} catch (error) {
1970+
log.error("Failed to copy agent:", error);
1971+
message.error(t("agentConfig.agents.copyFailed"));
1972+
}
1973+
};
1974+
18611975
// Handle delete agent from list
18621976
const handleDeleteAgentFromList = (agent: Agent) => {
18631977
setAgentToDelete(agent);
@@ -1977,6 +2091,7 @@ export default function AgentSetupOrchestrator({
19772091
isGeneratingAgent={isGeneratingAgent}
19782092
editingAgent={editingAgent}
19792093
isCreatingNewAgent={isCreatingNewAgent}
2094+
onCopyAgent={handleCopyAgentFromList}
19802095
onExportAgent={handleExportAgentFromList}
19812096
onDeleteAgent={handleDeleteAgentFromList}
19822097
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 } 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,
@@ -247,7 +248,7 @@ export default function SubAgentPool({
247248
isImporting ? "bg-gray-100" : "bg-green-100"
248249
}`}
249250
>
250-
<FileOutput
251+
<FileInput
251252
className={`w-4 h-4 ${
252253
isImporting ? "text-gray-400" : "text-green-600"
253254
}`}
@@ -353,6 +354,27 @@ export default function SubAgentPool({
353354

354355
{/* Operation button area */}
355356
<div className="flex items-center gap-1 ml-2 flex-shrink-0">
357+
{/* Copy agent button */}
358+
{onCopyAgent && (
359+
<Tooltip>
360+
<TooltipTrigger asChild>
361+
<Button
362+
type="text"
363+
size="small"
364+
icon={<Copy className="w-4 h-4" />}
365+
onClick={(e) => {
366+
e.preventDefault();
367+
e.stopPropagation();
368+
onCopyAgent(agent);
369+
}}
370+
className="agent-action-button agent-action-button-blue"
371+
/>
372+
</TooltipTrigger>
373+
<TooltipContent>
374+
{t("agent.contextMenu.copy")}
375+
</TooltipContent>
376+
</Tooltip>
377+
)}
356378
{/* View call relationship button */}
357379
<Tooltip>
358380
<TooltipTrigger asChild>
@@ -379,7 +401,7 @@ export default function SubAgentPool({
379401
<Button
380402
type="text"
381403
size="small"
382-
icon={<FileInput className="w-4 h-4" />}
404+
icon={<FileOutput className="w-4 h-4" />}
383405
onClick={(e) => {
384406
e.preventDefault();
385407
e.stopPropagation();

frontend/public/locales/en/common.json

Lines changed: 6 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",
@@ -909,6 +911,10 @@
909911
"agentConfig.agents.createSubAgentIdFailed": "Failed to fetch creating sub agent ID, please try again later",
910912
"agentConfig.agents.detailsFetchFailed": "Failed to fetch agent details, please try again later",
911913
"agentConfig.agents.callRelationshipFetchFailed": "Failed to fetch agent call relationship, please try again later",
914+
"agentConfig.agents.defaultDisplayName": "Agent",
915+
"agentConfig.agents.copySuccess": "Agent copied successfully",
916+
"agentConfig.agents.copyUnavailableTools": "Ignored {{count}} unavailable tools: {{names}}",
917+
"agentConfig.agents.copyFailed": "Failed to copy Agent",
912918
"agentConfig.tools.refreshFailedDebug": "Failed to refresh tools list:",
913919
"agentConfig.agents.detailsLoadFailed": "Failed to load Agent details:",
914920
"agentConfig.agents.importFailed": "Failed to import Agent:",

frontend/public/locales/zh/common.json

Lines changed: 6 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": "名称只能包含字母、数字和下划线,且必须以字母或下划线开头",
@@ -909,6 +911,10 @@
909911
"agentConfig.agents.createSubAgentIdFailed": "获取创建子Agent ID失败,请稍后重试",
910912
"agentConfig.agents.detailsFetchFailed": "获取Agent详情失败,请稍后重试",
911913
"agentConfig.agents.callRelationshipFetchFailed": "获取Agent调用关系失败,请稍后重试",
914+
"agentConfig.agents.defaultDisplayName": "智能体",
915+
"agentConfig.agents.copySuccess": "Agent复制成功",
916+
"agentConfig.agents.copyUnavailableTools": "已忽略{{count}}个不可用工具:{{names}}",
917+
"agentConfig.agents.copyFailed": "Agent复制失败",
912918
"agentConfig.tools.refreshFailedDebug": "刷新工具列表失败:",
913919
"agentConfig.agents.detailsLoadFailed": "加载Agent详情失败:",
914920
"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)