From 5047e99fd14ffc494ba14304fb700b7e1c5fac71 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 22 Apr 2025 18:50:05 +0200 Subject: [PATCH 1/3] fix(frontend): Hide Google Maps Key ID filter (#9861) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Changes 🏗️ ![image](https://github.com/user-attachments/assets/d6b9f971-d914-4ff1-9319-a903707a2c72) Hide Google Maps system id key on the frontend UI. ### 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 --- .../src/app/(platform)/profile/(user)/integrations/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx index 5ece783f28..91f01e8c99 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx @@ -116,6 +116,7 @@ export default function PrivatePage() { "544c62b5-1d0f-4156-8fb4-9525f11656eb", // Apollo "3bcdbda3-84a3-46af-8fdb-bfd2472298b8", // SmartLead "63a6e279-2dc2-448e-bf57-85776f7176dc", // ZeroBounce + "9aa1bde0-4947-4a70-a20c-84daa3850d52", // Google Maps ], [], ); From 160a622ba41335a54c5d5e068c5f77c90b791e71 Mon Sep 17 00:00:00 2001 From: Krzysztof Czerwinski <34861343+kcze@users.noreply.github.com> Date: Wed, 23 Apr 2025 18:28:42 +0200 Subject: [PATCH 2/3] feat(platform): Forking agent in Library (#9870) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces copying agents feature in the Library. Users can copy and download their library agents but they can edit only the ones they own (included copied ones). ### Changes 🏗️ - DB migration: add relation in `AgentGraph`: `forked_from_id` and `forked_from_version` - Add `fork_graph` function that makes a hardcopy of agent graph and its nodes (all with new ids) - Add `fork_library_agent` that copies library agent and its graph for a user - Add endpoint `/library/agents/{libraryAgentId}/fork` - Add UI to `library/agents/[id]/page.tsx`: `Edit a copy` button with dialog confirmation ### 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: - [x] Agent can be copied, edited and runs --- .../backend/backend/data/graph.py | 27 ++++++++ .../backend/backend/server/v2/library/db.py | 57 ++++++++++++++++- .../server/v2/library/routes/agents.py | 11 ++++ .../migration.sql | 7 ++ autogpt_platform/backend/schema.prisma | 5 ++ .../(platform)/library/agents/[id]/page.tsx | 64 ++++++++++++++++++- .../src/lib/autogpt-server-api/client.ts | 4 ++ .../src/lib/autogpt-server-api/types.ts | 2 + 8 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 autogpt_platform/backend/migrations/20250422125822_add_forked_relation/migration.sql diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index 326483054d..ea27b14372 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -172,6 +172,8 @@ class BaseGraph(BaseDbModel): description: str nodes: list[Node] = [] links: list[Link] = [] + forked_from_id: str | None = None + forked_from_version: int | None = None @computed_field @property @@ -570,6 +572,8 @@ class GraphModel(Graph): id=graph.id, user_id=graph.userId if not for_export else "", version=graph.version, + forked_from_id=graph.forkedFromId, + forked_from_version=graph.forkedFromVersion, is_active=graph.isActive, name=graph.name or "", description=graph.description or "", @@ -847,6 +851,27 @@ async def create_graph(graph: Graph, user_id: str) -> GraphModel: raise ValueError(f"Created graph {graph.id} v{graph.version} is not in DB") +async def fork_graph(graph_id: str, graph_version: int, user_id: str) -> GraphModel: + """ + Forks a graph by copying it and all its nodes and links to a new graph. + """ + async with transaction() as tx: + graph = await get_graph(graph_id, graph_version, user_id=user_id) + if not graph: + raise ValueError(f"Graph {graph_id} v{graph_version} not found") + + # Set forked from ID and version as itself as it's about ot be copied + graph.forked_from_id = graph.id + graph.forked_from_version = graph.version + graph.name = f"{graph.name} (copy)" + graph.reassign_ids(user_id=user_id, reassign_graph_id=True) + graph.validate_graph(for_run=False) + + await __create_graph(tx, graph, user_id) + + return graph + + async def __create_graph(tx, graph: Graph, user_id: str): graphs = [graph] + graph.sub_graphs @@ -859,6 +884,8 @@ async def __create_graph(tx, graph: Graph, user_id: str): description=graph.description, isActive=graph.is_active, userId=user_id, + forkedFromId=graph.forked_from_id, + forkedFromVersion=graph.forked_from_version, ) for graph in graphs ] diff --git a/autogpt_platform/backend/backend/server/v2/library/db.py b/autogpt_platform/backend/backend/server/v2/library/db.py index 15fc41182e..378b799dd2 100644 --- a/autogpt_platform/backend/backend/server/v2/library/db.py +++ b/autogpt_platform/backend/backend/server/v2/library/db.py @@ -13,12 +13,17 @@ import backend.server.v2.library.model as library_model import backend.server.v2.store.exceptions as store_exceptions import backend.server.v2.store.image_gen as store_image_gen import backend.server.v2.store.media as store_media +from backend.data import db +from backend.data import graph as graph_db from backend.data.db import locked_transaction from backend.data.includes import library_agent_include +from backend.integrations.creds_manager import IntegrationCredentialsManager +from backend.integrations.webhooks.graph_lifecycle_hooks import on_graph_activate from backend.util.settings import Config logger = logging.getLogger(__name__) config = Config() +integration_creds_manager = IntegrationCredentialsManager() async def list_library_agents( @@ -206,7 +211,7 @@ async def add_generated_agent_image( async def create_library_agent( graph: backend.data.graph.GraphModel, user_id: str, -) -> prisma.models.LibraryAgent: +) -> library_model.LibraryAgent: """ Adds an agent to the user's library (LibraryAgent table). @@ -227,7 +232,7 @@ async def create_library_agent( ) try: - return await prisma.models.LibraryAgent.prisma().create( + agent = await prisma.models.LibraryAgent.prisma().create( data=prisma.types.LibraryAgentCreateInput( isCreatedByUser=(user_id == graph.user_id), useGraphIsActiveVersion=True, @@ -238,8 +243,10 @@ async def create_library_agent( "graphVersionId": {"id": graph.id, "version": graph.version} } }, - ) + ), + include={"AgentGraph": True}, ) + return library_model.LibraryAgent.from_db(agent) except prisma.errors.PrismaError as e: logger.error(f"Database error creating agent in library: {e}") raise store_exceptions.DatabaseError("Failed to create agent in library") from e @@ -662,3 +669,47 @@ async def delete_preset(user_id: str, preset_id: str) -> None: except prisma.errors.PrismaError as e: logger.error(f"Database error deleting preset: {e}") raise store_exceptions.DatabaseError("Failed to delete preset") from e + + +async def fork_library_agent(library_agent_id: str, user_id: str): + """ + Clones a library agent and its underyling graph and nodes (with new ids) for the given user. + + Args: + library_agent_id: The ID of the library agent to fork. + user_id: The ID of the user who owns the library agent. + + Returns: + The forked LibraryAgent. + + Raises: + DatabaseError: If there's an error during the forking process. + """ + logger.debug(f"Forking library agent {library_agent_id} for user {user_id}") + try: + async with db.locked_transaction(f"usr_trx_{user_id}-fork_agent"): + # Fetch the original agent + original_agent = await get_library_agent(library_agent_id, user_id) + + # Check if user owns the library agent + # TODO: once we have open/closed sourced agents this needs to be enabled ~kcze + # + update library/agents/[id]/page.tsx agent actions + # if not original_agent.can_access_graph: + # raise store_exceptions.DatabaseError( + # f"User {user_id} cannot access library agent graph {library_agent_id}" + # ) + + # Fork the underlying graph and nodes + new_graph = await graph_db.fork_graph( + original_agent.graph_id, original_agent.graph_version, user_id + ) + new_graph = await on_graph_activate( + new_graph, + get_credentials=lambda id: integration_creds_manager.get(user_id, id), + ) + + # Create a library agent for the new graph + return await create_library_agent(new_graph, user_id) + except prisma.errors.PrismaError as e: + logger.error(f"Database error cloning library agent: {e}") + raise store_exceptions.DatabaseError("Failed to fork library agent") from e 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 2db5024388..3c1332ece4 100644 --- a/autogpt_platform/backend/backend/server/v2/library/routes/agents.py +++ b/autogpt_platform/backend/backend/server/v2/library/routes/agents.py @@ -190,3 +190,14 @@ async def update_library_agent( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update library agent", ) from e + + +@router.post("/{library_agent_id}/fork") +async def fork_library_agent( + library_agent_id: str, + user_id: str = Depends(autogpt_auth_lib.depends.get_user_id), +) -> library_model.LibraryAgent: + return await library_db.fork_library_agent( + library_agent_id=library_agent_id, + user_id=user_id, + ) diff --git a/autogpt_platform/backend/migrations/20250422125822_add_forked_relation/migration.sql b/autogpt_platform/backend/migrations/20250422125822_add_forked_relation/migration.sql new file mode 100644 index 0000000000..cc3e1256bb --- /dev/null +++ b/autogpt_platform/backend/migrations/20250422125822_add_forked_relation/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "AgentGraph" + ADD COLUMN "forkedFromId" TEXT, + ADD COLUMN "forkedFromVersion" INTEGER; + +-- AddForeignKey +ALTER TABLE "AgentGraph" ADD CONSTRAINT "AgentGraph_forkedFromId_forkedFromVersion_fkey" FOREIGN KEY ("forkedFromId", "forkedFromVersion") REFERENCES "AgentGraph"("id", "version") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/autogpt_platform/backend/schema.prisma b/autogpt_platform/backend/schema.prisma index f281c43d03..7f6c3d6ef4 100644 --- a/autogpt_platform/backend/schema.prisma +++ b/autogpt_platform/backend/schema.prisma @@ -118,6 +118,11 @@ model AgentGraph { // This allows us to delete user data with deleting the agent which maybe in use by other users User User @relation(fields: [userId], references: [id], onDelete: Cascade) + forkedFromId String? + forkedFromVersion Int? + forkedFrom AgentGraph? @relation("AgentGraphForks", fields: [forkedFromId, forkedFromVersion], references: [id, version]) + forks AgentGraph[] @relation("AgentGraphForks") + Nodes AgentNode[] Executions AgentGraphExecution[] diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx index f6df916a44..38bdd0ed02 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx @@ -23,6 +23,16 @@ 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 { useOnboarding } from "@/components/onboarding/onboarding-provider"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/components/ui/use-toast"; export default function AgentRunsPage(): React.ReactElement { const { id: agentID }: { id: LibraryAgentID } = useParams(); @@ -51,6 +61,8 @@ export default function AgentRunsPage(): React.ReactElement { const [confirmingDeleteAgentRun, setConfirmingDeleteAgentRun] = useState(null); const { state, updateState } = useOnboarding(); + const [copyAgentDialogOpen, setCopyAgentDialogOpen] = useState(false); + const { toast } = useToast(); const openRunDraftView = useCallback(() => { selectView({ type: "run" }); @@ -237,6 +249,23 @@ export default function AgentRunsPage(): React.ReactElement { [api, agent], ); + const copyAgent = useCallback(async () => { + setCopyAgentDialogOpen(false); + api + .forkLibraryAgent(agentID) + .then((newAgent) => { + router.push(`/library/agents/${newAgent.id}`); + }) + .catch((error) => { + console.error("Error copying agent:", error); + toast({ + title: "Error copying agent", + description: `An error occurred while copying the agent: ${error.message}`, + variant: "destructive", + }); + }); + }, [agentID, api, router, toast]); + const agentActions: ButtonAction[] = useMemo( () => [ ...(agent?.can_access_graph @@ -245,9 +274,13 @@ export default function AgentRunsPage(): React.ReactElement { label: "Open graph in builder", href: `/build?flowID=${agent.graph_id}&flowVersion=${agent.graph_version}`, }, - { label: "Export agent to file", callback: downloadGraph }, ] : []), + { label: "Export agent to file", callback: downloadGraph }, + { + label: "Edit a copy", + callback: () => setCopyAgentDialogOpen(true), + }, { label: "Delete agent", variant: "destructive", @@ -339,6 +372,35 @@ export default function AgentRunsPage(): React.ReactElement { confirmingDeleteAgentRun && deleteRun(confirmingDeleteAgentRun) } /> + {/* Copy agent confirmation dialog */} + + + + You're making an editable copy + + We'll save a new version of this agent to your library so + you can customize it however you'd like. You'll still + have the original from the marketplace too — it won't be + changed and can't be edited. + + + + + + + + ); diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts index 871d84bfd4..77046a64ed 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts @@ -600,6 +600,10 @@ export default class BackendAPI { await this._request("PUT", `/library/agents/${libraryAgentId}`, params); } + forkLibraryAgent(libraryAgentId: LibraryAgentID): Promise { + return this._request("POST", `/library/agents/${libraryAgentId}/fork`); + } + listLibraryAgentPresets(params?: { page?: number; page_size?: number; diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts index 000515e996..9a1abfa97d 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts @@ -280,6 +280,8 @@ export type GraphMeta = { is_active: boolean; name: string; description: string; + forked_from_id?: GraphID | null; + forked_from_version?: number | null; input_schema: GraphIOSchema; output_schema: GraphIOSchema; credentials_input_schema: { From 1e3236a04129c407c51356d139de0e548da42437 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Wed, 23 Apr 2025 23:24:17 +0200 Subject: [PATCH 3/3] feat(backend): Add retry on executor process initialization (#9865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Executor process initialization can fail and cause this error: ``` concurrent.futures.process.BrokenProcessPool: A child process terminated abruptly, the process pool is not usable anymore ``` ### Changes 🏗️ Add retry to reduce the chance of the initialization error to happen. ### 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: - [x] Existing tests --- autogpt_platform/backend/backend/data/credit.py | 8 ++------ autogpt_platform/backend/backend/executor/manager.py | 3 +++ autogpt_platform/backend/backend/util/retry.py | 7 +++++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/autogpt_platform/backend/backend/data/credit.py b/autogpt_platform/backend/backend/data/credit.py index b106efc3ac..d829f1a1e2 100644 --- a/autogpt_platform/backend/backend/data/credit.py +++ b/autogpt_platform/backend/backend/data/credit.py @@ -20,7 +20,6 @@ from prisma.types import ( CreditTransactionCreateInput, CreditTransactionWhereInput, ) -from tenacity import retry, stop_after_attempt, wait_exponential from backend.data import db from backend.data.block_cost_config import BLOCK_COSTS @@ -36,6 +35,7 @@ from backend.data.user import get_user_by_id from backend.executor.utils import UsageTransactionMetadata from backend.notifications import NotificationManager from backend.util.exceptions import InsufficientBalanceError +from backend.util.retry import func_retry from backend.util.service import get_service_client from backend.util.settings import Settings @@ -262,11 +262,7 @@ class UserCreditBase(ABC): ) return transaction_balance, transaction_time - @retry( - stop=stop_after_attempt(5), - wait=wait_exponential(multiplier=1, min=1, max=10), - reraise=True, - ) + @func_retry async def _enable_transaction( self, transaction_key: str, diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py index 3a622997b1..6ab48a385f 100644 --- a/autogpt_platform/backend/backend/executor/manager.py +++ b/autogpt_platform/backend/backend/executor/manager.py @@ -61,6 +61,7 @@ from backend.util.decorator import error_logged, time_measured from backend.util.file import clean_exec_files from backend.util.logging import configure_logging from backend.util.process import AppProcess, set_service_name +from backend.util.retry import func_retry from backend.util.service import close_service_client, get_service_client from backend.util.settings import Settings @@ -422,6 +423,7 @@ class Executor: """ @classmethod + @func_retry def on_node_executor_start(cls): configure_logging() set_service_name("NodeExecutor") @@ -527,6 +529,7 @@ class Executor: stats.error = e @classmethod + @func_retry def on_graph_executor_start(cls): configure_logging() set_service_name("GraphExecutor") diff --git a/autogpt_platform/backend/backend/util/retry.py b/autogpt_platform/backend/backend/util/retry.py index c345800e31..01fc69d7df 100644 --- a/autogpt_platform/backend/backend/util/retry.py +++ b/autogpt_platform/backend/backend/util/retry.py @@ -73,3 +73,10 @@ def conn_retry( return async_wrapper if is_coroutine else sync_wrapper return decorator + + +func_retry = retry( + reraise=False, + stop=stop_after_attempt(5), + wait=wait_exponential(multiplier=1, min=1, max=30), +)