From ee14e54e800a82aca336bbaa4eab209db1073761 Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Tue, 17 Feb 2026 12:03:53 +0530 Subject: [PATCH] feat(api): add FolderAlreadyExistsError exception handling - Introduced `FolderAlreadyExistsError` to handle cases where a folder with the same name already exists, improving error management in folder operations. - Updated the REST API to include this new exception in the error handling mechanism, providing clearer responses for folder-related requests. - Refactored import statements for better organization and clarity in the codebase. - Enhanced the `list_library_agents` and `list_favorite_library_agents` functions by simplifying the error handling logic and improving readability. --- .../backend/api/features/library/db.py | 1440 ++++++++--------- .../api/features/library/exceptions.py | 6 + .../backend/api/features/library/model.py | 2 +- .../backend/backend/api/rest_api.py | 6 +- 4 files changed, 659 insertions(+), 795 deletions(-) diff --git a/autogpt_platform/backend/backend/api/features/library/db.py b/autogpt_platform/backend/backend/api/features/library/db.py index ff1ae82c08..1c157589ec 100644 --- a/autogpt_platform/backend/backend/api/features/library/db.py +++ b/autogpt_platform/backend/backend/api/features/library/db.py @@ -1,4 +1,5 @@ import asyncio +import itertools import logging from typing import Literal, Optional @@ -12,7 +13,10 @@ import backend.api.features.store.image_gen as store_image_gen import backend.api.features.store.media as store_media import backend.data.graph as graph_db import backend.data.integrations as integrations_db -from backend.api.features.library.exceptions import FolderValidationError +from backend.api.features.library.exceptions import ( + FolderAlreadyExistsError, + FolderValidationError, +) from backend.data.db import transaction from backend.data.execution import get_graph_execution from backend.data.graph import GraphSettings @@ -24,7 +28,7 @@ from backend.integrations.webhooks.graph_lifecycle_hooks import ( on_graph_deactivate, ) from backend.util.clients import get_scheduler_client -from backend.util.exceptions import DatabaseError, InvalidInputError, NotFoundError +from backend.util.exceptions import InvalidInputError, NotFoundError from backend.util.json import SafeJson from backend.util.models import Pagination from backend.util.settings import Config @@ -117,52 +121,41 @@ async def list_library_agents( elif sort_by == library_model.LibraryAgentSort.UPDATED_AT: order_by = {"updatedAt": "desc"} - try: - library_agents = await prisma.models.LibraryAgent.prisma().find_many( - where=where_clause, - include=library_agent_include( - user_id, include_nodes=False, include_executions=include_executions - ), - order=order_by, - skip=(page - 1) * page_size, - take=page_size, - ) - agent_count = await prisma.models.LibraryAgent.prisma().count( - where=where_clause - ) + library_agents = await prisma.models.LibraryAgent.prisma().find_many( + where=where_clause, + include=library_agent_include( + user_id, include_nodes=False, include_executions=include_executions + ), + order=order_by, + skip=(page - 1) * page_size, + take=page_size, + ) + agent_count = await prisma.models.LibraryAgent.prisma().count(where=where_clause) - logger.debug( - f"Retrieved {len(library_agents)} library agents for user #{user_id}" - ) + logger.debug(f"Retrieved {len(library_agents)} library agents for user #{user_id}") - # Only pass valid agents to the response - valid_library_agents: list[library_model.LibraryAgent] = [] + # Only pass valid agents to the response + valid_library_agents: list[library_model.LibraryAgent] = [] - for agent in library_agents: - try: - library_agent = library_model.LibraryAgent.from_db(agent) - valid_library_agents.append(library_agent) - except Exception as e: - # Skip this agent if there was an error - logger.error( - f"Error parsing LibraryAgent #{agent.id} from DB item: {e}" - ) - continue + for agent in library_agents: + try: + library_agent = library_model.LibraryAgent.from_db(agent) + valid_library_agents.append(library_agent) + except Exception as e: + # Skip this agent if there was an error + logger.error(f"Error parsing LibraryAgent #{agent.id} from DB item: {e}") + continue - # Return the response with only valid agents - return library_model.LibraryAgentResponse( - agents=valid_library_agents, - pagination=Pagination( - total_items=agent_count, - total_pages=(agent_count + page_size - 1) // page_size, - current_page=page, - page_size=page_size, - ), - ) - - except prisma.errors.PrismaError as e: - logger.error(f"Database error fetching library agents: {e}") - raise DatabaseError("Failed to fetch library agents") from e + # Return the response with only valid agents + return library_model.LibraryAgentResponse( + agents=valid_library_agents, + pagination=Pagination( + total_items=agent_count, + total_pages=(agent_count + page_size - 1) // page_size, + current_page=page, + page_size=page_size, + ), + ) async def list_favorite_library_agents( @@ -203,52 +196,43 @@ async def list_favorite_library_agents( # Sort favorites by updated date descending order_by: prisma.types.LibraryAgentOrderByInput = {"updatedAt": "desc"} - try: - library_agents = await prisma.models.LibraryAgent.prisma().find_many( - where=where_clause, - include=library_agent_include( - user_id, include_nodes=False, include_executions=False - ), - order=order_by, - skip=(page - 1) * page_size, - take=page_size, - ) - agent_count = await prisma.models.LibraryAgent.prisma().count( - where=where_clause - ) + library_agents = await prisma.models.LibraryAgent.prisma().find_many( + where=where_clause, + include=library_agent_include( + user_id, include_nodes=False, include_executions=False + ), + order=order_by, + skip=(page - 1) * page_size, + take=page_size, + ) + agent_count = await prisma.models.LibraryAgent.prisma().count(where=where_clause) - logger.debug( - f"Retrieved {len(library_agents)} favorite library agents for user #{user_id}" - ) + logger.debug( + f"Retrieved {len(library_agents)} favorite library agents for user #{user_id}" + ) - # Only pass valid agents to the response - valid_library_agents: list[library_model.LibraryAgent] = [] + # Only pass valid agents to the response + valid_library_agents: list[library_model.LibraryAgent] = [] - for agent in library_agents: - try: - library_agent = library_model.LibraryAgent.from_db(agent) - valid_library_agents.append(library_agent) - except Exception as e: - # Skip this agent if there was an error - logger.error( - f"Error parsing LibraryAgent #{agent.id} from DB item: {e}" - ) - continue + for agent in library_agents: + try: + library_agent = library_model.LibraryAgent.from_db(agent) + valid_library_agents.append(library_agent) + except Exception as e: + # Skip this agent if there was an error + logger.error(f"Error parsing LibraryAgent #{agent.id} from DB item: {e}") + continue - # Return the response with only valid agents - return library_model.LibraryAgentResponse( - agents=valid_library_agents, - pagination=Pagination( - total_items=agent_count, - total_pages=(agent_count + page_size - 1) // page_size, - current_page=page, - page_size=page_size, - ), - ) - - except prisma.errors.PrismaError as e: - logger.error(f"Database error fetching favorite library agents: {e}") - raise DatabaseError("Failed to fetch favorite library agents") from e + # Return the response with only valid agents + return library_model.LibraryAgentResponse( + agents=valid_library_agents, + pagination=Pagination( + total_items=agent_count, + total_pages=(agent_count + page_size - 1) // page_size, + current_page=page, + page_size=page_size, + ), + ) async def get_library_agent(id: str, user_id: str) -> library_model.LibraryAgent: @@ -266,57 +250,48 @@ async def get_library_agent(id: str, user_id: str) -> library_model.LibraryAgent 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( + library_agent = await prisma.models.LibraryAgent.prisma().find_first( + where={ + "id": id, + "userId": user_id, + "isDeleted": False, + }, + include=library_agent_include(user_id), + ) + + if not library_agent: + raise NotFoundError(f"Library agent #{id} not found") + + # Fetch marketplace listing if the agent has been published + store_listing = None + profile = None + if library_agent.AgentGraph: + store_listing = await prisma.models.StoreListing.prisma().find_first( where={ - "id": id, - "userId": user_id, + "agentGraphId": library_agent.AgentGraph.id, "isDeleted": False, + "hasApprovedVersion": True, + }, + include={ + "ActiveVersion": True, }, - include=library_agent_include(user_id), ) - - if not library_agent: - raise NotFoundError(f"Library agent #{id} not found") - - # Fetch marketplace listing if the agent has been published - store_listing = None - profile = None - if library_agent.AgentGraph: - store_listing = await prisma.models.StoreListing.prisma().find_first( - where={ - "agentGraphId": library_agent.AgentGraph.id, - "isDeleted": False, - "hasApprovedVersion": True, - }, - include={ - "ActiveVersion": True, - }, + if store_listing and store_listing.ActiveVersion and store_listing.owningUserId: + # Fetch Profile separately since User doesn't have a direct Profile relation + profile = await prisma.models.Profile.prisma().find_first( + where={"userId": store_listing.owningUserId} ) - if ( - store_listing - and store_listing.ActiveVersion - and store_listing.owningUserId - ): - # Fetch Profile separately since User doesn't have a direct Profile relation - profile = await prisma.models.Profile.prisma().find_first( - where={"userId": store_listing.owningUserId} - ) - return library_model.LibraryAgent.from_db( - library_agent, - sub_graphs=( - await graph_db.get_sub_graphs(library_agent.AgentGraph) - if library_agent.AgentGraph - else None - ), - store_listing=store_listing, - profile=profile, - ) - - except prisma.errors.PrismaError as e: - logger.error(f"Database error fetching library agent: {e}") - raise DatabaseError("Failed to fetch library agent") from e + return library_model.LibraryAgent.from_db( + library_agent, + sub_graphs=( + await graph_db.get_sub_graphs(library_agent.AgentGraph) + if library_agent.AgentGraph + else None + ), + store_listing=store_listing, + profile=profile, + ) async def get_library_agent_by_store_version_id( @@ -359,29 +334,25 @@ async def get_library_agent_by_graph_id( graph_id: str, graph_version: Optional[int] = None, ) -> library_model.LibraryAgent | None: - try: - filter: prisma.types.LibraryAgentWhereInput = { - "agentGraphId": graph_id, - "userId": user_id, - "isDeleted": False, - } - if graph_version is not None: - filter["agentGraphVersion"] = graph_version + filter: prisma.types.LibraryAgentWhereInput = { + "agentGraphId": graph_id, + "userId": user_id, + "isDeleted": False, + } + if graph_version is not None: + filter["agentGraphVersion"] = graph_version - agent = await prisma.models.LibraryAgent.prisma().find_first( - where=filter, - include=library_agent_include(user_id), - ) - if not agent: - return None + agent = await prisma.models.LibraryAgent.prisma().find_first( + where=filter, + include=library_agent_include(user_id), + ) + if not agent: + return None - assert agent.AgentGraph # make type checker happy - # Include sub-graphs so we can make a full credentials input schema - sub_graphs = await graph_db.get_sub_graphs(agent.AgentGraph) - return library_model.LibraryAgent.from_db(agent, sub_graphs=sub_graphs) - except prisma.errors.PrismaError as e: - logger.error(f"Database error fetching library agent by graph ID: {e}") - raise DatabaseError("Failed to fetch library agent") from e + assert agent.AgentGraph # make type checker happy + # Include sub-graphs so we can make a full credentials input schema + sub_graphs = await graph_db.get_sub_graphs(agent.AgentGraph) + return library_model.LibraryAgent.from_db(agent, sub_graphs=sub_graphs) async def add_generated_agent_image( @@ -659,7 +630,7 @@ async def update_library_agent( is_favorite: Whether this agent is marked as a favorite. is_archived: Whether this agent is archived. settings: User-specific settings for this library agent. - folder_id: Folder ID to move agent to (empty string "" for root, None to skip). + folder_id: Folder ID to move agent to (None to skip). Returns: The updated LibraryAgent. @@ -695,52 +666,34 @@ async def update_library_agent( merged_settings = {**current_settings_dict, **new_settings} update_fields["settings"] = SafeJson(merged_settings) if folder_id is not None: - # Empty string means "move to root" (no folder) - if folder_id == "": - update_fields["folderId"] = None - else: - # Verify folder belongs to user - folder = await prisma.models.LibraryFolder.prisma().find_first( - where={ - "id": folder_id, - "userId": user_id, - "isDeleted": False, - } - ) - if not folder: - raise NotFoundError(f"Folder #{folder_id} not found") - update_fields["folderId"] = folder_id + update_fields["folderId"] = folder_id - try: - # If graph_version is provided, update to that specific version - if graph_version is not None: - # Get the current agent to find its graph_id - agent = await get_library_agent(id=library_agent_id, user_id=user_id) - # Update to the specified version using existing function - return await update_agent_version_in_library( - user_id=user_id, - agent_graph_id=agent.graph_id, - agent_graph_version=graph_version, - ) - - # Otherwise, just update the simple fields - if not update_fields: - raise ValueError("No values were passed to update") - - n_updated = await prisma.models.LibraryAgent.prisma().update_many( - where={"id": library_agent_id, "userId": user_id}, - data=update_fields, - ) - if n_updated < 1: - raise NotFoundError(f"Library agent {library_agent_id} not found") - - return await get_library_agent( - id=library_agent_id, + # If graph_version is provided, update to that specific version + if graph_version is not None: + # Get the current agent to find its graph_id + agent = await get_library_agent(id=library_agent_id, user_id=user_id) + # Update to the specified version using existing function + return await update_agent_version_in_library( user_id=user_id, + agent_graph_id=agent.graph_id, + agent_graph_version=graph_version, ) - except prisma.errors.PrismaError as e: - logger.error(f"Database error updating library agent: {str(e)}") - raise DatabaseError("Failed to update library agent") from e + + # Otherwise, just update the simple fields + if not update_fields: + raise ValueError("No values were passed to update") + + n_updated = await prisma.models.LibraryAgent.prisma().update_many( + where={"id": library_agent_id, "userId": user_id}, + data=update_fields, + ) + if n_updated < 1: + raise NotFoundError(f"Library agent {library_agent_id} not found") + + return await get_library_agent( + id=library_agent_id, + user_id=user_id, + ) async def delete_library_agent( @@ -832,13 +785,9 @@ async def delete_library_agent_by_graph_id(graph_id: str, user_id: str) -> None: """ Deletes a library agent for the given user """ - try: - await prisma.models.LibraryAgent.prisma().delete_many( - where={"agentGraphId": graph_id, "userId": user_id} - ) - except prisma.errors.PrismaError as e: - logger.error(f"Database error deleting library agent: {e}") - raise DatabaseError("Failed to delete library agent") from e + await prisma.models.LibraryAgent.prisma().delete_many( + where={"agentGraphId": graph_id, "userId": user_id} + ) async def add_store_agent_to_library( @@ -863,89 +812,78 @@ async def add_store_agent_to_library( f"to library for user #{user_id}" ) - try: - store_listing_version = ( - await prisma.models.StoreListingVersion.prisma().find_unique( - where={"id": store_listing_version_id}, include={"AgentGraph": True} - ) + store_listing_version = ( + await prisma.models.StoreListingVersion.prisma().find_unique( + where={"id": store_listing_version_id}, include={"AgentGraph": True} ) - if not store_listing_version or not store_listing_version.AgentGraph: - logger.warning( - f"Store listing version not found: {store_listing_version_id}" - ) - raise store_exceptions.AgentNotFoundError( - f"Store listing version {store_listing_version_id} not found or invalid" - ) - - graph = store_listing_version.AgentGraph - - # Convert to GraphModel to check for HITL blocks - graph_model = await graph_db.get_graph( - graph_id=graph.id, - version=graph.version, - user_id=user_id, - include_subgraphs=False, + ) + if not store_listing_version or not store_listing_version.AgentGraph: + logger.warning(f"Store listing version not found: {store_listing_version_id}") + raise store_exceptions.AgentNotFoundError( + f"Store listing version {store_listing_version_id} not found or invalid" ) - if not graph_model: - raise store_exceptions.AgentNotFoundError( - f"Graph #{graph.id} v{graph.version} not found or accessible" - ) - # Check if user already has this agent - existing_library_agent = await prisma.models.LibraryAgent.prisma().find_unique( - where={ - "userId_agentGraphId_agentGraphVersion": { - "userId": user_id, - "agentGraphId": graph.id, - "agentGraphVersion": graph.version, + graph = store_listing_version.AgentGraph + + # Convert to GraphModel to check for HITL blocks + graph_model = await graph_db.get_graph( + graph_id=graph.id, + version=graph.version, + user_id=user_id, + include_subgraphs=False, + ) + if not graph_model: + raise store_exceptions.AgentNotFoundError( + f"Graph #{graph.id} v{graph.version} not found or accessible" + ) + + # Check if user already has this agent + existing_library_agent = await prisma.models.LibraryAgent.prisma().find_unique( + where={ + "userId_agentGraphId_agentGraphVersion": { + "userId": user_id, + "agentGraphId": graph.id, + "agentGraphVersion": graph.version, + } + }, + include={"AgentGraph": True}, + ) + if existing_library_agent: + if existing_library_agent.isDeleted: + # Even if agent exists it needs to be marked as not deleted + await update_library_agent( + existing_library_agent.id, user_id, is_deleted=False + ) + else: + logger.debug( + f"User #{user_id} already has graph #{graph.id} " + f"v{graph.version} in their library" + ) + return library_model.LibraryAgent.from_db(existing_library_agent) + + # Create LibraryAgent entry + added_agent = await prisma.models.LibraryAgent.prisma().create( + data={ + "User": {"connect": {"id": user_id}}, + "AgentGraph": { + "connect": { + "graphVersionId": {"id": graph.id, "version": graph.version} } }, - include={"AgentGraph": True}, - ) - if existing_library_agent: - if existing_library_agent.isDeleted: - # Even if agent exists it needs to be marked as not deleted - await update_library_agent( - existing_library_agent.id, user_id, is_deleted=False - ) - else: - logger.debug( - f"User #{user_id} already has graph #{graph.id} " - f"v{graph.version} in their library" - ) - return library_model.LibraryAgent.from_db(existing_library_agent) - - # Create LibraryAgent entry - added_agent = await prisma.models.LibraryAgent.prisma().create( - data={ - "User": {"connect": {"id": user_id}}, - "AgentGraph": { - "connect": { - "graphVersionId": {"id": graph.id, "version": graph.version} - } - }, - "isCreatedByUser": False, - "useGraphIsActiveVersion": False, - "settings": SafeJson( - GraphSettings.from_graph(graph_model).model_dump() - ), - }, - include=library_agent_include( - user_id, include_nodes=False, include_executions=False - ), - ) - logger.debug( - f"Added graph #{graph.id} v{graph.version}" - f"for store listing version #{store_listing_version.id} " - f"to library for user #{user_id}" - ) - return library_model.LibraryAgent.from_db(added_agent) - except store_exceptions.AgentNotFoundError: - # Reraise for external handling. - raise - except prisma.errors.PrismaError as e: - logger.error(f"Database error adding agent to library: {e}") - raise DatabaseError("Failed to add agent to library") from e + "isCreatedByUser": False, + "useGraphIsActiveVersion": False, + "settings": SafeJson(GraphSettings.from_graph(graph_model).model_dump()), + }, + include=library_agent_include( + user_id, include_nodes=False, include_executions=False + ), + ) + logger.debug( + f"Added graph #{graph.id} v{graph.version}" + f"for store listing version #{store_listing_version.id} " + f"to library for user #{user_id}" + ) + return library_model.LibraryAgent.from_db(added_agent) ############################################## @@ -953,6 +891,43 @@ async def add_store_agent_to_library( ############################################## +async def _fetch_user_folders( + user_id: str, + extra_where: Optional[prisma.types.LibraryFolderWhereInput] = None, + include_counts: bool = True, +) -> list[prisma.models.LibraryFolder]: + """ + Shared helper to fetch folders for a user with consistent query params. + + Args: + user_id: The ID of the user. + extra_where: Additional where-clause filters to merge in. + include_counts: Whether to include agent and subfolder relations. + + Returns: + A list of raw Prisma LibraryFolder records. + """ + where_clause: prisma.types.LibraryFolderWhereInput = { + "userId": user_id, + "isDeleted": False, + } + if extra_where: + where_clause.update(extra_where) + + return await prisma.models.LibraryFolder.prisma().find_many( + where=where_clause, + order={"createdAt": "asc"}, + include=( + { + "LibraryAgents": {"where": {"isDeleted": False}}, + "Children": {"where": {"isDeleted": False}}, + } + if include_counts + else None + ), + ) + + async def list_folders( user_id: str, parent_id: Optional[str] = None, @@ -972,43 +947,20 @@ async def list_folders( """ logger.debug(f"Listing folders for user #{user_id}, parent_id={parent_id}") - try: - where_clause: prisma.types.LibraryFolderWhereInput = { - "userId": user_id, - "isDeleted": False, - "parentId": parent_id, - } + folders = await _fetch_user_folders( + user_id, + extra_where={"parentId": parent_id}, + include_counts=include_counts, + ) - folders = await prisma.models.LibraryFolder.prisma().find_many( - where=where_clause, - order={"createdAt": "asc"}, - include=( - { - "LibraryAgents": {"where": {"isDeleted": False}}, - "Children": {"where": {"isDeleted": False}}, - } - if include_counts - else None - ), + return [ + library_model.LibraryFolder.from_db( + folder, + agent_count=len(folder.LibraryAgents) if folder.LibraryAgents else 0, + subfolder_count=len(folder.Children) if folder.Children else 0, ) - - result = [] - for folder in folders: - agent_count = len(folder.LibraryAgents) if folder.LibraryAgents else 0 - subfolder_count = len(folder.Children) if folder.Children else 0 - result.append( - library_model.LibraryFolder.from_db( - folder, - agent_count=agent_count, - subfolder_count=subfolder_count, - ) - ) - - return result - - except prisma.errors.PrismaError as e: - logger.error(f"Database error listing folders: {e}") - raise DatabaseError("Failed to list folders") from e + for folder in folders + ] async def get_folder_tree( @@ -1025,48 +977,32 @@ async def get_folder_tree( """ logger.debug(f"Getting folder tree for user #{user_id}") - try: - # Fetch all folders for the user - all_folders = await prisma.models.LibraryFolder.prisma().find_many( - where={ - "userId": user_id, - "isDeleted": False, - }, - order={"createdAt": "asc"}, - include={ - "LibraryAgents": {"where": {"isDeleted": False}}, - "Children": {"where": {"isDeleted": False}}, - }, + # Fetch all folders for the user + all_folders = await _fetch_user_folders(user_id) + + # Build a map of folder ID to folder data + folder_map: dict[str, library_model.LibraryFolderTree] = { + folder.id: library_model.LibraryFolderTree( + **library_model.LibraryFolder.from_db( + folder, + agent_count=len(folder.LibraryAgents) if folder.LibraryAgents else 0, + subfolder_count=len(folder.Children) if folder.Children else 0, + ).model_dump(), + children=[], ) + for folder in all_folders + } - # Build a map of folder ID to folder data - folder_map: dict[str, library_model.LibraryFolderTree] = {} - for folder in all_folders: - agent_count = len(folder.LibraryAgents) if folder.LibraryAgents else 0 - subfolder_count = len(folder.Children) if folder.Children else 0 - folder_map[folder.id] = library_model.LibraryFolderTree( - **library_model.LibraryFolder.from_db( - folder, - agent_count=agent_count, - subfolder_count=subfolder_count, - ).model_dump(), - children=[], - ) + # Build the tree structure + root_folders: list[library_model.LibraryFolderTree] = [] + for folder in all_folders: + tree_folder = folder_map[folder.id] + if folder.parentId and folder.parentId in folder_map: + folder_map[folder.parentId].children.append(tree_folder) + else: + root_folders.append(tree_folder) - # Build the tree structure - root_folders: list[library_model.LibraryFolderTree] = [] - for folder in all_folders: - tree_folder = folder_map[folder.id] - if folder.parentId and folder.parentId in folder_map: - folder_map[folder.parentId].children.append(tree_folder) - else: - root_folders.append(tree_folder) - - return root_folders - - except prisma.errors.PrismaError as e: - logger.error(f"Database error getting folder tree: {e}") - raise DatabaseError("Failed to get folder tree") from e + return root_folders async def get_folder( @@ -1086,43 +1022,35 @@ async def get_folder( Raises: NotFoundError: If the folder doesn't exist or doesn't belong to the user. """ - try: - folder = await prisma.models.LibraryFolder.prisma().find_first( - where={ - "id": folder_id, - "userId": user_id, - "isDeleted": False, - }, - include={ - "LibraryAgents": {"where": {"isDeleted": False}}, - "Children": {"where": {"isDeleted": False}}, - }, - ) + folder = await prisma.models.LibraryFolder.prisma().find_first( + where={ + "id": folder_id, + "userId": user_id, + "isDeleted": False, + }, + include={ + "LibraryAgents": {"where": {"isDeleted": False}}, + "Children": {"where": {"isDeleted": False}}, + }, + ) - if not folder: - raise NotFoundError(f"Folder #{folder_id} not found") + if not folder: + raise NotFoundError(f"Folder #{folder_id} not found") - agent_count = len(folder.LibraryAgents) if folder.LibraryAgents else 0 - subfolder_count = len(folder.Children) if folder.Children else 0 - - return library_model.LibraryFolder.from_db( - folder, - agent_count=agent_count, - subfolder_count=subfolder_count, - ) - - except prisma.errors.PrismaError as e: - logger.error(f"Database error getting folder: {e}") - raise DatabaseError("Failed to get folder") from e + return library_model.LibraryFolder.from_db( + folder, + agent_count=len(folder.LibraryAgents) if folder.LibraryAgents else 0, + subfolder_count=len(folder.Children) if folder.Children else 0, + ) -async def is_descendant_of( +async def _is_descendant_of( folder_id: str, potential_ancestor_id: str, user_id: str, ) -> bool: """ - Check if folder_id is a descendant of potential_ancestor_id. + Check if folder_id is a descendant of (or equal to) potential_ancestor_id. Fetches all user folders in a single query and walks the parent chain in memory to avoid N database round-trips. @@ -1133,29 +1061,33 @@ async def is_descendant_of( user_id: The ID of the user. Returns: - True if folder_id is a descendant of potential_ancestor_id. + True if folder_id is a descendant of (or equal to) potential_ancestor_id. """ all_folders = await prisma.models.LibraryFolder.prisma().find_many( where={"userId": user_id, "isDeleted": False}, ) parent_map = {f.id: f.parentId for f in all_folders} + visited: set[str] = set() current_id: str | None = folder_id while current_id: if current_id == potential_ancestor_id: return True + if current_id in visited: + break # cycle detected + visited.add(current_id) current_id = parent_map.get(current_id) return False -async def validate_folder_operation( +async def _check_circular_reference( folder_id: Optional[str], target_parent_id: Optional[str], user_id: str, ) -> None: """ - Validate that a folder move/create operation is valid. + Check that moving folder_id under target_parent_id won't create a cycle. Args: folder_id: The ID of the folder being moved (None for create). @@ -1163,15 +1095,10 @@ async def validate_folder_operation( user_id: The ID of the user. Raises: - FolderValidationError: If the operation is invalid. + FolderValidationError: If the move would create a circular reference. """ - # Cannot move folder into itself - if folder_id and folder_id == target_parent_id: - raise FolderValidationError("Cannot move folder into itself") - - # Check for circular reference if folder_id and target_parent_id: - if await is_descendant_of(target_parent_id, folder_id, user_id): + if await _is_descendant_of(target_parent_id, folder_id, user_id): raise FolderValidationError("Cannot move folder into its own descendant") @@ -1196,54 +1123,43 @@ async def create_folder( The created LibraryFolder. Raises: - FolderValidationError: If validation fails. - DatabaseError: If there's a database error. + FolderAlreadyExistsError: If a folder with this name already exists. + NotFoundError: If the parent folder doesn't exist. """ logger.debug(f"Creating folder '{name}' for user #{user_id}") - try: - # Validate operation - await validate_folder_operation( - folder_id=None, - target_parent_id=parent_id, - user_id=user_id, + # Verify parent exists if provided + if parent_id: + parent = await prisma.models.LibraryFolder.prisma().find_first( + where={ + "id": parent_id, + "userId": user_id, + "isDeleted": False, + } ) + if not parent: + raise NotFoundError(f"Parent folder #{parent_id} not found") - # Verify parent exists if provided - if parent_id: - parent = await prisma.models.LibraryFolder.prisma().find_first( - where={ - "id": parent_id, - "userId": user_id, - "isDeleted": False, - } - ) - if not parent: - raise NotFoundError(f"Parent folder #{parent_id} not found") - - # Build data dict conditionally - don't include Parent key if no parent_id - create_data: dict = { - "name": name, - "User": {"connect": {"id": user_id}}, - } - if icon is not None: - create_data["icon"] = icon - if color is not None: - create_data["color"] = color - if parent_id: - create_data["Parent"] = {"connect": {"id": parent_id}} + # Build data dict conditionally - don't include Parent key if no parent_id + create_data: dict = { + "name": name, + "User": {"connect": {"id": user_id}}, + } + if icon is not None: + create_data["icon"] = icon + if color is not None: + create_data["color"] = color + if parent_id: + create_data["Parent"] = {"connect": {"id": parent_id}} + try: folder = await prisma.models.LibraryFolder.prisma().create(data=create_data) - - return library_model.LibraryFolder.from_db(folder) - except prisma.errors.UniqueViolationError: - raise FolderValidationError( + raise FolderAlreadyExistsError( "A folder with this name already exists in this location" ) - except prisma.errors.PrismaError as e: - logger.error(f"Database error creating folder: {e}") - raise DatabaseError("Failed to create folder") from e + + return library_model.LibraryFolder.from_db(folder) async def create_folder_with_unique_name( @@ -1266,10 +1182,8 @@ async def create_folder_with_unique_name( Returns: The created LibraryFolder. """ - name = base_name - suffix = 1 - - while True: + for i in itertools.count(): + name = base_name if i == 0 else f"{base_name} ({i + 1})" try: return await create_folder( user_id=user_id, @@ -1278,12 +1192,10 @@ async def create_folder_with_unique_name( icon=icon, color=color, ) - except FolderValidationError as e: - if "already exists" in str(e): - suffix += 1 - name = f"{base_name} ({suffix})" - else: - raise + except FolderAlreadyExistsError: + continue + + raise RuntimeError("Unreachable") async def update_folder( @@ -1312,29 +1224,29 @@ async def update_folder( """ logger.debug(f"Updating folder #{folder_id} for user #{user_id}") + # Verify folder exists and belongs to user + existing = await prisma.models.LibraryFolder.prisma().find_first( + where={ + "id": folder_id, + "userId": user_id, + "isDeleted": False, + } + ) + if not existing: + raise NotFoundError(f"Folder #{folder_id} not found") + + update_data: prisma.types.LibraryFolderUpdateInput = {} + if name is not None: + update_data["name"] = name + if icon is not None: + update_data["icon"] = icon + if color is not None: + update_data["color"] = color + + if not update_data: + return await get_folder(folder_id, user_id) + try: - # Verify folder exists and belongs to user - existing = await prisma.models.LibraryFolder.prisma().find_first( - where={ - "id": folder_id, - "userId": user_id, - "isDeleted": False, - } - ) - if not existing: - raise NotFoundError(f"Folder #{folder_id} not found") - - update_data: prisma.types.LibraryFolderUpdateInput = {} - if name is not None: - update_data["name"] = name - if icon is not None: - update_data["icon"] = icon - if color is not None: - update_data["color"] = color - - if not update_data: - return await get_folder(folder_id, user_id) - folder = await prisma.models.LibraryFolder.prisma().update( where={"id": folder_id}, data=update_data, @@ -1343,26 +1255,19 @@ async def update_folder( "Children": {"where": {"isDeleted": False}}, }, ) - - if not folder: - raise NotFoundError(f"Folder #{folder_id} not found") - - agent_count = len(folder.LibraryAgents) if folder.LibraryAgents else 0 - subfolder_count = len(folder.Children) if folder.Children else 0 - - return library_model.LibraryFolder.from_db( - folder, - agent_count=agent_count, - subfolder_count=subfolder_count, - ) - except prisma.errors.UniqueViolationError: - raise FolderValidationError( + raise FolderAlreadyExistsError( "A folder with this name already exists in this location" ) - except prisma.errors.PrismaError as e: - logger.error(f"Database error updating folder: {e}") - raise DatabaseError("Failed to update folder") from e + + if not folder: + raise NotFoundError(f"Folder #{folder_id} not found") + + return library_model.LibraryFolder.from_db( + folder, + agent_count=len(folder.LibraryAgents) if folder.LibraryAgents else 0, + subfolder_count=len(folder.Children) if folder.Children else 0, + ) async def move_folder( @@ -1388,39 +1293,37 @@ async def move_folder( """ logger.debug(f"Moving folder #{folder_id} to parent #{target_parent_id}") - try: - # Validate operation - await validate_folder_operation( - folder_id=folder_id, - target_parent_id=target_parent_id, - user_id=user_id, - ) + # Validate no circular reference + await _check_circular_reference( + folder_id=folder_id, + target_parent_id=target_parent_id, + user_id=user_id, + ) - # Verify folder exists - existing = await prisma.models.LibraryFolder.prisma().find_first( + # Verify folder exists + existing = await prisma.models.LibraryFolder.prisma().find_first( + where={ + "id": folder_id, + "userId": user_id, + "isDeleted": False, + } + ) + if not existing: + raise NotFoundError(f"Folder #{folder_id} not found") + + # Verify target parent exists if provided + if target_parent_id: + parent = await prisma.models.LibraryFolder.prisma().find_first( where={ - "id": folder_id, + "id": target_parent_id, "userId": user_id, "isDeleted": False, } ) - if not existing: - raise NotFoundError(f"Folder #{folder_id} not found") - - # Verify target parent exists if provided - if target_parent_id: - parent = await prisma.models.LibraryFolder.prisma().find_first( - where={ - "id": target_parent_id, - "userId": user_id, - "isDeleted": False, - } - ) - if not parent: - raise NotFoundError( - f"Target parent folder #{target_parent_id} not found" - ) + if not parent: + raise NotFoundError(f"Target parent folder #{target_parent_id} not found") + try: folder = await prisma.models.LibraryFolder.prisma().update( where={"id": folder_id}, data={ @@ -1431,26 +1334,19 @@ async def move_folder( "Children": {"where": {"isDeleted": False}}, }, ) - - if not folder: - raise NotFoundError(f"Folder #{folder_id} not found") - - agent_count = len(folder.LibraryAgents) if folder.LibraryAgents else 0 - subfolder_count = len(folder.Children) if folder.Children else 0 - - return library_model.LibraryFolder.from_db( - folder, - agent_count=agent_count, - subfolder_count=subfolder_count, - ) - except prisma.errors.UniqueViolationError: - raise FolderValidationError( + raise FolderAlreadyExistsError( "A folder with this name already exists in this location" ) - except prisma.errors.PrismaError as e: - logger.error(f"Database error moving folder: {e}") - raise DatabaseError("Failed to move folder") from e + + if not folder: + raise NotFoundError(f"Folder #{folder_id} not found") + + return library_model.LibraryFolder.from_db( + folder, + agent_count=len(folder.LibraryAgents) if folder.LibraryAgents else 0, + subfolder_count=len(folder.Children) if folder.Children else 0, + ) async def delete_folder( @@ -1472,88 +1368,81 @@ async def delete_folder( """ logger.debug(f"Deleting folder #{folder_id} for user #{user_id}") - try: - # Verify folder exists - existing = await prisma.models.LibraryFolder.prisma().find_first( + # Verify folder exists + existing = await prisma.models.LibraryFolder.prisma().find_first( + where={ + "id": folder_id, + "userId": user_id, + "isDeleted": False, + } + ) + if not existing: + raise NotFoundError(f"Folder #{folder_id} not found") + + # Collect all folder IDs (target + descendants) before the transaction + async with transaction() as tx: + descendant_ids = await _get_descendant_folder_ids(folder_id, user_id, tx) + all_folder_ids = [folder_id] + descendant_ids + + if soft_delete: + # Clean up schedules/webhooks for each affected agent before + # soft-deleting, matching what delete_library_agent() does. + affected_agents = await prisma.models.LibraryAgent.prisma().find_many( where={ - "id": folder_id, + "folderId": {"in": all_folder_ids}, "userId": user_id, "isDeleted": False, - } + }, ) - if not existing: - raise NotFoundError(f"Folder #{folder_id} not found") - # Collect all folder IDs (target + descendants) before the transaction - async with transaction() as tx: - descendant_ids = await _get_descendant_folder_ids(folder_id, user_id, tx) - all_folder_ids = [folder_id] + descendant_ids + async def _cleanup_agent(agent: prisma.models.LibraryAgent) -> None: + try: + await _cleanup_schedules_for_graph( + graph_id=agent.agentGraphId, user_id=user_id + ) + await _cleanup_webhooks_for_graph( + graph_id=agent.agentGraphId, user_id=user_id + ) + except Exception as e: + logger.warning( + f"Cleanup failed for agent {agent.id} " + f"(graph {agent.agentGraphId}): {e}" + ) + await asyncio.gather(*[_cleanup_agent(a) for a in affected_agents]) + + async with transaction() as tx: if soft_delete: - # Clean up schedules/webhooks for each affected agent before - # soft-deleting, matching what delete_library_agent() does. - affected_agents = await prisma.models.LibraryAgent.prisma().find_many( + # Soft-delete all agents in these folders + await prisma.models.LibraryAgent.prisma(tx).update_many( where={ "folderId": {"in": all_folder_ids}, "userId": user_id, - "isDeleted": False, }, + data={"isDeleted": True}, ) - async def _cleanup_agent(agent: prisma.models.LibraryAgent) -> None: - try: - await _cleanup_schedules_for_graph( - graph_id=agent.agentGraphId, user_id=user_id - ) - await _cleanup_webhooks_for_graph( - graph_id=agent.agentGraphId, user_id=user_id - ) - except Exception as e: - logger.warning( - f"Cleanup failed for agent {agent.id} " - f"(graph {agent.agentGraphId}): {e}" - ) + # Soft-delete all folders + await prisma.models.LibraryFolder.prisma(tx).update_many( + where={ + "id": {"in": all_folder_ids}, + "userId": user_id, + }, + data={"isDeleted": True}, + ) + else: + # Move agents to root (or could hard-delete them) + await prisma.models.LibraryAgent.prisma(tx).update_many( + where={ + "folderId": {"in": all_folder_ids}, + "userId": user_id, + }, + data={"folderId": None}, + ) - await asyncio.gather(*[_cleanup_agent(a) for a in affected_agents]) - - async with transaction() as tx: - if soft_delete: - # Soft-delete all agents in these folders - await prisma.models.LibraryAgent.prisma(tx).update_many( - where={ - "folderId": {"in": all_folder_ids}, - "userId": user_id, - }, - data={"isDeleted": True}, - ) - - # Soft-delete all folders - await prisma.models.LibraryFolder.prisma(tx).update_many( - where={ - "id": {"in": all_folder_ids}, - "userId": user_id, - }, - data={"isDeleted": True}, - ) - else: - # Move agents to root (or could hard-delete them) - await prisma.models.LibraryAgent.prisma(tx).update_many( - where={ - "folderId": {"in": all_folder_ids}, - "userId": user_id, - }, - data={"folderId": None}, - ) - - # Hard-delete folders (children first due to FK constraints) - for fid in reversed(all_folder_ids): - await prisma.models.LibraryFolder.prisma(tx).delete( - where={"id": fid} - ) - - except prisma.errors.PrismaError as e: - logger.error(f"Database error deleting folder: {e}") - raise DatabaseError("Failed to delete folder") from e + # Hard-delete folders (children first due to FK constraints) + for fid in reversed(all_folder_ids): + await prisma.models.LibraryFolder.prisma(tx).delete(where={"id": fid}) async def _get_descendant_folder_ids( @@ -1611,40 +1500,35 @@ async def move_agent_to_folder( """ logger.debug(f"Moving agent #{library_agent_id} to folder #{folder_id}") - try: - # Verify agent exists - agent = await prisma.models.LibraryAgent.prisma().find_first( + # Verify agent exists + agent = await prisma.models.LibraryAgent.prisma().find_first( + where={ + "id": library_agent_id, + "userId": user_id, + "isDeleted": False, + } + ) + if not agent: + raise NotFoundError(f"Library agent #{library_agent_id} not found") + + # Verify folder exists if provided + if folder_id: + folder = await prisma.models.LibraryFolder.prisma().find_first( where={ - "id": library_agent_id, + "id": folder_id, "userId": user_id, "isDeleted": False, } ) - if not agent: - raise NotFoundError(f"Library agent #{library_agent_id} not found") + if not folder: + raise NotFoundError(f"Folder #{folder_id} not found") - # Verify folder exists if provided - if folder_id: - folder = await prisma.models.LibraryFolder.prisma().find_first( - where={ - "id": folder_id, - "userId": user_id, - "isDeleted": False, - } - ) - if not folder: - raise NotFoundError(f"Folder #{folder_id} not found") + await prisma.models.LibraryAgent.prisma().update( + where={"id": library_agent_id}, + data={"folderId": folder_id}, + ) - await prisma.models.LibraryAgent.prisma().update( - where={"id": library_agent_id}, - data={"folderId": folder_id}, - ) - - return await get_library_agent(library_agent_id, user_id) - - except prisma.errors.PrismaError as e: - logger.error(f"Database error moving agent to folder: {e}") - raise DatabaseError("Failed to move agent to folder") from e + return await get_library_agent(library_agent_id, user_id) async def bulk_move_agents_to_folder( @@ -1669,45 +1553,40 @@ async def bulk_move_agents_to_folder( """ logger.debug(f"Bulk moving {len(agent_ids)} agents to folder #{folder_id}") - try: - # Verify folder exists if provided - if folder_id: - folder = await prisma.models.LibraryFolder.prisma().find_first( - where={ - "id": folder_id, - "userId": user_id, - "isDeleted": False, - } - ) - if not folder: - raise NotFoundError(f"Folder #{folder_id} not found") - - # Update all agents - await prisma.models.LibraryAgent.prisma().update_many( + # Verify folder exists if provided + if folder_id: + folder = await prisma.models.LibraryFolder.prisma().find_first( where={ - "id": {"in": agent_ids}, + "id": folder_id, "userId": user_id, "isDeleted": False, - }, - data={"folderId": folder_id}, + } ) + if not folder: + raise NotFoundError(f"Folder #{folder_id} not found") - # Fetch and return updated agents - agents = await prisma.models.LibraryAgent.prisma().find_many( - where={ - "id": {"in": agent_ids}, - "userId": user_id, - }, - include=library_agent_include( - user_id, include_nodes=False, include_executions=False - ), - ) + # Update all agents + await prisma.models.LibraryAgent.prisma().update_many( + where={ + "id": {"in": agent_ids}, + "userId": user_id, + "isDeleted": False, + }, + data={"folderId": folder_id}, + ) - return [library_model.LibraryAgent.from_db(agent) for agent in agents] + # Fetch and return updated agents + agents = await prisma.models.LibraryAgent.prisma().find_many( + where={ + "id": {"in": agent_ids}, + "userId": user_id, + }, + include=library_agent_include( + user_id, include_nodes=False, include_executions=False + ), + ) - except prisma.errors.PrismaError as e: - logger.error(f"Database error bulk moving agents to folder: {e}") - raise DatabaseError("Failed to bulk move agents to folder") from e + return [library_model.LibraryAgent.from_db(agent) for agent in agents] ############################################## @@ -1741,7 +1620,7 @@ async def list_presets( logger.warning( "Invalid pagination input: page=%d, page_size=%d", page, page_size ) - raise DatabaseError("Invalid pagination parameters") + raise InvalidInputError("Invalid pagination parameters") query_filter: prisma.types.AgentPresetWhereInput = { "userId": user_id, @@ -1750,34 +1629,28 @@ async def list_presets( if graph_id: query_filter["agentGraphId"] = graph_id - try: - presets_records = await prisma.models.AgentPreset.prisma().find_many( - where=query_filter, - skip=(page - 1) * page_size, - take=page_size, - include=AGENT_PRESET_INCLUDE, - ) - total_items = await prisma.models.AgentPreset.prisma().count(where=query_filter) - total_pages = (total_items + page_size - 1) // page_size + presets_records = await prisma.models.AgentPreset.prisma().find_many( + where=query_filter, + skip=(page - 1) * page_size, + take=page_size, + include=AGENT_PRESET_INCLUDE, + ) + total_items = await prisma.models.AgentPreset.prisma().count(where=query_filter) + total_pages = (total_items + page_size - 1) // page_size - presets = [ - library_model.LibraryAgentPreset.from_db(preset) - for preset in presets_records - ] + presets = [ + library_model.LibraryAgentPreset.from_db(preset) for preset in presets_records + ] - return library_model.LibraryAgentPresetResponse( - presets=presets, - pagination=Pagination( - total_items=total_items, - total_pages=total_pages, - current_page=page, - page_size=page_size, - ), - ) - - except prisma.errors.PrismaError as e: - logger.error(f"Database error getting presets: {e}") - raise DatabaseError("Failed to fetch presets") from e + return library_model.LibraryAgentPresetResponse( + presets=presets, + pagination=Pagination( + total_items=total_items, + total_pages=total_pages, + current_page=page, + page_size=page_size, + ), + ) async def get_preset( @@ -1797,17 +1670,13 @@ async def get_preset( DatabaseError: If there's a database error during the fetch. """ logger.debug(f"Fetching preset #{preset_id} for user #{user_id}") - try: - preset = await prisma.models.AgentPreset.prisma().find_unique( - where={"id": preset_id}, - include=AGENT_PRESET_INCLUDE, - ) - if not preset or preset.userId != user_id or preset.isDeleted: - return None - return library_model.LibraryAgentPreset.from_db(preset) - except prisma.errors.PrismaError as e: - logger.error(f"Database error getting preset: {e}") - raise DatabaseError("Failed to fetch preset") from e + preset = await prisma.models.AgentPreset.prisma().find_unique( + where={"id": preset_id}, + include=AGENT_PRESET_INCLUDE, + ) + if not preset or preset.userId != user_id or preset.isDeleted: + return None + return library_model.LibraryAgentPreset.from_db(preset) async def create_preset( @@ -1830,34 +1699,30 @@ async def create_preset( logger.debug( f"Creating preset ({repr(preset.name)}) for user #{user_id}", ) - try: - new_preset = await prisma.models.AgentPreset.prisma().create( - data=prisma.types.AgentPresetCreateInput( - userId=user_id, - name=preset.name, - description=preset.description, - agentGraphId=preset.graph_id, - agentGraphVersion=preset.graph_version, - isActive=preset.is_active, - webhookId=preset.webhook_id, - InputPresets={ - "create": [ - prisma.types.AgentNodeExecutionInputOutputCreateWithoutRelationsInput( # noqa - name=name, data=SafeJson(data) - ) - for name, data in { - **preset.inputs, - **preset.credentials, - }.items() - ] - }, - ), - include=AGENT_PRESET_INCLUDE, - ) - return library_model.LibraryAgentPreset.from_db(new_preset) - except prisma.errors.PrismaError as e: - logger.error(f"Database error creating preset: {e}") - raise DatabaseError("Failed to create preset") from e + new_preset = await prisma.models.AgentPreset.prisma().create( + data=prisma.types.AgentPresetCreateInput( + userId=user_id, + name=preset.name, + description=preset.description, + agentGraphId=preset.graph_id, + agentGraphVersion=preset.graph_version, + isActive=preset.is_active, + webhookId=preset.webhook_id, + InputPresets={ + "create": [ + prisma.types.AgentNodeExecutionInputOutputCreateWithoutRelationsInput( # noqa + name=name, data=SafeJson(data) + ) + for name, data in { + **preset.inputs, + **preset.credentials, + }.items() + ] + }, + ), + include=AGENT_PRESET_INCLUDE, + ) + return library_model.LibraryAgentPreset.from_db(new_preset) async def create_preset_from_graph_execution( @@ -1952,50 +1817,46 @@ async def update_preset( logger.debug( f"Updating preset #{preset_id} ({repr(current.name)}) for user #{user_id}", ) - try: - async with transaction() as tx: - update_data: prisma.types.AgentPresetUpdateInput = {} - if name: - update_data["name"] = name - if description: - update_data["description"] = description - if is_active is not None: - update_data["isActive"] = is_active - if inputs or credentials: - if not (inputs and credentials): - raise ValueError( - "Preset inputs and credentials must be provided together" + async with transaction() as tx: + update_data: prisma.types.AgentPresetUpdateInput = {} + if name: + update_data["name"] = name + if description: + update_data["description"] = description + if is_active is not None: + update_data["isActive"] = is_active + if inputs or credentials: + if not (inputs and credentials): + raise ValueError( + "Preset inputs and credentials must be provided together" + ) + update_data["InputPresets"] = { + "create": [ + prisma.types.AgentNodeExecutionInputOutputCreateWithoutRelationsInput( # noqa + name=name, data=SafeJson(data) ) - update_data["InputPresets"] = { - "create": [ - prisma.types.AgentNodeExecutionInputOutputCreateWithoutRelationsInput( # noqa - name=name, data=SafeJson(data) - ) - for name, data in { - **inputs, - **{ - key: creds_meta.model_dump(exclude_none=True) - for key, creds_meta in credentials.items() - }, - }.items() - ], - } - # Existing InputPresets must be deleted, in a separate query - await prisma.models.AgentNodeExecutionInputOutput.prisma( - tx - ).delete_many(where={"agentPresetId": preset_id}) - - updated = await prisma.models.AgentPreset.prisma(tx).update( - where={"id": preset_id}, - data=update_data, - include=AGENT_PRESET_INCLUDE, + for name, data in { + **inputs, + **{ + key: creds_meta.model_dump(exclude_none=True) + for key, creds_meta in credentials.items() + }, + }.items() + ], + } + # Existing InputPresets must be deleted, in a separate query + await prisma.models.AgentNodeExecutionInputOutput.prisma(tx).delete_many( + where={"agentPresetId": preset_id} ) - if not updated: - raise RuntimeError(f"AgentPreset #{preset_id} vanished while updating") - return library_model.LibraryAgentPreset.from_db(updated) - except prisma.errors.PrismaError as e: - logger.error(f"Database error updating preset: {e}") - raise DatabaseError("Failed to update preset") from e + + updated = await prisma.models.AgentPreset.prisma(tx).update( + where={"id": preset_id}, + data=update_data, + include=AGENT_PRESET_INCLUDE, + ) + if not updated: + raise RuntimeError(f"AgentPreset #{preset_id} vanished while updating") + return library_model.LibraryAgentPreset.from_db(updated) async def set_preset_webhook( @@ -2034,14 +1895,10 @@ async def delete_preset(user_id: str, preset_id: str) -> None: DatabaseError: If there's a database error during deletion. """ logger.debug(f"Setting preset #{preset_id} for user #{user_id} to deleted") - try: - await prisma.models.AgentPreset.prisma().update_many( - where={"id": preset_id, "userId": user_id}, - data={"isDeleted": True}, - ) - except prisma.errors.PrismaError as e: - logger.error(f"Database error deleting preset: {e}") - raise DatabaseError("Failed to delete preset") from e + await prisma.models.AgentPreset.prisma().update_many( + where={"id": preset_id, "userId": user_id}, + data={"isDeleted": True}, + ) async def fork_library_agent( @@ -2061,33 +1918,30 @@ async def fork_library_agent( DatabaseError: If there's an error during the forking process. """ logger.debug(f"Forking library agent {library_agent_id} for user {user_id}") - try: - # 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 DatabaseError( - # f"User {user_id} cannot access library agent graph {library_agent_id}" - # ) + # Fetch the original agent + original_agent = await get_library_agent(library_agent_id, user_id) - # Fork the underlying graph and nodes - new_graph = await graph_db.fork_graph( - original_agent.graph_id, original_agent.graph_version, 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 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, user_id=user_id) + + # Create a library agent for the new graph, preserving safe mode settings + return ( + await create_library_agent( + new_graph, + user_id, + hitl_safe_mode=original_agent.settings.human_in_the_loop_safe_mode, + sensitive_action_safe_mode=original_agent.settings.sensitive_action_safe_mode, ) - new_graph = await on_graph_activate(new_graph, user_id=user_id) - - # Create a library agent for the new graph, preserving safe mode settings - return ( - await create_library_agent( - new_graph, - user_id, - hitl_safe_mode=original_agent.settings.human_in_the_loop_safe_mode, - sensitive_action_safe_mode=original_agent.settings.sensitive_action_safe_mode, - ) - )[0] - except prisma.errors.PrismaError as e: - logger.error(f"Database error cloning library agent: {e}") - raise DatabaseError("Failed to fork library agent") from e + )[0] diff --git a/autogpt_platform/backend/backend/api/features/library/exceptions.py b/autogpt_platform/backend/backend/api/features/library/exceptions.py index bd9d307b80..b5c8cab63f 100644 --- a/autogpt_platform/backend/backend/api/features/library/exceptions.py +++ b/autogpt_platform/backend/backend/api/features/library/exceptions.py @@ -2,3 +2,9 @@ class FolderValidationError(Exception): """Raised when folder operations fail validation.""" pass + + +class FolderAlreadyExistsError(FolderValidationError): + """Raised when a folder with the same name already exists in the location.""" + + pass diff --git a/autogpt_platform/backend/backend/api/features/library/model.py b/autogpt_platform/backend/backend/api/features/library/model.py index efd28ed9ef..2c2f9df061 100644 --- a/autogpt_platform/backend/backend/api/features/library/model.py +++ b/autogpt_platform/backend/backend/api/features/library/model.py @@ -566,5 +566,5 @@ class LibraryAgentUpdateRequest(pydantic.BaseModel): ) folder_id: Optional[str] = pydantic.Field( default=None, - description="Folder ID to move agent to (empty string for root)", + description="Folder ID to move agent to (None to move to root)", ) diff --git a/autogpt_platform/backend/backend/api/rest_api.py b/autogpt_platform/backend/backend/api/rest_api.py index fc063ad5c6..b494fe8429 100644 --- a/autogpt_platform/backend/backend/api/rest_api.py +++ b/autogpt_platform/backend/backend/api/rest_api.py @@ -45,7 +45,10 @@ from backend.api.features.chat.completion_consumer import ( start_completion_consumer, stop_completion_consumer, ) -from backend.api.features.library.exceptions import FolderValidationError +from backend.api.features.library.exceptions import ( + FolderAlreadyExistsError, + FolderValidationError, +) from backend.blocks.llm import DEFAULT_LLM_MODEL from backend.data.model import Credentials from backend.integrations.providers import ProviderName @@ -278,6 +281,7 @@ async def validation_error_handler( app.add_exception_handler(PrismaError, handle_internal_http_error(500)) +app.add_exception_handler(FolderAlreadyExistsError, handle_internal_http_error(409, False)) app.add_exception_handler(FolderValidationError, handle_internal_http_error(400, False)) app.add_exception_handler(NotFoundError, handle_internal_http_error(404, False)) app.add_exception_handler(NotAuthorizedError, handle_internal_http_error(403, False))