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>
This commit is contained in:
parent
00bb7c67b3
commit
4b17cc9963
|
@ -136,6 +136,7 @@ async def create_graph_execution(
|
||||||
graph_version: int,
|
graph_version: int,
|
||||||
nodes_input: list[tuple[str, BlockInput]],
|
nodes_input: list[tuple[str, BlockInput]],
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
preset_id: str | None = None,
|
||||||
) -> tuple[str, list[ExecutionResult]]:
|
) -> tuple[str, list[ExecutionResult]]:
|
||||||
"""
|
"""
|
||||||
Create a new AgentGraphExecution record.
|
Create a new AgentGraphExecution record.
|
||||||
|
@ -163,6 +164,7 @@ async def create_graph_execution(
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"userId": user_id,
|
"userId": user_id,
|
||||||
|
"agentPresetId": preset_id,
|
||||||
},
|
},
|
||||||
include=GRAPH_EXECUTION_INCLUDE,
|
include=GRAPH_EXECUTION_INCLUDE,
|
||||||
)
|
)
|
||||||
|
|
|
@ -781,6 +781,7 @@ class ExecutionManager(AppService):
|
||||||
data: BlockInput,
|
data: BlockInput,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
graph_version: int,
|
graph_version: int,
|
||||||
|
preset_id: str | None = None,
|
||||||
) -> GraphExecutionEntry:
|
) -> GraphExecutionEntry:
|
||||||
graph: GraphModel | None = self.db_client.get_graph(
|
graph: GraphModel | None = self.db_client.get_graph(
|
||||||
graph_id=graph_id, user_id=user_id, version=graph_version
|
graph_id=graph_id, user_id=user_id, version=graph_version
|
||||||
|
@ -829,6 +830,7 @@ class ExecutionManager(AppService):
|
||||||
graph_version=graph.version,
|
graph_version=graph.version,
|
||||||
nodes_input=nodes_input,
|
nodes_input=nodes_input,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
preset_id=preset_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
starting_node_execs = []
|
starting_node_execs = []
|
||||||
|
|
|
@ -56,3 +56,18 @@ class SetGraphActiveVersion(pydantic.BaseModel):
|
||||||
|
|
||||||
class UpdatePermissionsRequest(pydantic.BaseModel):
|
class UpdatePermissionsRequest(pydantic.BaseModel):
|
||||||
permissions: List[APIKeyPermission]
|
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]
|
||||||
|
)
|
||||||
|
|
|
@ -17,6 +17,7 @@ import backend.data.db
|
||||||
import backend.data.graph
|
import backend.data.graph
|
||||||
import backend.data.user
|
import backend.data.user
|
||||||
import backend.server.routers.v1
|
import backend.server.routers.v1
|
||||||
|
import backend.server.v2.library.model
|
||||||
import backend.server.v2.library.routes
|
import backend.server.v2.library.routes
|
||||||
import backend.server.v2.store.model
|
import backend.server.v2.store.model
|
||||||
import backend.server.v2.store.routes
|
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):
|
async def test_delete_graph(graph_id: str, user_id: str):
|
||||||
return await backend.server.routers.v1.delete_graph(graph_id, user_id)
|
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
|
@staticmethod
|
||||||
async def test_create_store_listing(
|
async def test_create_store_listing(
|
||||||
request: backend.server.v2.store.model.StoreSubmissionRequest, user_id: str
|
request: backend.server.v2.store.model.StoreSubmissionRequest, user_id: str
|
||||||
|
|
|
@ -13,6 +13,7 @@ from typing_extensions import Optional, TypedDict
|
||||||
import backend.data.block
|
import backend.data.block
|
||||||
import backend.server.integrations.router
|
import backend.server.integrations.router
|
||||||
import backend.server.routers.analytics
|
import backend.server.routers.analytics
|
||||||
|
import backend.server.v2.library.db
|
||||||
from backend.data import execution as execution_db
|
from backend.data import execution as execution_db
|
||||||
from backend.data import graph as graph_db
|
from backend.data import graph as graph_db
|
||||||
from backend.data.api_key import (
|
from backend.data.api_key import (
|
||||||
|
@ -180,11 +181,6 @@ async def get_graph(
|
||||||
tags=["graphs"],
|
tags=["graphs"],
|
||||||
dependencies=[Depends(auth_middleware)],
|
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(
|
async def get_graph_all_versions(
|
||||||
graph_id: str, user_id: Annotated[str, Depends(get_user_id)]
|
graph_id: str, user_id: Annotated[str, Depends(get_user_id)]
|
||||||
) -> Sequence[graph_db.GraphModel]:
|
) -> Sequence[graph_db.GraphModel]:
|
||||||
|
@ -223,6 +219,13 @@ async def do_create_graph(
|
||||||
400, detail=f"Template #{create_graph.template_id} not found"
|
400, detail=f"Template #{create_graph.template_id} not found"
|
||||||
)
|
)
|
||||||
graph.version = 1
|
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:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400, detail="Either graph or template_id must be provided."
|
status_code=400, detail="Either graph or template_id must be provided."
|
||||||
|
@ -257,11 +260,6 @@ async def delete_graph(
|
||||||
@v1_router.put(
|
@v1_router.put(
|
||||||
path="/graphs/{graph_id}", tags=["graphs"], dependencies=[Depends(auth_middleware)]
|
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(
|
async def update_graph(
|
||||||
graph_id: str,
|
graph_id: str,
|
||||||
graph: graph_db.Graph,
|
graph: graph_db.Graph,
|
||||||
|
@ -294,6 +292,11 @@ async def update_graph(
|
||||||
|
|
||||||
if new_graph_version.is_active:
|
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":
|
def get_credentials(credentials_id: str) -> "Credentials | None":
|
||||||
return integration_creds_manager.get(user_id, credentials_id)
|
return integration_creds_manager.get(user_id, credentials_id)
|
||||||
|
|
||||||
|
@ -349,6 +352,12 @@ async def set_graph_active_version(
|
||||||
version=new_active_version,
|
version=new_active_version,
|
||||||
user_id=user_id,
|
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:
|
if current_active_graph and current_active_graph.version != new_active_version:
|
||||||
# Handle deactivation of the previously active version
|
# Handle deactivation of the previously active version
|
||||||
await on_graph_deactivate(
|
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)
|
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 ########################
|
##################### Schedules ########################
|
||||||
########################################################
|
########################################################
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import prisma.errors
|
import prisma.errors
|
||||||
import prisma.models
|
import prisma.models
|
||||||
|
@ -7,6 +7,7 @@ import prisma.types
|
||||||
|
|
||||||
import backend.data.graph
|
import backend.data.graph
|
||||||
import backend.data.includes
|
import backend.data.includes
|
||||||
|
import backend.server.model
|
||||||
import backend.server.v2.library.model
|
import backend.server.v2.library.model
|
||||||
import backend.server.v2.store.exceptions
|
import backend.server.v2.store.exceptions
|
||||||
|
|
||||||
|
@ -14,90 +15,152 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def get_library_agents(
|
async def get_library_agents(
|
||||||
user_id: str,
|
user_id: str, search_query: str | None = None
|
||||||
) -> List[backend.server.v2.library.model.LibraryAgent]:
|
) -> list[backend.server.v2.library.model.LibraryAgent]:
|
||||||
"""
|
logger.debug(
|
||||||
Returns all agents (AgentGraph) that belong to the user and all agents in their library (UserAgent table)
|
f"Fetching library agents for user_id={user_id} search_query={search_query}"
|
||||||
"""
|
|
||||||
logger.debug(f"Getting library agents for user {user_id}")
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get agents in user's library with nodes and links
|
if search_query and len(search_query.strip()) > 100:
|
||||||
library_agents = await prisma.models.UserAgent.prisma().find_many(
|
logger.warning(f"Search query too long: {search_query}")
|
||||||
where=prisma.types.UserAgentWhereInput(
|
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||||
userId=user_id, isDeleted=False, isArchived=False
|
"Search query is too long."
|
||||||
),
|
)
|
||||||
include={
|
|
||||||
|
where_clause = prisma.types.LibraryAgentWhereInput(
|
||||||
|
userId=user_id,
|
||||||
|
isDeleted=False,
|
||||||
|
isArchived=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if search_query:
|
||||||
|
where_clause["OR"] = [
|
||||||
|
{
|
||||||
"Agent": {
|
"Agent": {
|
||||||
"include": {
|
"is": {"name": {"contains": search_query, "mode": "insensitive"}}
|
||||||
"AgentNodes": {
|
|
||||||
"include": {
|
|
||||||
"Input": True,
|
|
||||||
"Output": True,
|
|
||||||
"Webhook": True,
|
|
||||||
"AgentBlock": True,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Agent": {
|
||||||
|
"is": {
|
||||||
|
"description": {"contains": search_query, "mode": "insensitive"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
]
|
||||||
|
|
||||||
# Convert to Graph models first
|
|
||||||
graphs = []
|
|
||||||
|
|
||||||
# Add user created agents
|
|
||||||
for agent in user_created:
|
|
||||||
try:
|
try:
|
||||||
graphs.append(backend.data.graph.GraphModel.from_db(agent))
|
library_agents = await prisma.models.LibraryAgent.prisma().find_many(
|
||||||
except Exception as e:
|
where=where_clause,
|
||||||
logger.error(f"Error processing user created agent {agent.id}: {e}")
|
include={
|
||||||
continue
|
"Agent": {
|
||||||
|
"include": {
|
||||||
# Add library agents
|
"AgentNodes": {"include": {"Input": True, "Output": True}}
|
||||||
for agent in library_agents:
|
}
|
||||||
if agent.Agent:
|
}
|
||||||
try:
|
},
|
||||||
graphs.append(backend.data.graph.GraphModel.from_db(agent.Agent))
|
order=[{"updatedAt": "desc"}],
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error processing library agent {agent.agentId}: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 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,
|
|
||||||
)
|
)
|
||||||
)
|
logger.debug(f"Retrieved {len(library_agents)} agents for user_id={user_id}.")
|
||||||
|
return [
|
||||||
logger.debug(f"Found {len(result)} library agents")
|
backend.server.v2.library.model.LibraryAgent.from_db(agent)
|
||||||
return result
|
for agent in library_agents
|
||||||
|
]
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error getting library agents: {str(e)}")
|
logger.error(f"Database error fetching library agents: {e}")
|
||||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||||
"Failed to fetch library agents"
|
"Unable to fetch library agents."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return library_agent
|
||||||
|
except prisma.errors.PrismaError as e:
|
||||||
|
logger.error(f"Database error creating agent to library: {str(e)}")
|
||||||
|
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||||
|
"Failed to create agent to library"
|
||||||
) from e
|
) 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
|
if they don't already have it
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
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
|
# 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={
|
where={
|
||||||
"userId": user_id,
|
"userId": user_id,
|
||||||
"agentId": agent.id,
|
"agentId": agent.id,
|
||||||
|
@ -145,9 +208,9 @@ async def add_agent_to_library(store_listing_version_id: str, user_id: str) -> N
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create UserAgent entry
|
# Create LibraryAgent entry
|
||||||
await prisma.models.UserAgent.prisma().create(
|
await prisma.models.LibraryAgent.prisma().create(
|
||||||
data=prisma.types.UserAgentCreateInput(
|
data=prisma.types.LibraryAgentCreateInput(
|
||||||
userId=user_id,
|
userId=user_id,
|
||||||
agentId=agent.id,
|
agentId=agent.id,
|
||||||
agentVersion=agent.version,
|
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(
|
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||||
"Failed to add agent to library"
|
"Failed to add agent to library"
|
||||||
) from e
|
) 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
|
||||||
|
|
|
@ -37,7 +37,7 @@ async def test_get_library_agents(mocker):
|
||||||
]
|
]
|
||||||
|
|
||||||
mock_library_agents = [
|
mock_library_agents = [
|
||||||
prisma.models.UserAgent(
|
prisma.models.LibraryAgent(
|
||||||
id="ua1",
|
id="ua1",
|
||||||
userId="test-user",
|
userId="test-user",
|
||||||
agentId="agent2",
|
agentId="agent2",
|
||||||
|
@ -48,6 +48,7 @@ async def test_get_library_agents(mocker):
|
||||||
createdAt=datetime.now(),
|
createdAt=datetime.now(),
|
||||||
updatedAt=datetime.now(),
|
updatedAt=datetime.now(),
|
||||||
isFavorite=False,
|
isFavorite=False,
|
||||||
|
useGraphIsActiveVersion=True,
|
||||||
Agent=prisma.models.AgentGraph(
|
Agent=prisma.models.AgentGraph(
|
||||||
id="agent2",
|
id="agent2",
|
||||||
version=1,
|
version=1,
|
||||||
|
@ -67,8 +68,8 @@ async def test_get_library_agents(mocker):
|
||||||
return_value=mock_user_created
|
return_value=mock_user_created
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_user_agent = mocker.patch("prisma.models.UserAgent.prisma")
|
mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma")
|
||||||
mock_user_agent.return_value.find_many = mocker.AsyncMock(
|
mock_library_agent.return_value.find_many = mocker.AsyncMock(
|
||||||
return_value=mock_library_agents
|
return_value=mock_library_agents
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -76,40 +77,16 @@ async def test_get_library_agents(mocker):
|
||||||
result = await db.get_library_agents("test-user")
|
result = await db.get_library_agents("test-user")
|
||||||
|
|
||||||
# Verify results
|
# Verify results
|
||||||
assert len(result) == 2
|
assert len(result) == 1
|
||||||
assert result[0].id == "agent1"
|
assert result[0].id == "ua1"
|
||||||
assert result[0].name == "Test Agent 1"
|
assert result[0].name == "Test Agent 2"
|
||||||
assert result[0].description == "Test Description 1"
|
assert result[0].description == "Test Description 2"
|
||||||
assert result[0].isCreatedByUser is True
|
assert result[0].is_created_by_user is False
|
||||||
assert result[1].id == "agent2"
|
assert result[0].is_latest_version is True
|
||||||
assert result[1].name == "Test Agent 2"
|
assert result[0].is_favorite is False
|
||||||
assert result[1].description == "Test Description 2"
|
assert result[0].agent_id == "agent2"
|
||||||
assert result[1].isCreatedByUser is False
|
assert result[0].agent_version == 1
|
||||||
|
assert result[0].preset_id is None
|
||||||
# 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
@ -152,26 +129,26 @@ async def test_add_agent_to_library(mocker):
|
||||||
return_value=mock_store_listing
|
return_value=mock_store_listing
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_user_agent = mocker.patch("prisma.models.UserAgent.prisma")
|
mock_library_agent = mocker.patch("prisma.models.LibraryAgent.prisma")
|
||||||
mock_user_agent.return_value.find_first = mocker.AsyncMock(return_value=None)
|
mock_library_agent.return_value.find_first = mocker.AsyncMock(return_value=None)
|
||||||
mock_user_agent.return_value.create = mocker.AsyncMock()
|
mock_library_agent.return_value.create = mocker.AsyncMock()
|
||||||
|
|
||||||
# Call function
|
# 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
|
# Verify mocks called correctly
|
||||||
mock_store_listing_version.return_value.find_unique.assert_called_once_with(
|
mock_store_listing_version.return_value.find_unique.assert_called_once_with(
|
||||||
where={"id": "version123"}, include={"Agent": True}
|
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={
|
where={
|
||||||
"userId": "test-user",
|
"userId": "test-user",
|
||||||
"agentId": "agent1",
|
"agentId": "agent1",
|
||||||
"agentVersion": 1,
|
"agentVersion": 1,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
mock_user_agent.return_value.create.assert_called_once_with(
|
mock_library_agent.return_value.create.assert_called_once_with(
|
||||||
data=prisma.types.UserAgentCreateInput(
|
data=prisma.types.LibraryAgentCreateInput(
|
||||||
userId="test-user", agentId="agent1", agentVersion=1, isCreatedByUser=False
|
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
|
# Call function and verify exception
|
||||||
with pytest.raises(backend.server.v2.store.exceptions.AgentNotFoundError):
|
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
|
# Verify mock called correctly
|
||||||
mock_store_listing_version.return_value.find_unique.assert_called_once_with(
|
mock_store_listing_version.return_value.find_unique.assert_called_once_with(
|
||||||
|
|
|
@ -1,16 +1,111 @@
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
import prisma.models
|
||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
|
import backend.data.block
|
||||||
|
import backend.data.graph
|
||||||
|
import backend.server.model
|
||||||
|
|
||||||
|
|
||||||
class LibraryAgent(pydantic.BaseModel):
|
class LibraryAgent(pydantic.BaseModel):
|
||||||
id: str # Changed from agent_id to match GraphMeta
|
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
|
name: str
|
||||||
description: str
|
description: str
|
||||||
|
|
||||||
isCreatedByUser: bool
|
|
||||||
# Made input_schema and output_schema match GraphMeta's type
|
# Made input_schema and output_schema match GraphMeta's type
|
||||||
input_schema: dict[str, typing.Any] # Should be BlockIOObjectSubSchema in frontend
|
input_schema: dict[str, typing.Any] # Should be BlockIOObjectSubSchema in frontend
|
||||||
output_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
|
||||||
|
|
|
@ -1,23 +1,35 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import prisma.models
|
||||||
|
|
||||||
|
import backend.data.block
|
||||||
|
import backend.server.model
|
||||||
import backend.server.v2.library.model
|
import backend.server.v2.library.model
|
||||||
|
|
||||||
|
|
||||||
def test_library_agent():
|
def test_library_agent():
|
||||||
agent = backend.server.v2.library.model.LibraryAgent(
|
agent = backend.server.v2.library.model.LibraryAgent(
|
||||||
id="test-agent-123",
|
id="test-agent-123",
|
||||||
version=1,
|
agent_id="agent-123",
|
||||||
is_active=True,
|
agent_version=1,
|
||||||
|
preset_id=None,
|
||||||
|
updated_at=datetime.datetime.now(),
|
||||||
name="Test Agent",
|
name="Test Agent",
|
||||||
description="Test description",
|
description="Test description",
|
||||||
isCreatedByUser=False,
|
|
||||||
input_schema={"type": "object", "properties": {}},
|
input_schema={"type": "object", "properties": {}},
|
||||||
output_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.id == "test-agent-123"
|
||||||
assert agent.version == 1
|
assert agent.agent_id == "agent-123"
|
||||||
assert agent.is_active is True
|
assert agent.agent_version == 1
|
||||||
assert agent.name == "Test Agent"
|
assert agent.name == "Test Agent"
|
||||||
assert agent.description == "Test description"
|
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.input_schema == {"type": "object", "properties": {}}
|
||||||
assert agent.output_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():
|
def test_library_agent_with_user_created():
|
||||||
agent = backend.server.v2.library.model.LibraryAgent(
|
agent = backend.server.v2.library.model.LibraryAgent(
|
||||||
id="user-agent-456",
|
id="user-agent-456",
|
||||||
version=2,
|
agent_id="agent-456",
|
||||||
is_active=True,
|
agent_version=2,
|
||||||
|
preset_id=None,
|
||||||
|
updated_at=datetime.datetime.now(),
|
||||||
name="User Created Agent",
|
name="User Created Agent",
|
||||||
description="An agent created by the user",
|
description="An agent created by the user",
|
||||||
isCreatedByUser=True,
|
|
||||||
input_schema={"type": "object", "properties": {}},
|
input_schema={"type": "object", "properties": {}},
|
||||||
output_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.id == "user-agent-456"
|
||||||
assert agent.version == 2
|
assert agent.agent_id == "agent-456"
|
||||||
assert agent.is_active is True
|
assert agent.agent_version == 2
|
||||||
assert agent.name == "User Created Agent"
|
assert agent.name == "User Created Agent"
|
||||||
assert agent.description == "An agent created by the user"
|
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.input_schema == {"type": "object", "properties": {}}
|
||||||
assert agent.output_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"}}
|
||||||
|
|
|
@ -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"
|
|
||||||
)
|
|
|
@ -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)
|
|
@ -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"
|
||||||
|
)
|
|
@ -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)
|
|
@ -1,3 +1,5 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
import autogpt_libs.auth.depends
|
import autogpt_libs.auth.depends
|
||||||
import autogpt_libs.auth.middleware
|
import autogpt_libs.auth.middleware
|
||||||
import fastapi
|
import fastapi
|
||||||
|
@ -35,21 +37,29 @@ def test_get_library_agents_success(mocker: pytest_mock.MockFixture):
|
||||||
mocked_value = [
|
mocked_value = [
|
||||||
backend.server.v2.library.model.LibraryAgent(
|
backend.server.v2.library.model.LibraryAgent(
|
||||||
id="test-agent-1",
|
id="test-agent-1",
|
||||||
version=1,
|
agent_id="test-agent-1",
|
||||||
is_active=True,
|
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",
|
name="Test Agent 1",
|
||||||
description="Test Description 1",
|
description="Test Description 1",
|
||||||
isCreatedByUser=True,
|
|
||||||
input_schema={"type": "object", "properties": {}},
|
input_schema={"type": "object", "properties": {}},
|
||||||
output_schema={"type": "object", "properties": {}},
|
output_schema={"type": "object", "properties": {}},
|
||||||
),
|
),
|
||||||
backend.server.v2.library.model.LibraryAgent(
|
backend.server.v2.library.model.LibraryAgent(
|
||||||
id="test-agent-2",
|
id="test-agent-2",
|
||||||
version=1,
|
agent_id="test-agent-2",
|
||||||
is_active=True,
|
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",
|
name="Test Agent 2",
|
||||||
description="Test Description 2",
|
description="Test Description 2",
|
||||||
isCreatedByUser=False,
|
|
||||||
input_schema={"type": "object", "properties": {}},
|
input_schema={"type": "object", "properties": {}},
|
||||||
output_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()
|
for agent in response.json()
|
||||||
]
|
]
|
||||||
assert len(data) == 2
|
assert len(data) == 2
|
||||||
assert data[0].id == "test-agent-1"
|
assert data[0].agent_id == "test-agent-1"
|
||||||
assert data[0].isCreatedByUser is True
|
assert data[0].is_created_by_user is True
|
||||||
assert data[1].id == "test-agent-2"
|
assert data[1].agent_id == "test-agent-2"
|
||||||
assert data[1].isCreatedByUser is False
|
assert data[1].is_created_by_user is False
|
||||||
mock_db_call.assert_called_once_with("test-user-id")
|
mock_db_call.assert_called_once_with("test-user-id")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "AgentPreset" ADD COLUMN "isDeleted" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LibraryAgent" ADD COLUMN "useGraphIsActiveVersion" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -30,7 +30,7 @@ model User {
|
||||||
CreditTransaction CreditTransaction[]
|
CreditTransaction CreditTransaction[]
|
||||||
|
|
||||||
AgentPreset AgentPreset[]
|
AgentPreset AgentPreset[]
|
||||||
UserAgent UserAgent[]
|
LibraryAgent LibraryAgent[]
|
||||||
|
|
||||||
Profile Profile[]
|
Profile Profile[]
|
||||||
StoreListing StoreListing[]
|
StoreListing StoreListing[]
|
||||||
|
@ -65,7 +65,7 @@ model AgentGraph {
|
||||||
AgentGraphExecution AgentGraphExecution[]
|
AgentGraphExecution AgentGraphExecution[]
|
||||||
|
|
||||||
AgentPreset AgentPreset[]
|
AgentPreset AgentPreset[]
|
||||||
UserAgent UserAgent[]
|
LibraryAgent LibraryAgent[]
|
||||||
StoreListing StoreListing[]
|
StoreListing StoreListing[]
|
||||||
StoreListingVersion StoreListingVersion?
|
StoreListingVersion StoreListingVersion?
|
||||||
|
|
||||||
|
@ -103,15 +103,17 @@ model AgentPreset {
|
||||||
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
|
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
|
||||||
|
|
||||||
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
|
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
|
||||||
UserAgents UserAgent[]
|
LibraryAgents LibraryAgent[]
|
||||||
AgentExecution AgentGraphExecution[]
|
AgentExecution AgentGraphExecution[]
|
||||||
|
|
||||||
|
isDeleted Boolean @default(false)
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
// For the library page
|
// For the library page
|
||||||
// It is a user controlled list of agents, that they will see in there library
|
// It is a user controlled list of agents, that they will see in there library
|
||||||
model UserAgent {
|
model LibraryAgent {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
@ -126,6 +128,8 @@ model UserAgent {
|
||||||
agentPresetId String?
|
agentPresetId String?
|
||||||
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
|
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
|
||||||
|
|
||||||
|
useGraphIsActiveVersion Boolean @default(false)
|
||||||
|
|
||||||
isFavorite Boolean @default(false)
|
isFavorite Boolean @default(false)
|
||||||
isCreatedByUser Boolean @default(false)
|
isCreatedByUser Boolean @default(false)
|
||||||
isArchived Boolean @default(false)
|
isArchived Boolean @default(false)
|
||||||
|
|
|
@ -5,8 +5,9 @@ import fastapi.responses
|
||||||
import pytest
|
import pytest
|
||||||
from prisma.models import User
|
from prisma.models import User
|
||||||
|
|
||||||
|
import backend.server.v2.library.model
|
||||||
import backend.server.v2.store.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.blocks.maths import CalculatorBlock, Operation
|
||||||
from backend.data import execution, graph
|
from backend.data import execution, graph
|
||||||
from backend.server.model import CreateGraph
|
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")
|
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")
|
@pytest.mark.asyncio(scope="session")
|
||||||
async def test_store_listing_graph(server: SpinTestServer):
|
async def test_store_listing_graph(server: SpinTestServer):
|
||||||
logger.info("Starting test_agent_execution")
|
logger.info("Starting test_agent_execution")
|
||||||
|
|
|
@ -140,10 +140,10 @@ async def main():
|
||||||
print(f"Inserting {NUM_USERS * MAX_AGENTS_PER_USER} user agents")
|
print(f"Inserting {NUM_USERS * MAX_AGENTS_PER_USER} user agents")
|
||||||
for user in users:
|
for user in users:
|
||||||
num_agents = random.randint(MIN_AGENTS_PER_USER, MAX_AGENTS_PER_USER)
|
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)
|
graph = random.choice(agent_graphs)
|
||||||
preset = random.choice(agent_presets)
|
preset = random.choice(agent_presets)
|
||||||
user_agent = await db.useragent.create(
|
user_agent = await db.libraryagent.create(
|
||||||
data={
|
data={
|
||||||
"userId": user.id,
|
"userId": user.id,
|
||||||
"agentId": graph.id,
|
"agentId": graph.id,
|
||||||
|
|
Loading…
Reference in New Issue