From 770806c22ffd3a359a63aaf8ffe1a9ca8ea5fed9 Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Mon, 23 Dec 2024 10:09:37 +0530 Subject: [PATCH 1/8] feat(store): Implement agent download functionality and UI enhancements - Added a new endpoint to download agent files as JSON, allowing users to retrieve agent data by store listing version ID and version number. - Introduced a new `get_agent` function in the database module to fetch agent details and prepare the graph data for download. - Enhanced the frontend `AgentInfo` component to include a download button, which triggers the download of the agent file. - Integrated loading state and user feedback via toast notifications during the download process. - Updated the API client to support the new download functionality. --- .../backend/backend/server/v2/store/db.py | 43 +++++++++ .../backend/backend/server/v2/store/routes.py | 43 +++++++++ .../src/components/agptui/AgentInfo.tsx | 88 ++++++++++++++++--- .../src/lib/autogpt-server-api/client.ts | 9 +- 4 files changed, 172 insertions(+), 11 deletions(-) diff --git a/autogpt_platform/backend/backend/server/v2/store/db.py b/autogpt_platform/backend/backend/server/v2/store/db.py index f3536326c28a..d47e70a2d5e9 100644 --- a/autogpt_platform/backend/backend/server/v2/store/db.py +++ b/autogpt_platform/backend/backend/server/v2/store/db.py @@ -1,14 +1,18 @@ import logging import random from datetime import datetime +from typing import Optional +import fastapi import prisma.enums import prisma.errors import prisma.models import prisma.types +import backend.data.graph import backend.server.v2.store.exceptions import backend.server.v2.store.model +from backend.data.graph import GraphModel logger = logging.getLogger(__name__) @@ -776,3 +780,42 @@ async def get_my_agents( raise backend.server.v2.store.exceptions.DatabaseError( "Failed to fetch my agents" ) from e + +async def get_agent(store_listing_version_id: str, version_id:Optional[int]) -> GraphModel: + """Get agent using the version ID and store listing version ID.""" + try: + 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 + + graph = await backend.data.graph.get_graph( + agent.id, agent.version, template=True + ) + + if not graph: + raise fastapi.HTTPException( + status_code=404, detail=f"Agent {agent.id} not found" + ) + + graph.version = 1 + graph.is_template = False + graph.is_active = True + delattr(graph, 'user_id') + + return graph + + except Exception as e: + logger.error(f"Error getting agent: {str(e)}") + raise backend.server.v2.store.exceptions.DatabaseError( + "Failed to fetch agent" + ) from e diff --git a/autogpt_platform/backend/backend/server/v2/store/routes.py b/autogpt_platform/backend/backend/server/v2/store/routes.py index 0ef5815afede..ce19f310d440 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -1,6 +1,9 @@ import logging import typing import urllib.parse +import tempfile +import json + import autogpt_libs.auth.depends import autogpt_libs.auth.middleware @@ -12,6 +15,8 @@ import backend.server.v2.store.image_gen import backend.server.v2.store.media import backend.server.v2.store.model +from fastapi.encoders import jsonable_encoder + logger = logging.getLogger(__name__) @@ -508,3 +513,41 @@ async def generate_image( raise fastapi.HTTPException( status_code=500, detail=f"Failed to generate image: {str(e)}" ) + +@router.get("/download/agents/{store_listing_version_id}/{version}",tags=["store","public"],) +async def download_agent_file( + store_listing_version_id: str = fastapi.Path(..., description="The ID of the agent to download"), + version: typing.Optional[int] = fastapi.Query( + None, description="Specific version of the agent" + ), +) -> fastapi.responses.FileResponse: + """ + Download the agent file by streaming its content. + + Args: + agent_id (str): The ID of the agent to download. + version (Optional[int]): Specific version of the agent to download. + + Returns: + StreamingResponse: A streaming response containing the agent's graph data. + + Raises: + HTTPException: If the agent is not found or an unexpected error occurs. + """ + + graph_data = await backend.server.v2.store.db.get_agent(store_listing_version_id=store_listing_version_id, version_id=version) + + graph_date_dict = jsonable_encoder(graph_data) + + file_name = f"agent_{store_listing_version_id}_v{version or 'latest'}.json" + + # Sending graph as a stream (similar to marketplace v1) + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as tmp_file: + tmp_file.write(json.dumps(graph_date_dict)) + tmp_file.flush() + + return fastapi.responses.FileResponse( + tmp_file.name, filename=file_name, media_type="application/json" + ) diff --git a/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx b/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx index fe12e75b4979..03f7d141d7a8 100644 --- a/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx +++ b/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx @@ -6,6 +6,10 @@ import { Separator } from "@/components/ui/separator"; import BackendAPI from "@/lib/autogpt-server-api"; import { useRouter } from "next/navigation"; import Link from "next/link"; +import { useToast } from "@/components/ui/use-toast"; + +import useSupabase from "@/hooks/useSupabase"; +import { DownloadIcon, LoaderIcon } from "lucide-react"; interface AgentInfoProps { name: string; creator: string; @@ -32,8 +36,11 @@ export const AgentInfo: React.FC = ({ storeListingVersionId, }) => { const router = useRouter(); - const api = React.useMemo(() => new BackendAPI(), []); + const { user } = useSupabase(); + const { toast } = useToast(); + + const [downloading, setDownloading] = React.useState(false); const handleAddToLibrary = async () => { try { @@ -45,6 +52,46 @@ export const AgentInfo: React.FC = ({ } }; + const handleDownloadToLibrary = async () => { + const downloadAgent = async (): Promise => { + setDownloading(true); + try { + const file = await api.downloadStoreAgent(storeListingVersionId); + + // Similar to Marketplace v1 + const jsonData = JSON.stringify(file, null, 2); + // Create a Blob from the file content + const blob = new Blob([jsonData], { type: "application/json" }); + + // Create a temporary URL for the Blob + const url = window.URL.createObjectURL(blob); + + // Create a temporary anchor element + const a = document.createElement("a"); + a.href = url; + a.download = `agent_${storeListingVersionId}.json`; // Set the filename + + // Append the anchor to the body, click it, and remove it + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + // Revoke the temporary URL + window.URL.revokeObjectURL(url); + + toast({ + title: "Download Complete", + description: "Your agent has been successfully downloaded.", + }); + } catch (error) { + console.error(`Error downloading agent:`, error); + throw error; + } + }; + await downloadAgent(); + setDownloading(false); + }; + return (
{/* Title */} @@ -72,15 +119,36 @@ export const AgentInfo: React.FC = ({ {/* Run Agent Button */}
- + {user ? ( + + ) : ( + + )}
{/* Rating and Runs */} 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..a01b5b9426d8 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}`); } @@ -348,6 +348,13 @@ export default class BackendAPI { return this._get("/store/myagents", params); } + downloadStoreAgent( + storeListingVersionId: string, + version?: number, + ): Promise { + return this._get(`/download/agents/${storeListingVersionId}/${version}`); + } + ///////////////////////////////////////// /////////// V2 LIBRARY API ////////////// ///////////////////////////////////////// From c856907c93b0ee4e89a73b4f79885af7e5034acb Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Mon, 23 Dec 2024 10:29:59 +0530 Subject: [PATCH 2/8] fix version query --- autogpt_platform/backend/backend/server/v2/store/routes.py | 2 +- .../frontend/src/lib/autogpt-server-api/client.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/autogpt_platform/backend/backend/server/v2/store/routes.py b/autogpt_platform/backend/backend/server/v2/store/routes.py index ce19f310d440..0af5e4380766 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -514,7 +514,7 @@ async def generate_image( status_code=500, detail=f"Failed to generate image: {str(e)}" ) -@router.get("/download/agents/{store_listing_version_id}/{version}",tags=["store","public"],) +@router.get("/download/agents/{store_listing_version_id}",tags=["store","public"],) async def download_agent_file( store_listing_version_id: str = fastapi.Path(..., description="The ID of the agent to download"), version: typing.Optional[int] = fastapi.Query( 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 a01b5b9426d8..88fcd6817c38 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts @@ -352,7 +352,9 @@ export default class BackendAPI { storeListingVersionId: string, version?: number, ): Promise { - return this._get(`/download/agents/${storeListingVersionId}/${version}`); + return this._get( + `/download/agents/${storeListingVersionId}?version=${version}`, + ); } ///////////////////////////////////////// From bb251aec0ab60a8dd383bbcff2dafad5cd523e87 Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Mon, 23 Dec 2024 11:01:09 +0530 Subject: [PATCH 3/8] fix:lint --- .../backend/backend/server/v2/store/db.py | 7 ++++-- .../backend/backend/server/v2/store/routes.py | 22 ++++++++++++------- .../backend/backend/util/settings.py | 2 +- .../src/lib/autogpt-server-api/client.ts | 8 ++++--- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/autogpt_platform/backend/backend/server/v2/store/db.py b/autogpt_platform/backend/backend/server/v2/store/db.py index d47e70a2d5e9..1ad1cff67edb 100644 --- a/autogpt_platform/backend/backend/server/v2/store/db.py +++ b/autogpt_platform/backend/backend/server/v2/store/db.py @@ -781,7 +781,10 @@ async def get_my_agents( "Failed to fetch my agents" ) from e -async def get_agent(store_listing_version_id: str, version_id:Optional[int]) -> GraphModel: + +async def get_agent( + store_listing_version_id: str, version_id: Optional[int] +) -> GraphModel: """Get agent using the version ID and store listing version ID.""" try: store_listing_version = ( @@ -810,7 +813,7 @@ async def get_agent(store_listing_version_id: str, version_id:Optional[int]) -> graph.version = 1 graph.is_template = False graph.is_active = True - delattr(graph, 'user_id') + delattr(graph, "user_id") return graph diff --git a/autogpt_platform/backend/backend/server/v2/store/routes.py b/autogpt_platform/backend/backend/server/v2/store/routes.py index 0af5e4380766..8011970355b9 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -1,22 +1,20 @@ +import json import logging +import tempfile import typing import urllib.parse -import tempfile -import json - import autogpt_libs.auth.depends import autogpt_libs.auth.middleware import fastapi import fastapi.responses +from fastapi.encoders import jsonable_encoder import backend.data.graph import backend.server.v2.store.db import backend.server.v2.store.image_gen import backend.server.v2.store.media import backend.server.v2.store.model -from fastapi.encoders import jsonable_encoder - logger = logging.getLogger(__name__) @@ -514,9 +512,15 @@ async def generate_image( status_code=500, detail=f"Failed to generate image: {str(e)}" ) -@router.get("/download/agents/{store_listing_version_id}",tags=["store","public"],) + +@router.get( + "/download/agents/{store_listing_version_id}", + tags=["store", "public"], +) async def download_agent_file( - store_listing_version_id: str = fastapi.Path(..., description="The ID of the agent to download"), + store_listing_version_id: str = fastapi.Path( + ..., description="The ID of the agent to download" + ), version: typing.Optional[int] = fastapi.Query( None, description="Specific version of the agent" ), @@ -535,7 +539,9 @@ async def download_agent_file( HTTPException: If the agent is not found or an unexpected error occurs. """ - graph_data = await backend.server.v2.store.db.get_agent(store_listing_version_id=store_listing_version_id, version_id=version) + graph_data = await backend.server.v2.store.db.get_agent( + store_listing_version_id=store_listing_version_id, version_id=version + ) graph_date_dict = jsonable_encoder(graph_data) diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index 69504f528f3c..763c9e163ca0 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -149,7 +149,7 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): ) media_gcs_bucket_name: str = Field( - default="", + default="autogpt_bucket", description="The name of the Google Cloud Storage bucket for media files", ) 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 88fcd6817c38..1f74e67f636a 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts @@ -352,9 +352,11 @@ export default class BackendAPI { storeListingVersionId: string, version?: number, ): Promise { - return this._get( - `/download/agents/${storeListingVersionId}?version=${version}`, - ); + const url = version + ? `/store/download/agents/${storeListingVersionId}?version=${version}` + : `/store/download/agents/${storeListingVersionId}`; + + return this._get(url); } ///////////////////////////////////////// From 50b713bb2ac11f4e653808393d64cd726c38d4da Mon Sep 17 00:00:00 2001 From: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:12:07 +0530 Subject: [PATCH 4/8] Update settings.py --- autogpt_platform/backend/backend/util/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index 763c9e163ca0..69504f528f3c 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -149,7 +149,7 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): ) media_gcs_bucket_name: str = Field( - default="autogpt_bucket", + default="", description="The name of the Google Cloud Storage bucket for media files", ) From 9e048453eba2d60c0bed307fd6d5c63b0ffcf612 Mon Sep 17 00:00:00 2001 From: SwiftyOS Date: Tue, 31 Dec 2024 10:50:21 +0100 Subject: [PATCH 5/8] Adding stripping value form agent input blocks --- .../backend/backend/server/v2/store/routes.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/autogpt_platform/backend/backend/server/v2/store/routes.py b/autogpt_platform/backend/backend/server/v2/store/routes.py index 8011970355b9..c151a69f1ee0 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -10,6 +10,7 @@ import fastapi.responses from fastapi.encoders import jsonable_encoder +import backend.data.block import backend.data.graph import backend.server.v2.store.db import backend.server.v2.store.image_gen @@ -543,6 +544,28 @@ async def download_agent_file( store_listing_version_id=store_listing_version_id, version_id=version ) + + + def remove_agent_input_block_values(graph): + # Remove input block values before returning + blocks = [block() for block in backend.data.block.get_blocks().values()] + + input_blocks = [ + node for node in graph["nodes"] + if next((b for b in blocks if b.id == node["block_id"] and b.block_type == backend.data.block.BlockType.INPUT), None) + ] + + modified_nodes = [] + for node in graph["nodes"]: + if any(input_block["id"] == node["id"] for input_block in input_blocks): + node = {**node} + if "input_default" in node: + node["input_default"] = {**node["input_default"], "value": ""} + modified_nodes.append(node) + + return {**graph, "nodes": modified_nodes} + + graph_data = remove_agent_input_block_values(graph_data) graph_date_dict = jsonable_encoder(graph_data) file_name = f"agent_{store_listing_version_id}_v{version or 'latest'}.json" From 279471884feb6d64a89571cfe64837ca4410baf3 Mon Sep 17 00:00:00 2001 From: SwiftyOS Date: Thu, 2 Jan 2025 09:54:27 +0100 Subject: [PATCH 6/8] updated sanitization logic --- .../backend/backend/data/graph.py | 20 +++++++++ .../backend/backend/server/v2/store/routes.py | 36 +++++++--------- .../backend/test/data/test_graph.py | 42 +++++++++++++++++++ 3 files changed, 76 insertions(+), 22 deletions(-) diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index 551b6072d1c3..34c0f1e943e5 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -423,6 +423,26 @@ def _hide_node_input_credentials(input_data: dict[str, Any]) -> dict[str, Any]: result[key] = value return result + def clean_graph(self): + blocks = [block() for block in get_blocks().values()] + + input_blocks = [ + node + for node in self.nodes + if next( + ( + b + for b in blocks + if b.id == node.block_id and b.block_type == BlockType.INPUT + ), + None, + ) + ] + + for node in self.nodes: + if any(input_block.id == node.id for input_block in input_blocks): + node.input_default["value"] = "" + # --------------------- CRUD functions --------------------- # diff --git a/autogpt_platform/backend/backend/server/v2/store/routes.py b/autogpt_platform/backend/backend/server/v2/store/routes.py index c151a69f1ee0..875dd30e6728 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -544,30 +544,22 @@ async def download_agent_file( store_listing_version_id=store_listing_version_id, version_id=version ) - - - def remove_agent_input_block_values(graph): - # Remove input block values before returning - blocks = [block() for block in backend.data.block.get_blocks().values()] - - input_blocks = [ - node for node in graph["nodes"] - if next((b for b in blocks if b.id == node["block_id"] and b.block_type == backend.data.block.BlockType.INPUT), None) - ] - - modified_nodes = [] - for node in graph["nodes"]: - if any(input_block["id"] == node["id"] for input_block in input_blocks): - node = {**node} - if "input_default" in node: - node["input_default"] = {**node["input_default"], "value": ""} - modified_nodes.append(node) - - return {**graph, "nodes": modified_nodes} - - graph_data = remove_agent_input_block_values(graph_data) + graph_data.clean_graph() graph_date_dict = jsonable_encoder(graph_data) + def remove_credentials(obj): + if obj and isinstance(obj, dict): + if "credentials" in obj: + obj["credentials"] = "" + for value in obj.values(): + remove_credentials(value) + elif isinstance(obj, list): + for item in obj: + remove_credentials(item) + return obj + + graph_date_dict = remove_credentials(graph_date_dict) + file_name = f"agent_{store_listing_version_id}_v{version or 'latest'}.json" # Sending graph as a stream (similar to marketplace v1) diff --git a/autogpt_platform/backend/test/data/test_graph.py b/autogpt_platform/backend/test/data/test_graph.py index 050e20fdc04b..8888e64f2df7 100644 --- a/autogpt_platform/backend/test/data/test_graph.py +++ b/autogpt_platform/backend/test/data/test_graph.py @@ -155,3 +155,45 @@ class ExpectedOutputSchema(BlockSchema): output_schema = created_graph.output_schema output_schema["title"] = "ExpectedOutputSchema" assert output_schema == ExpectedOutputSchema.jsonschema() + + +@pytest.mark.asyncio(scope="session") +async def test_clean_graph(server: SpinTestServer): + """ + Test the clean_graph function that: + 1. Clears input block values + 2. Removes credentials from nodes + """ + # Create a graph with input blocks and credentials + graph = Graph( + id="test_clean_graph", + name="Test Clean Graph", + description="Test graph cleaning", + nodes=[ + Node( + id="input_node", + block_id=AgentInputBlock().id, + input_default={ + "name": "test_input", + "value": "test value", + "description": "Test input description", + }, + ), + ], + links=[], + ) + + # Create graph and get model + create_graph = CreateGraph(graph=graph) + created_graph = await server.agent_server.test_create_graph( + create_graph, DEFAULT_USER_ID + ) + + # Clean the graph + created_graph.clean_graph() + + # # Verify input block value is cleared + input_node = next( + n for n in created_graph.nodes if n.block_id == AgentInputBlock().id + ) + assert input_node.input_default["value"] == "" From 1adbb70962bc442e07c839ba81272ba9df9ca431 Mon Sep 17 00:00:00 2001 From: SwiftyOS Date: Thu, 2 Jan 2025 10:21:03 +0100 Subject: [PATCH 7/8] removed reddit creds too --- autogpt_platform/backend/backend/server/v2/store/routes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/autogpt_platform/backend/backend/server/v2/store/routes.py b/autogpt_platform/backend/backend/server/v2/store/routes.py index db6098712eb5..79323f636413 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -617,7 +617,10 @@ async def download_agent_file( def remove_credentials(obj): if obj and isinstance(obj, dict): if "credentials" in obj: - obj["credentials"] = "" + del obj["credentials"] + if "creds" in obj: + del obj["creds"] + for value in obj.values(): remove_credentials(value) elif isinstance(obj, list): From 3100738da95f243de03712e78531e6e2d0e4b313 Mon Sep 17 00:00:00 2001 From: SwiftyOS Date: Thu, 2 Jan 2025 10:21:24 +0100 Subject: [PATCH 8/8] fmt --- autogpt_platform/backend/backend/server/v2/store/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogpt_platform/backend/backend/server/v2/store/routes.py b/autogpt_platform/backend/backend/server/v2/store/routes.py index 79323f636413..6dc9d7594963 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -620,7 +620,7 @@ def remove_credentials(obj): del obj["credentials"] if "creds" in obj: del obj["creds"] - + for value in obj.values(): remove_credentials(value) elif isinstance(obj, list):