diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index 75449febe..7dd99a75e 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -424,6 +424,26 @@ class GraphModel(Graph): 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/db.py b/autogpt_platform/backend/backend/server/v2/store/db.py index 76206768e..deed4e158 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__) @@ -786,3 +790,45 @@ 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 6aa264ca0..6dc9d7594 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -1,4 +1,6 @@ +import json import logging +import tempfile import typing import urllib.parse @@ -6,7 +8,9 @@ import autogpt_libs.auth.depends import autogpt_libs.auth.middleware import fastapi 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 @@ -575,3 +579,66 @@ async def generate_image( status_code=500, content={"detail": "An error occurred while generating the image"}, ) + + +@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( + 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_data.clean_graph() + graph_date_dict = jsonable_encoder(graph_data) + + def remove_credentials(obj): + if obj and isinstance(obj, dict): + if "credentials" in obj: + del obj["credentials"] + if "creds" in obj: + del obj["creds"] + + 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) + 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/backend/test/data/test_graph.py b/autogpt_platform/backend/test/data/test_graph.py index 0075fde9d..ddff6f3ad 100644 --- a/autogpt_platform/backend/test/data/test_graph.py +++ b/autogpt_platform/backend/test/data/test_graph.py @@ -160,3 +160,45 @@ async def test_get_input_schema(server: SpinTestServer): 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"] == "" diff --git a/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx b/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx index fe12e75b4..03f7d141d 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 2f2b26e94..1f74e67f6 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts @@ -348,6 +348,17 @@ export default class BackendAPI { return this._get("/store/myagents", params); } + downloadStoreAgent( + storeListingVersionId: string, + version?: number, + ): Promise { + const url = version + ? `/store/download/agents/${storeListingVersionId}?version=${version}` + : `/store/download/agents/${storeListingVersionId}`; + + return this._get(url); + } + ///////////////////////////////////////// /////////// V2 LIBRARY API ////////////// /////////////////////////////////////////