From 4b17cc996363beb15abc636f84cf2149a0de0ce4 Mon Sep 17 00:00:00 2001
From: Swifty <craigswift13@gmail.com>
Date: Fri, 10 Jan 2025 12:57:35 +0100
Subject: [PATCH] feat(backend): Add Support for Managing Agent Presets with
 Pagination and Soft Delete (#9211)

#### Summary
- **New Models**: Added `LibraryAgentPreset`,
`LibraryAgentPresetResponse`, `Pagination`, and
`CreateLibraryAgentPresetRequest`.
- **Database Changes**:
  - Added `isDeleted` column in `AgentPreset` for soft delete.
  - CRUD operations for `AgentPreset`:
    - `get_presets` with pagination.
    - `get_preset` by ID.
    - `create_or_update_preset` for upsert.
    - `delete_preset` to soft delete.
- **API Routes**:
  - `GET /presets`: Fetch paginated presets.
  - `GET /presets/{preset_id}`: Fetch a single preset.
  - `POST /presets`: Create a preset.
  - `PUT /presets/{preset_id}`: Update a preset.
  - `DELETE /presets/{preset_id}`: Soft delete a preset.
- **Tests**:
  - Coverage for models, CRUD operations, and pagination.
- **Migration**:
  - Added `isDeleted` field to support soft delete.

#### Review Notes
- Validate migration scripts and test coverage.
- Ensure API aligns with project standards.

---------

Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
---
 .../backend/backend/data/execution.py         |   2 +
 .../backend/backend/executor/manager.py       |   2 +
 .../backend/backend/server/model.py           |  15 +
 .../backend/backend/server/rest_api.py        |  54 +++
 .../backend/backend/server/routers/v1.py      |  70 ++--
 .../backend/backend/server/v2/library/db.py   | 318 ++++++++++++++----
 .../backend/server/v2/library/db_test.py      |  67 ++--
 .../backend/server/v2/library/model.py        | 101 +++++-
 .../backend/server/v2/library/model_test.py   | 165 ++++++++-
 .../backend/server/v2/library/routes.py       | 123 -------
 .../server/v2/library/routes/__init__.py      |   9 +
 .../server/v2/library/routes/agents.py        | 148 ++++++++
 .../server/v2/library/routes/presets.py       | 156 +++++++++
 .../backend/server/v2/library/routes_test.py  |  30 +-
 .../migration.sql                             |   2 +
 .../migration.sql                             |  46 +++
 .../migration.sql                             |   2 +
 autogpt_platform/backend/schema.prisma        |  12 +-
 .../backend/test/executor/test_manager.py     | 190 ++++++++++-
 .../backend/test/test_data_creator.py         |   4 +-
 20 files changed, 1195 insertions(+), 321 deletions(-)
 delete mode 100644 autogpt_platform/backend/backend/server/v2/library/routes.py
 create mode 100644 autogpt_platform/backend/backend/server/v2/library/routes/__init__.py
 create mode 100644 autogpt_platform/backend/backend/server/v2/library/routes/agents.py
 create mode 100644 autogpt_platform/backend/backend/server/v2/library/routes/presets.py
 create mode 100644 autogpt_platform/backend/migrations/20250107095812_preset_soft_delete/migration.sql
 create mode 100644 autogpt_platform/backend/migrations/20250108084101_user_agent_to_library_agent/migration.sql
 create mode 100644 autogpt_platform/backend/migrations/20250108084305_use_graph_is_active_version/migration.sql

diff --git a/autogpt_platform/backend/backend/data/execution.py b/autogpt_platform/backend/backend/data/execution.py
index 230c8e497..8bce9611f 100644
--- a/autogpt_platform/backend/backend/data/execution.py
+++ b/autogpt_platform/backend/backend/data/execution.py
@@ -136,6 +136,7 @@ async def create_graph_execution(
     graph_version: int,
     nodes_input: list[tuple[str, BlockInput]],
     user_id: str,
+    preset_id: str | None = None,
 ) -> tuple[str, list[ExecutionResult]]:
     """
     Create a new AgentGraphExecution record.
@@ -163,6 +164,7 @@ async def create_graph_execution(
                 ]
             },
             "userId": user_id,
+            "agentPresetId": preset_id,
         },
         include=GRAPH_EXECUTION_INCLUDE,
     )
diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py
index e58f80133..d253309fe 100644
--- a/autogpt_platform/backend/backend/executor/manager.py
+++ b/autogpt_platform/backend/backend/executor/manager.py
@@ -781,6 +781,7 @@ class ExecutionManager(AppService):
         data: BlockInput,
         user_id: str,
         graph_version: int,
+        preset_id: str | None = None,
     ) -> GraphExecutionEntry:
         graph: GraphModel | None = self.db_client.get_graph(
             graph_id=graph_id, user_id=user_id, version=graph_version
@@ -829,6 +830,7 @@ class ExecutionManager(AppService):
             graph_version=graph.version,
             nodes_input=nodes_input,
             user_id=user_id,
+            preset_id=preset_id,
         )
 
         starting_node_execs = []
diff --git a/autogpt_platform/backend/backend/server/model.py b/autogpt_platform/backend/backend/server/model.py
index 7c554b445..9c1d357da 100644
--- a/autogpt_platform/backend/backend/server/model.py
+++ b/autogpt_platform/backend/backend/server/model.py
@@ -56,3 +56,18 @@ class SetGraphActiveVersion(pydantic.BaseModel):
 
 class UpdatePermissionsRequest(pydantic.BaseModel):
     permissions: List[APIKeyPermission]
+
+
+class Pagination(pydantic.BaseModel):
+    total_items: int = pydantic.Field(
+        description="Total number of items.", examples=[42]
+    )
+    total_pages: int = pydantic.Field(
+        description="Total number of pages.", examples=[2]
+    )
+    current_page: int = pydantic.Field(
+        description="Current_page page number.", examples=[1]
+    )
+    page_size: int = pydantic.Field(
+        description="Number of items per page.", examples=[25]
+    )
diff --git a/autogpt_platform/backend/backend/server/rest_api.py b/autogpt_platform/backend/backend/server/rest_api.py
index 762cf48fe..f62bf578d 100644
--- a/autogpt_platform/backend/backend/server/rest_api.py
+++ b/autogpt_platform/backend/backend/server/rest_api.py
@@ -17,6 +17,7 @@ import backend.data.db
 import backend.data.graph
 import backend.data.user
 import backend.server.routers.v1
+import backend.server.v2.library.model
 import backend.server.v2.library.routes
 import backend.server.v2.store.model
 import backend.server.v2.store.routes
@@ -166,6 +167,59 @@ class AgentServer(backend.util.service.AppProcess):
     async def test_delete_graph(graph_id: str, user_id: str):
         return await backend.server.routers.v1.delete_graph(graph_id, user_id)
 
+    @staticmethod
+    async def test_get_presets(user_id: str, page: int = 1, page_size: int = 10):
+        return await backend.server.v2.library.routes.presets.get_presets(
+            user_id=user_id, page=page, page_size=page_size
+        )
+
+    @staticmethod
+    async def test_get_preset(preset_id: str, user_id: str):
+        return await backend.server.v2.library.routes.presets.get_preset(
+            preset_id=preset_id, user_id=user_id
+        )
+
+    @staticmethod
+    async def test_create_preset(
+        preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
+        user_id: str,
+    ):
+        return await backend.server.v2.library.routes.presets.create_preset(
+            preset=preset, user_id=user_id
+        )
+
+    @staticmethod
+    async def test_update_preset(
+        preset_id: str,
+        preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
+        user_id: str,
+    ):
+        return await backend.server.v2.library.routes.presets.update_preset(
+            preset_id=preset_id, preset=preset, user_id=user_id
+        )
+
+    @staticmethod
+    async def test_delete_preset(preset_id: str, user_id: str):
+        return await backend.server.v2.library.routes.presets.delete_preset(
+            preset_id=preset_id, user_id=user_id
+        )
+
+    @staticmethod
+    async def test_execute_preset(
+        graph_id: str,
+        graph_version: int,
+        preset_id: str,
+        node_input: dict[typing.Any, typing.Any],
+        user_id: str,
+    ):
+        return await backend.server.v2.library.routes.presets.execute_preset(
+            graph_id=graph_id,
+            graph_version=graph_version,
+            preset_id=preset_id,
+            node_input=node_input,
+            user_id=user_id,
+        )
+
     @staticmethod
     async def test_create_store_listing(
         request: backend.server.v2.store.model.StoreSubmissionRequest, user_id: str
diff --git a/autogpt_platform/backend/backend/server/routers/v1.py b/autogpt_platform/backend/backend/server/routers/v1.py
index 7bfd41b50..36a45fae7 100644
--- a/autogpt_platform/backend/backend/server/routers/v1.py
+++ b/autogpt_platform/backend/backend/server/routers/v1.py
@@ -13,6 +13,7 @@ from typing_extensions import Optional, TypedDict
 import backend.data.block
 import backend.server.integrations.router
 import backend.server.routers.analytics
+import backend.server.v2.library.db
 from backend.data import execution as execution_db
 from backend.data import graph as graph_db
 from backend.data.api_key import (
@@ -180,11 +181,6 @@ async def get_graph(
     tags=["graphs"],
     dependencies=[Depends(auth_middleware)],
 )
-@v1_router.get(
-    path="/templates/{graph_id}/versions",
-    tags=["templates", "graphs"],
-    dependencies=[Depends(auth_middleware)],
-)
 async def get_graph_all_versions(
     graph_id: str, user_id: Annotated[str, Depends(get_user_id)]
 ) -> Sequence[graph_db.GraphModel]:
@@ -223,6 +219,13 @@ async def do_create_graph(
                 400, detail=f"Template #{create_graph.template_id} not found"
             )
         graph.version = 1
+
+        # Create a library agent for the new graph
+        await backend.server.v2.library.db.create_library_agent(
+            graph.id,
+            graph.version,
+            user_id,
+        )
     else:
         raise HTTPException(
             status_code=400, detail="Either graph or template_id must be provided."
@@ -257,11 +260,6 @@ async def delete_graph(
 @v1_router.put(
     path="/graphs/{graph_id}", tags=["graphs"], dependencies=[Depends(auth_middleware)]
 )
-@v1_router.put(
-    path="/templates/{graph_id}",
-    tags=["templates", "graphs"],
-    dependencies=[Depends(auth_middleware)],
-)
 async def update_graph(
     graph_id: str,
     graph: graph_db.Graph,
@@ -294,6 +292,11 @@ async def update_graph(
 
     if new_graph_version.is_active:
 
+        # Keep the library agent up to date with the new active version
+        await backend.server.v2.library.db.update_agent_version_in_library(
+            user_id, graph.id, graph.version
+        )
+
         def get_credentials(credentials_id: str) -> "Credentials | None":
             return integration_creds_manager.get(user_id, credentials_id)
 
@@ -349,6 +352,12 @@ async def set_graph_active_version(
         version=new_active_version,
         user_id=user_id,
     )
+
+    # Keep the library agent up to date with the new active version
+    await backend.server.v2.library.db.update_agent_version_in_library(
+        user_id, new_active_graph.id, new_active_graph.version
+    )
+
     if current_active_graph and current_active_graph.version != new_active_version:
         # Handle deactivation of the previously active version
         await on_graph_deactivate(
@@ -425,47 +434,6 @@ async def get_graph_run_node_execution_results(
     return await execution_db.get_execution_results(graph_exec_id)
 
 
-########################################################
-##################### Templates ########################
-########################################################
-
-
-@v1_router.get(
-    path="/templates",
-    tags=["graphs", "templates"],
-    dependencies=[Depends(auth_middleware)],
-)
-async def get_templates(
-    user_id: Annotated[str, Depends(get_user_id)]
-) -> Sequence[graph_db.GraphModel]:
-    return await graph_db.get_graphs(filter_by="template", user_id=user_id)
-
-
-@v1_router.get(
-    path="/templates/{graph_id}",
-    tags=["templates", "graphs"],
-    dependencies=[Depends(auth_middleware)],
-)
-async def get_template(
-    graph_id: str, version: int | None = None
-) -> graph_db.GraphModel:
-    graph = await graph_db.get_graph(graph_id, version)
-    if not graph:
-        raise HTTPException(status_code=404, detail=f"Template #{graph_id} not found.")
-    return graph
-
-
-@v1_router.post(
-    path="/templates",
-    tags=["templates", "graphs"],
-    dependencies=[Depends(auth_middleware)],
-)
-async def create_new_template(
-    create_graph: CreateGraph, user_id: Annotated[str, Depends(get_user_id)]
-) -> graph_db.GraphModel:
-    return await do_create_graph(create_graph, user_id=user_id)
-
-
 ########################################################
 ##################### Schedules ########################
 ########################################################
diff --git a/autogpt_platform/backend/backend/server/v2/library/db.py b/autogpt_platform/backend/backend/server/v2/library/db.py
index 8d142ef40..97313ccdb 100644
--- a/autogpt_platform/backend/backend/server/v2/library/db.py
+++ b/autogpt_platform/backend/backend/server/v2/library/db.py
@@ -1,5 +1,5 @@
+import json
 import logging
-from typing import List
 
 import prisma.errors
 import prisma.models
@@ -7,6 +7,7 @@ import prisma.types
 
 import backend.data.graph
 import backend.data.includes
+import backend.server.model
 import backend.server.v2.library.model
 import backend.server.v2.store.exceptions
 
@@ -14,90 +15,152 @@ logger = logging.getLogger(__name__)
 
 
 async def get_library_agents(
-    user_id: str,
-) -> List[backend.server.v2.library.model.LibraryAgent]:
-    """
-    Returns all agents (AgentGraph) that belong to the user and all agents in their library (UserAgent table)
-    """
-    logger.debug(f"Getting library agents for user {user_id}")
+    user_id: str, search_query: str | None = None
+) -> list[backend.server.v2.library.model.LibraryAgent]:
+    logger.debug(
+        f"Fetching library agents for user_id={user_id} search_query={search_query}"
+    )
 
-    try:
-        # Get agents created by user with nodes and links
-        user_created = await prisma.models.AgentGraph.prisma().find_many(
-            where=prisma.types.AgentGraphWhereInput(userId=user_id, isActive=True),
-            include=backend.data.includes.AGENT_GRAPH_INCLUDE,
+    if search_query and len(search_query.strip()) > 100:
+        logger.warning(f"Search query too long: {search_query}")
+        raise backend.server.v2.store.exceptions.DatabaseError(
+            "Search query is too long."
         )
 
-        # Get agents in user's library with nodes and links
-        library_agents = await prisma.models.UserAgent.prisma().find_many(
-            where=prisma.types.UserAgentWhereInput(
-                userId=user_id, isDeleted=False, isArchived=False
-            ),
-            include={
+    where_clause = prisma.types.LibraryAgentWhereInput(
+        userId=user_id,
+        isDeleted=False,
+        isArchived=False,
+    )
+
+    if search_query:
+        where_clause["OR"] = [
+            {
                 "Agent": {
-                    "include": {
-                        "AgentNodes": {
-                            "include": {
-                                "Input": True,
-                                "Output": True,
-                                "Webhook": True,
-                                "AgentBlock": True,
-                            }
-                        }
+                    "is": {"name": {"contains": search_query, "mode": "insensitive"}}
+                }
+            },
+            {
+                "Agent": {
+                    "is": {
+                        "description": {"contains": search_query, "mode": "insensitive"}
                     }
                 }
             },
+        ]
+
+    try:
+        library_agents = await prisma.models.LibraryAgent.prisma().find_many(
+            where=where_clause,
+            include={
+                "Agent": {
+                    "include": {
+                        "AgentNodes": {"include": {"Input": True, "Output": True}}
+                    }
+                }
+            },
+            order=[{"updatedAt": "desc"}],
+        )
+        logger.debug(f"Retrieved {len(library_agents)} agents for user_id={user_id}.")
+        return [
+            backend.server.v2.library.model.LibraryAgent.from_db(agent)
+            for agent in library_agents
+        ]
+    except prisma.errors.PrismaError as e:
+        logger.error(f"Database error fetching library agents: {e}")
+        raise backend.server.v2.store.exceptions.DatabaseError(
+            "Unable to fetch library agents."
         )
 
-        # Convert to Graph models first
-        graphs = []
 
-        # Add user created agents
-        for agent in user_created:
-            try:
-                graphs.append(backend.data.graph.GraphModel.from_db(agent))
-            except Exception as e:
-                logger.error(f"Error processing user created agent {agent.id}: {e}")
-                continue
+async def create_library_agent(
+    agent_id: str, agent_version: int, user_id: str
+) -> prisma.models.LibraryAgent:
+    """
+    Adds an agent to the user's library (LibraryAgent table)
+    """
 
-        # Add library agents
-        for agent in library_agents:
-            if agent.Agent:
-                try:
-                    graphs.append(backend.data.graph.GraphModel.from_db(agent.Agent))
-                except Exception as e:
-                    logger.error(f"Error processing library agent {agent.agentId}: {e}")
-                    continue
+    try:
 
-        # Convert Graph models to LibraryAgent models
-        result = []
-        for graph in graphs:
-            result.append(
-                backend.server.v2.library.model.LibraryAgent(
-                    id=graph.id,
-                    version=graph.version,
-                    is_active=graph.is_active,
-                    name=graph.name,
-                    description=graph.description,
-                    isCreatedByUser=any(a.id == graph.id for a in user_created),
-                    input_schema=graph.input_schema,
-                    output_schema=graph.output_schema,
-                )
+        library_agent = await prisma.models.LibraryAgent.prisma().create(
+            data=prisma.types.LibraryAgentCreateInput(
+                userId=user_id,
+                agentId=agent_id,
+                agentVersion=agent_version,
+                isCreatedByUser=False,
+                useGraphIsActiveVersion=True,
             )
-
-        logger.debug(f"Found {len(result)} library agents")
-        return result
-
+        )
+        return library_agent
     except prisma.errors.PrismaError as e:
-        logger.error(f"Database error getting library agents: {str(e)}")
+        logger.error(f"Database error creating agent to library: {str(e)}")
         raise backend.server.v2.store.exceptions.DatabaseError(
-            "Failed to fetch library agents"
+            "Failed to create agent to library"
         ) from e
 
 
-async def add_agent_to_library(store_listing_version_id: str, user_id: str) -> None:
+async def update_agent_version_in_library(
+    user_id: str, agent_id: str, agent_version: int
+) -> None:
     """
-    Finds the agent from the store listing version and adds it to the user's library (UserAgent table)
+    Updates the agent version in the library
+    """
+    try:
+        await prisma.models.LibraryAgent.prisma().update(
+            where={
+                "userId": user_id,
+                "agentId": agent_id,
+                "useGraphIsActiveVersion": True,
+            },
+            data=prisma.types.LibraryAgentUpdateInput(
+                Agent=prisma.types.AgentGraphUpdateOneWithoutRelationsInput(
+                    connect=prisma.types.AgentGraphWhereUniqueInput(
+                        id=agent_id,
+                        version=agent_version,
+                    ),
+                ),
+            ),
+        )
+    except prisma.errors.PrismaError as e:
+        logger.error(f"Database error updating agent version in library: {str(e)}")
+        raise backend.server.v2.store.exceptions.DatabaseError(
+            "Failed to update agent version in library"
+        ) from e
+
+
+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,
+) -> None:
+    """
+    Updates the library agent with the given fields
+    """
+    try:
+        await prisma.models.LibraryAgent.prisma().update(
+            where={"id": library_agent_id, "userId": user_id},
+            data=prisma.types.LibraryAgentUpdateInput(
+                useGraphIsActiveVersion=auto_update_version,
+                isFavorite=is_favorite,
+                isArchived=is_archived,
+                isDeleted=is_deleted,
+            ),
+        )
+    except prisma.errors.PrismaError as e:
+        logger.error(f"Database error updating library agent: {str(e)}")
+        raise backend.server.v2.store.exceptions.DatabaseError(
+            "Failed to update library agent"
+        ) from e
+
+
+async def add_store_agent_to_library(
+    store_listing_version_id: str, user_id: str
+) -> None:
+    """
+    Finds the agent from the store listing version and adds it to the user's library (LibraryAgent table)
     if they don't already have it
     """
     logger.debug(
@@ -131,7 +194,7 @@ async def add_agent_to_library(store_listing_version_id: str, user_id: str) -> N
             )
 
         # Check if user already has this agent
-        existing_user_agent = await prisma.models.UserAgent.prisma().find_first(
+        existing_user_agent = await prisma.models.LibraryAgent.prisma().find_first(
             where={
                 "userId": user_id,
                 "agentId": agent.id,
@@ -145,9 +208,9 @@ async def add_agent_to_library(store_listing_version_id: str, user_id: str) -> N
             )
             return
 
-        # Create UserAgent entry
-        await prisma.models.UserAgent.prisma().create(
-            data=prisma.types.UserAgentCreateInput(
+        # Create LibraryAgent entry
+        await prisma.models.LibraryAgent.prisma().create(
+            data=prisma.types.LibraryAgentCreateInput(
                 userId=user_id,
                 agentId=agent.id,
                 agentVersion=agent.version,
@@ -163,3 +226,118 @@ async def add_agent_to_library(store_listing_version_id: str, user_id: str) -> N
         raise backend.server.v2.store.exceptions.DatabaseError(
             "Failed to add agent to library"
         ) from e
+
+
+##############################################
+########### Presets DB Functions #############
+##############################################
+
+
+async def get_presets(
+    user_id: str, page: int, page_size: int
+) -> backend.server.v2.library.model.LibraryAgentPresetResponse:
+
+    try:
+        presets = await prisma.models.AgentPreset.prisma().find_many(
+            where={"userId": user_id},
+            skip=page * page_size,
+            take=page_size,
+        )
+
+        total_items = await prisma.models.AgentPreset.prisma().count(
+            where={"userId": user_id},
+        )
+        total_pages = (total_items + page_size - 1) // page_size
+
+        presets = [
+            backend.server.v2.library.model.LibraryAgentPreset.from_db(preset)
+            for preset in presets
+        ]
+
+        return backend.server.v2.library.model.LibraryAgentPresetResponse(
+            presets=presets,
+            pagination=backend.server.model.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: {str(e)}")
+        raise backend.server.v2.store.exceptions.DatabaseError(
+            "Failed to fetch presets"
+        ) from e
+
+
+async def get_preset(
+    user_id: str, preset_id: str
+) -> backend.server.v2.library.model.LibraryAgentPreset | None:
+    try:
+        preset = await prisma.models.AgentPreset.prisma().find_unique(
+            where={"id": preset_id, "userId": user_id}, include={"InputPresets": True}
+        )
+        if not preset:
+            return None
+        return backend.server.v2.library.model.LibraryAgentPreset.from_db(preset)
+    except prisma.errors.PrismaError as e:
+        logger.error(f"Database error getting preset: {str(e)}")
+        raise backend.server.v2.store.exceptions.DatabaseError(
+            "Failed to fetch preset"
+        ) from e
+
+
+async def create_or_update_preset(
+    user_id: str,
+    preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
+    preset_id: str | None = None,
+) -> backend.server.v2.library.model.LibraryAgentPreset:
+    try:
+
+        logger.info(f"DB Creating Preset with inputs: {preset.inputs}")
+        new_preset = await prisma.models.AgentPreset.prisma().upsert(
+            where={
+                "id": preset_id if preset_id else "",
+            },
+            data={
+                "create": {
+                    "userId": user_id,
+                    "name": preset.name,
+                    "description": preset.description,
+                    "agentId": preset.agent_id,
+                    "agentVersion": preset.agent_version,
+                    "isActive": preset.is_active,
+                    "InputPresets": {
+                        "create": [
+                            {"name": name, "data": json.dumps(data)}
+                            for name, data in preset.inputs.items()
+                        ]
+                    },
+                },
+                "update": {
+                    "name": preset.name,
+                    "description": preset.description,
+                    "isActive": preset.is_active,
+                },
+            },
+        )
+        return backend.server.v2.library.model.LibraryAgentPreset.from_db(new_preset)
+    except prisma.errors.PrismaError as e:
+        logger.error(f"Database error creating preset: {str(e)}")
+        raise backend.server.v2.store.exceptions.DatabaseError(
+            "Failed to create preset"
+        ) from e
+
+
+async def delete_preset(user_id: str, preset_id: str) -> None:
+    try:
+        await prisma.models.AgentPreset.prisma().update(
+            where={"id": preset_id, "userId": user_id},
+            data={"isDeleted": True},
+        )
+    except prisma.errors.PrismaError as e:
+        logger.error(f"Database error deleting preset: {str(e)}")
+        raise backend.server.v2.store.exceptions.DatabaseError(
+            "Failed to delete preset"
+        ) from 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 e06d4bfa9..17e3bdb4e 100644
--- a/autogpt_platform/backend/backend/server/v2/library/db_test.py
+++ b/autogpt_platform/backend/backend/server/v2/library/db_test.py
@@ -37,7 +37,7 @@ async def test_get_library_agents(mocker):
     ]
 
     mock_library_agents = [
-        prisma.models.UserAgent(
+        prisma.models.LibraryAgent(
             id="ua1",
             userId="test-user",
             agentId="agent2",
@@ -48,6 +48,7 @@ async def test_get_library_agents(mocker):
             createdAt=datetime.now(),
             updatedAt=datetime.now(),
             isFavorite=False,
+            useGraphIsActiveVersion=True,
             Agent=prisma.models.AgentGraph(
                 id="agent2",
                 version=1,
@@ -67,8 +68,8 @@ async def test_get_library_agents(mocker):
         return_value=mock_user_created
     )
 
-    mock_user_agent = mocker.patch("prisma.models.UserAgent.prisma")
-    mock_user_agent.return_value.find_many = mocker.AsyncMock(
+    mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma")
+    mock_library_agent.return_value.find_many = mocker.AsyncMock(
         return_value=mock_library_agents
     )
 
@@ -76,40 +77,16 @@ async def test_get_library_agents(mocker):
     result = await db.get_library_agents("test-user")
 
     # Verify results
-    assert len(result) == 2
-    assert result[0].id == "agent1"
-    assert result[0].name == "Test Agent 1"
-    assert result[0].description == "Test Description 1"
-    assert result[0].isCreatedByUser is True
-    assert result[1].id == "agent2"
-    assert result[1].name == "Test Agent 2"
-    assert result[1].description == "Test Description 2"
-    assert result[1].isCreatedByUser is False
-
-    # Verify mocks called correctly
-    mock_agent_graph.return_value.find_many.assert_called_once_with(
-        where=prisma.types.AgentGraphWhereInput(userId="test-user", isActive=True),
-        include=backend.data.includes.AGENT_GRAPH_INCLUDE,
-    )
-    mock_user_agent.return_value.find_many.assert_called_once_with(
-        where=prisma.types.UserAgentWhereInput(
-            userId="test-user", isDeleted=False, isArchived=False
-        ),
-        include={
-            "Agent": {
-                "include": {
-                    "AgentNodes": {
-                        "include": {
-                            "Input": True,
-                            "Output": True,
-                            "Webhook": True,
-                            "AgentBlock": True,
-                        }
-                    }
-                }
-            }
-        },
-    )
+    assert len(result) == 1
+    assert result[0].id == "ua1"
+    assert result[0].name == "Test Agent 2"
+    assert result[0].description == "Test Description 2"
+    assert result[0].is_created_by_user is False
+    assert result[0].is_latest_version is True
+    assert result[0].is_favorite is False
+    assert result[0].agent_id == "agent2"
+    assert result[0].agent_version == 1
+    assert result[0].preset_id is None
 
 
 @pytest.mark.asyncio
@@ -152,26 +129,26 @@ async def test_add_agent_to_library(mocker):
         return_value=mock_store_listing
     )
 
-    mock_user_agent = mocker.patch("prisma.models.UserAgent.prisma")
-    mock_user_agent.return_value.find_first = mocker.AsyncMock(return_value=None)
-    mock_user_agent.return_value.create = mocker.AsyncMock()
+    mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma")
+    mock_library_agent.return_value.find_first = mocker.AsyncMock(return_value=None)
+    mock_library_agent.return_value.create = mocker.AsyncMock()
 
     # Call function
-    await db.add_agent_to_library("version123", "test-user")
+    await db.add_store_agent_to_library("version123", "test-user")
 
     # Verify mocks called correctly
     mock_store_listing_version.return_value.find_unique.assert_called_once_with(
         where={"id": "version123"}, include={"Agent": True}
     )
-    mock_user_agent.return_value.find_first.assert_called_once_with(
+    mock_library_agent.return_value.find_first.assert_called_once_with(
         where={
             "userId": "test-user",
             "agentId": "agent1",
             "agentVersion": 1,
         }
     )
-    mock_user_agent.return_value.create.assert_called_once_with(
-        data=prisma.types.UserAgentCreateInput(
+    mock_library_agent.return_value.create.assert_called_once_with(
+        data=prisma.types.LibraryAgentCreateInput(
             userId="test-user", agentId="agent1", agentVersion=1, isCreatedByUser=False
         )
     )
@@ -189,7 +166,7 @@ async def test_add_agent_to_library_not_found(mocker):
 
     # Call function and verify exception
     with pytest.raises(backend.server.v2.store.exceptions.AgentNotFoundError):
-        await db.add_agent_to_library("version123", "test-user")
+        await db.add_store_agent_to_library("version123", "test-user")
 
     # Verify mock called correctly
     mock_store_listing_version.return_value.find_unique.assert_called_once_with(
diff --git a/autogpt_platform/backend/backend/server/v2/library/model.py b/autogpt_platform/backend/backend/server/v2/library/model.py
index 88a81f6d7..86e42f8a0 100644
--- a/autogpt_platform/backend/backend/server/v2/library/model.py
+++ b/autogpt_platform/backend/backend/server/v2/library/model.py
@@ -1,16 +1,111 @@
+import datetime
+import json
 import typing
 
+import prisma.models
 import pydantic
 
+import backend.data.block
+import backend.data.graph
+import backend.server.model
+
 
 class LibraryAgent(pydantic.BaseModel):
     id: str  # Changed from agent_id to match GraphMeta
-    version: int  # Changed from agent_version to match GraphMeta
-    is_active: bool  # Added to match GraphMeta
+
+    agent_id: str
+    agent_version: int  # Changed from agent_version to match GraphMeta
+
+    preset_id: str | None
+
+    updated_at: datetime.datetime
+
     name: str
     description: str
 
-    isCreatedByUser: bool
     # Made input_schema and output_schema match GraphMeta's type
     input_schema: dict[str, typing.Any]  # Should be BlockIOObjectSubSchema in frontend
     output_schema: dict[str, typing.Any]  # Should be BlockIOObjectSubSchema in frontend
+
+    is_favorite: bool
+    is_created_by_user: bool
+
+    is_latest_version: bool
+
+    @staticmethod
+    def from_db(agent: prisma.models.LibraryAgent):
+        if not agent.Agent:
+            raise ValueError("AgentGraph is required")
+
+        graph = backend.data.graph.GraphModel.from_db(agent.Agent)
+
+        agent_updated_at = agent.Agent.updatedAt
+        lib_agent_updated_at = agent.updatedAt
+
+        # Take the latest updated_at timestamp either when the graph was updated or the library agent was updated
+        updated_at = (
+            max(agent_updated_at, lib_agent_updated_at)
+            if agent_updated_at
+            else lib_agent_updated_at
+        )
+
+        return LibraryAgent(
+            id=agent.id,
+            agent_id=agent.agentId,
+            agent_version=agent.agentVersion,
+            updated_at=updated_at,
+            name=graph.name,
+            description=graph.description,
+            input_schema=graph.input_schema,
+            output_schema=graph.output_schema,
+            is_favorite=agent.isFavorite,
+            is_created_by_user=agent.isCreatedByUser,
+            is_latest_version=graph.is_active,
+            preset_id=agent.AgentPreset.id if agent.AgentPreset else None,
+        )
+
+
+class LibraryAgentPreset(pydantic.BaseModel):
+    id: str
+    updated_at: datetime.datetime
+
+    agent_id: str
+    agent_version: int
+
+    name: str
+    description: str
+
+    is_active: bool
+    inputs: dict[str, typing.Union[backend.data.block.BlockInput, typing.Any]]
+
+    @staticmethod
+    def from_db(preset: prisma.models.AgentPreset):
+        input_data = {}
+
+        for data in preset.InputPresets or []:
+            input_data[data.name] = json.loads(data.data)
+
+        return LibraryAgentPreset(
+            id=preset.id,
+            updated_at=preset.updatedAt,
+            agent_id=preset.agentId,
+            agent_version=preset.agentVersion,
+            name=preset.name,
+            description=preset.description,
+            is_active=preset.isActive,
+            inputs=input_data,
+        )
+
+
+class LibraryAgentPresetResponse(pydantic.BaseModel):
+    presets: list[LibraryAgentPreset]
+    pagination: backend.server.model.Pagination
+
+
+class CreateLibraryAgentPresetRequest(pydantic.BaseModel):
+    name: str
+    description: str
+    inputs: dict[str, typing.Union[backend.data.block.BlockInput, typing.Any]]
+    agent_id: str
+    agent_version: int
+    is_active: bool
diff --git a/autogpt_platform/backend/backend/server/v2/library/model_test.py b/autogpt_platform/backend/backend/server/v2/library/model_test.py
index 81aa8fe07..a2e7aa8f8 100644
--- a/autogpt_platform/backend/backend/server/v2/library/model_test.py
+++ b/autogpt_platform/backend/backend/server/v2/library/model_test.py
@@ -1,23 +1,35 @@
+import datetime
+
+import prisma.models
+
+import backend.data.block
+import backend.server.model
 import backend.server.v2.library.model
 
 
 def test_library_agent():
     agent = backend.server.v2.library.model.LibraryAgent(
         id="test-agent-123",
-        version=1,
-        is_active=True,
+        agent_id="agent-123",
+        agent_version=1,
+        preset_id=None,
+        updated_at=datetime.datetime.now(),
         name="Test Agent",
         description="Test description",
-        isCreatedByUser=False,
         input_schema={"type": "object", "properties": {}},
         output_schema={"type": "object", "properties": {}},
+        is_favorite=False,
+        is_created_by_user=False,
+        is_latest_version=True,
     )
     assert agent.id == "test-agent-123"
-    assert agent.version == 1
-    assert agent.is_active is True
+    assert agent.agent_id == "agent-123"
+    assert agent.agent_version == 1
     assert agent.name == "Test Agent"
     assert agent.description == "Test description"
-    assert agent.isCreatedByUser is False
+    assert agent.is_favorite is False
+    assert agent.is_created_by_user is False
+    assert agent.is_latest_version is True
     assert agent.input_schema == {"type": "object", "properties": {}}
     assert agent.output_schema == {"type": "object", "properties": {}}
 
@@ -25,19 +37,148 @@ def test_library_agent():
 def test_library_agent_with_user_created():
     agent = backend.server.v2.library.model.LibraryAgent(
         id="user-agent-456",
-        version=2,
-        is_active=True,
+        agent_id="agent-456",
+        agent_version=2,
+        preset_id=None,
+        updated_at=datetime.datetime.now(),
         name="User Created Agent",
         description="An agent created by the user",
-        isCreatedByUser=True,
         input_schema={"type": "object", "properties": {}},
         output_schema={"type": "object", "properties": {}},
+        is_favorite=False,
+        is_created_by_user=True,
+        is_latest_version=True,
     )
     assert agent.id == "user-agent-456"
-    assert agent.version == 2
-    assert agent.is_active is True
+    assert agent.agent_id == "agent-456"
+    assert agent.agent_version == 2
     assert agent.name == "User Created Agent"
     assert agent.description == "An agent created by the user"
-    assert agent.isCreatedByUser is True
+    assert agent.is_favorite is False
+    assert agent.is_created_by_user is True
+    assert agent.is_latest_version is True
     assert agent.input_schema == {"type": "object", "properties": {}}
     assert agent.output_schema == {"type": "object", "properties": {}}
+
+
+def test_library_agent_preset():
+    preset = backend.server.v2.library.model.LibraryAgentPreset(
+        id="preset-123",
+        name="Test Preset",
+        description="Test preset description",
+        agent_id="test-agent-123",
+        agent_version=1,
+        is_active=True,
+        inputs={
+            "input1": backend.data.block.BlockInput(
+                name="input1",
+                data={"type": "string", "value": "test value"},
+            )
+        },
+        updated_at=datetime.datetime.now(),
+    )
+    assert preset.id == "preset-123"
+    assert preset.name == "Test Preset"
+    assert preset.description == "Test preset description"
+    assert preset.agent_id == "test-agent-123"
+    assert preset.agent_version == 1
+    assert preset.is_active is True
+    assert preset.inputs == {
+        "input1": backend.data.block.BlockInput(
+            name="input1", data={"type": "string", "value": "test value"}
+        )
+    }
+
+
+def test_library_agent_preset_response():
+    preset = backend.server.v2.library.model.LibraryAgentPreset(
+        id="preset-123",
+        name="Test Preset",
+        description="Test preset description",
+        agent_id="test-agent-123",
+        agent_version=1,
+        is_active=True,
+        inputs={
+            "input1": backend.data.block.BlockInput(
+                name="input1",
+                data={"type": "string", "value": "test value"},
+            )
+        },
+        updated_at=datetime.datetime.now(),
+    )
+
+    pagination = backend.server.model.Pagination(
+        total_items=1, total_pages=1, current_page=1, page_size=10
+    )
+
+    response = backend.server.v2.library.model.LibraryAgentPresetResponse(
+        presets=[preset], pagination=pagination
+    )
+
+    assert len(response.presets) == 1
+    assert response.presets[0].id == "preset-123"
+    assert response.pagination.total_items == 1
+    assert response.pagination.total_pages == 1
+    assert response.pagination.current_page == 1
+    assert response.pagination.page_size == 10
+
+
+def test_create_library_agent_preset_request():
+    request = backend.server.v2.library.model.CreateLibraryAgentPresetRequest(
+        name="New Preset",
+        description="New preset description",
+        agent_id="agent-123",
+        agent_version=1,
+        is_active=True,
+        inputs={
+            "input1": backend.data.block.BlockInput(
+                name="input1",
+                data={"type": "string", "value": "test value"},
+            )
+        },
+    )
+
+    assert request.name == "New Preset"
+    assert request.description == "New preset description"
+    assert request.agent_id == "agent-123"
+    assert request.agent_version == 1
+    assert request.is_active is True
+    assert request.inputs == {
+        "input1": backend.data.block.BlockInput(
+            name="input1", data={"type": "string", "value": "test value"}
+        )
+    }
+
+
+def test_library_agent_from_db():
+    # Create mock DB agent
+    db_agent = prisma.models.AgentPreset(
+        id="test-agent-123",
+        createdAt=datetime.datetime.now(),
+        updatedAt=datetime.datetime.now(),
+        agentId="agent-123",
+        agentVersion=1,
+        name="Test Agent",
+        description="Test agent description",
+        isActive=True,
+        userId="test-user-123",
+        isDeleted=False,
+        InputPresets=[
+            prisma.models.AgentNodeExecutionInputOutput(
+                id="input-123",
+                time=datetime.datetime.now(),
+                name="input1",
+                data='{"type": "string", "value": "test value"}',
+            )
+        ],
+    )
+
+    # Convert to LibraryAgentPreset
+    agent = backend.server.v2.library.model.LibraryAgentPreset.from_db(db_agent)
+
+    assert agent.id == "test-agent-123"
+    assert agent.agent_version == 1
+    assert agent.is_active is True
+    assert agent.name == "Test Agent"
+    assert agent.description == "Test agent description"
+    assert agent.inputs == {"input1": {"type": "string", "value": "test value"}}
diff --git a/autogpt_platform/backend/backend/server/v2/library/routes.py b/autogpt_platform/backend/backend/server/v2/library/routes.py
deleted file mode 100644
index 3ee868025..000000000
--- a/autogpt_platform/backend/backend/server/v2/library/routes.py
+++ /dev/null
@@ -1,123 +0,0 @@
-import logging
-import typing
-
-import autogpt_libs.auth.depends
-import autogpt_libs.auth.middleware
-import fastapi
-import prisma
-
-import backend.data.graph
-import backend.integrations.creds_manager
-import backend.integrations.webhooks.graph_lifecycle_hooks
-import backend.server.v2.library.db
-import backend.server.v2.library.model
-
-logger = logging.getLogger(__name__)
-
-router = fastapi.APIRouter()
-integration_creds_manager = (
-    backend.integrations.creds_manager.IntegrationCredentialsManager()
-)
-
-
-@router.get(
-    "/agents",
-    tags=["library", "private"],
-    dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
-)
-async def get_library_agents(
-    user_id: typing.Annotated[
-        str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
-    ]
-) -> typing.Sequence[backend.server.v2.library.model.LibraryAgent]:
-    """
-    Get all agents in the user's library, including both created and saved agents.
-    """
-    try:
-        agents = await backend.server.v2.library.db.get_library_agents(user_id)
-        return agents
-    except Exception:
-        logger.exception("Exception occurred whilst getting library agents")
-        raise fastapi.HTTPException(
-            status_code=500, detail="Failed to get library agents"
-        )
-
-
-@router.post(
-    "/agents/{store_listing_version_id}",
-    tags=["library", "private"],
-    dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
-    status_code=201,
-)
-async def add_agent_to_library(
-    store_listing_version_id: str,
-    user_id: typing.Annotated[
-        str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
-    ],
-) -> fastapi.Response:
-    """
-    Add an agent from the store to the user's library.
-
-    Args:
-        store_listing_version_id (str): ID of the store listing version to add
-        user_id (str): ID of the authenticated user
-
-    Returns:
-        fastapi.Response: 201 status code on success
-
-    Raises:
-        HTTPException: If there is an error adding the agent to the library
-    """
-    try:
-        # Get the graph from the store listing
-        store_listing_version = (
-            await prisma.models.StoreListingVersion.prisma().find_unique(
-                where={"id": store_listing_version_id}, include={"Agent": True}
-            )
-        )
-
-        if not store_listing_version or not store_listing_version.Agent:
-            raise fastapi.HTTPException(
-                status_code=404,
-                detail=f"Store listing version {store_listing_version_id} not found",
-            )
-
-        agent = store_listing_version.Agent
-
-        if agent.userId == user_id:
-            raise fastapi.HTTPException(
-                status_code=400, detail="Cannot add own agent to library"
-            )
-
-        # Create a new graph from the template
-        graph = await backend.data.graph.get_graph(
-            agent.id, agent.version, user_id=user_id
-        )
-
-        if not graph:
-            raise fastapi.HTTPException(
-                status_code=404, detail=f"Agent {agent.id} not found"
-            )
-
-        # Create a deep copy with new IDs
-        graph.version = 1
-        graph.is_template = False
-        graph.is_active = True
-        graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
-
-        # Save the new graph
-        graph = await backend.data.graph.create_graph(graph, user_id=user_id)
-        graph = (
-            await backend.integrations.webhooks.graph_lifecycle_hooks.on_graph_activate(
-                graph,
-                get_credentials=lambda id: integration_creds_manager.get(user_id, id),
-            )
-        )
-
-        return fastapi.Response(status_code=201)
-
-    except Exception:
-        logger.exception("Exception occurred whilst adding agent to library")
-        raise fastapi.HTTPException(
-            status_code=500, detail="Failed to add agent to library"
-        )
diff --git a/autogpt_platform/backend/backend/server/v2/library/routes/__init__.py b/autogpt_platform/backend/backend/server/v2/library/routes/__init__.py
new file mode 100644
index 000000000..f62cbe7ff
--- /dev/null
+++ b/autogpt_platform/backend/backend/server/v2/library/routes/__init__.py
@@ -0,0 +1,9 @@
+import fastapi
+
+from .agents import router as agents_router
+from .presets import router as presets_router
+
+router = fastapi.APIRouter()
+
+router.include_router(presets_router)
+router.include_router(agents_router)
diff --git a/autogpt_platform/backend/backend/server/v2/library/routes/agents.py b/autogpt_platform/backend/backend/server/v2/library/routes/agents.py
new file mode 100644
index 000000000..e446e08ea
--- /dev/null
+++ b/autogpt_platform/backend/backend/server/v2/library/routes/agents.py
@@ -0,0 +1,148 @@
+import logging
+import typing
+
+import autogpt_libs.auth.depends
+import autogpt_libs.auth.middleware
+import autogpt_libs.utils.cache
+import fastapi
+
+import backend.server.v2.library.db
+import backend.server.v2.library.model
+import backend.server.v2.store.exceptions
+
+logger = logging.getLogger(__name__)
+
+router = fastapi.APIRouter()
+
+
+@router.get(
+    "/agents",
+    tags=["library", "private"],
+    dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
+)
+async def get_library_agents(
+    user_id: typing.Annotated[
+        str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
+    ]
+) -> typing.Sequence[backend.server.v2.library.model.LibraryAgent]:
+    """
+    Get all agents in the user's library, including both created and saved agents.
+    """
+    try:
+        agents = await backend.server.v2.library.db.get_library_agents(user_id)
+        return agents
+    except Exception as e:
+        logger.exception(f"Exception occurred whilst getting library agents: {e}")
+        raise fastapi.HTTPException(
+            status_code=500, detail="Failed to get library agents"
+        )
+
+
+@router.post(
+    "/agents/{store_listing_version_id}",
+    tags=["library", "private"],
+    dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
+    status_code=201,
+)
+async def add_agent_to_library(
+    store_listing_version_id: str,
+    user_id: typing.Annotated[
+        str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
+    ],
+) -> fastapi.Response:
+    """
+    Add an agent from the store to the user's library.
+
+    Args:
+        store_listing_version_id (str): ID of the store listing version to add
+        user_id (str): ID of the authenticated user
+
+    Returns:
+        fastapi.Response: 201 status code on success
+
+    Raises:
+        HTTPException: If there is an error adding the agent to the library
+    """
+    try:
+        # Use the database function to add the agent to the library
+        await backend.server.v2.library.db.add_store_agent_to_library(
+            store_listing_version_id, user_id
+        )
+        return fastapi.Response(status_code=201)
+
+    except backend.server.v2.store.exceptions.AgentNotFoundError:
+        raise fastapi.HTTPException(
+            status_code=404,
+            detail=f"Store listing version {store_listing_version_id} not found",
+        )
+    except backend.server.v2.store.exceptions.DatabaseError as e:
+        logger.exception(f"Database error occurred whilst adding agent to library: {e}")
+        raise fastapi.HTTPException(
+            status_code=500, detail="Failed to add agent to library"
+        )
+    except Exception as e:
+        logger.exception(
+            f"Unexpected exception occurred whilst adding agent to library: {e}"
+        )
+        raise fastapi.HTTPException(
+            status_code=500, detail="Failed to add agent to library"
+        )
+
+
+@router.put(
+    "/agents/{library_agent_id}",
+    tags=["library", "private"],
+    dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
+    status_code=204,
+)
+async def update_library_agent(
+    library_agent_id: str,
+    user_id: typing.Annotated[
+        str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
+    ],
+    auto_update_version: bool = False,
+    is_favorite: bool = False,
+    is_archived: bool = False,
+    is_deleted: bool = False,
+) -> fastapi.Response:
+    """
+    Update the library agent with the given fields.
+
+    Args:
+        library_agent_id (str): ID of the library agent to update
+        user_id (str): ID of the authenticated user
+        auto_update_version (bool): Whether to auto-update the agent version
+        is_favorite (bool): Whether the agent is marked as favorite
+        is_archived (bool): Whether the agent is archived
+        is_deleted (bool): Whether the agent is deleted
+
+    Returns:
+        fastapi.Response: 204 status code on success
+
+    Raises:
+        HTTPException: If there is an error updating the library agent
+    """
+    try:
+        # Use the database function to update the library agent
+        await backend.server.v2.library.db.update_library_agent(
+            library_agent_id,
+            user_id,
+            auto_update_version,
+            is_favorite,
+            is_archived,
+            is_deleted,
+        )
+        return fastapi.Response(status_code=204)
+
+    except backend.server.v2.store.exceptions.DatabaseError as e:
+        logger.exception(f"Database error occurred whilst updating library agent: {e}")
+        raise fastapi.HTTPException(
+            status_code=500, detail="Failed to update library agent"
+        )
+    except Exception as e:
+        logger.exception(
+            f"Unexpected exception occurred whilst updating library agent: {e}"
+        )
+        raise fastapi.HTTPException(
+            status_code=500, detail="Failed to update library agent"
+        )
diff --git a/autogpt_platform/backend/backend/server/v2/library/routes/presets.py b/autogpt_platform/backend/backend/server/v2/library/routes/presets.py
new file mode 100644
index 000000000..090a23231
--- /dev/null
+++ b/autogpt_platform/backend/backend/server/v2/library/routes/presets.py
@@ -0,0 +1,156 @@
+import logging
+import typing
+
+import autogpt_libs.auth.depends
+import autogpt_libs.auth.middleware
+import autogpt_libs.utils.cache
+import fastapi
+
+import backend.data.graph
+import backend.executor
+import backend.integrations.creds_manager
+import backend.integrations.webhooks.graph_lifecycle_hooks
+import backend.server.v2.library.db
+import backend.server.v2.library.model
+import backend.util.service
+
+logger = logging.getLogger(__name__)
+
+router = fastapi.APIRouter()
+integration_creds_manager = (
+    backend.integrations.creds_manager.IntegrationCredentialsManager()
+)
+
+
+@autogpt_libs.utils.cache.thread_cached
+def execution_manager_client() -> backend.executor.ExecutionManager:
+    return backend.util.service.get_service_client(backend.executor.ExecutionManager)
+
+
+@router.get("/presets")
+async def get_presets(
+    user_id: typing.Annotated[
+        str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
+    ],
+    page: int = 1,
+    page_size: int = 10,
+) -> backend.server.v2.library.model.LibraryAgentPresetResponse:
+    try:
+        presets = await backend.server.v2.library.db.get_presets(
+            user_id, page, page_size
+        )
+        return presets
+    except Exception as e:
+        logger.exception(f"Exception occurred whilst getting presets: {e}")
+        raise fastapi.HTTPException(status_code=500, detail="Failed to get presets")
+
+
+@router.get("/presets/{preset_id}")
+async def get_preset(
+    preset_id: str,
+    user_id: typing.Annotated[
+        str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
+    ],
+) -> backend.server.v2.library.model.LibraryAgentPreset:
+    try:
+        preset = await backend.server.v2.library.db.get_preset(user_id, preset_id)
+        if not preset:
+            raise fastapi.HTTPException(
+                status_code=404,
+                detail=f"Preset {preset_id} not found",
+            )
+        return preset
+    except Exception as e:
+        logger.exception(f"Exception occurred whilst getting preset: {e}")
+        raise fastapi.HTTPException(status_code=500, detail="Failed to get preset")
+
+
+@router.post("/presets")
+async def create_preset(
+    preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
+    user_id: typing.Annotated[
+        str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
+    ],
+) -> backend.server.v2.library.model.LibraryAgentPreset:
+    try:
+        return await backend.server.v2.library.db.create_or_update_preset(
+            user_id, preset
+        )
+    except Exception as e:
+        logger.exception(f"Exception occurred whilst creating preset: {e}")
+        raise fastapi.HTTPException(status_code=500, detail="Failed to create preset")
+
+
+@router.put("/presets/{preset_id}")
+async def update_preset(
+    preset_id: str,
+    preset: backend.server.v2.library.model.CreateLibraryAgentPresetRequest,
+    user_id: typing.Annotated[
+        str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
+    ],
+) -> backend.server.v2.library.model.LibraryAgentPreset:
+    try:
+        return await backend.server.v2.library.db.create_or_update_preset(
+            user_id, preset, preset_id
+        )
+    except Exception as e:
+        logger.exception(f"Exception occurred whilst updating preset: {e}")
+        raise fastapi.HTTPException(status_code=500, detail="Failed to update preset")
+
+
+@router.delete("/presets/{preset_id}")
+async def delete_preset(
+    preset_id: str,
+    user_id: typing.Annotated[
+        str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
+    ],
+):
+    try:
+        await backend.server.v2.library.db.delete_preset(user_id, preset_id)
+        return fastapi.Response(status_code=204)
+    except Exception as e:
+        logger.exception(f"Exception occurred whilst deleting preset: {e}")
+        raise fastapi.HTTPException(status_code=500, detail="Failed to delete preset")
+
+
+@router.post(
+    path="/presets/{preset_id}/execute",
+    tags=["presets"],
+    dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
+)
+async def execute_preset(
+    graph_id: str,
+    graph_version: int,
+    preset_id: str,
+    node_input: dict[typing.Any, typing.Any],
+    user_id: typing.Annotated[
+        str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
+    ],
+) -> dict[str, typing.Any]:  # FIXME: add proper return type
+    try:
+        preset = await backend.server.v2.library.db.get_preset(user_id, preset_id)
+        if not preset:
+            raise fastapi.HTTPException(status_code=404, detail="Preset not found")
+
+        logger.info(f"Preset inputs: {preset.inputs}")
+
+        updated_node_input = node_input.copy()
+        # Merge in preset input values
+        for key, value in preset.inputs.items():
+            if key not in updated_node_input:
+                updated_node_input[key] = value
+
+        execution = execution_manager_client().add_execution(
+            graph_id=graph_id,
+            graph_version=graph_version,
+            data=updated_node_input,
+            user_id=user_id,
+            preset_id=preset_id,
+        )
+
+        logger.info(f"Execution added: {execution} with input: {updated_node_input}")
+
+        return {"id": execution.graph_exec_id}
+    except Exception as e:
+        msg = e.__str__().encode().decode("unicode_escape")
+        raise fastapi.HTTPException(status_code=400, detail=msg)
diff --git a/autogpt_platform/backend/backend/server/v2/library/routes_test.py b/autogpt_platform/backend/backend/server/v2/library/routes_test.py
index d793ce13b..410f07942 100644
--- a/autogpt_platform/backend/backend/server/v2/library/routes_test.py
+++ b/autogpt_platform/backend/backend/server/v2/library/routes_test.py
@@ -1,3 +1,5 @@
+import datetime
+
 import autogpt_libs.auth.depends
 import autogpt_libs.auth.middleware
 import fastapi
@@ -35,21 +37,29 @@ def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
     mocked_value = [
         backend.server.v2.library.model.LibraryAgent(
             id="test-agent-1",
-            version=1,
-            is_active=True,
+            agent_id="test-agent-1",
+            agent_version=1,
+            preset_id="preset-1",
+            updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
+            is_favorite=False,
+            is_created_by_user=True,
+            is_latest_version=True,
             name="Test Agent 1",
             description="Test Description 1",
-            isCreatedByUser=True,
             input_schema={"type": "object", "properties": {}},
             output_schema={"type": "object", "properties": {}},
         ),
         backend.server.v2.library.model.LibraryAgent(
             id="test-agent-2",
-            version=1,
-            is_active=True,
+            agent_id="test-agent-2",
+            agent_version=1,
+            preset_id="preset-2",
+            updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
+            is_favorite=False,
+            is_created_by_user=False,
+            is_latest_version=True,
             name="Test Agent 2",
             description="Test Description 2",
-            isCreatedByUser=False,
             input_schema={"type": "object", "properties": {}},
             output_schema={"type": "object", "properties": {}},
         ),
@@ -65,10 +75,10 @@ def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
         for agent in response.json()
     ]
     assert len(data) == 2
-    assert data[0].id == "test-agent-1"
-    assert data[0].isCreatedByUser is True
-    assert data[1].id == "test-agent-2"
-    assert data[1].isCreatedByUser is False
+    assert data[0].agent_id == "test-agent-1"
+    assert data[0].is_created_by_user is True
+    assert data[1].agent_id == "test-agent-2"
+    assert data[1].is_created_by_user is False
     mock_db_call.assert_called_once_with("test-user-id")
 
 
diff --git a/autogpt_platform/backend/migrations/20250107095812_preset_soft_delete/migration.sql b/autogpt_platform/backend/migrations/20250107095812_preset_soft_delete/migration.sql
new file mode 100644
index 000000000..e85cee965
--- /dev/null
+++ b/autogpt_platform/backend/migrations/20250107095812_preset_soft_delete/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "AgentPreset" ADD COLUMN     "isDeleted" BOOLEAN NOT NULL DEFAULT false;
diff --git a/autogpt_platform/backend/migrations/20250108084101_user_agent_to_library_agent/migration.sql b/autogpt_platform/backend/migrations/20250108084101_user_agent_to_library_agent/migration.sql
new file mode 100644
index 000000000..a822161a1
--- /dev/null
+++ b/autogpt_platform/backend/migrations/20250108084101_user_agent_to_library_agent/migration.sql
@@ -0,0 +1,46 @@
+/*
+  Warnings:
+
+  - You are about to drop the `UserAgent` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "UserAgent" DROP CONSTRAINT "UserAgent_agentId_agentVersion_fkey";
+
+-- DropForeignKey
+ALTER TABLE "UserAgent" DROP CONSTRAINT "UserAgent_agentPresetId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "UserAgent" DROP CONSTRAINT "UserAgent_userId_fkey";
+
+-- DropTable
+DROP TABLE "UserAgent";
+
+-- CreateTable
+CREATE TABLE "LibraryAgent" (
+    "id" TEXT NOT NULL,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "userId" TEXT NOT NULL,
+    "agentId" TEXT NOT NULL,
+    "agentVersion" INTEGER NOT NULL,
+    "agentPresetId" TEXT,
+    "isFavorite" BOOLEAN NOT NULL DEFAULT false,
+    "isCreatedByUser" BOOLEAN NOT NULL DEFAULT false,
+    "isArchived" BOOLEAN NOT NULL DEFAULT false,
+    "isDeleted" BOOLEAN NOT NULL DEFAULT false,
+
+    CONSTRAINT "LibraryAgent_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "LibraryAgent_userId_idx" ON "LibraryAgent"("userId");
+
+-- AddForeignKey
+ALTER TABLE "LibraryAgent" ADD CONSTRAINT "LibraryAgent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "LibraryAgent" ADD CONSTRAINT "LibraryAgent_agentId_agentVersion_fkey" FOREIGN KEY ("agentId", "agentVersion") REFERENCES "AgentGraph"("id", "version") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "LibraryAgent" ADD CONSTRAINT "LibraryAgent_agentPresetId_fkey" FOREIGN KEY ("agentPresetId") REFERENCES "AgentPreset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/autogpt_platform/backend/migrations/20250108084305_use_graph_is_active_version/migration.sql b/autogpt_platform/backend/migrations/20250108084305_use_graph_is_active_version/migration.sql
new file mode 100644
index 000000000..5ffca8202
--- /dev/null
+++ b/autogpt_platform/backend/migrations/20250108084305_use_graph_is_active_version/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "LibraryAgent" ADD COLUMN     "useGraphIsActiveVersion" BOOLEAN NOT NULL DEFAULT false;
diff --git a/autogpt_platform/backend/schema.prisma b/autogpt_platform/backend/schema.prisma
index 470688aa4..4d219bf8a 100644
--- a/autogpt_platform/backend/schema.prisma
+++ b/autogpt_platform/backend/schema.prisma
@@ -30,7 +30,7 @@ model User {
   CreditTransaction    CreditTransaction[]
 
   AgentPreset AgentPreset[]
-  UserAgent   UserAgent[]
+  LibraryAgent   LibraryAgent[]
 
   Profile                     Profile[]
   StoreListing                StoreListing[]
@@ -65,7 +65,7 @@ model AgentGraph {
   AgentGraphExecution AgentGraphExecution[]
 
   AgentPreset                 AgentPreset[]
-  UserAgent                   UserAgent[]
+  LibraryAgent                   LibraryAgent[]
   StoreListing                StoreListing[]
   StoreListingVersion         StoreListingVersion?
 
@@ -103,15 +103,17 @@ model AgentPreset {
   Agent        AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
 
   InputPresets           AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
-  UserAgents             UserAgent[]
+  LibraryAgents             LibraryAgent[]
   AgentExecution         AgentGraphExecution[]
 
+  isDeleted Boolean @default(false)
+
   @@index([userId])
 }
 
 // For the library page
 // It is a user controlled list of agents, that they will see in there library
-model UserAgent {
+model LibraryAgent {
   id        String   @id @default(uuid())
   createdAt DateTime @default(now())
   updatedAt DateTime @default(now()) @updatedAt
@@ -126,6 +128,8 @@ model UserAgent {
   agentPresetId String?
   AgentPreset   AgentPreset? @relation(fields: [agentPresetId], references: [id])
 
+  useGraphIsActiveVersion Boolean @default(false)
+
   isFavorite      Boolean @default(false)
   isCreatedByUser Boolean @default(false)
   isArchived      Boolean @default(false)
diff --git a/autogpt_platform/backend/test/executor/test_manager.py b/autogpt_platform/backend/test/executor/test_manager.py
index e85712545..456500466 100644
--- a/autogpt_platform/backend/test/executor/test_manager.py
+++ b/autogpt_platform/backend/test/executor/test_manager.py
@@ -5,8 +5,9 @@ import fastapi.responses
 import pytest
 from prisma.models import User
 
+import backend.server.v2.library.model
 import backend.server.v2.store.model
-from backend.blocks.basic import FindInDictionaryBlock, StoreValueBlock
+from backend.blocks.basic import AgentInputBlock, FindInDictionaryBlock, StoreValueBlock
 from backend.blocks.maths import CalculatorBlock, Operation
 from backend.data import execution, graph
 from backend.server.model import CreateGraph
@@ -292,6 +293,193 @@ async def test_static_input_link_on_graph(server: SpinTestServer):
     logger.info("Completed test_static_input_link_on_graph")
 
 
+@pytest.mark.asyncio(scope="session")
+async def test_execute_preset(server: SpinTestServer):
+    """
+    Test executing a preset.
+
+    This test ensures that:
+    1. A preset can be successfully executed
+    2. The execution results are correct
+
+    Args:
+        server (SpinTestServer): The test server instance.
+    """
+    # Create test graph and user
+    nodes = [
+        graph.Node(  # 0
+            block_id=AgentInputBlock().id,
+            input_default={"name": "dictionary"},
+        ),
+        graph.Node(  # 1
+            block_id=AgentInputBlock().id,
+            input_default={"name": "selected_value"},
+        ),
+        graph.Node(  # 2
+            block_id=StoreValueBlock().id,
+            input_default={"input": {"key1": "Hi", "key2": "Everyone"}},
+        ),
+        graph.Node(  # 3
+            block_id=FindInDictionaryBlock().id,
+            input_default={"key": "", "input": {}},
+        ),
+    ]
+    links = [
+        graph.Link(
+            source_id=nodes[0].id,
+            sink_id=nodes[2].id,
+            source_name="result",
+            sink_name="input",
+        ),
+        graph.Link(
+            source_id=nodes[1].id,
+            sink_id=nodes[3].id,
+            source_name="result",
+            sink_name="key",
+        ),
+        graph.Link(
+            source_id=nodes[2].id,
+            sink_id=nodes[3].id,
+            source_name="output",
+            sink_name="input",
+        ),
+    ]
+    test_graph = graph.Graph(
+        name="TestGraph",
+        description="Test graph",
+        nodes=nodes,
+        links=links,
+    )
+    test_user = await create_test_user()
+    test_graph = await create_graph(server, test_graph, test_user)
+
+    # Create preset with initial values
+    preset = backend.server.v2.library.model.CreateLibraryAgentPresetRequest(
+        name="Test Preset With Clash",
+        description="Test preset with clashing input values",
+        agent_id=test_graph.id,
+        agent_version=test_graph.version,
+        inputs={
+            "dictionary": {"key1": "Hello", "key2": "World"},
+            "selected_value": "key2",
+        },
+        is_active=True,
+    )
+    created_preset = await server.agent_server.test_create_preset(preset, test_user.id)
+
+    # Execute preset with overriding values
+    result = await server.agent_server.test_execute_preset(
+        graph_id=test_graph.id,
+        graph_version=test_graph.version,
+        preset_id=created_preset.id,
+        node_input={},
+        user_id=test_user.id,
+    )
+
+    # Verify execution
+    assert result is not None
+    graph_exec_id = result["id"]
+
+    # Wait for execution to complete
+    executions = await wait_execution(test_user.id, test_graph.id, graph_exec_id)
+    assert len(executions) == 4
+
+    # FindInDictionaryBlock should wait for the input pin to be provided,
+    # Hence executing extraction of "key" from {"key1": "value1", "key2": "value2"}
+    assert executions[3].status == execution.ExecutionStatus.COMPLETED
+    assert executions[3].output_data == {"output": ["World"]}
+
+
+@pytest.mark.asyncio(scope="session")
+async def test_execute_preset_with_clash(server: SpinTestServer):
+    """
+    Test executing a preset with clashing input data.
+    """
+    # Create test graph and user
+    nodes = [
+        graph.Node(  # 0
+            block_id=AgentInputBlock().id,
+            input_default={"name": "dictionary"},
+        ),
+        graph.Node(  # 1
+            block_id=AgentInputBlock().id,
+            input_default={"name": "selected_value"},
+        ),
+        graph.Node(  # 2
+            block_id=StoreValueBlock().id,
+            input_default={"input": {"key1": "Hi", "key2": "Everyone"}},
+        ),
+        graph.Node(  # 3
+            block_id=FindInDictionaryBlock().id,
+            input_default={"key": "", "input": {}},
+        ),
+    ]
+    links = [
+        graph.Link(
+            source_id=nodes[0].id,
+            sink_id=nodes[2].id,
+            source_name="result",
+            sink_name="input",
+        ),
+        graph.Link(
+            source_id=nodes[1].id,
+            sink_id=nodes[3].id,
+            source_name="result",
+            sink_name="key",
+        ),
+        graph.Link(
+            source_id=nodes[2].id,
+            sink_id=nodes[3].id,
+            source_name="output",
+            sink_name="input",
+        ),
+    ]
+    test_graph = graph.Graph(
+        name="TestGraph",
+        description="Test graph",
+        nodes=nodes,
+        links=links,
+    )
+    test_user = await create_test_user()
+    test_graph = await create_graph(server, test_graph, test_user)
+
+    # Create preset with initial values
+    preset = backend.server.v2.library.model.CreateLibraryAgentPresetRequest(
+        name="Test Preset With Clash",
+        description="Test preset with clashing input values",
+        agent_id=test_graph.id,
+        agent_version=test_graph.version,
+        inputs={
+            "dictionary": {"key1": "Hello", "key2": "World"},
+            "selected_value": "key2",
+        },
+        is_active=True,
+    )
+    created_preset = await server.agent_server.test_create_preset(preset, test_user.id)
+
+    # Execute preset with overriding values
+    result = await server.agent_server.test_execute_preset(
+        graph_id=test_graph.id,
+        graph_version=test_graph.version,
+        preset_id=created_preset.id,
+        node_input={"selected_value": "key1"},
+        user_id=test_user.id,
+    )
+
+    # Verify execution
+    assert result is not None
+    graph_exec_id = result["id"]
+
+    # Wait for execution to complete
+    executions = await wait_execution(test_user.id, test_graph.id, graph_exec_id)
+    assert len(executions) == 4
+
+    # FindInDictionaryBlock should wait for the input pin to be provided,
+    # Hence executing extraction of "key" from {"key1": "value1", "key2": "value2"}
+    assert executions[3].status == execution.ExecutionStatus.COMPLETED
+    assert executions[3].output_data == {"output": ["Hello"]}
+
+
 @pytest.mark.asyncio(scope="session")
 async def test_store_listing_graph(server: SpinTestServer):
     logger.info("Starting test_agent_execution")
diff --git a/autogpt_platform/backend/test/test_data_creator.py b/autogpt_platform/backend/test/test_data_creator.py
index 1f79386cc..3ae581892 100644
--- a/autogpt_platform/backend/test/test_data_creator.py
+++ b/autogpt_platform/backend/test/test_data_creator.py
@@ -140,10 +140,10 @@ async def main():
     print(f"Inserting {NUM_USERS * MAX_AGENTS_PER_USER} user agents")
     for user in users:
         num_agents = random.randint(MIN_AGENTS_PER_USER, MAX_AGENTS_PER_USER)
-        for _ in range(num_agents):  # Create 1 UserAgent per user
+        for _ in range(num_agents):  # Create 1 LibraryAgent per user
             graph = random.choice(agent_graphs)
             preset = random.choice(agent_presets)
-            user_agent = await db.useragent.create(
+            user_agent = await db.libraryagent.create(
                 data={
                     "userId": user.id,
                     "agentId": graph.id,