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 a48b416de437..d793ce13b6da 100644 --- a/autogpt_platform/backend/backend/server/v2/library/routes_test.py +++ b/autogpt_platform/backend/backend/server/v2/library/routes_test.py @@ -2,6 +2,7 @@ import autogpt_libs.auth.middleware import fastapi import fastapi.testclient +import pytest import pytest_mock import backend.server.v2.library.db @@ -80,6 +81,7 @@ def test_get_library_agents_error(mocker: pytest_mock.MockFixture): mock_db_call.assert_called_once_with("test-user-id") +@pytest.mark.skip(reason="Mocker Not implemented") def test_add_agent_to_library_success(mocker: pytest_mock.MockFixture): mock_db_call = mocker.patch("backend.server.v2.library.db.add_agent_to_library") mock_db_call.return_value = None @@ -91,6 +93,7 @@ def test_add_agent_to_library_success(mocker: pytest_mock.MockFixture): ) +@pytest.mark.skip(reason="Mocker Not implemented") def test_add_agent_to_library_error(mocker: pytest_mock.MockFixture): mock_db_call = mocker.patch("backend.server.v2.library.db.add_agent_to_library") mock_db_call.side_effect = Exception("Test error") diff --git a/autogpt_platform/backend/backend/server/v2/store/db.py b/autogpt_platform/backend/backend/server/v2/store/db.py index f3536326c28a..76206768ed67 100644 --- a/autogpt_platform/backend/backend/server/v2/store/db.py +++ b/autogpt_platform/backend/backend/server/v2/store/db.py @@ -31,7 +31,7 @@ async def get_store_agents( sanitized_query = search_query.strip() if not sanitized_query or len(sanitized_query) > 100: # Reasonable length limit raise backend.server.v2.store.exceptions.DatabaseError( - "Invalid search query" + f"Invalid search query: len({len(sanitized_query)}) query: {search_query}" ) # Escape special SQL characters @@ -449,6 +449,11 @@ async def create_store_submission( ) try: + # Sanitize slug to only allow letters and hyphens + slug = "".join( + c if c.isalpha() or c == "-" or c.isnumeric() else "" for c in slug + ).lower() + # First verify the agent belongs to this user agent = await prisma.models.AgentGraph.prisma().find_first( where=prisma.types.AgentGraphWhereInput( @@ -636,7 +641,12 @@ async def update_or_create_profile( logger.info(f"Updating profile for user {user_id} data: {profile}") try: - # Check if profile exists for user + # Sanitize username to only allow letters and hyphens + username = "".join( + c if c.isalpha() or c == "-" or c.isnumeric() else "" + for c in profile.username + ).lower() + existing_profile = await prisma.models.Profile.prisma().find_first( where={"userId": user_id} ) @@ -651,7 +661,7 @@ async def update_or_create_profile( data={ "userId": user_id, "name": profile.name, - "username": profile.username.lower(), + "username": username, "description": profile.description, "links": profile.links or [], "avatarUrl": profile.avatar_url, @@ -676,7 +686,7 @@ async def update_or_create_profile( if profile.name is not None: update_data["name"] = profile.name if profile.username is not None: - update_data["username"] = profile.username.lower() + update_data["username"] = username if profile.description is not None: update_data["description"] = profile.description if profile.links is not None: diff --git a/autogpt_platform/backend/backend/server/v2/store/routes.py b/autogpt_platform/backend/backend/server/v2/store/routes.py index 0ef5815afede..6aa264ca0054 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -23,12 +23,16 @@ ############################################## -@router.get("/profile", tags=["store", "private"]) +@router.get( + "/profile", + tags=["store", "private"], + response_model=backend.server.v2.store.model.ProfileDetails, +) async def get_profile( user_id: typing.Annotated[ str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id) ] -) -> backend.server.v2.store.model.ProfileDetails: +): """ Get the profile details for the authenticated user. """ @@ -37,20 +41,24 @@ async def get_profile( return profile except Exception: logger.exception("Exception occurred whilst getting user profile") - raise + return fastapi.responses.JSONResponse( + status_code=500, + content={"detail": "An error occurred while retrieving the user profile"}, + ) @router.post( "/profile", tags=["store", "private"], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], + response_model=backend.server.v2.store.model.CreatorDetails, ) async def update_or_create_profile( profile: backend.server.v2.store.model.Profile, user_id: typing.Annotated[ str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id) ], -) -> backend.server.v2.store.model.CreatorDetails: +): """ Update the store profile for the authenticated user. @@ -71,7 +79,10 @@ async def update_or_create_profile( return updated_profile except Exception: logger.exception("Exception occurred whilst updating profile") - raise + return fastapi.responses.JSONResponse( + status_code=500, + content={"detail": "An error occurred while updating the user profile"}, + ) ############################################## @@ -79,7 +90,11 @@ async def update_or_create_profile( ############################################## -@router.get("/agents", tags=["store", "public"]) +@router.get( + "/agents", + tags=["store", "public"], + response_model=backend.server.v2.store.model.StoreAgentsResponse, +) async def get_agents( featured: bool = False, creator: str | None = None, @@ -88,7 +103,7 @@ async def get_agents( category: str | None = None, page: int = 1, page_size: int = 20, -) -> backend.server.v2.store.model.StoreAgentsResponse: +): """ Get a paginated list of agents from the store with optional filtering and sorting. @@ -138,13 +153,18 @@ async def get_agents( return agents except Exception: logger.exception("Exception occured whilst getting store agents") - raise + return fastapi.responses.JSONResponse( + status_code=500, + content={"detail": "An error occurred while retrieving the store agents"}, + ) -@router.get("/agents/{username}/{agent_name}", tags=["store", "public"]) -async def get_agent( - username: str, agent_name: str -) -> backend.server.v2.store.model.StoreAgentDetails: +@router.get( + "/agents/{username}/{agent_name}", + tags=["store", "public"], + response_model=backend.server.v2.store.model.StoreAgentDetails, +) +async def get_agent(username: str, agent_name: str): """ This is only used on the AgentDetails Page @@ -153,20 +173,26 @@ async def get_agent( try: username = urllib.parse.unquote(username).lower() # URL decode the agent name since it comes from the URL path - agent_name = urllib.parse.unquote(agent_name) + agent_name = urllib.parse.unquote(agent_name).lower() agent = await backend.server.v2.store.db.get_store_agent_details( username=username, agent_name=agent_name ) return agent except Exception: logger.exception("Exception occurred whilst getting store agent details") - raise + return fastapi.responses.JSONResponse( + status_code=500, + content={ + "detail": "An error occurred while retrieving the store agent details" + }, + ) @router.post( "/agents/{username}/{agent_name}/review", tags=["store"], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], + response_model=backend.server.v2.store.model.StoreReview, ) async def create_review( username: str, @@ -175,7 +201,7 @@ async def create_review( user_id: typing.Annotated[ str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id) ], -) -> backend.server.v2.store.model.StoreReview: +): """ Create a review for a store agent. @@ -202,7 +228,10 @@ async def create_review( return created_review except Exception: logger.exception("Exception occurred whilst creating store review") - raise + return fastapi.responses.JSONResponse( + status_code=500, + content={"detail": "An error occurred while creating the store review"}, + ) ############################################## @@ -210,14 +239,18 @@ async def create_review( ############################################## -@router.get("/creators", tags=["store", "public"]) +@router.get( + "/creators", + tags=["store", "public"], + response_model=backend.server.v2.store.model.CreatorsResponse, +) async def get_creators( featured: bool = False, search_query: str | None = None, sorted_by: str | None = None, page: int = 1, page_size: int = 20, -) -> backend.server.v2.store.model.CreatorsResponse: +): """ This is needed for: - Home Page Featured Creators @@ -251,11 +284,20 @@ async def get_creators( return creators except Exception: logger.exception("Exception occurred whilst getting store creators") - raise + return fastapi.responses.JSONResponse( + status_code=500, + content={"detail": "An error occurred while retrieving the store creators"}, + ) -@router.get("/creator/{username}", tags=["store", "public"]) -async def get_creator(username: str) -> backend.server.v2.store.model.CreatorDetails: +@router.get( + "/creator/{username}", + tags=["store", "public"], + response_model=backend.server.v2.store.model.CreatorDetails, +) +async def get_creator( + username: str, +): """ Get the details of a creator - Creator Details Page @@ -268,7 +310,12 @@ async def get_creator(username: str) -> backend.server.v2.store.model.CreatorDet return creator except Exception: logger.exception("Exception occurred whilst getting creator details") - raise + return fastapi.responses.JSONResponse( + status_code=500, + content={ + "detail": "An error occurred while retrieving the creator details" + }, + ) ############################################ @@ -278,31 +325,36 @@ async def get_creator(username: str) -> backend.server.v2.store.model.CreatorDet "/myagents", tags=["store", "private"], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], + response_model=backend.server.v2.store.model.MyAgentsResponse, ) async def get_my_agents( user_id: typing.Annotated[ str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id) ] -) -> backend.server.v2.store.model.MyAgentsResponse: +): try: agents = await backend.server.v2.store.db.get_my_agents(user_id) return agents except Exception: logger.exception("Exception occurred whilst getting my agents") - raise + return fastapi.responses.JSONResponse( + status_code=500, + content={"detail": "An error occurred while retrieving the my agents"}, + ) @router.delete( "/submissions/{submission_id}", tags=["store", "private"], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], + response_model=bool, ) async def delete_submission( user_id: typing.Annotated[ str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id) ], submission_id: str, -) -> bool: +): """ Delete a store listing submission. @@ -321,13 +373,17 @@ async def delete_submission( return result except Exception: logger.exception("Exception occurred whilst deleting store submission") - raise + return fastapi.responses.JSONResponse( + status_code=500, + content={"detail": "An error occurred while deleting the store submission"}, + ) @router.get( "/submissions", tags=["store", "private"], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], + response_model=backend.server.v2.store.model.StoreSubmissionsResponse, ) async def get_submissions( user_id: typing.Annotated[ @@ -335,7 +391,7 @@ async def get_submissions( ], page: int = 1, page_size: int = 20, -) -> backend.server.v2.store.model.StoreSubmissionsResponse: +): """ Get a paginated list of store submissions for the authenticated user. @@ -368,20 +424,26 @@ async def get_submissions( return listings except Exception: logger.exception("Exception occurred whilst getting store submissions") - raise + return fastapi.responses.JSONResponse( + status_code=500, + content={ + "detail": "An error occurred while retrieving the store submissions" + }, + ) @router.post( "/submissions", tags=["store", "private"], dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], + response_model=backend.server.v2.store.model.StoreSubmission, ) async def create_submission( submission_request: backend.server.v2.store.model.StoreSubmissionRequest, user_id: typing.Annotated[ str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id) ], -) -> backend.server.v2.store.model.StoreSubmission: +): """ Create a new store listing submission. @@ -411,7 +473,10 @@ async def create_submission( return submission except Exception: logger.exception("Exception occurred whilst creating store submission") - raise + return fastapi.responses.JSONResponse( + status_code=500, + content={"detail": "An error occurred while creating the store submission"}, + ) @router.post( @@ -424,7 +489,7 @@ async def upload_submission_media( user_id: typing.Annotated[ str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id) ], -) -> str: +): """ Upload media (images/videos) for a store listing submission. @@ -443,10 +508,11 @@ async def upload_submission_media( user_id=user_id, file=file ) return media_url - except Exception as e: + except Exception: logger.exception("Exception occurred whilst uploading submission media") - raise fastapi.HTTPException( - status_code=500, detail=f"Failed to upload media file: {str(e)}" + return fastapi.responses.JSONResponse( + status_code=500, + content={"detail": "An error occurred while uploading the media file"}, ) @@ -503,8 +569,9 @@ async def generate_image( ) return fastapi.responses.JSONResponse(content={"image_url": image_url}) - except Exception as e: + except Exception: logger.exception("Exception occurred whilst generating submission image") - raise fastapi.HTTPException( - status_code=500, detail=f"Failed to generate image: {str(e)}" + return fastapi.responses.JSONResponse( + status_code=500, + content={"detail": "An error occurred while generating the image"}, ) diff --git a/autogpt_platform/backend/migrations/20241230102007_update_store_agent_view/migration.sql b/autogpt_platform/backend/migrations/20241230102007_update_store_agent_view/migration.sql new file mode 100644 index 000000000000..76cabcb57426 --- /dev/null +++ b/autogpt_platform/backend/migrations/20241230102007_update_store_agent_view/migration.sql @@ -0,0 +1,50 @@ +BEGIN; + +DROP VIEW IF EXISTS "StoreAgent"; + +CREATE VIEW "StoreAgent" AS +WITH ReviewStats AS ( + SELECT sl."id" AS "storeListingId", + COUNT(sr.id) AS review_count, + AVG(CAST(sr.score AS DECIMAL)) AS avg_rating + FROM "StoreListing" sl + JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl."id" + JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id + WHERE sl."isDeleted" = FALSE + GROUP BY sl."id" +), +AgentRuns AS ( + SELECT "agentGraphId", COUNT(*) AS run_count + FROM "AgentGraphExecution" + GROUP BY "agentGraphId" +) +SELECT + sl.id AS listing_id, + slv.id AS "storeListingVersionId", + slv."createdAt" AS updated_at, + slv.slug, + slv.name AS agent_name, + slv."videoUrl" AS agent_video, + COALESCE(slv."imageUrls", ARRAY[]::TEXT[]) AS agent_image, + slv."isFeatured" AS featured, + p.username AS creator_username, + p."avatarUrl" AS creator_avatar, + slv."subHeading" AS sub_heading, + slv.description, + slv.categories, + COALESCE(ar.run_count, 0) AS runs, + CAST(COALESCE(rs.avg_rating, 0.0) AS DOUBLE PRECISION) AS rating, + ARRAY_AGG(DISTINCT CAST(slv.version AS TEXT)) AS versions +FROM "StoreListing" sl +JOIN "AgentGraph" a ON sl."agentId" = a.id AND sl."agentVersion" = a."version" +LEFT JOIN "Profile" p ON sl."owningUserId" = p."userId" +LEFT JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id +LEFT JOIN ReviewStats rs ON sl.id = rs."storeListingId" +LEFT JOIN AgentRuns ar ON a.id = ar."agentGraphId" +WHERE sl."isDeleted" = FALSE + AND sl."isApproved" = TRUE +GROUP BY sl.id, slv.id, slv.slug, slv."createdAt", slv.name, slv."videoUrl", slv."imageUrls", slv."isFeatured", + p.username, p."avatarUrl", slv."subHeading", slv.description, slv.categories, + ar.run_count, rs.avg_rating; + +COMMIT; diff --git a/autogpt_platform/frontend/src/app/marketplace/page.tsx b/autogpt_platform/frontend/src/app/marketplace/page.tsx new file mode 100644 index 000000000000..b59d651f0b42 --- /dev/null +++ b/autogpt_platform/frontend/src/app/marketplace/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/store"); +} diff --git a/autogpt_platform/frontend/src/app/store/agent/[creator]/[slug]/page.tsx b/autogpt_platform/frontend/src/app/store/agent/[creator]/[slug]/page.tsx index 7db11eaf9abc..b5d0ebeaa242 100644 --- a/autogpt_platform/frontend/src/app/store/agent/[creator]/[slug]/page.tsx +++ b/autogpt_platform/frontend/src/app/store/agent/[creator]/[slug]/page.tsx @@ -40,7 +40,8 @@ export default async function Page({ const agent = await api.getStoreAgent(creator_lower, params.slug); const otherAgents = await api.getStoreAgents({ creator: creator_lower }); const similarAgents = await api.getStoreAgents({ - search_query: agent.categories[0], + // We are using slug as we know its has been sanitized and is not null + search_query: agent.slug.replace(/-/g, " "), }); const breadcrumbs = [ diff --git a/autogpt_platform/frontend/src/components/agptui/composite/AgentsSection.tsx b/autogpt_platform/frontend/src/components/agptui/composite/AgentsSection.tsx index 7132dd0b7c1b..fc4e13af4f53 100644 --- a/autogpt_platform/frontend/src/components/agptui/composite/AgentsSection.tsx +++ b/autogpt_platform/frontend/src/components/agptui/composite/AgentsSection.tsx @@ -34,8 +34,8 @@ export const AgentsSection: React.FC = ({ }) => { const router = useRouter(); - // Take only the first 9 agents - const displayedAgents = allAgents.slice(0, 9); + // TODO: Update this when we have pagination + const displayedAgents = allAgents; const handleCardClick = (creator: string, slug: string) => { router.push( diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts index fcfbaa7014d6..2f2b26e945b5 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts @@ -311,7 +311,7 @@ export default class BackendAPI { "/store/submissions/generate_image?agent_id=" + agent_id, ); } - c; + deleteStoreSubmission(submission_id: string): Promise { return this._request("DELETE", `/store/submissions/${submission_id}`); } diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/utils.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/utils.ts index 601fe68b0193..cd7507421893 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/utils.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/utils.ts @@ -1,7 +1,8 @@ -import { Graph, Block, Node } from "./types"; +import { Graph, Block, Node, BlockUIType } from "./types"; /** Creates a copy of the graph with all secrets removed */ export function safeCopyGraph(graph: Graph, block_defs: Block[]): Graph { + graph = removeAgentInputBlockValues(graph, block_defs); return { ...graph, nodes: graph.nodes.map((node) => { @@ -18,3 +19,28 @@ export function safeCopyGraph(graph: Graph, block_defs: Block[]): Graph { }), }; } + +export function removeAgentInputBlockValues(graph: Graph, blocks: Block[]) { + const inputBlocks = graph.nodes.filter( + (node) => + blocks.find((b) => b.id === node.block_id)?.uiType === BlockUIType.INPUT, + ); + + const modifiedNodes = graph.nodes.map((node) => { + if (inputBlocks.find((inputNode) => inputNode.id === node.id)) { + return { + ...node, + input_default: { + ...node.input_default, + value: "", + }, + }; + } + return node; + }); + + return { + ...graph, + nodes: modifiedNodes, + }; +}