feat(platform): Library Agent "Delete" functionality (#9546)

- Resolves #9545

### Changes 🏗️

- fix(platform): Make "Delete" button on `/monitoring` soft-delete the
`LibraryAgent` instead of hard-deleting the corresponding `AgentGraph`
- feat(frontend): Add "Delete agent" button on `/library/agents/[id]`

Technical:
- fix(backend): Accept partial input on `update_library_agent` endpoint
- feat(backend): Add `GET /api/library/agents/{library_agent_id}`
endpoint
- refactor(frontend): Replace use of `GraphMeta` by `LibraryAgent` where
possible on `/library/agents/[id]`

Also, out of scope but important:
- fix(frontend): Hide buttons that require direct graph access depending
on `agent.can_access_graph`

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - Go to the Library and select an agent
    - [x] -> `/library/agents/[id]` should load normally
      - Save this URL for later (especially the ID)!
  - Click "Delete agent" on `/library/agents/[id]`
    - [x] -> should show a confirmation dialog
    - Click "Delete" to confirm
      - [x] -> should redirect back to `/library`
      - [x] -> deleted agent should no longer be listed in the Library
  - Click "Delete agent" on `/monitoring`
    - [x] -> should show a confirmation dialog
    - Click "Delete" to confirm
      - [x] -> agent should disappear from agent list
- [x] -> views should reset / deselect the deleted agent where
applicable
This commit is contained in:
Reinier van der Leer
2025-03-03 17:49:44 +01:00
committed by GitHub
parent ef00ab51e0
commit 27a5635607
14 changed files with 302 additions and 134 deletions

View File

@@ -17,7 +17,7 @@ import backend.server.v2.store.media as store_media
logger = logging.getLogger(__name__)
async def get_library_agents(
async def list_library_agents(
user_id: str,
search_term: Optional[str] = None,
sort_by: library_model.LibraryAgentSort = library_model.LibraryAgentSort.UPDATED_AT,
@@ -125,6 +125,49 @@ async def get_library_agents(
raise store_exceptions.DatabaseError("Failed to fetch library agents") from e
async def get_library_agent(id: str, user_id: str) -> library_model.LibraryAgent:
"""
Get a specific agent from the user's library.
Args:
library_agent_id: ID of the library agent to retrieve.
user_id: ID of the authenticated user.
Returns:
The requested LibraryAgent.
Raises:
AgentNotFoundError: If the specified agent does not exist.
DatabaseError: If there's an error during retrieval.
"""
try:
library_agent = await prisma.models.LibraryAgent.prisma().find_first(
where={
"id": id,
"userId": user_id,
"isDeleted": False,
},
include={
"Agent": {
"include": {
**backend.data.includes.AGENT_GRAPH_INCLUDE,
"AgentGraphExecution": {"where": {"userId": user_id}},
}
},
"Creator": True,
},
)
if not library_agent:
raise store_exceptions.AgentNotFoundError(f"Library agent #{id} not found")
return library_model.LibraryAgent.from_db(library_agent)
except prisma.errors.PrismaError as e:
logger.error(f"Database error fetching library agent: {e}")
raise store_exceptions.DatabaseError("Failed to fetch library agent") from e
async def create_library_agent(
agent_id: str,
agent_version: int,
@@ -249,10 +292,10 @@ async def update_agent_version_in_library(
async def update_library_agent(
library_agent_id: str,
user_id: str,
auto_update_version: bool = False,
is_favorite: bool = False,
is_archived: bool = False,
is_deleted: bool = False,
auto_update_version: Optional[bool] = None,
is_favorite: Optional[bool] = None,
is_archived: Optional[bool] = None,
is_deleted: Optional[bool] = None,
) -> None:
"""
Updates the specified LibraryAgent record.
@@ -273,15 +316,19 @@ async def update_library_agent(
f"auto_update_version={auto_update_version}, is_favorite={is_favorite}, "
f"is_archived={is_archived}, is_deleted={is_deleted}"
)
update_fields: prisma.types.LibraryAgentUpdateManyMutationInput = {}
if auto_update_version is not None:
update_fields["useGraphIsActiveVersion"] = auto_update_version
if is_favorite is not None:
update_fields["isFavorite"] = is_favorite
if is_archived is not None:
update_fields["isArchived"] = is_archived
if is_deleted is not None:
update_fields["isDeleted"] = is_deleted
try:
await prisma.models.LibraryAgent.prisma().update_many(
where={"id": library_agent_id, "userId": user_id},
data={
"useGraphIsActiveVersion": auto_update_version,
"isFavorite": is_favorite,
"isArchived": is_archived,
"isDeleted": is_deleted,
},
where={"id": library_agent_id, "userId": user_id}, data=update_fields
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error updating library agent: {str(e)}")

View File

@@ -74,7 +74,7 @@ async def test_get_library_agents(mocker):
mock_library_agent.return_value.count = mocker.AsyncMock(return_value=1)
# Call function
result = await db.get_library_agents("test-user")
result = await db.list_library_agents("test-user")
# Verify results
assert len(result.agents) == 1

View File

@@ -1,6 +1,6 @@
import datetime
from enum import Enum
from typing import Any
from typing import Any, Optional
import prisma.enums
import prisma.models
@@ -245,11 +245,15 @@ class LibraryAgentUpdateRequest(pydantic.BaseModel):
archiving, or deleting.
"""
auto_update_version: bool = pydantic.Field(
False, description="Auto-update the agent version"
auto_update_version: Optional[bool] = pydantic.Field(
default=None, description="Auto-update the agent version"
)
is_favorite: bool = pydantic.Field(
False, description="Mark the agent as a favorite"
is_favorite: Optional[bool] = pydantic.Field(
default=None, description="Mark the agent as a favorite"
)
is_archived: Optional[bool] = pydantic.Field(
default=None, description="Archive the agent"
)
is_deleted: Optional[bool] = pydantic.Field(
default=None, description="Delete the agent"
)
is_archived: bool = pydantic.Field(False, description="Archive the agent")
is_deleted: bool = pydantic.Field(False, description="Delete the agent")

View File

@@ -24,14 +24,14 @@ router = APIRouter(
500: {"description": "Server error", "content": {"application/json": {}}},
},
)
async def get_library_agents(
async def list_library_agents(
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
search_term: Optional[str] = Query(
None, description="Search term to filter agents"
),
sort_by: library_model.LibraryAgentSort = Query(
library_model.LibraryAgentSort.UPDATED_AT,
description="Sort results by criteria",
description="Criteria to sort results by",
),
page: int = Query(
1,
@@ -62,7 +62,7 @@ async def get_library_agents(
HTTPException: If a server/database error occurs.
"""
try:
return await library_db.get_library_agents(
return await library_db.list_library_agents(
user_id=user_id,
search_term=search_term,
sort_by=sort_by,
@@ -77,6 +77,14 @@ async def get_library_agents(
) from e
@router.get("/{library_agent_id}")
async def get_library_agent(
library_agent_id: str,
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
) -> library_model.LibraryAgent:
return await library_db.get_library_agent(id=library_agent_id, user_id=user_id)
@router.post(
"",
status_code=status.HTTP_201_CREATED,

View File

@@ -6,22 +6,26 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
GraphExecution,
GraphExecutionMeta,
GraphID,
GraphMeta,
LibraryAgent,
LibraryAgentID,
Schedule,
} from "@/lib/autogpt-server-api";
import type { ButtonAction } from "@/components/agptui/types";
import AgentRunDraftView from "@/components/agents/agent-run-draft-view";
import AgentRunDetailsView from "@/components/agents/agent-run-details-view";
import AgentRunsSelectorList from "@/components/agents/agent-runs-selector-list";
import AgentScheduleDetailsView from "@/components/agents/agent-schedule-details-view";
import AgentDeleteConfirmDialog from "@/components/agents/agent-delete-confirm-dialog";
export default function AgentRunsPage(): React.ReactElement {
const { id: agentID }: { id: GraphID } = useParams();
const { id: agentID }: { id: LibraryAgentID } = useParams();
const router = useRouter();
const api = useBackendAPI();
const [agent, setAgent] = useState<GraphMeta | null>(null);
const [graph, setGraph] = useState<GraphMeta | null>(null);
const [agent, setAgent] = useState<LibraryAgent | null>(null);
const [agentRuns, setAgentRuns] = useState<GraphExecutionMeta[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [selectedView, selectView] = useState<{
@@ -35,6 +39,8 @@ export default function AgentRunsPage(): React.ReactElement {
null,
);
const [isFirstLoad, setIsFirstLoad] = useState<boolean>(true);
const [agentDeleteDialogOpen, setAgentDeleteDialogOpen] =
useState<boolean>(false);
const openRunDraftView = useCallback(() => {
selectView({ type: "run" });
@@ -50,22 +56,28 @@ export default function AgentRunsPage(): React.ReactElement {
}, []);
const fetchAgents = useCallback(() => {
api.getGraph(agentID).then(setAgent);
api.getGraphExecutions(agentID).then((agentRuns) => {
const sortedRuns = agentRuns.toSorted(
(a, b) => b.started_at - a.started_at,
);
setAgentRuns(sortedRuns);
api.getLibraryAgent(agentID).then((agent) => {
setAgent(agent);
if (!selectedView.id && isFirstLoad && sortedRuns.length > 0) {
// only for first load or first execution
setIsFirstLoad(false);
selectView({ type: "run", id: sortedRuns[0].execution_id });
setSelectedRun(sortedRuns[0]);
}
api.getGraph(agent.agent_id).then(setGraph);
api.getGraphExecutions(agent.agent_id).then((agentRuns) => {
const sortedRuns = agentRuns.toSorted(
(a, b) => b.started_at - a.started_at,
);
setAgentRuns(sortedRuns);
if (!selectedView.id && isFirstLoad && sortedRuns.length > 0) {
// only for first load or first execution
setIsFirstLoad(false);
selectView({ type: "run", id: sortedRuns[0].execution_id });
setSelectedRun(sortedRuns[0]);
}
});
});
if (selectedView.type == "run" && selectedView.id) {
api.getGraphExecutionInfo(agentID, selectedView.id).then(setSelectedRun);
if (selectedView.type == "run" && selectedView.id && agent) {
api
.getGraphExecutionInfo(agent.agent_id, selectedView.id)
.then(setSelectedRun);
}
}, [api, agentID, selectedView, isFirstLoad]);
@@ -75,7 +87,7 @@ export default function AgentRunsPage(): React.ReactElement {
// load selectedRun based on selectedView
useEffect(() => {
if (selectedView.type != "run" || !selectedView.id) return;
if (selectedView.type != "run" || !selectedView.id || !agent) return;
// pull partial data from "cache" while waiting for the rest to load
if (selectedView.id !== selectedRun?.execution_id) {
@@ -84,15 +96,19 @@ export default function AgentRunsPage(): React.ReactElement {
);
}
api.getGraphExecutionInfo(agentID, selectedView.id).then(setSelectedRun);
}, [api, selectedView, agentRuns, agentID]);
api
.getGraphExecutionInfo(agent.agent_id, selectedView.id)
.then(setSelectedRun);
}, [api, selectedView, agentID]);
const fetchSchedules = useCallback(async () => {
if (!agent) return;
// TODO: filter in backend - https://github.com/Significant-Gravitas/AutoGPT/issues/9183
setSchedules(
(await api.listSchedules()).filter((s) => s.graph_id == agentID),
(await api.listSchedules()).filter((s) => s.graph_id == agent.agent_id),
);
}, [api, agentID]);
}, [api, agent]);
useEffect(() => {
fetchSchedules();
@@ -110,19 +126,24 @@ export default function AgentRunsPage(): React.ReactElement {
useEffect(() => {
const intervalId = setInterval(() => fetchAgents(), 5000);
return () => clearInterval(intervalId);
}, [fetchAgents, agent]);
}, [fetchAgents]);
const agentActions: { label: string; callback: () => void }[] = useMemo(
const agentActions: ButtonAction[] = useMemo(
() => [
{
label: "Open in builder",
callback: () => agent && router.push(`/build?flowID=${agent.id}`),
callback: () => agent && router.push(`/build?flowID=${agent.agent_id}`),
},
{
label: "Delete agent",
variant: "destructive",
callback: () => setAgentDeleteDialogOpen(true),
},
],
[agent, router],
);
if (!agent) {
if (!agent || !graph) {
/* TODO: implement loading indicators / skeleton page */
return <span>Loading...</span>;
}
@@ -156,27 +177,38 @@ export default function AgentRunsPage(): React.ReactElement {
{(selectedView.type == "run" && selectedView.id ? (
selectedRun && (
<AgentRunDetailsView
agent={agent}
graph={graph}
run={selectedRun}
agentActions={agentActions}
/>
)
) : selectedView.type == "run" ? (
<AgentRunDraftView
agent={agent}
graph={graph}
onRun={(runID) => selectRun(runID)}
agentActions={agentActions}
/>
) : selectedView.type == "schedule" ? (
selectedSchedule && (
<AgentScheduleDetailsView
agent={agent}
graph={graph}
schedule={selectedSchedule}
onForcedRun={(runID) => selectRun(runID)}
agentActions={agentActions}
/>
)
) : null) || <p>Loading...</p>}
<AgentDeleteConfirmDialog
open={agentDeleteDialogOpen}
onOpenChange={setAgentDeleteDialogOpen}
onDoDelete={() =>
agent &&
api
.updateLibraryAgent(agent.id, { is_deleted: true })
.then(() => router.push("/library"))
}
/>
</div>
</div>
);

View File

@@ -0,0 +1,41 @@
import { Button } from "@/components/agptui/Button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
export default function AgentDeleteConfirmDialog({
open,
onOpenChange,
onDoDelete,
className,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
onDoDelete: () => void;
className?: string;
}): React.ReactNode {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={className}>
<DialogHeader>
<DialogTitle>Delete Agent</DialogTitle>
<DialogDescription>
Are you sure you want to delete this agent? <br />
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={() => onOpenChange(false)}>Cancel</Button>
<Button variant="destructive" onClick={onDoDelete}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -9,6 +9,7 @@ import {
GraphMeta,
} from "@/lib/autogpt-server-api";
import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/agptui/Button";
import { Input } from "@/components/ui/input";
@@ -19,13 +20,13 @@ import {
} from "@/components/agents/agent-run-status-chip";
export default function AgentRunDetailsView({
agent,
graph,
run,
agentActions,
}: {
agent: GraphMeta;
graph: GraphMeta;
run: GraphExecution | GraphExecutionMeta;
agentActions: { label: string; callback: () => void }[];
agentActions: ButtonAction[];
}): React.ReactNode {
const api = useBackendAPI();
@@ -64,25 +65,25 @@ export default function AgentRunDetailsView({
Object.entries(run.inputs).map(([k, v]) => [
k,
{
title: agent.input_schema.properties[k].title,
// type: agent.input_schema.properties[k].type, // TODO: implement typed graph inputs
title: graph.input_schema.properties[k].title,
// type: graph.input_schema.properties[k].type, // TODO: implement typed graph inputs
value: v,
},
]),
);
}, [agent, run]);
}, [graph, run]);
const runAgain = useCallback(
() =>
agentRunInputs &&
api.executeGraph(
agent.id,
agent.version,
graph.id,
graph.version,
Object.fromEntries(
Object.entries(agentRunInputs).map(([k, v]) => [k, v.value]),
),
),
[api, agent, agentRunInputs],
[api, graph, agentRunInputs],
);
const agentRunOutputs:
@@ -100,13 +101,13 @@ export default function AgentRunDetailsView({
Object.entries(run.outputs).map(([k, v]) => [
k,
{
title: agent.output_schema.properties[k].title,
title: graph.output_schema.properties[k].title,
/* type: agent.output_schema.properties[k].type */
values: v,
},
]),
);
}, [agent, run, runStatus]);
}, [graph, run, runStatus]);
const runActions: { label: string; callback: () => void }[] = useMemo(
() => [{ label: "Run again", callback: () => runAgain() }],
@@ -198,7 +199,11 @@ export default function AgentRunDetailsView({
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
<Button
key={i}
variant={action.variant ?? "outline"}
onClick={action.callback}
>
{action.label}
</Button>
))}

View File

@@ -4,30 +4,31 @@ import React, { useCallback, useMemo, useState } from "react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { GraphMeta } from "@/lib/autogpt-server-api";
import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button, ButtonProps } from "@/components/agptui/Button";
import { Input } from "@/components/ui/input";
export default function AgentRunDraftView({
agent,
graph,
onRun,
agentActions,
}: {
agent: GraphMeta;
graph: GraphMeta;
onRun: (runID: string) => void;
agentActions: { label: string; callback: () => void }[];
agentActions: ButtonAction[];
}): React.ReactNode {
const api = useBackendAPI();
const agentInputs = agent.input_schema.properties;
const agentInputs = graph.input_schema.properties;
const [inputValues, setInputValues] = useState<Record<string, any>>({});
const doRun = useCallback(
() =>
api
.executeGraph(agent.id, agent.version, inputValues)
.executeGraph(graph.id, graph.version, inputValues)
.then((newRun) => onRun(newRun.graph_exec_id)),
[api, agent, inputValues, onRun],
[api, graph, inputValues, onRun],
);
const runActions: {
@@ -87,7 +88,11 @@ export default function AgentRunDraftView({
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
<Button
key={i}
variant={action.variant ?? "outline"}
onClick={action.callback}
>
{action.label}
</Button>
))}

View File

@@ -5,7 +5,7 @@ import { Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import {
GraphExecutionMeta,
GraphMeta,
LibraryAgent,
Schedule,
} from "@/lib/autogpt-server-api";
@@ -17,7 +17,7 @@ import { agentRunStatusMap } from "@/components/agents/agent-run-status-chip";
import AgentRunSummaryCard from "@/components/agents/agent-run-summary-card";
interface AgentRunsSelectorListProps {
agent: GraphMeta;
agent: LibraryAgent;
agentRuns: GraphExecutionMeta[];
schedules: Schedule[];
selectedView: { type: "run" | "schedule"; id?: string };
@@ -74,7 +74,7 @@ export default function AgentRunsSelectorList({
>
<span>Scheduled</span>
<span className="text-neutral-600">
{schedules.filter((s) => s.graph_id === agent.id).length}
{schedules.filter((s) => s.graph_id === agent.agent_id).length}
</span>
</Badge>
</div>
@@ -112,7 +112,7 @@ export default function AgentRunsSelectorList({
/>
))
: schedules
.filter((schedule) => schedule.graph_id === agent.id)
.filter((schedule) => schedule.graph_id === agent.agent_id)
.map((schedule, i) => (
<AgentRunSummaryCard
className="h-28 w-72 lg:h-32 xl:w-80"

View File

@@ -1,24 +1,25 @@
"use client";
import React, { useCallback, useMemo } from "react";
import { BlockIOSubType, GraphMeta, Schedule } from "@/lib/autogpt-server-api";
import { GraphMeta, Schedule } from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import type { ButtonAction } from "@/components/agptui/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AgentRunStatus } from "@/components/agents/agent-run-status-chip";
import { Button } from "@/components/agptui/Button";
import { Input } from "@/components/ui/input";
export default function AgentScheduleDetailsView({
agent,
graph,
schedule,
onForcedRun,
agentActions,
}: {
agent: GraphMeta;
graph: GraphMeta;
schedule: Schedule;
onForcedRun: (runID: string) => void;
agentActions: { label: string; callback: () => void }[];
agentActions: ButtonAction[];
}): React.ReactNode {
const api = useBackendAPI();
@@ -50,20 +51,20 @@ export default function AgentScheduleDetailsView({
Object.entries(schedule.input_data).map(([k, v]) => [
k,
{
title: agent.input_schema.properties[k].title,
title: graph.input_schema.properties[k].title,
/* TODO: type: agent.input_schema.properties[k].type */
value: v,
},
]),
);
}, [agent, schedule]);
}, [graph, schedule]);
const runNow = useCallback(
() =>
api
.executeGraph(agent.id, agent.version, schedule.input_data)
.executeGraph(graph.id, graph.version, schedule.input_data)
.then((run) => onForcedRun(run.graph_exec_id)),
[api, agent, schedule, onForcedRun],
[api, graph, schedule, onForcedRun],
);
const runActions: { label: string; callback: () => void }[] = useMemo(
@@ -129,7 +130,11 @@ export default function AgentScheduleDetailsView({
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Agent actions</h3>
{agentActions.map((action, i) => (
<Button key={i} variant="outline" onClick={action.callback}>
<Button
key={i}
variant={action.variant ?? "outline"}
onClick={action.callback}
>
{action.label}
</Button>
))}

View File

@@ -0,0 +1,7 @@
import type { ButtonProps } from "@/components/agptui/Button";
export type ButtonAction = {
label: string;
variant?: ButtonProps["variant"];
callback: () => void;
};

View File

@@ -17,7 +17,7 @@ export default function LibraryAgentCard({
agent: LibraryAgent;
}): React.ReactNode {
return (
<Link href={`/library/agents/${agent_id}`}>
<Link href={`/library/agents/${id}`}>
<div className="inline-flex w-full max-w-[434px] cursor-pointer flex-col items-start justify-start gap-2.5 rounded-[26px] bg-white transition-all duration-300 hover:shadow-lg dark:bg-transparent dark:hover:shadow-gray-700">
<div className="relative h-[200px] w-full overflow-hidden rounded-[20px]">
{!image_url ? (

View File

@@ -227,32 +227,36 @@ export const FlowInfo: React.FC<
</DropdownMenuContent>
</DropdownMenu>
)}
<Link
className={buttonVariants({ variant: "default" })}
href={`/build?flowID=${flow.agent_id}&flowVersion=${flow.agent_version}`}
>
<Pencil2Icon className="mr-2" />
Open in Builder
</Link>
<Button
variant="outline"
className="px-2.5"
title="Export to a JSON-file"
data-testid="export-button"
onClick={async () =>
exportAsJSONFile(
safeCopyGraph(
flowVersions!.find(
(v) => v.version == selectedFlowVersion!.version,
)!,
await api.getBlocks(),
),
`${flow.name}_v${selectedFlowVersion!.version}.json`,
)
}
>
<ExitIcon className="mr-2" /> Export
</Button>
{flow.can_access_graph && (
<Link
className={buttonVariants({ variant: "default" })}
href={`/build?flowID=${flow.agent_id}&flowVersion=${flow.agent_version}`}
>
<Pencil2Icon className="mr-2" />
Open in Builder
</Link>
)}
{flow.can_access_graph && (
<Button
variant="outline"
className="px-2.5"
title="Export to a JSON-file"
data-testid="export-button"
onClick={async () =>
exportAsJSONFile(
safeCopyGraph(
flowVersions!.find(
(v) => v.version == selectedFlowVersion!.version,
)!,
await api.getBlocks(),
),
`${flow.name}_v${selectedFlowVersion!.version}.json`,
)
}
>
<ExitIcon className="mr-2" /> Export
</Button>
)}
<Button
variant="secondary"
className="bg-purple-500 text-white hover:bg-purple-700"
@@ -263,14 +267,16 @@ export const FlowInfo: React.FC<
<PlayIcon className="mr-2" />
{isRunning ? "Stop Agent" : "Run Agent"}
</Button>
<Button
variant="destructive"
onClick={() => setIsDeleteModalOpen(true)}
data-testid="delete-button"
>
<TrashIcon className="mr-2" />
Delete Agent
</Button>
{flow.can_access_graph && (
<Button
variant="destructive"
onClick={() => setIsDeleteModalOpen(true)}
data-testid="delete-button"
>
<TrashIcon className="mr-2" />
Delete Agent
</Button>
)}
</div>
</CardHeader>
<CardContent>
@@ -303,10 +309,12 @@ export const FlowInfo: React.FC<
<Button
variant="destructive"
onClick={() => {
api.deleteGraph(flow.agent_id).then(() => {
setIsDeleteModalOpen(false);
refresh();
});
api
.updateLibraryAgent(flow.id, { is_deleted: true })
.then(() => {
setIsDeleteModalOpen(false);
refresh();
});
}}
>
Delete

View File

@@ -18,9 +18,11 @@ import {
GraphCreatable,
GraphExecution,
GraphExecutionMeta,
GraphID,
GraphMeta,
GraphUpdateable,
LibraryAgent,
LibraryAgentID,
LibraryAgentPreset,
LibraryAgentPresetResponse,
LibraryAgentResponse,
@@ -167,7 +169,7 @@ export default class BackendAPI {
}
getGraph(
id: string,
id: GraphID,
version?: number,
hide_credentials?: boolean,
): Promise<Graph> {
@@ -181,7 +183,7 @@ export default class BackendAPI {
return this._get(`/graphs/${id}`, query);
}
getGraphAllVersions(id: string): Promise<Graph[]> {
getGraphAllVersions(id: GraphID): Promise<Graph[]> {
return this._get(`/graphs/${id}/versions`);
}
@@ -191,22 +193,22 @@ export default class BackendAPI {
return this._request("POST", "/graphs", requestBody);
}
updateGraph(id: string, graph: GraphUpdateable): Promise<Graph> {
updateGraph(id: GraphID, graph: GraphUpdateable): Promise<Graph> {
return this._request("PUT", `/graphs/${id}`, graph);
}
deleteGraph(id: string): Promise<void> {
deleteGraph(id: GraphID): Promise<void> {
return this._request("DELETE", `/graphs/${id}`);
}
setGraphActiveVersion(id: string, version: number): Promise<Graph> {
setGraphActiveVersion(id: GraphID, version: number): Promise<Graph> {
return this._request("PUT", `/graphs/${id}/versions/active`, {
active_graph_version: version,
});
}
executeGraph(
id: string,
id: GraphID,
version: number,
inputData: { [key: string]: any } = {},
): Promise<{ graph_exec_id: string }> {
@@ -217,12 +219,12 @@ export default class BackendAPI {
return this._get(`/executions`);
}
getGraphExecutions(graphID: string): Promise<GraphExecutionMeta[]> {
getGraphExecutions(graphID: GraphID): Promise<GraphExecutionMeta[]> {
return this._get(`/graphs/${graphID}/executions`);
}
async getGraphExecutionInfo(
graphID: string,
graphID: GraphID,
runID: string,
): Promise<GraphExecution> {
const result = await this._get(`/graphs/${graphID}/executions/${runID}`);
@@ -233,7 +235,7 @@ export default class BackendAPI {
}
async stopGraphExecution(
graphID: string,
graphID: GraphID,
runID: string,
): Promise<GraphExecution> {
const result = await this._request(
@@ -491,6 +493,10 @@ export default class BackendAPI {
return this._get("/library/agents", params);
}
getLibraryAgent(id: LibraryAgentID): Promise<LibraryAgent> {
return this._get(`/library/agents/${id}`);
}
addMarketplaceAgentToLibrary(
storeListingVersionID: string,
): Promise<LibraryAgent> {
@@ -500,7 +506,7 @@ export default class BackendAPI {
}
async updateLibraryAgent(
libraryAgentId: string,
libraryAgentId: LibraryAgentID,
params: {
auto_update_version?: boolean;
is_favorite?: boolean;
@@ -541,7 +547,7 @@ export default class BackendAPI {
executeLibraryAgentPreset(
presetId: string,
graphId: string,
graphId: GraphID,
graphVersion: number,
nodeInput: { [key: string]: any },
): Promise<{ id: string }> {