diff --git a/.github/workflows/platform-market-ci.yml b/.github/workflows/platform-market-ci.yml new file mode 100644 index 000000000000..560c05d64ae6 --- /dev/null +++ b/.github/workflows/platform-market-ci.yml @@ -0,0 +1,125 @@ +name: AutoGPT Platform - Backend CI + +on: + push: + branches: [master, dev, ci-test*] + paths: + - ".github/workflows/platform-market-ci.yml" + - "autogpt_platform/market/**" + pull_request: + branches: [master, dev, release-*] + paths: + - ".github/workflows/platform-market-ci.yml" + - "autogpt_platform/market/**" + +concurrency: + group: ${{ format('backend-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull_request') }} + +defaults: + run: + shell: bash + working-directory: autogpt_platform/market + +jobs: + test: + permissions: + contents: read + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + python-version: ["3.10"] + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Setup Supabase + uses: supabase/setup-cli@v1 + with: + version: latest + + - id: get_date + name: Get date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + + - name: Set up Python dependency cache + uses: actions/cache@v4 + with: + path: ~/.cache/pypoetry + key: poetry-${{ runner.os }}-${{ hashFiles('autogpt_platform/market/poetry.lock') }} + + - name: Install Poetry (Unix) + run: | + curl -sSL https://install.python-poetry.org | python3 - + + if [ "${{ runner.os }}" = "macOS" ]; then + PATH="$HOME/.local/bin:$PATH" + echo "$HOME/.local/bin" >> $GITHUB_PATH + fi + + - name: Install Python dependencies + run: poetry install + + - name: Generate Prisma Client + run: poetry run prisma generate + + - id: supabase + name: Start Supabase + working-directory: . + run: | + supabase init + supabase start --exclude postgres-meta,realtime,storage-api,imgproxy,inbucket,studio,edge-runtime,logflare,vector,supavisor + supabase status -o env | sed 's/="/=/; s/"$//' >> $GITHUB_OUTPUT + # outputs: + # DB_URL, API_URL, GRAPHQL_URL, ANON_KEY, SERVICE_ROLE_KEY, JWT_SECRET + + - name: Run Database Migrations + run: poetry run prisma migrate dev --name updates + env: + DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }} + + - id: lint + name: Run Linter + run: poetry run lint + + # Tests comment out because they do not work with prisma mock, nor have they been updated since they were created + # - name: Run pytest with coverage + # run: | + # if [[ "${{ runner.debug }}" == "1" ]]; then + # poetry run pytest -s -vv -o log_cli=true -o log_cli_level=DEBUG test + # else + # poetry run pytest -s -vv test + # fi + # if: success() || (failure() && steps.lint.outcome == 'failure') + # env: + # LOG_LEVEL: ${{ runner.debug && 'DEBUG' || 'INFO' }} + # DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }} + # SUPABASE_URL: ${{ steps.supabase.outputs.API_URL }} + # SUPABASE_SERVICE_ROLE_KEY: ${{ steps.supabase.outputs.SERVICE_ROLE_KEY }} + # SUPABASE_JWT_SECRET: ${{ steps.supabase.outputs.JWT_SECRET }} + # REDIS_HOST: 'localhost' + # REDIS_PORT: '6379' + # REDIS_PASSWORD: 'testpassword' + + env: + CI: true + PLAIN_OUTPUT: True + RUN_ENV: local + PORT: 8080 + + # - name: Upload coverage reports to Codecov + # uses: codecov/codecov-action@v4 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} + # flags: backend,${{ runner.os }} diff --git a/autogpt_platform/backend/backend/blocks/llm.py b/autogpt_platform/backend/backend/blocks/llm.py index f38b5f5da72d..a55dbdf10565 100644 --- a/autogpt_platform/backend/backend/blocks/llm.py +++ b/autogpt_platform/backend/backend/blocks/llm.py @@ -96,25 +96,25 @@ def cost_factor(self) -> int: MODEL_METADATA = { - LlmModel.O1_PREVIEW: ModelMetadata("openai", 32000, cost_factor=60), - LlmModel.O1_MINI: ModelMetadata("openai", 62000, cost_factor=30), - LlmModel.GPT4O_MINI: ModelMetadata("openai", 128000, cost_factor=10), - LlmModel.GPT4O: ModelMetadata("openai", 128000, cost_factor=12), - LlmModel.GPT4_TURBO: ModelMetadata("openai", 128000, cost_factor=11), - LlmModel.GPT3_5_TURBO: ModelMetadata("openai", 16385, cost_factor=8), - LlmModel.CLAUDE_3_5_SONNET: ModelMetadata("anthropic", 200000, cost_factor=14), - LlmModel.CLAUDE_3_HAIKU: ModelMetadata("anthropic", 200000, cost_factor=13), - LlmModel.LLAMA3_8B: ModelMetadata("groq", 8192, cost_factor=6), - LlmModel.LLAMA3_70B: ModelMetadata("groq", 8192, cost_factor=9), - LlmModel.MIXTRAL_8X7B: ModelMetadata("groq", 32768, cost_factor=7), - LlmModel.GEMMA_7B: ModelMetadata("groq", 8192, cost_factor=6), - LlmModel.GEMMA2_9B: ModelMetadata("groq", 8192, cost_factor=7), - LlmModel.LLAMA3_1_405B: ModelMetadata("groq", 8192, cost_factor=10), + LlmModel.O1_PREVIEW: ModelMetadata("openai", 32000, cost_factor=16), + LlmModel.O1_MINI: ModelMetadata("openai", 62000, cost_factor=4), + LlmModel.GPT4O_MINI: ModelMetadata("openai", 128000, cost_factor=1), + LlmModel.GPT4O: ModelMetadata("openai", 128000, cost_factor=3), + LlmModel.GPT4_TURBO: ModelMetadata("openai", 128000, cost_factor=10), + LlmModel.GPT3_5_TURBO: ModelMetadata("openai", 16385, cost_factor=1), + LlmModel.CLAUDE_3_5_SONNET: ModelMetadata("anthropic", 200000, cost_factor=4), + LlmModel.CLAUDE_3_HAIKU: ModelMetadata("anthropic", 200000, cost_factor=1), + LlmModel.LLAMA3_8B: ModelMetadata("groq", 8192, cost_factor=1), + LlmModel.LLAMA3_70B: ModelMetadata("groq", 8192, cost_factor=1), + LlmModel.MIXTRAL_8X7B: ModelMetadata("groq", 32768, cost_factor=1), + LlmModel.GEMMA_7B: ModelMetadata("groq", 8192, cost_factor=1), + LlmModel.GEMMA2_9B: ModelMetadata("groq", 8192, cost_factor=1), + LlmModel.LLAMA3_1_405B: ModelMetadata("groq", 8192, cost_factor=1), # Limited to 16k during preview - LlmModel.LLAMA3_1_70B: ModelMetadata("groq", 131072, cost_factor=15), - LlmModel.LLAMA3_1_8B: ModelMetadata("groq", 131072, cost_factor=13), - LlmModel.OLLAMA_LLAMA3_8B: ModelMetadata("ollama", 8192, cost_factor=7), - LlmModel.OLLAMA_LLAMA3_405B: ModelMetadata("ollama", 8192, cost_factor=11), + LlmModel.LLAMA3_1_70B: ModelMetadata("groq", 131072, cost_factor=1), + LlmModel.LLAMA3_1_8B: ModelMetadata("groq", 131072, cost_factor=1), + LlmModel.OLLAMA_LLAMA3_8B: ModelMetadata("ollama", 8192, cost_factor=1), + LlmModel.OLLAMA_LLAMA3_405B: ModelMetadata("ollama", 8192, cost_factor=1), } for model in LlmModel: diff --git a/autogpt_platform/backend/backend/data/credit.py b/autogpt_platform/backend/backend/data/credit.py index e1fccb42897f..5581a7854226 100644 --- a/autogpt_platform/backend/backend/data/credit.py +++ b/autogpt_platform/backend/backend/data/credit.py @@ -17,6 +17,7 @@ AITextSummarizerBlock, LlmModel, ) +from backend.blocks.search import ExtractWebsiteContentBlock, SearchTheWebBlock from backend.blocks.talking_head import CreateTalkingAvatarVideoBlock from backend.data.block import Block, BlockInput, get_block from backend.util.settings import Config @@ -74,6 +75,10 @@ def __init__( CreateTalkingAvatarVideoBlock: [ BlockCost(cost_amount=15, cost_filter={"api_key": None}) ], + SearchTheWebBlock: [BlockCost(cost_amount=1)], + ExtractWebsiteContentBlock: [ + BlockCost(cost_amount=1, cost_filter={"raw_content": False}) + ], } diff --git a/autogpt_platform/backend/backend/server/rest_api.py b/autogpt_platform/backend/backend/server/rest_api.py index 6860f3f60fee..880d41817f21 100644 --- a/autogpt_platform/backend/backend/server/rest_api.py +++ b/autogpt_platform/backend/backend/server/rest_api.py @@ -267,6 +267,7 @@ def run_service(self): app.add_exception_handler(500, self.handle_internal_http_error) app.include_router(api_router) + app.include_router(health_router) uvicorn.run( app, diff --git a/autogpt_platform/frontend/src/app/admin/marketplace/page.tsx b/autogpt_platform/frontend/src/app/admin/marketplace/page.tsx index 7eb82d7d328d..00fce107899c 100644 --- a/autogpt_platform/frontend/src/app/admin/marketplace/page.tsx +++ b/autogpt_platform/frontend/src/app/admin/marketplace/page.tsx @@ -10,7 +10,7 @@ async function AdminMarketplace() { return ( <> - + diff --git a/autogpt_platform/frontend/src/app/marketplace/page.tsx b/autogpt_platform/frontend/src/app/marketplace/page.tsx index ded8fdf777b2..232f94c96dc5 100644 --- a/autogpt_platform/frontend/src/app/marketplace/page.tsx +++ b/autogpt_platform/frontend/src/app/marketplace/page.tsx @@ -6,7 +6,6 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import MarketplaceAPI, { AgentResponse, - AgentListResponse, AgentWithRank, } from "@/lib/marketplace-api"; import { @@ -192,17 +191,19 @@ const Marketplace: React.FC = () => { const [searchResults, setSearchResults] = useState([]); const [featuredAgents, setFeaturedAgents] = useState([]); const [topAgents, setTopAgents] = useState([]); - const [page, setPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); const [isLoading, setIsLoading] = useState(false); + const [topAgentsPage, setTopAgentsPage] = useState(1); + const [searchPage, setSearchPage] = useState(1); + const [topAgentsTotalPages, setTopAgentsTotalPages] = useState(1); + const [searchTotalPages, setSearchTotalPages] = useState(1); const fetchTopAgents = useCallback( async (currentPage: number) => { setIsLoading(true); try { const response = await api.getTopDownloadedAgents(currentPage, 9); - setTopAgents(response.agents); - setTotalPages(response.total_pages); + setTopAgents(response.items); + setTopAgentsTotalPages(response.total_pages); } catch (error) { console.error("Error fetching top agents:", error); } finally { @@ -215,19 +216,20 @@ const Marketplace: React.FC = () => { const fetchFeaturedAgents = useCallback(async () => { try { const featured = await api.getFeaturedAgents(); - setFeaturedAgents(featured.agents); + setFeaturedAgents(featured.items); } catch (error) { console.error("Error fetching featured agents:", error); } }, [api]); const searchAgents = useCallback( - async (searchTerm: string) => { + async (searchTerm: string, currentPage: number) => { setIsLoading(true); try { - const response = await api.searchAgents(searchTerm, 1, 30); - const filteredAgents = response.filter((agent) => agent.rank > 0); + const response = await api.searchAgents(searchTerm, currentPage, 9); + const filteredAgents = response.items.filter((agent) => agent.rank > 0); setSearchResults(filteredAgents); + setSearchTotalPages(response.total_pages); } catch (error) { console.error("Error searching agents:", error); } finally { @@ -244,11 +246,11 @@ const Marketplace: React.FC = () => { useEffect(() => { if (searchValue) { - debouncedSearch(searchValue); + searchAgents(searchValue, searchPage); } else { - fetchTopAgents(page); + fetchTopAgents(topAgentsPage); } - }, [searchValue, page, debouncedSearch, fetchTopAgents]); + }, [searchValue, searchPage, topAgentsPage, searchAgents, fetchTopAgents]); useEffect(() => { fetchFeaturedAgents(); @@ -256,18 +258,30 @@ const Marketplace: React.FC = () => { const handleInputChange = (e: React.ChangeEvent) => { setSearchValue(e.target.value); - setPage(1); + setSearchPage(1); }; const handleNextPage = () => { - if (page < totalPages) { - setPage(page + 1); + if (searchValue) { + if (searchPage < searchTotalPages) { + setSearchPage(searchPage + 1); + } + } else { + if (topAgentsPage < topAgentsTotalPages) { + setTopAgentsPage(topAgentsPage + 1); + } } }; const handlePrevPage = () => { - if (page > 1) { - setPage(page - 1); + if (searchValue) { + if (searchPage > 1) { + setSearchPage(searchPage - 1); + } + } else { + if (topAgentsPage > 1) { + setTopAgentsPage(topAgentsPage - 1); + } } }; @@ -283,7 +297,15 @@ const Marketplace: React.FC = () => { ) : searchValue ? ( searchResults.length > 0 ? ( - + <> + + + ) : (

@@ -302,8 +324,8 @@ const Marketplace: React.FC = () => { )} diff --git a/autogpt_platform/frontend/src/components/Flow.tsx b/autogpt_platform/frontend/src/components/Flow.tsx index 7d7af8c2de80..7501ca1e4588 100644 --- a/autogpt_platform/frontend/src/components/Flow.tsx +++ b/autogpt_platform/frontend/src/components/Flow.tsx @@ -45,6 +45,7 @@ import RunnerUIWrapper, { import PrimaryActionBar from "@/components/PrimaryActionButton"; import { useToast } from "@/components/ui/use-toast"; import { forceLoad } from "@sentry/nextjs"; +import { useCopyPaste } from "../hooks/useCopyPaste"; // This is for the history, this is the minimum distance a block must move before it is logged // It helps to prevent spamming the history with small movements especially when pressing on a input in a block @@ -459,6 +460,8 @@ const FlowEditor: React.FC<{ history.redo(); }; + const handleCopyPaste = useCopyPaste(getNextNodeId); + const handleKeyDown = useCallback( (event: KeyboardEvent) => { // Prevent copy/paste if any modal is open or if the focus is on an input element @@ -470,68 +473,9 @@ const FlowEditor: React.FC<{ if (isAnyModalOpen || isInputField) return; - if (event.ctrlKey || event.metaKey) { - if (event.key === "c" || event.key === "C") { - // Copy selected nodes - const selectedNodes = nodes.filter((node) => node.selected); - const selectedEdges = edges.filter((edge) => edge.selected); - setCopiedNodes(selectedNodes); - setCopiedEdges(selectedEdges); - } - if (event.key === "v" || event.key === "V") { - // Paste copied nodes - if (copiedNodes.length > 0) { - const oldToNewNodeIDMap: Record = {}; - const pastedNodes = copiedNodes.map((node, index) => { - const newNodeId = (nodeId + index).toString(); - oldToNewNodeIDMap[node.id] = newNodeId; - return { - ...node, - id: newNodeId, - position: { - x: node.position.x + 20, // Offset pasted nodes - y: node.position.y + 20, - }, - data: { - ...node.data, - status: undefined, // Reset status - executionResults: undefined, // Clear output data - }, - }; - }); - setNodes((existingNodes) => - // Deselect copied nodes - existingNodes.map((node) => ({ ...node, selected: false })), - ); - addNodes(pastedNodes); - setNodeId((prevId) => prevId + copiedNodes.length); - - const pastedEdges = copiedEdges.map((edge) => { - const newSourceId = oldToNewNodeIDMap[edge.source] ?? edge.source; - const newTargetId = oldToNewNodeIDMap[edge.target] ?? edge.target; - return { - ...edge, - id: `${newSourceId}_${edge.sourceHandle}_${newTargetId}_${edge.targetHandle}_${Date.now()}`, - source: newSourceId, - target: newTargetId, - }; - }); - addEdges(pastedEdges); - } - } - } + handleCopyPaste(event); }, - [ - isAnyModalOpen, - nodes, - edges, - copiedNodes, - setNodes, - addNodes, - copiedEdges, - addEdges, - nodeId, - ], + [isAnyModalOpen, handleCopyPaste], ); useEffect(() => { diff --git a/autogpt_platform/frontend/src/components/admin/marketplace/AdminFeaturedAgentsControl.tsx b/autogpt_platform/frontend/src/components/admin/marketplace/AdminFeaturedAgentsControl.tsx index e0a9f1d6ec98..cc1a2c8de6e3 100644 --- a/autogpt_platform/frontend/src/components/admin/marketplace/AdminFeaturedAgentsControl.tsx +++ b/autogpt_platform/frontend/src/components/admin/marketplace/AdminFeaturedAgentsControl.tsx @@ -46,11 +46,11 @@ export default async function AdminFeaturedAgentsControl({

Featured Agent Controls

Remove, diff --git a/autogpt_platform/frontend/src/components/admin/marketplace/actions.ts b/autogpt_platform/frontend/src/components/admin/marketplace/actions.ts index 80b871585694..b7febfa14321 100644 --- a/autogpt_platform/frontend/src/components/admin/marketplace/actions.ts +++ b/autogpt_platform/frontend/src/components/admin/marketplace/actions.ts @@ -59,7 +59,7 @@ export async function getFeaturedAgents( async () => { const api = new ServerSideMarketplaceAPI(); const featured = await api.getFeaturedAgents(page, pageSize); - console.debug(`Getting featured agents ${featured.agents.length}`); + console.debug(`Getting featured agents ${featured.items.length}`); return featured; }, ); @@ -135,7 +135,7 @@ export async function getNotFeaturedAgents( async () => { const api = new ServerSideMarketplaceAPI(); const agents = await api.getNotFeaturedAgents(page, pageSize); - console.debug(`Getting not featured agents ${agents.agents.length}`); + console.debug(`Getting not featured agents ${agents.items.length}`); return agents; }, ); diff --git a/autogpt_platform/frontend/src/hooks/useCopyPaste.ts b/autogpt_platform/frontend/src/hooks/useCopyPaste.ts new file mode 100644 index 000000000000..c5c6400fa865 --- /dev/null +++ b/autogpt_platform/frontend/src/hooks/useCopyPaste.ts @@ -0,0 +1,122 @@ +import { useCallback } from "react"; +import { Node, Edge, useReactFlow, useViewport } from "@xyflow/react"; + +export function useCopyPaste(getNextNodeId: () => string) { + const { setNodes, addEdges, getNodes, getEdges } = useReactFlow(); + const { x, y, zoom } = useViewport(); + + const handleCopyPaste = useCallback( + (event: KeyboardEvent) => { + if (event.ctrlKey || event.metaKey) { + if (event.key === "c" || event.key === "C") { + const selectedNodes = getNodes().filter((node) => node.selected); + const selectedEdges = getEdges().filter((edge) => edge.selected); + + const copiedData = { + nodes: selectedNodes.map((node) => ({ + ...node, + data: { + ...node.data, + connections: [], + }, + })), + edges: selectedEdges, + }; + + localStorage.setItem("copiedFlowData", JSON.stringify(copiedData)); + } + if (event.key === "v" || event.key === "V") { + const copiedDataString = localStorage.getItem("copiedFlowData"); + if (copiedDataString) { + const copiedData = JSON.parse(copiedDataString); + const oldToNewIdMap: Record = {}; + + const viewportCenter = { + x: (window.innerWidth / 2 - x) / zoom, + y: (window.innerHeight / 2 - y) / zoom, + }; + + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity; + copiedData.nodes.forEach((node: Node) => { + minX = Math.min(minX, node.position.x); + minY = Math.min(minY, node.position.y); + maxX = Math.max(maxX, node.position.x); + maxY = Math.max(maxY, node.position.y); + }); + + const offsetX = viewportCenter.x - (minX + maxX) / 2; + const offsetY = viewportCenter.y - (minY + maxY) / 2; + + const pastedNodes = copiedData.nodes.map((node: Node) => { + const newNodeId = getNextNodeId(); + oldToNewIdMap[node.id] = newNodeId; + return { + ...node, + id: newNodeId, + position: { + x: node.position.x + offsetX, + y: node.position.y + offsetY, + }, + data: { + ...node.data, + status: undefined, + executionResults: undefined, + }, + }; + }); + + const pastedEdges = copiedData.edges.map((edge: Edge) => { + const newSourceId = oldToNewIdMap[edge.source] ?? edge.source; + const newTargetId = oldToNewIdMap[edge.target] ?? edge.target; + return { + ...edge, + id: `${newSourceId}_${edge.sourceHandle}_${newTargetId}_${edge.targetHandle}_${Date.now()}`, + source: newSourceId, + target: newTargetId, + }; + }); + + setNodes((existingNodes) => [ + ...existingNodes.map((node) => ({ ...node, selected: false })), + ...pastedNodes, + ]); + addEdges(pastedEdges); + + setNodes((nodes) => { + return nodes.map((node) => { + if (oldToNewIdMap[node.id]) { + const nodeConnections = pastedEdges + .filter( + (edge) => + edge.source === node.id || edge.target === node.id, + ) + .map((edge) => ({ + edge_id: edge.id, + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle, + })); + return { + ...node, + data: { + ...node.data, + connections: nodeConnections, + }, + }; + } + return node; + }); + }); + } + } + } + }, + [setNodes, addEdges, getNodes, getEdges, getNextNodeId, x, y, zoom], + ); + + return handleCopyPaste; +} diff --git a/autogpt_platform/frontend/src/lib/marketplace-api/base-client.ts b/autogpt_platform/frontend/src/lib/marketplace-api/base-client.ts index 9d3866779974..0c579358056e 100644 --- a/autogpt_platform/frontend/src/lib/marketplace-api/base-client.ts +++ b/autogpt_platform/frontend/src/lib/marketplace-api/base-client.ts @@ -3,12 +3,14 @@ import { AddAgentRequest, AgentResponse, ListAgentsParams, - AgentListResponse, AgentDetailResponse, AgentWithRank, FeaturedAgentResponse, UniqueCategoriesResponse, AnalyticsEvent, + ListResponse, + Agent, + AgentListResponse, } from "./types"; export default class BaseMarketplaceAPI { @@ -46,7 +48,7 @@ export default class BaseMarketplaceAPI { async getTopDownloadedAgents( page: number = 1, pageSize: number = 10, - ): Promise { + ): Promise> { return this._get( `/top-downloads/agents?page=${page}&page_size=${pageSize}`, ); @@ -55,7 +57,7 @@ export default class BaseMarketplaceAPI { async getFeaturedAgents( page: number = 1, pageSize: number = 10, - ): Promise { + ): Promise> { return this._get(`/featured/agents?page=${page}&page_size=${pageSize}`); } @@ -67,7 +69,7 @@ export default class BaseMarketplaceAPI { descriptionThreshold: number = 60, sortBy: string = "rank", sortOrder: "asc" | "desc" = "desc", - ): Promise { + ): Promise> { const queryParams = new URLSearchParams({ query, page: page.toString(), @@ -126,7 +128,7 @@ export default class BaseMarketplaceAPI { ); } - async getAgentSubmissions(): Promise { + async getAgentSubmissions(): Promise> { return this._get("/admin/agent/submissions"); } @@ -186,7 +188,7 @@ export default class BaseMarketplaceAPI { async getNotFeaturedAgents( page: number = 1, pageSize: number = 10, - ): Promise { + ): Promise> { return this._get( `/admin/agent/not-featured?page=${page}&page_size=${pageSize}`, ); diff --git a/autogpt_platform/frontend/src/lib/marketplace-api/types.ts b/autogpt_platform/frontend/src/lib/marketplace-api/types.ts index 87772e1224dd..82516d8acfde 100644 --- a/autogpt_platform/frontend/src/lib/marketplace-api/types.ts +++ b/autogpt_platform/frontend/src/lib/marketplace-api/types.ts @@ -67,7 +67,16 @@ export type AgentWithRank = Agent & { rank: number; }; -export type AgentListResponse = AgentList; +export type ListResponse = { + items: T[]; + total_count: number; + page: number; + page_size: number; + total_pages: number; +}; + +export type AgentListResponse = ListResponse; +export type AgentWithRankListResponse = ListResponse; export type AgentDetailResponse = AgentDetail; diff --git a/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml b/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml index 793ef400ec12..1821acc24a39 100644 --- a/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml +++ b/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml @@ -66,4 +66,5 @@ env: SENTRY_AUTH_TOKEN: "" NEXT_PUBLIC_AUTH_CALLBACK_URL: "https://dev-server.agpt.co/auth/callback" NEXT_PUBLIC_AGPT_WS_SERVER_URL: "wss://dev-ws-server.agpt.co/ws" - NEXT_PUBLIC_AGPT_MARKETPLACE_URL: "https://dev-market.agpt.co/api/v1/market" \ No newline at end of file + NEXT_PUBLIC_AGPT_MARKETPLACE_URL: "https://dev-market.agpt.co/api/v1/market" + NEXT_PUBLIC_BEHAVE_AS: "CLOUD" \ No newline at end of file diff --git a/autogpt_platform/infra/helm/autogpt-builder/values.prod.yaml b/autogpt_platform/infra/helm/autogpt-builder/values.prod.yaml index f1f3a90efca6..4c65a1b17c53 100644 --- a/autogpt_platform/infra/helm/autogpt-builder/values.prod.yaml +++ b/autogpt_platform/infra/helm/autogpt-builder/values.prod.yaml @@ -85,4 +85,5 @@ env: NEXT_PUBLIC_SUPABASE_URL: "https://bgwpwdsxblryihinutbx.supabase.co" GOOGLE_CLIENT_ID: "" GOOGLE_CLIENT_SECRET: "" - SENTRY_AUTH_TOKEN: "" \ No newline at end of file + SENTRY_AUTH_TOKEN: "" + NEXT_PUBLIC_BEHAVE_AS: "CLOUD" \ No newline at end of file diff --git a/autogpt_platform/infra/helm/autogpt-market/values.dev.yaml b/autogpt_platform/infra/helm/autogpt-market/values.dev.yaml index 9d2066a1d8cf..96d63b40db1a 100644 --- a/autogpt_platform/infra/helm/autogpt-market/values.dev.yaml +++ b/autogpt_platform/infra/helm/autogpt-market/values.dev.yaml @@ -95,6 +95,6 @@ env: secrets: SUPABASE_JWT_SECRET: "AgBL6H6byPKXVN0nYWgqyoZBHJJJqk8S3rNYSu3JvwOsNn3Sw94SrJcXuE48fxcojcbKvYcDshetFhrSNEsfUwvbSFoaIj+MItPIZ8FGHyfXnmeZJ5j/sdvcjaMmWS7CiL2jhzI+nc8rL2ATV2TgA7E6FA9MvhWqkAyZu04pCd1c9DsAlpcfQ3pxywXjOV+BU0+1+++Z+fTnaugt+hRbhHrKTddaPhi72KrIyJPOUlqfht0JcgflT1f5frvmNnDwkiP862knhJsqg7XyrSy4msCN7eH4BvV01pO3KhAEnMG4Lk/5FeE/2t05+HqgB4mvPbkqfY6g8Lvf6qmd5g+stB10wRmr+TxzokOom36r7sYd3tyXZMgemJOi3BZjYk7774zf/TI081pJx1FPvM6dDPQdrgnU1nhshq/gLyKB5tTTCkPQHW+YhUtApWgrC5mq8ezMqfuwrUuR+NvyO59K56ERJJFAw1fDKHIVl4TYmftet42lkVOchml30cje9gjBtpOrTkFf8lMv5DLQ/ygdwsAYmVrpYtXbT+GAaRI9QIv/rZVuckfz4uIhTp7IjAtRXJreMH2V8GZ22h6Of/BQG+XA42lMA2huIyLwsqmtL0qxNmIu5EGjcXBvlOt5eNquzts68jgNqEfE6fEuqS9lDepvtTSxGp7wmcmWwOBbIgbCnxl8t4IW/e1qvOPuvxMldH+YtVIvfHMf3DnvbehhCIYuLmRM+E6139/qdGKreRyCi4PA4PKTMEG962rSu/7z0X5wu+UYY+kESrs7HhksL3PhB3dpbZ0HF+b6lchziTI0atG+UNX05ysL" - DATABASE_URL: "AgAWMrGQg5ON4uR4ZBWZWvt47nuT57L+D63i9vnfSMH9IGAthi+Z056Om4sBfyjpIOT4uowECnznRWQOZLXMrNeIk3Hwc2cBqFuO2LpnN4UnaRQHYNzp2gn8onwq54XhOhQF/aw0sSMBcXWJKfBOY44wZI0vebFaOE1h615tXVvTz9g8OFavj2Wky25ydHZiukpUs7M9eu7FE5RSGiG8saHL8+VLQRWqODf4dcykFiFcp0hNbaNSmYpAWyI8oe44SkzVb2AWbEZEUjkNLVxV95iMeTi0SRNaJ/hW6wcN7O2ZByb2jwRKiXXRa1l9b5Uy9hcueY1VU5ZCDuX5XNtM9BnfbO+Ez0FCc9J8DhfSi6woG4WceWksZ7unEUspPFKVRTFlKqn5MPR/ppfNwatlCpgf9q+zDybrMRoiD8aOcn7QSLNcs+dRRmFNDtu2+2RMBhoYSuENyOxZZRfjg+xYOL29BGx0SOQ46Jk7W/ZC706+T91SHfvTFMqStWFrYQOBxUcoDSK3XidJ/CYcXw3xzXpM4DTSbKVTwKVkPy72DeQf+eTxk0vS0xnbz3SHZVBMrRAcCfvS1HwZGc9TuV6dMfH6i4WKpPt2PxyuDL/sqt0RdHCKjPL8dC+XozqPTv0tTMQ9Xof2bllXWCN5QDWuC4Gam9M8aWKvALqtxwjDJmeqfqImPe045gsIUEX3pGYvB4PHfRPaocPe9sutjfa5EVk1md0m2yBpBcxouJBwV72sqK3yMVFJ+UYu+1nasiNkG828/hFYhHxlV+oUrGHpDV+p2c4faFUMEyo6FC+fBvM1h8JgM+Bk8kaf8YSUuop6Xwc4s/yF46dB6CdCaVJPvI9m3elj6Cg9EAXq7GY=" + DATABASE_URL: "AgACJkybaNon1X7ZkvtyM0mJ3Gfbjh1LWSJdWGC3ny6QU/8yERomTjvDylHuFoaoGeEs5ewYtgH87G/t1q3LF35cjmipGTSJbxFKfyoRcGBZSajen2Ijs+satVD7T6bmCNsurEsUD16QDLoV5lx+THXdEjv4VrBtFwY0HO2BIP15X/vMie9Mk91Uk1eze5dj8WoQ5OywH8O8Ugh7/iOleEyiPaMzxdRAfwvYHgX5QYqDno0ktZFyKnDOpCnegIwcUei4tt+EiTAG1SxESk48DQQokNrZc+lqlPwozX6FOYkgcQiZZqIN+9qOy5hB+1wKggS8zcd7/YAlSpSd8LMeAokflxOabN5Ctyvx3k5tGgstKQabW1TErwL8RjB3WYClLvJl8bfc4qILr4jpfA4all59f8oinATiyeqJ3Hx267FdHH2aywnXnNdEmycGhKkuH0vAa74oYsUiD9BrjkxmMdxCwmEjl47ob5fwcraKOc7hNF+hPi2C61/z1T6yoIeu4ediMTk0m1ZPIOotlbj/nKkmrKDfD+WMia/YvBpUtlM/SaEyX66gmO03kxW92cmXVCjK65oGh0ueCyK84J9e/2y+qO6yFXh7991+wTQyAedBBoc2myNcebKzuAmFNwanCs+FMOktPZsMMzfFy3oHJQxgFkPEp+jKennODqTe0A3LBhJC3ddwCYgd1TAABC2+DqFGaCRiyaSZ4BZIitEPFzpJwITIFZoRyxrCibrGKKnILVjGNaiV/KdIzfj70AAdzG/7GFdA9SKzRQimnVw99YTjNouCYzt7iLBV/8KrcMvyyeHZle1A6zg0gjjj4Yp6p0ssIOvhuDLjec4NMi/E5EbgYzKQFr7jN2u9%" SENTRY_DSN: "AgB9i02k9BgaIXF0p9Qyyeo0PRa9bd3UiPBWQ3V4Jn19Vy5XAzKfYvqP8t+vafN2ffY+wCk1FlhYzdIuFjh3oRvdKvtwGEBZk6nLFiUrw/GSum0ueR2OzEy+AwGFXA9FstD0KCMJvyehSv9xRm9kqLOC4Xb/5lOWwTNF3AKqkEMEeKrOWx4OLXG6MLdR7OicY45BCE5WvcV2PizDaN5w3J72eUxFP0HjXit/aW/gK32IJME0RxeuQZ5TnPKTNrooYPR0eWXd2PgYshFjQ2ARy/OsvOrD10y8tQ3M5qx/HNWLC/r0lEu2np+9iUIAE1ufSwjmNSyi4V8usdZWq7xnf3vuKlSgmveqKkLbwQUWj1BpLNIjUvyY+1Rk1rxup/WCgaw+xOZd6sR/qTIjILv5GuzpU0AiwEm7sgl2pmpFXq6n6QjNOfZoPBTL73f4bpXNJ3EyMYDbPxOtGDz91B+bDtOsMr1DNWQslKkk3EIilm/l0+NuLKxf/e2HwM3sB15mkQqVZBdbiVOr7B27cR9xAnr296KE/BU6E9dp/fl+IgcaonMpTsE61pCLHWxQXNBO5X078/zhmaXBQyEBNQ5SPDr9u3pHWrrLkBtXwldZvgmLMMVFMAzrVVkJB4lC9sZj0pXPhda0/BsA4xcGRELj/PizwSr+kb3lDumNMqzEap5ZjEGCBpeeIVSo19v+RoEDw0AFmyxfYx2+91HsgiEqjEUg+J6yDmjAoRpOD1wRZOnnpR8ufMiqdBteCG8B5SXkhgto1WtDyOMVlX2wbmBFVetv2nAbMIA/l4E/Yv8HXiJsTqAkeYc5Qak6/SMGnZTw7Q==" SUPABASE_SERVICE_ROLE_KEY: "AgCrHCd2AdIv++sX7AAf0YoV+qDFhPuErd/Q9Jgj8/1wDJklqv0giI/M7TRUV6j2Gqa/yLP90Hoiy2BboE0V3FrTtHzasxtSK0hd93+bxvZ34FEfKQyAiddBR8OxzlPwaplzaJ+/Tu+yHf1EesgXrUdydk38D4AQqkrC30FRKJcCxJNTHHzsZiHGQLZNP2l0cEsmqtMXMk8TqbcqHvJZRpr8jP1dSJ7bxEdU9mH/zB4HV+EsPLDFWFAnFjbEQwv8FEGgqpy81ifch4Hz7S0wjwk0x/QsagKavBTvI579K6Sx7uJyMyilpzm5Ct8kDXTEGUWv7pFINXM5cAbcBNzuvwvtXmshMwRsl9e/5Y2/T2VgS7/wPTJA4AmyyrSK976SOjo7imb4XfMwc6Cc/2GE0BRW9jiKvzjQ1TC2ovQpNujTYYgPzIq8sFXEVss31DIcfwbRAzgKTTQZKl+H+i9AS0q6iYHtKORwTQ2bv2XwQwxogXMHTUq1oC3MkzjKfV9DcoTHU2o/+gTyOBW5i3BRatuublA1x0EwDoEVmWA1+i1h2bpkl4QYuyeNlhJnRHzuQU3RdFLWn3MkDM8Q4Y9n0/XXwwTCgqtdAExqNh3YJYumWiGiWfdBpEUqlUtOUurNMXy6rHH4odnNKeLQfMOa9406x5H5xiwNkl3mzGjNiPDMS7JMTptlsoL8DshE2TM0PqZVrQy81OsGNpdiU8MVeUdHO6/bDBe7j9v0FipqpeehX1AZEYb/4CWosTJACWpaTnLYRh+w12bk3x6Sj0kriDKMOuJLBRh1fveZXUC9C0FsEPhq2rBLQDVh78DkBIeKVGUzuxDP/6mT3OSBPe4aCye0vTmwtEOEvB+A7rcMkOl+j90bKAveE9H+f7UVU6Og40Nc3sSuMolKHbQyB9TNd4+jOfmySSN675riL6BpFFCSuWqjrqWFr0yI1h/xAg+YMg8WzWarwSeWr3ykXrbhQvu7Oj27ffLXEIvS0gU=" \ No newline at end of file diff --git a/autogpt_platform/market/market/app.py b/autogpt_platform/market/market/app.py index 864de8cb85cf..63736acd31e1 100644 --- a/autogpt_platform/market/market/app.py +++ b/autogpt_platform/market/market/app.py @@ -49,7 +49,8 @@ async def lifespan(app: fastapi.FastAPI): yield await db_client.disconnect() -docs_url = "/docs" if os.environ.get("APP_ENV") == "local" else None + +docs_url = "/docs" app = fastapi.FastAPI( title="Marketplace API", description="AutoGPT Marketplace API is a service that allows users to share AI agents.", diff --git a/autogpt_platform/market/market/db.py b/autogpt_platform/market/market/db.py index f6b2abcdc658..6b4418bd8f78 100644 --- a/autogpt_platform/market/market/db.py +++ b/autogpt_platform/market/market/db.py @@ -55,6 +55,7 @@ class FeaturedAgentResponse(pydantic.BaseModel): page_size: int total_pages: int + async def delete_agent(agent_id: str) -> prisma.models.Agents | None: """ Delete an agent from the database. @@ -299,7 +300,7 @@ async def search_db( sort_by: str = "rank", sort_order: typing.Literal["desc"] | typing.Literal["asc"] = "desc", submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED, -) -> typing.List[market.utils.extension_types.AgentsWithRank]: +) -> market.model.ListResponse[market.utils.extension_types.AgentsWithRank]: """Perform a search for agents based on the provided query string. Args: @@ -321,7 +322,7 @@ async def search_db( try: offset = (page - 1) * page_size - category_filter = "" + category_filter = "1=1" if categories: category_conditions = [f"'{cat}' = ANY(categories)" for cat in categories] category_filter = "AND (" + " OR ".join(category_conditions) + ")" @@ -354,9 +355,15 @@ async def search_db( graph, "submissionStatus", "submissionDate", - ts_rank(CAST(search AS tsvector), query.q) AS rank - FROM "Agents", query - WHERE 1=1 {category_filter} AND {submission_status_filter} + CASE + WHEN query.q::text = '' THEN 1.0 + ELSE COALESCE(ts_rank(CAST(search AS tsvector), query.q), 0.0) + END AS rank + FROM market."Agents", query + WHERE + (query.q::text = '' OR search @@ query.q) + AND {category_filter} + AND {submission_status_filter} ORDER BY {order_by_clause} LIMIT {page_size} OFFSET {offset}; @@ -367,7 +374,32 @@ async def search_db( model=market.utils.extension_types.AgentsWithRank, ) - return results + class CountResponse(pydantic.BaseModel): + count: int + + count_query = f""" + WITH query AS ( + SELECT to_tsquery(string_agg(lexeme || ':*', ' & ' ORDER BY positions)) AS q + FROM unnest(to_tsvector('{query}')) + ) + SELECT COUNT(*) + FROM market."Agents", query + WHERE (search @@ query.q OR query.q = '') AND {category_filter} AND {submission_status_filter}; + """ + + total_count = await prisma.client.get_client().query_first( + query=count_query, + model=CountResponse, + ) + total_count = total_count.count if total_count else 0 + + return market.model.ListResponse( + items=results, + total_count=total_count, + page=page, + page_size=page_size, + total_pages=(total_count + page_size - 1) // page_size, + ) except prisma.errors.PrismaError as e: raise AgentQueryError(f"Database query failed: {str(e)}") @@ -379,7 +411,7 @@ async def get_top_agents_by_downloads( page: int = 1, page_size: int = 10, submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED, -) -> TopAgentsDBResponse: +) -> market.model.ListResponse[prisma.models.AnalyticsTracker]: """Retrieve the top agents by download count. Args: @@ -406,11 +438,15 @@ async def get_top_agents_by_downloads( except prisma.errors.PrismaError as e: raise AgentQueryError(f"Database query failed: {str(e)}") - # Get total count for pagination info - total_count = len(analytics) + try: + total_count = await prisma.models.AnalyticsTracker.prisma().count( + where={"agent": {"is": {"submissionStatus": submission_status}}}, + ) + except prisma.errors.PrismaError as e: + raise AgentQueryError(f"Database query failed: {str(e)}") - return TopAgentsDBResponse( - analytics=analytics, + return market.model.ListResponse( + items=analytics, total_count=total_count, page=page, page_size=page_size, @@ -665,7 +701,7 @@ async def get_all_categories() -> market.model.CategoriesResponse: return market.model.CategoriesResponse(unique_categories=unique_categories) except prisma.errors.PrismaError as e: raise AgentQueryError(f"Database query failed: {str(e)}") - except Exception as e: + except Exception: # Return an empty list of categories in case of unexpected errors return market.model.CategoriesResponse(unique_categories=[]) diff --git a/autogpt_platform/market/market/model.py b/autogpt_platform/market/market/model.py index 66dda0d0b823..14bd017a1158 100644 --- a/autogpt_platform/market/market/model.py +++ b/autogpt_platform/market/market/model.py @@ -1,39 +1,48 @@ import datetime import typing +from enum import Enum +from typing import Generic, Literal, TypeVar, Union import prisma.enums import pydantic -from enum import Enum -from typing import Literal, Union class InstallationLocation(str, Enum): LOCAL = "local" CLOUD = "cloud" + class AgentInstalledFromMarketplaceEventData(pydantic.BaseModel): marketplace_agent_id: str installed_agent_id: str installation_location: InstallationLocation + class AgentInstalledFromTemplateEventData(pydantic.BaseModel): template_id: str installed_agent_id: str installation_location: InstallationLocation + class AgentInstalledFromMarketplaceEvent(pydantic.BaseModel): event_name: Literal["agent_installed_from_marketplace"] event_data: AgentInstalledFromMarketplaceEventData + class AgentInstalledFromTemplateEvent(pydantic.BaseModel): event_name: Literal["agent_installed_from_template"] event_data: AgentInstalledFromTemplateEventData -AnalyticsEvent = Union[AgentInstalledFromMarketplaceEvent, AgentInstalledFromTemplateEvent] + +AnalyticsEvent = Union[ + AgentInstalledFromMarketplaceEvent, AgentInstalledFromTemplateEvent +] + class AnalyticsRequest(pydantic.BaseModel): event: AnalyticsEvent + class AddAgentRequest(pydantic.BaseModel): graph: dict[str, typing.Any] author: str @@ -78,25 +87,6 @@ class AgentResponse(pydantic.BaseModel): downloads: int = 0 -class AgentListResponse(pydantic.BaseModel): - """ - Represents a response containing a list of agents. - - Attributes: - agents (list[AgentResponse]): The list of agents. - total_count (int): The total count of agents. - page (int): The current page number. - page_size (int): The number of agents per page. - total_pages (int): The total number of pages. - """ - - agents: list[AgentResponse] - total_count: int - page: int - page_size: int - total_pages: int - - class AgentDetailResponse(pydantic.BaseModel): """ Represents the response data for an agent detail. @@ -147,3 +137,25 @@ class CategoriesResponse(pydantic.BaseModel): """ unique_categories: list[str] + + +T = TypeVar("T") + + +class ListResponse(pydantic.BaseModel, Generic[T]): + """ + Represents a list response. + + Attributes: + items (list[T]): The list of items. + total_count (int): The total count of items. + page (int): The current page number. + page_size (int): The number of items per page. + total_pages (int): The total number of pages. + """ + + items: list[T] + total_count: int + page: int + page_size: int + total_pages: int diff --git a/autogpt_platform/market/market/routes/admin.py b/autogpt_platform/market/market/routes/admin.py index 07d7774884e4..a3fe65cc962e 100644 --- a/autogpt_platform/market/market/routes/admin.py +++ b/autogpt_platform/market/market/routes/admin.py @@ -45,7 +45,10 @@ async def delete_agent( raise fastapi.HTTPException(status_code=500, detail=str(e)) except Exception as e: logger.error(f"Unexpected error deleting agent: {e}") - raise fastapi.HTTPException(status_code=500, detail="An unexpected error occurred") + raise fastapi.HTTPException( + status_code=500, detail="An unexpected error occurred" + ) + @router.post("/agent", response_model=market.model.AgentResponse) async def create_agent_entry( @@ -154,14 +157,14 @@ async def get_not_featured_agents( user: autogpt_libs.auth.User = fastapi.Depends( autogpt_libs.auth.requires_admin_user ), -) -> market.model.AgentListResponse: +) -> market.model.ListResponse[market.model.AgentResponse]: """ A basic endpoint to get all not featured agents in the database. """ try: agents = await market.db.get_not_featured_agents(page=page, page_size=page_size) - return market.model.AgentListResponse( - agents=[ + return market.model.ListResponse( + items=[ market.model.AgentResponse(**agent.model_dump()) for agent in agents ], total_count=len(agents), @@ -175,7 +178,10 @@ async def get_not_featured_agents( raise fastapi.HTTPException(status_code=500, detail=str(e)) -@router.get("/agent/submissions", response_model=market.model.AgentListResponse) +@router.get( + "/agent/submissions", + response_model=market.model.ListResponse[market.model.AgentResponse], +) async def get_agent_submissions( page: int = fastapi.Query(1, ge=1, description="Page number"), page_size: int = fastapi.Query( @@ -203,7 +209,7 @@ async def get_agent_submissions( user: autogpt_libs.auth.User = fastapi.Depends( autogpt_libs.auth.requires_admin_user ), -): +) -> market.model.ListResponse[market.model.AgentResponse]: logger.info("Getting agent submissions") try: result = await market.db.get_agents( @@ -223,8 +229,8 @@ async def get_agent_submissions( market.model.AgentResponse(**agent.dict()) for agent in result["agents"] ] - return market.model.AgentListResponse( - agents=agents, + return market.model.ListResponse( + items=agents, total_count=result["total_count"], page=result["page"], page_size=result["page_size"], diff --git a/autogpt_platform/market/market/routes/agents.py b/autogpt_platform/market/market/routes/agents.py index 2342b0061247..672dfb64e54e 100644 --- a/autogpt_platform/market/market/routes/agents.py +++ b/autogpt_platform/market/market/routes/agents.py @@ -14,7 +14,9 @@ router = fastapi.APIRouter() -@router.get("/agents", response_model=market.model.AgentListResponse) +@router.get( + "/agents", response_model=market.model.ListResponse[market.model.AgentResponse] +) async def list_agents( page: int = fastapi.Query(1, ge=1, description="Page number"), page_size: int = fastapi.Query( @@ -60,7 +62,7 @@ async def list_agents( submission_status (str): Filter by submission status (default: "APPROVED"). Returns: - market.model.AgentListResponse: A response containing the list of agents and pagination information. + market.model.ListResponse[market.model.AgentResponse]: A response containing the list of agents and pagination information. Raises: HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500). @@ -83,8 +85,8 @@ async def list_agents( market.model.AgentResponse(**agent.dict()) for agent in result["agents"] ] - return market.model.AgentListResponse( - agents=agents, + return market.model.ListResponse( + items=agents, total_count=result["total_count"], page=result["page"], page_size=result["page_size"], @@ -211,7 +213,10 @@ async def download_agent_file( # top agents by downloads -@router.get("/top-downloads/agents", response_model=market.model.AgentListResponse) +@router.get( + "/top-downloads/agents", + response_model=market.model.ListResponse[market.model.AgentResponse], +) async def top_agents_by_downloads( page: int = fastapi.Query(1, ge=1, description="Page number"), page_size: int = fastapi.Query( @@ -221,7 +226,7 @@ async def top_agents_by_downloads( default=prisma.enums.SubmissionStatus.APPROVED, description="Filter by submission status", ), -): +) -> market.model.ListResponse[market.model.AgentResponse]: """ Retrieve a list of top agents based on the number of downloads. @@ -231,7 +236,7 @@ async def top_agents_by_downloads( submission_status (str): Filter by submission status (default: "APPROVED"). Returns: - market.model.AgentListResponse: A response containing the list of top agents and pagination information. + market.model.ListResponse[market.model.AgentResponse]: A response containing the list of top agents and pagination information. Raises: HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500). @@ -243,12 +248,12 @@ async def top_agents_by_downloads( submission_status=submission_status, ) - ret = market.model.AgentListResponse( + ret = market.model.ListResponse( total_count=result.total_count, page=result.page, page_size=result.page_size, total_pages=result.total_pages, - agents=[ + items=[ market.model.AgentResponse( id=item.agent.id, name=item.agent.name, @@ -263,7 +268,7 @@ async def top_agents_by_downloads( downloads=item.downloads, submissionStatus=item.agent.submissionStatus, ) - for item in result.analytics + for item in result.items if item.agent is not None ], ) @@ -278,7 +283,10 @@ async def top_agents_by_downloads( ) from e -@router.get("/featured/agents", response_model=market.model.AgentListResponse) +@router.get( + "/featured/agents", + response_model=market.model.ListResponse[market.model.AgentResponse], +) async def get_featured_agents( category: str = fastapi.Query( "featured", description="Category of featured agents" @@ -302,7 +310,7 @@ async def get_featured_agents( submission_status (str): Filter by submission status (default: "APPROVED"). Returns: - market.model.AgentListResponse: A response containing the list of featured agents and pagination information. + market.model.ListResponse[market.model.AgentResponse]: A response containing the list of featured agents and pagination information. Raises: HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500). @@ -315,12 +323,12 @@ async def get_featured_agents( submission_status=submission_status, ) - ret = market.model.AgentListResponse( + ret = market.model.ListResponse( total_count=result.total_count, page=result.page, page_size=result.page_size, total_pages=result.total_pages, - agents=[ + items=[ market.model.AgentResponse( id=item.agent.id, name=item.agent.name, diff --git a/autogpt_platform/market/market/routes/search.py b/autogpt_platform/market/market/routes/search.py index 8f26abf5bd60..15ef3ffa30e0 100644 --- a/autogpt_platform/market/market/routes/search.py +++ b/autogpt_platform/market/market/routes/search.py @@ -4,6 +4,7 @@ import prisma.enums import market.db +import market.model import market.utils.extension_types router = fastapi.APIRouter() @@ -27,9 +28,10 @@ async def search( "desc", description="The sort order based on sort_by" ), submission_status: prisma.enums.SubmissionStatus = fastapi.Query( - None, description="The submission status to filter by" + prisma.enums.SubmissionStatus.APPROVED, + description="The submission status to filter by", ), -) -> typing.List[market.utils.extension_types.AgentsWithRank]: +) -> market.model.ListResponse[market.utils.extension_types.AgentsWithRank]: """searches endpoint for agents Args: @@ -41,7 +43,7 @@ async def search( sort_by (str, optional): Sorting by column. Defaults to "rank". sort_order ('asc' | 'desc', optional): the sort order based on sort_by. Defaults to "desc". """ - return await market.db.search_db( + agents = await market.db.search_db( query=query, page=page, page_size=page_size, @@ -51,3 +53,4 @@ async def search( sort_order=sort_order, submission_status=submission_status, ) + return agents diff --git a/autogpt_platform/market/migrations/20241014173713_add_unique_restriction/migration.sql b/autogpt_platform/market/migrations/20241014173713_add_unique_restriction/migration.sql new file mode 100644 index 000000000000..c1ce4b5dee1c --- /dev/null +++ b/autogpt_platform/market/migrations/20241014173713_add_unique_restriction/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[agentId]` on the table `AnalyticsTracker` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "AnalyticsTracker_agentId_key" ON "AnalyticsTracker"("agentId"); diff --git a/autogpt_platform/market/schema.prisma b/autogpt_platform/market/schema.prisma index 1d3a29338596..8a29c2b2ade7 100644 --- a/autogpt_platform/market/schema.prisma +++ b/autogpt_platform/market/schema.prisma @@ -47,7 +47,7 @@ model Agents { model AnalyticsTracker { id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid - agentId String @db.Uuid + agentId String @unique @db.Uuid agent Agents @relation(fields: [agentId], references: [id], onDelete: Cascade) views Int downloads Int