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/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/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/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/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