diff --git a/autogpt_platform/backend/backend/server/v2/library/db.py b/autogpt_platform/backend/backend/server/v2/library/db.py index a2dd0062e2..f8128f8d0e 100644 --- a/autogpt_platform/backend/backend/server/v2/library/db.py +++ b/autogpt_platform/backend/backend/server/v2/library/db.py @@ -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)}") diff --git a/autogpt_platform/backend/backend/server/v2/library/db_test.py b/autogpt_platform/backend/backend/server/v2/library/db_test.py index 26bc0ff42b..b5920df2a5 100644 --- a/autogpt_platform/backend/backend/server/v2/library/db_test.py +++ b/autogpt_platform/backend/backend/server/v2/library/db_test.py @@ -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 diff --git a/autogpt_platform/backend/backend/server/v2/library/model.py b/autogpt_platform/backend/backend/server/v2/library/model.py index 44a1fc3f1c..548ecfa79c 100644 --- a/autogpt_platform/backend/backend/server/v2/library/model.py +++ b/autogpt_platform/backend/backend/server/v2/library/model.py @@ -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") diff --git a/autogpt_platform/backend/backend/server/v2/library/routes/agents.py b/autogpt_platform/backend/backend/server/v2/library/routes/agents.py index f297374e1d..1570219298 100644 --- a/autogpt_platform/backend/backend/server/v2/library/routes/agents.py +++ b/autogpt_platform/backend/backend/server/v2/library/routes/agents.py @@ -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, diff --git a/autogpt_platform/frontend/src/app/library/agents/[id]/page.tsx b/autogpt_platform/frontend/src/app/library/agents/[id]/page.tsx index 5435b88a5c..c4f246eda3 100644 --- a/autogpt_platform/frontend/src/app/library/agents/[id]/page.tsx +++ b/autogpt_platform/frontend/src/app/library/agents/[id]/page.tsx @@ -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(null); + const [graph, setGraph] = useState(null); + const [agent, setAgent] = useState(null); const [agentRuns, setAgentRuns] = useState([]); const [schedules, setSchedules] = useState([]); const [selectedView, selectView] = useState<{ @@ -35,6 +39,8 @@ export default function AgentRunsPage(): React.ReactElement { null, ); const [isFirstLoad, setIsFirstLoad] = useState(true); + const [agentDeleteDialogOpen, setAgentDeleteDialogOpen] = + useState(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 Loading...; } @@ -156,27 +177,38 @@ export default function AgentRunsPage(): React.ReactElement { {(selectedView.type == "run" && selectedView.id ? ( selectedRun && ( ) ) : selectedView.type == "run" ? ( selectRun(runID)} agentActions={agentActions} /> ) : selectedView.type == "schedule" ? ( selectedSchedule && ( selectRun(runID)} agentActions={agentActions} /> ) ) : null) ||

Loading...

} + + + agent && + api + .updateLibraryAgent(agent.id, { is_deleted: true }) + .then(() => router.push("/library")) + } + /> ); diff --git a/autogpt_platform/frontend/src/components/agents/agent-delete-confirm-dialog.tsx b/autogpt_platform/frontend/src/components/agents/agent-delete-confirm-dialog.tsx new file mode 100644 index 0000000000..c8934e06eb --- /dev/null +++ b/autogpt_platform/frontend/src/components/agents/agent-delete-confirm-dialog.tsx @@ -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 ( + + + + Delete Agent + + Are you sure you want to delete this agent?
+ This action cannot be undone. +
+
+ + + + +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/components/agents/agent-run-details-view.tsx b/autogpt_platform/frontend/src/components/agents/agent-run-details-view.tsx index 9772bf4bc9..c6df19ce90 100644 --- a/autogpt_platform/frontend/src/components/agents/agent-run-details-view.tsx +++ b/autogpt_platform/frontend/src/components/agents/agent-run-details-view.tsx @@ -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({

Agent actions

{agentActions.map((action, i) => ( - ))} diff --git a/autogpt_platform/frontend/src/components/agents/agent-run-draft-view.tsx b/autogpt_platform/frontend/src/components/agents/agent-run-draft-view.tsx index 8727d23056..a4f096780f 100644 --- a/autogpt_platform/frontend/src/components/agents/agent-run-draft-view.tsx +++ b/autogpt_platform/frontend/src/components/agents/agent-run-draft-view.tsx @@ -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>({}); 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({

Agent actions

{agentActions.map((action, i) => ( - ))} diff --git a/autogpt_platform/frontend/src/components/agents/agent-runs-selector-list.tsx b/autogpt_platform/frontend/src/components/agents/agent-runs-selector-list.tsx index b452c26114..783a31e945 100644 --- a/autogpt_platform/frontend/src/components/agents/agent-runs-selector-list.tsx +++ b/autogpt_platform/frontend/src/components/agents/agent-runs-selector-list.tsx @@ -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({ > Scheduled - {schedules.filter((s) => s.graph_id === agent.id).length} + {schedules.filter((s) => s.graph_id === agent.agent_id).length}
@@ -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) => ( 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({

Agent actions

{agentActions.map((action, i) => ( - ))} diff --git a/autogpt_platform/frontend/src/components/agptui/types.ts b/autogpt_platform/frontend/src/components/agptui/types.ts new file mode 100644 index 0000000000..b02e0aa5ec --- /dev/null +++ b/autogpt_platform/frontend/src/components/agptui/types.ts @@ -0,0 +1,7 @@ +import type { ButtonProps } from "@/components/agptui/Button"; + +export type ButtonAction = { + label: string; + variant?: ButtonProps["variant"]; + callback: () => void; +}; diff --git a/autogpt_platform/frontend/src/components/library/library-agent-card.tsx b/autogpt_platform/frontend/src/components/library/library-agent-card.tsx index c31dd63ca4..199d2f9b21 100644 --- a/autogpt_platform/frontend/src/components/library/library-agent-card.tsx +++ b/autogpt_platform/frontend/src/components/library/library-agent-card.tsx @@ -17,7 +17,7 @@ export default function LibraryAgentCard({ agent: LibraryAgent; }): React.ReactNode { return ( - +
{!image_url ? ( diff --git a/autogpt_platform/frontend/src/components/monitor/FlowInfo.tsx b/autogpt_platform/frontend/src/components/monitor/FlowInfo.tsx index 9004cd033f..ad7c463ca0 100644 --- a/autogpt_platform/frontend/src/components/monitor/FlowInfo.tsx +++ b/autogpt_platform/frontend/src/components/monitor/FlowInfo.tsx @@ -227,32 +227,36 @@ export const FlowInfo: React.FC< )} - - - Open in Builder - - + {flow.can_access_graph && ( + + + Open in Builder + + )} + {flow.can_access_graph && ( + + )} - + {flow.can_access_graph && ( + + )}
@@ -303,10 +309,12 @@ export const FlowInfo: React.FC<