mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
refactor(backend/db): Improve & clean up Marketplace DB layer & API (#12284)
These changes were part of #12206, but here they are separately for easier review. This is all primarily to make the v2 API (#11678) work possible/easier. ### Changes 🏗️ - Fix relations between `Profile`, `StoreListing`, and `AgentGraph` - Redefine `StoreSubmission` view with more efficient joins (100x speed-up on dev DB) and more consistent field names - Clean up query functions in `store/db.py` - Clean up models in `store/model.py` - Add missing fields to `StoreAgent` and `StoreSubmission` views - Rename ambiguous `agent_id` -> `graph_id` - Clean up API route definitions & docs in `store/routes.py` - Make routes more consistent - Avoid collision edge-case between `/agents/{username}/{agent_name}` and `/agents/{store_listing_version_id}/*` - Replace all usages of legacy `BackendAPI` for store endpoints with generated client - Remove scope requirements on public store endpoints in v1 external API ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Test all Marketplace views (including admin views) - [x] Download an agent from the marketplace - [x] Submit an agent to the Marketplace - [x] Approve/reject Marketplace submission
This commit is contained in:
committed by
GitHub
parent
bde6a4c0df
commit
aa08063939
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import urllib.parse
|
||||
from collections import defaultdict
|
||||
from typing import Annotated, Any, Literal, Optional, Sequence
|
||||
from typing import Annotated, Any, Optional, Sequence
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException, Security
|
||||
from prisma.enums import AgentExecutionStatus, APIKeyPermission
|
||||
@@ -9,9 +9,10 @@ from pydantic import BaseModel, Field
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
import backend.api.features.store.cache as store_cache
|
||||
import backend.api.features.store.db as store_db
|
||||
import backend.api.features.store.model as store_model
|
||||
import backend.blocks
|
||||
from backend.api.external.middleware import require_permission
|
||||
from backend.api.external.middleware import require_auth, require_permission
|
||||
from backend.data import execution as execution_db
|
||||
from backend.data import graph as graph_db
|
||||
from backend.data import user as user_db
|
||||
@@ -230,13 +231,13 @@ async def get_graph_execution_results(
|
||||
@v1_router.get(
|
||||
path="/store/agents",
|
||||
tags=["store"],
|
||||
dependencies=[Security(require_permission(APIKeyPermission.READ_STORE))],
|
||||
dependencies=[Security(require_auth)], # data is public; auth required as anti-DDoS
|
||||
response_model=store_model.StoreAgentsResponse,
|
||||
)
|
||||
async def get_store_agents(
|
||||
featured: bool = False,
|
||||
creator: str | None = None,
|
||||
sorted_by: Literal["rating", "runs", "name", "updated_at"] | None = None,
|
||||
sorted_by: store_db.StoreAgentsSortOptions | None = None,
|
||||
search_query: str | None = None,
|
||||
category: str | None = None,
|
||||
page: int = 1,
|
||||
@@ -278,7 +279,7 @@ async def get_store_agents(
|
||||
@v1_router.get(
|
||||
path="/store/agents/{username}/{agent_name}",
|
||||
tags=["store"],
|
||||
dependencies=[Security(require_permission(APIKeyPermission.READ_STORE))],
|
||||
dependencies=[Security(require_auth)], # data is public; auth required as anti-DDoS
|
||||
response_model=store_model.StoreAgentDetails,
|
||||
)
|
||||
async def get_store_agent(
|
||||
@@ -306,13 +307,13 @@ async def get_store_agent(
|
||||
@v1_router.get(
|
||||
path="/store/creators",
|
||||
tags=["store"],
|
||||
dependencies=[Security(require_permission(APIKeyPermission.READ_STORE))],
|
||||
dependencies=[Security(require_auth)], # data is public; auth required as anti-DDoS
|
||||
response_model=store_model.CreatorsResponse,
|
||||
)
|
||||
async def get_store_creators(
|
||||
featured: bool = False,
|
||||
search_query: str | None = None,
|
||||
sorted_by: Literal["agent_rating", "agent_runs", "num_agents"] | None = None,
|
||||
sorted_by: store_db.StoreCreatorsSortOptions | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> store_model.CreatorsResponse:
|
||||
@@ -348,7 +349,7 @@ async def get_store_creators(
|
||||
@v1_router.get(
|
||||
path="/store/creators/{username}",
|
||||
tags=["store"],
|
||||
dependencies=[Security(require_permission(APIKeyPermission.READ_STORE))],
|
||||
dependencies=[Security(require_auth)], # data is public; auth required as anti-DDoS
|
||||
response_model=store_model.CreatorDetails,
|
||||
)
|
||||
async def get_store_creator(
|
||||
|
||||
@@ -24,14 +24,13 @@ router = fastapi.APIRouter(
|
||||
@router.get(
|
||||
"/listings",
|
||||
summary="Get Admin Listings History",
|
||||
response_model=store_model.StoreListingsWithVersionsResponse,
|
||||
)
|
||||
async def get_admin_listings_with_versions(
|
||||
status: typing.Optional[prisma.enums.SubmissionStatus] = None,
|
||||
search: typing.Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
):
|
||||
) -> store_model.StoreListingsWithVersionsAdminViewResponse:
|
||||
"""
|
||||
Get store listings with their version history for admins.
|
||||
|
||||
@@ -45,36 +44,26 @@ async def get_admin_listings_with_versions(
|
||||
page_size: Number of items per page
|
||||
|
||||
Returns:
|
||||
StoreListingsWithVersionsResponse with listings and their versions
|
||||
Paginated listings with their versions
|
||||
"""
|
||||
try:
|
||||
listings = await store_db.get_admin_listings_with_versions(
|
||||
status=status,
|
||||
search_query=search,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return listings
|
||||
except Exception as e:
|
||||
logger.exception("Error getting admin listings with versions: %s", e)
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"detail": "An error occurred while retrieving listings with versions"
|
||||
},
|
||||
)
|
||||
listings = await store_db.get_admin_listings_with_versions(
|
||||
status=status,
|
||||
search_query=search,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return listings
|
||||
|
||||
|
||||
@router.post(
|
||||
"/submissions/{store_listing_version_id}/review",
|
||||
summary="Review Store Submission",
|
||||
response_model=store_model.StoreSubmission,
|
||||
)
|
||||
async def review_submission(
|
||||
store_listing_version_id: str,
|
||||
request: store_model.ReviewSubmissionRequest,
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
):
|
||||
) -> store_model.StoreSubmissionAdminView:
|
||||
"""
|
||||
Review a store listing submission.
|
||||
|
||||
@@ -84,31 +73,24 @@ async def review_submission(
|
||||
user_id: Authenticated admin user performing the review
|
||||
|
||||
Returns:
|
||||
StoreSubmission with updated review information
|
||||
StoreSubmissionAdminView with updated review information
|
||||
"""
|
||||
try:
|
||||
already_approved = await store_db.check_submission_already_approved(
|
||||
store_listing_version_id=store_listing_version_id,
|
||||
)
|
||||
submission = await store_db.review_store_submission(
|
||||
store_listing_version_id=store_listing_version_id,
|
||||
is_approved=request.is_approved,
|
||||
external_comments=request.comments,
|
||||
internal_comments=request.internal_comments or "",
|
||||
reviewer_id=user_id,
|
||||
)
|
||||
already_approved = await store_db.check_submission_already_approved(
|
||||
store_listing_version_id=store_listing_version_id,
|
||||
)
|
||||
submission = await store_db.review_store_submission(
|
||||
store_listing_version_id=store_listing_version_id,
|
||||
is_approved=request.is_approved,
|
||||
external_comments=request.comments,
|
||||
internal_comments=request.internal_comments or "",
|
||||
reviewer_id=user_id,
|
||||
)
|
||||
|
||||
state_changed = already_approved != request.is_approved
|
||||
# Clear caches when the request is approved as it updates what is shown on the store
|
||||
if state_changed:
|
||||
store_cache.clear_all_caches()
|
||||
return submission
|
||||
except Exception as e:
|
||||
logger.exception("Error reviewing submission: %s", e)
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while reviewing the submission"},
|
||||
)
|
||||
state_changed = already_approved != request.is_approved
|
||||
# Clear caches whenever approval state changes, since store visibility can change
|
||||
if state_changed:
|
||||
store_cache.clear_all_caches()
|
||||
return submission
|
||||
|
||||
|
||||
@router.get(
|
||||
|
||||
@@ -8,7 +8,6 @@ import prisma.errors
|
||||
import prisma.models
|
||||
import prisma.types
|
||||
|
||||
import backend.api.features.store.exceptions as store_exceptions
|
||||
import backend.api.features.store.image_gen as store_image_gen
|
||||
import backend.api.features.store.media as store_media
|
||||
import backend.data.graph as graph_db
|
||||
@@ -251,7 +250,7 @@ async def get_library_agent(id: str, user_id: str) -> library_model.LibraryAgent
|
||||
The requested LibraryAgent.
|
||||
|
||||
Raises:
|
||||
AgentNotFoundError: If the specified agent does not exist.
|
||||
NotFoundError: If the specified agent does not exist.
|
||||
DatabaseError: If there's an error during retrieval.
|
||||
"""
|
||||
library_agent = await prisma.models.LibraryAgent.prisma().find_first(
|
||||
@@ -414,7 +413,7 @@ async def create_library_agent(
|
||||
If the graph has sub-graphs, the parent graph will always be the first entry in the list.
|
||||
|
||||
Raises:
|
||||
AgentNotFoundError: If the specified agent does not exist.
|
||||
NotFoundError: If the specified agent does not exist.
|
||||
DatabaseError: If there's an error during creation or if image generation fails.
|
||||
"""
|
||||
logger.info(
|
||||
@@ -817,7 +816,7 @@ async def add_store_agent_to_library(
|
||||
The newly created LibraryAgent if successfully added, the existing corresponding one if any.
|
||||
|
||||
Raises:
|
||||
AgentNotFoundError: If the store listing or associated agent is not found.
|
||||
NotFoundError: If the store listing or associated agent is not found.
|
||||
DatabaseError: If there's an issue creating the LibraryAgent record.
|
||||
"""
|
||||
logger.debug(
|
||||
@@ -832,7 +831,7 @@ async def add_store_agent_to_library(
|
||||
)
|
||||
if not store_listing_version or not store_listing_version.AgentGraph:
|
||||
logger.warning(f"Store listing version not found: {store_listing_version_id}")
|
||||
raise store_exceptions.AgentNotFoundError(
|
||||
raise NotFoundError(
|
||||
f"Store listing version {store_listing_version_id} not found or invalid"
|
||||
)
|
||||
|
||||
@@ -846,7 +845,7 @@ async def add_store_agent_to_library(
|
||||
include_subgraphs=False,
|
||||
)
|
||||
if not graph_model:
|
||||
raise store_exceptions.AgentNotFoundError(
|
||||
raise NotFoundError(
|
||||
f"Graph #{graph.id} v{graph.version} not found or accessible"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import prisma.enums
|
||||
import prisma.models
|
||||
import pytest
|
||||
|
||||
import backend.api.features.store.exceptions
|
||||
from backend.data.db import connect
|
||||
from backend.data.includes import library_agent_include
|
||||
|
||||
@@ -218,7 +217,7 @@ async def test_add_agent_to_library_not_found(mocker):
|
||||
)
|
||||
|
||||
# Call function and verify exception
|
||||
with pytest.raises(backend.api.features.store.exceptions.AgentNotFoundError):
|
||||
with pytest.raises(db.NotFoundError):
|
||||
await db.add_store_agent_to_library("version123", "test-user")
|
||||
|
||||
# Verify mock called correctly
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Literal
|
||||
|
||||
from backend.util.cache import cached
|
||||
|
||||
from . import db as store_db
|
||||
@@ -23,7 +21,7 @@ def clear_all_caches():
|
||||
async def _get_cached_store_agents(
|
||||
featured: bool,
|
||||
creator: str | None,
|
||||
sorted_by: Literal["rating", "runs", "name", "updated_at"] | None,
|
||||
sorted_by: store_db.StoreAgentsSortOptions | None,
|
||||
search_query: str | None,
|
||||
category: str | None,
|
||||
page: int,
|
||||
@@ -57,7 +55,7 @@ async def _get_cached_agent_details(
|
||||
async def _get_cached_store_creators(
|
||||
featured: bool,
|
||||
search_query: str | None,
|
||||
sorted_by: Literal["agent_rating", "agent_runs", "num_agents"] | None,
|
||||
sorted_by: store_db.StoreCreatorsSortOptions | None,
|
||||
page: int,
|
||||
page_size: int,
|
||||
):
|
||||
@@ -75,4 +73,4 @@ async def _get_cached_store_creators(
|
||||
@cached(maxsize=100, ttl_seconds=300, shared_cache=True)
|
||||
async def _get_cached_creator_details(username: str):
|
||||
"""Cached helper to get creator details."""
|
||||
return await store_db.get_store_creator_details(username=username.lower())
|
||||
return await store_db.get_store_creator(username=username.lower())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ async def test_get_store_agents(mocker):
|
||||
mock_agents = [
|
||||
prisma.models.StoreAgent(
|
||||
listing_id="test-id",
|
||||
storeListingVersionId="version123",
|
||||
listing_version_id="version123",
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_video=None,
|
||||
@@ -40,11 +40,11 @@ async def test_get_store_agents(mocker):
|
||||
runs=10,
|
||||
rating=4.5,
|
||||
versions=["1.0"],
|
||||
agentGraphVersions=["1"],
|
||||
agentGraphId="test-graph-id",
|
||||
graph_id="test-graph-id",
|
||||
graph_versions=["1"],
|
||||
updated_at=datetime.now(),
|
||||
is_available=False,
|
||||
useForOnboarding=False,
|
||||
use_for_onboarding=False,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -68,10 +68,10 @@ async def test_get_store_agents(mocker):
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_get_store_agent_details(mocker):
|
||||
# Mock data
|
||||
# Mock data - StoreAgent view already contains the active version data
|
||||
mock_agent = prisma.models.StoreAgent(
|
||||
listing_id="test-id",
|
||||
storeListingVersionId="version123",
|
||||
listing_version_id="version123",
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_video="video.mp4",
|
||||
@@ -85,102 +85,38 @@ async def test_get_store_agent_details(mocker):
|
||||
runs=10,
|
||||
rating=4.5,
|
||||
versions=["1.0"],
|
||||
agentGraphVersions=["1"],
|
||||
agentGraphId="test-graph-id",
|
||||
updated_at=datetime.now(),
|
||||
is_available=False,
|
||||
useForOnboarding=False,
|
||||
)
|
||||
|
||||
# Mock active version agent (what we want to return for active version)
|
||||
mock_active_agent = prisma.models.StoreAgent(
|
||||
listing_id="test-id",
|
||||
storeListingVersionId="active-version-id",
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent Active",
|
||||
agent_video="active_video.mp4",
|
||||
agent_image=["active_image.jpg"],
|
||||
featured=False,
|
||||
creator_username="creator",
|
||||
creator_avatar="avatar.jpg",
|
||||
sub_heading="Test heading active",
|
||||
description="Test description active",
|
||||
categories=["test"],
|
||||
runs=15,
|
||||
rating=4.8,
|
||||
versions=["1.0", "2.0"],
|
||||
agentGraphVersions=["1", "2"],
|
||||
agentGraphId="test-graph-id-active",
|
||||
graph_id="test-graph-id",
|
||||
graph_versions=["1"],
|
||||
updated_at=datetime.now(),
|
||||
is_available=True,
|
||||
useForOnboarding=False,
|
||||
use_for_onboarding=False,
|
||||
)
|
||||
|
||||
# Create a mock StoreListing result
|
||||
mock_store_listing = mocker.MagicMock()
|
||||
mock_store_listing.activeVersionId = "active-version-id"
|
||||
mock_store_listing.hasApprovedVersion = True
|
||||
mock_store_listing.ActiveVersion = mocker.MagicMock()
|
||||
mock_store_listing.ActiveVersion.recommendedScheduleCron = None
|
||||
|
||||
# Mock StoreAgent prisma call - need to handle multiple calls
|
||||
# Mock StoreAgent prisma call
|
||||
mock_store_agent = mocker.patch("prisma.models.StoreAgent.prisma")
|
||||
|
||||
# Set up side_effect to return different results for different calls
|
||||
def mock_find_first_side_effect(*args, **kwargs):
|
||||
where_clause = kwargs.get("where", {})
|
||||
if "storeListingVersionId" in where_clause:
|
||||
# Second call for active version
|
||||
return mock_active_agent
|
||||
else:
|
||||
# First call for initial lookup
|
||||
return mock_agent
|
||||
|
||||
mock_store_agent.return_value.find_first = mocker.AsyncMock(
|
||||
side_effect=mock_find_first_side_effect
|
||||
)
|
||||
|
||||
# Mock Profile prisma call
|
||||
mock_profile = mocker.MagicMock()
|
||||
mock_profile.userId = "user-id-123"
|
||||
mock_profile_db = mocker.patch("prisma.models.Profile.prisma")
|
||||
mock_profile_db.return_value.find_first = mocker.AsyncMock(
|
||||
return_value=mock_profile
|
||||
)
|
||||
|
||||
# Mock StoreListing prisma call
|
||||
mock_store_listing_db = mocker.patch("prisma.models.StoreListing.prisma")
|
||||
mock_store_listing_db.return_value.find_first = mocker.AsyncMock(
|
||||
return_value=mock_store_listing
|
||||
)
|
||||
mock_store_agent.return_value.find_first = mocker.AsyncMock(return_value=mock_agent)
|
||||
|
||||
# Call function
|
||||
result = await db.get_store_agent_details("creator", "test-agent")
|
||||
|
||||
# Verify results - should use active version data
|
||||
# Verify results - constructed from the StoreAgent view
|
||||
assert result.slug == "test-agent"
|
||||
assert result.agent_name == "Test Agent Active" # From active version
|
||||
assert result.active_version_id == "active-version-id"
|
||||
assert result.agent_name == "Test Agent"
|
||||
assert result.active_version_id == "version123"
|
||||
assert result.has_approved_version is True
|
||||
assert (
|
||||
result.store_listing_version_id == "active-version-id"
|
||||
) # Should be active version ID
|
||||
assert result.store_listing_version_id == "version123"
|
||||
assert result.graph_id == "test-graph-id"
|
||||
assert result.runs == 10
|
||||
assert result.rating == 4.5
|
||||
|
||||
# Verify mocks called correctly - now expecting 2 calls
|
||||
assert mock_store_agent.return_value.find_first.call_count == 2
|
||||
|
||||
# Check the specific calls
|
||||
calls = mock_store_agent.return_value.find_first.call_args_list
|
||||
assert calls[0] == mocker.call(
|
||||
# Verify single StoreAgent lookup
|
||||
mock_store_agent.return_value.find_first.assert_called_once_with(
|
||||
where={"creator_username": "creator", "slug": "test-agent"}
|
||||
)
|
||||
assert calls[1] == mocker.call(where={"storeListingVersionId": "active-version-id"})
|
||||
|
||||
mock_store_listing_db.return_value.find_first.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_get_store_creator_details(mocker):
|
||||
async def test_get_store_creator(mocker):
|
||||
# Mock data
|
||||
mock_creator_data = prisma.models.Creator(
|
||||
name="Test Creator",
|
||||
@@ -202,7 +138,7 @@ async def test_get_store_creator_details(mocker):
|
||||
mock_creator.return_value.find_unique.return_value = mock_creator_data
|
||||
|
||||
# Call function
|
||||
result = await db.get_store_creator_details("creator")
|
||||
result = await db.get_store_creator("creator")
|
||||
|
||||
# Verify results
|
||||
assert result.username == "creator"
|
||||
@@ -218,61 +154,110 @@ async def test_get_store_creator_details(mocker):
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_create_store_submission(mocker):
|
||||
# Mock data
|
||||
now = datetime.now()
|
||||
|
||||
# Mock agent graph (with no pending submissions) and user with profile
|
||||
mock_profile = prisma.models.Profile(
|
||||
id="profile-id",
|
||||
userId="user-id",
|
||||
name="Test User",
|
||||
username="testuser",
|
||||
description="Test",
|
||||
isFeatured=False,
|
||||
links=[],
|
||||
createdAt=now,
|
||||
updatedAt=now,
|
||||
)
|
||||
mock_user = prisma.models.User(
|
||||
id="user-id",
|
||||
email="test@example.com",
|
||||
createdAt=now,
|
||||
updatedAt=now,
|
||||
Profile=[mock_profile],
|
||||
emailVerified=True,
|
||||
metadata="{}", # type: ignore[reportArgumentType]
|
||||
integrations="",
|
||||
maxEmailsPerDay=1,
|
||||
notifyOnAgentRun=True,
|
||||
notifyOnZeroBalance=True,
|
||||
notifyOnLowBalance=True,
|
||||
notifyOnBlockExecutionFailed=True,
|
||||
notifyOnContinuousAgentError=True,
|
||||
notifyOnDailySummary=True,
|
||||
notifyOnWeeklySummary=True,
|
||||
notifyOnMonthlySummary=True,
|
||||
notifyOnAgentApproved=True,
|
||||
notifyOnAgentRejected=True,
|
||||
timezone="Europe/Delft",
|
||||
)
|
||||
mock_agent = prisma.models.AgentGraph(
|
||||
id="agent-id",
|
||||
version=1,
|
||||
userId="user-id",
|
||||
createdAt=datetime.now(),
|
||||
createdAt=now,
|
||||
isActive=True,
|
||||
StoreListingVersions=[],
|
||||
User=mock_user,
|
||||
)
|
||||
|
||||
mock_listing = prisma.models.StoreListing(
|
||||
# Mock the created StoreListingVersion (returned by create)
|
||||
mock_store_listing_obj = prisma.models.StoreListing(
|
||||
id="listing-id",
|
||||
createdAt=datetime.now(),
|
||||
updatedAt=datetime.now(),
|
||||
createdAt=now,
|
||||
updatedAt=now,
|
||||
isDeleted=False,
|
||||
hasApprovedVersion=False,
|
||||
slug="test-agent",
|
||||
agentGraphId="agent-id",
|
||||
agentGraphVersion=1,
|
||||
owningUserId="user-id",
|
||||
Versions=[
|
||||
prisma.models.StoreListingVersion(
|
||||
id="version-id",
|
||||
agentGraphId="agent-id",
|
||||
agentGraphVersion=1,
|
||||
name="Test Agent",
|
||||
description="Test description",
|
||||
createdAt=datetime.now(),
|
||||
updatedAt=datetime.now(),
|
||||
subHeading="Test heading",
|
||||
imageUrls=["image.jpg"],
|
||||
categories=["test"],
|
||||
isFeatured=False,
|
||||
isDeleted=False,
|
||||
version=1,
|
||||
storeListingId="listing-id",
|
||||
submissionStatus=prisma.enums.SubmissionStatus.PENDING,
|
||||
isAvailable=True,
|
||||
)
|
||||
],
|
||||
useForOnboarding=False,
|
||||
)
|
||||
mock_version = prisma.models.StoreListingVersion(
|
||||
id="version-id",
|
||||
agentGraphId="agent-id",
|
||||
agentGraphVersion=1,
|
||||
name="Test Agent",
|
||||
description="Test description",
|
||||
createdAt=now,
|
||||
updatedAt=now,
|
||||
subHeading="",
|
||||
imageUrls=[],
|
||||
categories=[],
|
||||
isFeatured=False,
|
||||
isDeleted=False,
|
||||
version=1,
|
||||
storeListingId="listing-id",
|
||||
submissionStatus=prisma.enums.SubmissionStatus.PENDING,
|
||||
isAvailable=True,
|
||||
submittedAt=now,
|
||||
StoreListing=mock_store_listing_obj,
|
||||
)
|
||||
|
||||
# Mock prisma calls
|
||||
mock_agent_graph = mocker.patch("prisma.models.AgentGraph.prisma")
|
||||
mock_agent_graph.return_value.find_first = mocker.AsyncMock(return_value=mock_agent)
|
||||
|
||||
mock_store_listing = mocker.patch("prisma.models.StoreListing.prisma")
|
||||
mock_store_listing.return_value.find_first = mocker.AsyncMock(return_value=None)
|
||||
mock_store_listing.return_value.create = mocker.AsyncMock(return_value=mock_listing)
|
||||
# Mock transaction context manager
|
||||
mock_tx = mocker.MagicMock()
|
||||
mocker.patch(
|
||||
"backend.api.features.store.db.transaction",
|
||||
return_value=mocker.AsyncMock(
|
||||
__aenter__=mocker.AsyncMock(return_value=mock_tx),
|
||||
__aexit__=mocker.AsyncMock(return_value=False),
|
||||
),
|
||||
)
|
||||
|
||||
mock_sl = mocker.patch("prisma.models.StoreListing.prisma")
|
||||
mock_sl.return_value.find_unique = mocker.AsyncMock(return_value=None)
|
||||
|
||||
mock_slv = mocker.patch("prisma.models.StoreListingVersion.prisma")
|
||||
mock_slv.return_value.create = mocker.AsyncMock(return_value=mock_version)
|
||||
|
||||
# Call function
|
||||
result = await db.create_store_submission(
|
||||
user_id="user-id",
|
||||
agent_id="agent-id",
|
||||
agent_version=1,
|
||||
graph_id="agent-id",
|
||||
graph_version=1,
|
||||
slug="test-agent",
|
||||
name="Test Agent",
|
||||
description="Test description",
|
||||
@@ -281,11 +266,11 @@ async def test_create_store_submission(mocker):
|
||||
# Verify results
|
||||
assert result.name == "Test Agent"
|
||||
assert result.description == "Test description"
|
||||
assert result.store_listing_version_id == "version-id"
|
||||
assert result.listing_version_id == "version-id"
|
||||
|
||||
# Verify mocks called correctly
|
||||
mock_agent_graph.return_value.find_first.assert_called_once()
|
||||
mock_store_listing.return_value.create.assert_called_once()
|
||||
mock_slv.return_value.create.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
@@ -318,7 +303,6 @@ async def test_update_profile(mocker):
|
||||
description="Test description",
|
||||
links=["link1"],
|
||||
avatar_url="avatar.jpg",
|
||||
is_featured=False,
|
||||
)
|
||||
|
||||
# Call function
|
||||
@@ -389,7 +373,7 @@ async def test_get_store_agents_with_search_and_filters_parameterized():
|
||||
creators=["creator1'; DROP TABLE Users; --", "creator2"],
|
||||
category="AI'; DELETE FROM StoreAgent; --",
|
||||
featured=True,
|
||||
sorted_by="rating",
|
||||
sorted_by=db.StoreAgentsSortOptions.RATING,
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
@@ -57,12 +57,6 @@ class StoreError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class AgentNotFoundError(NotFoundError):
|
||||
"""Raised when an agent is not found"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CreatorNotFoundError(NotFoundError):
|
||||
"""Raised when a creator is not found"""
|
||||
|
||||
|
||||
@@ -568,7 +568,7 @@ async def hybrid_search(
|
||||
SELECT uce."contentId" as "storeListingVersionId"
|
||||
FROM {{schema_prefix}}"UnifiedContentEmbedding" uce
|
||||
INNER JOIN {{schema_prefix}}"StoreAgent" sa
|
||||
ON uce."contentId" = sa."storeListingVersionId"
|
||||
ON uce."contentId" = sa.listing_version_id
|
||||
WHERE uce."contentType" = 'STORE_AGENT'::{{schema_prefix}}"ContentType"
|
||||
AND uce."userId" IS NULL
|
||||
AND uce.search @@ plainto_tsquery('english', {query_param})
|
||||
@@ -582,7 +582,7 @@ async def hybrid_search(
|
||||
SELECT uce."contentId", uce.embedding
|
||||
FROM {{schema_prefix}}"UnifiedContentEmbedding" uce
|
||||
INNER JOIN {{schema_prefix}}"StoreAgent" sa
|
||||
ON uce."contentId" = sa."storeListingVersionId"
|
||||
ON uce."contentId" = sa.listing_version_id
|
||||
WHERE uce."contentType" = 'STORE_AGENT'::{{schema_prefix}}"ContentType"
|
||||
AND uce."userId" IS NULL
|
||||
AND {where_clause}
|
||||
@@ -605,7 +605,7 @@ async def hybrid_search(
|
||||
sa.featured,
|
||||
sa.is_available,
|
||||
sa.updated_at,
|
||||
sa."agentGraphId",
|
||||
sa.graph_id,
|
||||
-- Searchable text for BM25 reranking
|
||||
COALESCE(sa.agent_name, '') || ' ' || COALESCE(sa.sub_heading, '') || ' ' || COALESCE(sa.description, '') as searchable_text,
|
||||
-- Semantic score
|
||||
@@ -627,9 +627,9 @@ async def hybrid_search(
|
||||
sa.runs as popularity_raw
|
||||
FROM candidates c
|
||||
INNER JOIN {{schema_prefix}}"StoreAgent" sa
|
||||
ON c."storeListingVersionId" = sa."storeListingVersionId"
|
||||
ON c."storeListingVersionId" = sa.listing_version_id
|
||||
INNER JOIN {{schema_prefix}}"UnifiedContentEmbedding" uce
|
||||
ON sa."storeListingVersionId" = uce."contentId"
|
||||
ON sa.listing_version_id = uce."contentId"
|
||||
AND uce."contentType" = 'STORE_AGENT'::{{schema_prefix}}"ContentType"
|
||||
),
|
||||
max_vals AS (
|
||||
@@ -665,7 +665,7 @@ async def hybrid_search(
|
||||
featured,
|
||||
is_available,
|
||||
updated_at,
|
||||
"agentGraphId",
|
||||
graph_id,
|
||||
searchable_text,
|
||||
semantic_score,
|
||||
lexical_score,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import datetime
|
||||
from typing import List
|
||||
from typing import TYPE_CHECKING, List, Self
|
||||
|
||||
import prisma.enums
|
||||
import pydantic
|
||||
|
||||
from backend.util.models import Pagination
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import prisma.models
|
||||
|
||||
|
||||
class ChangelogEntry(pydantic.BaseModel):
|
||||
version: str
|
||||
@@ -13,9 +16,9 @@ class ChangelogEntry(pydantic.BaseModel):
|
||||
date: datetime.datetime
|
||||
|
||||
|
||||
class MyAgent(pydantic.BaseModel):
|
||||
agent_id: str
|
||||
agent_version: int
|
||||
class MyUnpublishedAgent(pydantic.BaseModel):
|
||||
graph_id: str
|
||||
graph_version: int
|
||||
agent_name: str
|
||||
agent_image: str | None = None
|
||||
description: str
|
||||
@@ -23,8 +26,8 @@ class MyAgent(pydantic.BaseModel):
|
||||
recommended_schedule_cron: str | None = None
|
||||
|
||||
|
||||
class MyAgentsResponse(pydantic.BaseModel):
|
||||
agents: list[MyAgent]
|
||||
class MyUnpublishedAgentsResponse(pydantic.BaseModel):
|
||||
agents: list[MyUnpublishedAgent]
|
||||
pagination: Pagination
|
||||
|
||||
|
||||
@@ -40,6 +43,21 @@ class StoreAgent(pydantic.BaseModel):
|
||||
rating: float
|
||||
agent_graph_id: str
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, agent: "prisma.models.StoreAgent") -> "StoreAgent":
|
||||
return cls(
|
||||
slug=agent.slug,
|
||||
agent_name=agent.agent_name,
|
||||
agent_image=agent.agent_image[0] if agent.agent_image else "",
|
||||
creator=agent.creator_username or "Needs Profile",
|
||||
creator_avatar=agent.creator_avatar or "",
|
||||
sub_heading=agent.sub_heading,
|
||||
description=agent.description,
|
||||
runs=agent.runs,
|
||||
rating=agent.rating,
|
||||
agent_graph_id=agent.graph_id,
|
||||
)
|
||||
|
||||
|
||||
class StoreAgentsResponse(pydantic.BaseModel):
|
||||
agents: list[StoreAgent]
|
||||
@@ -62,81 +80,192 @@ class StoreAgentDetails(pydantic.BaseModel):
|
||||
runs: int
|
||||
rating: float
|
||||
versions: list[str]
|
||||
agentGraphVersions: list[str]
|
||||
agentGraphId: str
|
||||
graph_id: str
|
||||
graph_versions: list[str]
|
||||
last_updated: datetime.datetime
|
||||
recommended_schedule_cron: str | None = None
|
||||
|
||||
active_version_id: str | None = None
|
||||
has_approved_version: bool = False
|
||||
active_version_id: str
|
||||
has_approved_version: bool
|
||||
|
||||
# Optional changelog data when include_changelog=True
|
||||
changelog: list[ChangelogEntry] | None = None
|
||||
|
||||
|
||||
class Creator(pydantic.BaseModel):
|
||||
name: str
|
||||
username: str
|
||||
description: str
|
||||
avatar_url: str
|
||||
num_agents: int
|
||||
agent_rating: float
|
||||
agent_runs: int
|
||||
is_featured: bool
|
||||
|
||||
|
||||
class CreatorsResponse(pydantic.BaseModel):
|
||||
creators: List[Creator]
|
||||
pagination: Pagination
|
||||
|
||||
|
||||
class CreatorDetails(pydantic.BaseModel):
|
||||
name: str
|
||||
username: str
|
||||
description: str
|
||||
links: list[str]
|
||||
avatar_url: str
|
||||
agent_rating: float
|
||||
agent_runs: int
|
||||
top_categories: list[str]
|
||||
@classmethod
|
||||
def from_db(cls, agent: "prisma.models.StoreAgent") -> "StoreAgentDetails":
|
||||
return cls(
|
||||
store_listing_version_id=agent.listing_version_id,
|
||||
slug=agent.slug,
|
||||
agent_name=agent.agent_name,
|
||||
agent_video=agent.agent_video or "",
|
||||
agent_output_demo=agent.agent_output_demo or "",
|
||||
agent_image=agent.agent_image,
|
||||
creator=agent.creator_username or "",
|
||||
creator_avatar=agent.creator_avatar or "",
|
||||
sub_heading=agent.sub_heading,
|
||||
description=agent.description,
|
||||
categories=agent.categories,
|
||||
runs=agent.runs,
|
||||
rating=agent.rating,
|
||||
versions=agent.versions,
|
||||
graph_id=agent.graph_id,
|
||||
graph_versions=agent.graph_versions,
|
||||
last_updated=agent.updated_at,
|
||||
recommended_schedule_cron=agent.recommended_schedule_cron,
|
||||
active_version_id=agent.listing_version_id,
|
||||
has_approved_version=True, # StoreAgent view only has approved agents
|
||||
)
|
||||
|
||||
|
||||
class Profile(pydantic.BaseModel):
|
||||
name: str
|
||||
"""Marketplace user profile (only attributes that the user can update)"""
|
||||
|
||||
username: str
|
||||
name: str
|
||||
description: str
|
||||
avatar_url: str | None
|
||||
links: list[str]
|
||||
avatar_url: str
|
||||
is_featured: bool = False
|
||||
|
||||
|
||||
class ProfileDetails(Profile):
|
||||
"""Marketplace user profile (including read-only fields)"""
|
||||
|
||||
is_featured: bool
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, profile: "prisma.models.Profile") -> "ProfileDetails":
|
||||
return cls(
|
||||
name=profile.name,
|
||||
username=profile.username,
|
||||
avatar_url=profile.avatarUrl,
|
||||
description=profile.description,
|
||||
links=profile.links,
|
||||
is_featured=profile.isFeatured,
|
||||
)
|
||||
|
||||
|
||||
class CreatorDetails(ProfileDetails):
|
||||
"""Marketplace creator profile details, including aggregated stats"""
|
||||
|
||||
num_agents: int
|
||||
agent_runs: int
|
||||
agent_rating: float
|
||||
top_categories: list[str]
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, creator: "prisma.models.Creator") -> "CreatorDetails": # type: ignore[override]
|
||||
return cls(
|
||||
name=creator.name,
|
||||
username=creator.username,
|
||||
avatar_url=creator.avatar_url,
|
||||
description=creator.description,
|
||||
links=creator.links,
|
||||
is_featured=creator.is_featured,
|
||||
num_agents=creator.num_agents,
|
||||
agent_runs=creator.agent_runs,
|
||||
agent_rating=creator.agent_rating,
|
||||
top_categories=creator.top_categories,
|
||||
)
|
||||
|
||||
|
||||
class CreatorsResponse(pydantic.BaseModel):
|
||||
creators: List[CreatorDetails]
|
||||
pagination: Pagination
|
||||
|
||||
|
||||
class StoreSubmission(pydantic.BaseModel):
|
||||
# From StoreListing:
|
||||
listing_id: str
|
||||
agent_id: str
|
||||
agent_version: int
|
||||
user_id: str
|
||||
slug: str
|
||||
|
||||
# From StoreListingVersion:
|
||||
listing_version_id: str
|
||||
listing_version: int
|
||||
graph_id: str
|
||||
graph_version: int
|
||||
name: str
|
||||
sub_heading: str
|
||||
slug: str
|
||||
description: str
|
||||
instructions: str | None = None
|
||||
instructions: str | None
|
||||
categories: list[str]
|
||||
image_urls: list[str]
|
||||
date_submitted: datetime.datetime
|
||||
status: prisma.enums.SubmissionStatus
|
||||
runs: int
|
||||
rating: float
|
||||
store_listing_version_id: str | None = None
|
||||
version: int | None = None # Actual version number from the database
|
||||
video_url: str | None
|
||||
agent_output_demo_url: str | None
|
||||
|
||||
submitted_at: datetime.datetime | None
|
||||
changes_summary: str | None
|
||||
status: prisma.enums.SubmissionStatus
|
||||
reviewed_at: datetime.datetime | None = None
|
||||
reviewer_id: str | None = None
|
||||
review_comments: str | None = None # External comments visible to creator
|
||||
internal_comments: str | None = None # Private notes for admin use only
|
||||
reviewed_at: datetime.datetime | None = None
|
||||
changes_summary: str | None = None
|
||||
|
||||
# Additional fields for editing
|
||||
video_url: str | None = None
|
||||
agent_output_demo_url: str | None = None
|
||||
categories: list[str] = []
|
||||
# Aggregated from AgentGraphExecutions and StoreListingReviews:
|
||||
run_count: int = 0
|
||||
review_count: int = 0
|
||||
review_avg_rating: float = 0.0
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, _sub: "prisma.models.StoreSubmission") -> Self:
|
||||
"""Construct from the StoreSubmission Prisma view."""
|
||||
return cls(
|
||||
listing_id=_sub.listing_id,
|
||||
user_id=_sub.user_id,
|
||||
slug=_sub.slug,
|
||||
listing_version_id=_sub.listing_version_id,
|
||||
listing_version=_sub.listing_version,
|
||||
graph_id=_sub.graph_id,
|
||||
graph_version=_sub.graph_version,
|
||||
name=_sub.name,
|
||||
sub_heading=_sub.sub_heading,
|
||||
description=_sub.description,
|
||||
instructions=_sub.instructions,
|
||||
categories=_sub.categories,
|
||||
image_urls=_sub.image_urls,
|
||||
video_url=_sub.video_url,
|
||||
agent_output_demo_url=_sub.agent_output_demo_url,
|
||||
submitted_at=_sub.submitted_at,
|
||||
changes_summary=_sub.changes_summary,
|
||||
status=_sub.status,
|
||||
reviewed_at=_sub.reviewed_at,
|
||||
reviewer_id=_sub.reviewer_id,
|
||||
review_comments=_sub.review_comments,
|
||||
run_count=_sub.run_count,
|
||||
review_count=_sub.review_count,
|
||||
review_avg_rating=_sub.review_avg_rating,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_listing_version(cls, _lv: "prisma.models.StoreListingVersion") -> Self:
|
||||
"""
|
||||
Construct from the StoreListingVersion Prisma model (with StoreListing included)
|
||||
"""
|
||||
if not (_l := _lv.StoreListing):
|
||||
raise ValueError("StoreListingVersion must have included StoreListing")
|
||||
|
||||
return cls(
|
||||
listing_id=_l.id,
|
||||
user_id=_l.owningUserId,
|
||||
slug=_l.slug,
|
||||
listing_version_id=_lv.id,
|
||||
listing_version=_lv.version,
|
||||
graph_id=_lv.agentGraphId,
|
||||
graph_version=_lv.agentGraphVersion,
|
||||
name=_lv.name,
|
||||
sub_heading=_lv.subHeading,
|
||||
description=_lv.description,
|
||||
instructions=_lv.instructions,
|
||||
categories=_lv.categories,
|
||||
image_urls=_lv.imageUrls,
|
||||
video_url=_lv.videoUrl,
|
||||
agent_output_demo_url=_lv.agentOutputDemoUrl,
|
||||
submitted_at=_lv.submittedAt,
|
||||
changes_summary=_lv.changesSummary,
|
||||
status=_lv.submissionStatus,
|
||||
reviewed_at=_lv.reviewedAt,
|
||||
reviewer_id=_lv.reviewerId,
|
||||
review_comments=_lv.reviewComments,
|
||||
)
|
||||
|
||||
|
||||
class StoreSubmissionsResponse(pydantic.BaseModel):
|
||||
@@ -144,33 +273,12 @@ class StoreSubmissionsResponse(pydantic.BaseModel):
|
||||
pagination: Pagination
|
||||
|
||||
|
||||
class StoreListingWithVersions(pydantic.BaseModel):
|
||||
"""A store listing with its version history"""
|
||||
|
||||
listing_id: str
|
||||
slug: str
|
||||
agent_id: str
|
||||
agent_version: int
|
||||
active_version_id: str | None = None
|
||||
has_approved_version: bool = False
|
||||
creator_email: str | None = None
|
||||
latest_version: StoreSubmission | None = None
|
||||
versions: list[StoreSubmission] = []
|
||||
|
||||
|
||||
class StoreListingsWithVersionsResponse(pydantic.BaseModel):
|
||||
"""Response model for listings with version history"""
|
||||
|
||||
listings: list[StoreListingWithVersions]
|
||||
pagination: Pagination
|
||||
|
||||
|
||||
class StoreSubmissionRequest(pydantic.BaseModel):
|
||||
agent_id: str = pydantic.Field(
|
||||
..., min_length=1, description="Agent ID cannot be empty"
|
||||
graph_id: str = pydantic.Field(
|
||||
..., min_length=1, description="Graph ID cannot be empty"
|
||||
)
|
||||
agent_version: int = pydantic.Field(
|
||||
..., gt=0, description="Agent version must be greater than 0"
|
||||
graph_version: int = pydantic.Field(
|
||||
..., gt=0, description="Graph version must be greater than 0"
|
||||
)
|
||||
slug: str
|
||||
name: str
|
||||
@@ -198,12 +306,42 @@ class StoreSubmissionEditRequest(pydantic.BaseModel):
|
||||
recommended_schedule_cron: str | None = None
|
||||
|
||||
|
||||
class ProfileDetails(pydantic.BaseModel):
|
||||
name: str
|
||||
username: str
|
||||
description: str
|
||||
links: list[str]
|
||||
avatar_url: str | None = None
|
||||
class StoreSubmissionAdminView(StoreSubmission):
|
||||
internal_comments: str | None # Private admin notes
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, _sub: "prisma.models.StoreSubmission") -> Self:
|
||||
return cls(
|
||||
**StoreSubmission.from_db(_sub).model_dump(),
|
||||
internal_comments=_sub.internal_comments,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_listing_version(cls, _lv: "prisma.models.StoreListingVersion") -> Self:
|
||||
return cls(
|
||||
**StoreSubmission.from_listing_version(_lv).model_dump(),
|
||||
internal_comments=_lv.internalComments,
|
||||
)
|
||||
|
||||
|
||||
class StoreListingWithVersionsAdminView(pydantic.BaseModel):
|
||||
"""A store listing with its version history"""
|
||||
|
||||
listing_id: str
|
||||
graph_id: str
|
||||
slug: str
|
||||
active_listing_version_id: str | None = None
|
||||
has_approved_version: bool = False
|
||||
creator_email: str | None = None
|
||||
latest_version: StoreSubmissionAdminView | None = None
|
||||
versions: list[StoreSubmissionAdminView] = []
|
||||
|
||||
|
||||
class StoreListingsWithVersionsAdminViewResponse(pydantic.BaseModel):
|
||||
"""Response model for listings with version history"""
|
||||
|
||||
listings: list[StoreListingWithVersionsAdminView]
|
||||
pagination: Pagination
|
||||
|
||||
|
||||
class StoreReview(pydantic.BaseModel):
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import datetime
|
||||
|
||||
import prisma.enums
|
||||
|
||||
from . import model as store_model
|
||||
|
||||
|
||||
def test_pagination():
|
||||
pagination = store_model.Pagination(
|
||||
total_items=100, total_pages=5, current_page=2, page_size=20
|
||||
)
|
||||
assert pagination.total_items == 100
|
||||
assert pagination.total_pages == 5
|
||||
assert pagination.current_page == 2
|
||||
assert pagination.page_size == 20
|
||||
|
||||
|
||||
def test_store_agent():
|
||||
agent = store_model.StoreAgent(
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_image="test.jpg",
|
||||
creator="creator1",
|
||||
creator_avatar="avatar.jpg",
|
||||
sub_heading="Test subheading",
|
||||
description="Test description",
|
||||
runs=50,
|
||||
rating=4.5,
|
||||
agent_graph_id="test-graph-id",
|
||||
)
|
||||
assert agent.slug == "test-agent"
|
||||
assert agent.agent_name == "Test Agent"
|
||||
assert agent.runs == 50
|
||||
assert agent.rating == 4.5
|
||||
assert agent.agent_graph_id == "test-graph-id"
|
||||
|
||||
|
||||
def test_store_agents_response():
|
||||
response = store_model.StoreAgentsResponse(
|
||||
agents=[
|
||||
store_model.StoreAgent(
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_image="test.jpg",
|
||||
creator="creator1",
|
||||
creator_avatar="avatar.jpg",
|
||||
sub_heading="Test subheading",
|
||||
description="Test description",
|
||||
runs=50,
|
||||
rating=4.5,
|
||||
agent_graph_id="test-graph-id",
|
||||
)
|
||||
],
|
||||
pagination=store_model.Pagination(
|
||||
total_items=1, total_pages=1, current_page=1, page_size=20
|
||||
),
|
||||
)
|
||||
assert len(response.agents) == 1
|
||||
assert response.pagination.total_items == 1
|
||||
|
||||
|
||||
def test_store_agent_details():
|
||||
details = store_model.StoreAgentDetails(
|
||||
store_listing_version_id="version123",
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_video="video.mp4",
|
||||
agent_output_demo="demo.mp4",
|
||||
agent_image=["image1.jpg", "image2.jpg"],
|
||||
creator="creator1",
|
||||
creator_avatar="avatar.jpg",
|
||||
sub_heading="Test subheading",
|
||||
description="Test description",
|
||||
categories=["cat1", "cat2"],
|
||||
runs=50,
|
||||
rating=4.5,
|
||||
versions=["1.0", "2.0"],
|
||||
agentGraphVersions=["1", "2"],
|
||||
agentGraphId="test-graph-id",
|
||||
last_updated=datetime.datetime.now(),
|
||||
)
|
||||
assert details.slug == "test-agent"
|
||||
assert len(details.agent_image) == 2
|
||||
assert len(details.categories) == 2
|
||||
assert len(details.versions) == 2
|
||||
|
||||
|
||||
def test_creator():
|
||||
creator = store_model.Creator(
|
||||
agent_rating=4.8,
|
||||
agent_runs=1000,
|
||||
name="Test Creator",
|
||||
username="creator1",
|
||||
description="Test description",
|
||||
avatar_url="avatar.jpg",
|
||||
num_agents=5,
|
||||
is_featured=False,
|
||||
)
|
||||
assert creator.name == "Test Creator"
|
||||
assert creator.num_agents == 5
|
||||
|
||||
|
||||
def test_creators_response():
|
||||
response = store_model.CreatorsResponse(
|
||||
creators=[
|
||||
store_model.Creator(
|
||||
agent_rating=4.8,
|
||||
agent_runs=1000,
|
||||
name="Test Creator",
|
||||
username="creator1",
|
||||
description="Test description",
|
||||
avatar_url="avatar.jpg",
|
||||
num_agents=5,
|
||||
is_featured=False,
|
||||
)
|
||||
],
|
||||
pagination=store_model.Pagination(
|
||||
total_items=1, total_pages=1, current_page=1, page_size=20
|
||||
),
|
||||
)
|
||||
assert len(response.creators) == 1
|
||||
assert response.pagination.total_items == 1
|
||||
|
||||
|
||||
def test_creator_details():
|
||||
details = store_model.CreatorDetails(
|
||||
name="Test Creator",
|
||||
username="creator1",
|
||||
description="Test description",
|
||||
links=["link1.com", "link2.com"],
|
||||
avatar_url="avatar.jpg",
|
||||
agent_rating=4.8,
|
||||
agent_runs=1000,
|
||||
top_categories=["cat1", "cat2"],
|
||||
)
|
||||
assert details.name == "Test Creator"
|
||||
assert len(details.links) == 2
|
||||
assert details.agent_rating == 4.8
|
||||
assert len(details.top_categories) == 2
|
||||
|
||||
|
||||
def test_store_submission():
|
||||
submission = store_model.StoreSubmission(
|
||||
listing_id="listing123",
|
||||
agent_id="agent123",
|
||||
agent_version=1,
|
||||
sub_heading="Test subheading",
|
||||
name="Test Agent",
|
||||
slug="test-agent",
|
||||
description="Test description",
|
||||
image_urls=["image1.jpg", "image2.jpg"],
|
||||
date_submitted=datetime.datetime(2023, 1, 1),
|
||||
status=prisma.enums.SubmissionStatus.PENDING,
|
||||
runs=50,
|
||||
rating=4.5,
|
||||
)
|
||||
assert submission.name == "Test Agent"
|
||||
assert len(submission.image_urls) == 2
|
||||
assert submission.status == prisma.enums.SubmissionStatus.PENDING
|
||||
|
||||
|
||||
def test_store_submissions_response():
|
||||
response = store_model.StoreSubmissionsResponse(
|
||||
submissions=[
|
||||
store_model.StoreSubmission(
|
||||
listing_id="listing123",
|
||||
agent_id="agent123",
|
||||
agent_version=1,
|
||||
sub_heading="Test subheading",
|
||||
name="Test Agent",
|
||||
slug="test-agent",
|
||||
description="Test description",
|
||||
image_urls=["image1.jpg"],
|
||||
date_submitted=datetime.datetime(2023, 1, 1),
|
||||
status=prisma.enums.SubmissionStatus.PENDING,
|
||||
runs=50,
|
||||
rating=4.5,
|
||||
)
|
||||
],
|
||||
pagination=store_model.Pagination(
|
||||
total_items=1, total_pages=1, current_page=1, page_size=20
|
||||
),
|
||||
)
|
||||
assert len(response.submissions) == 1
|
||||
assert response.pagination.total_items == 1
|
||||
|
||||
|
||||
def test_store_submission_request():
|
||||
request = store_model.StoreSubmissionRequest(
|
||||
agent_id="agent123",
|
||||
agent_version=1,
|
||||
slug="test-agent",
|
||||
name="Test Agent",
|
||||
sub_heading="Test subheading",
|
||||
video_url="video.mp4",
|
||||
image_urls=["image1.jpg", "image2.jpg"],
|
||||
description="Test description",
|
||||
categories=["cat1", "cat2"],
|
||||
)
|
||||
assert request.agent_id == "agent123"
|
||||
assert request.agent_version == 1
|
||||
assert len(request.image_urls) == 2
|
||||
assert len(request.categories) == 2
|
||||
@@ -1,16 +1,17 @@
|
||||
import logging
|
||||
import tempfile
|
||||
import typing
|
||||
import urllib.parse
|
||||
from typing import Literal
|
||||
|
||||
import autogpt_libs.auth
|
||||
import fastapi
|
||||
import fastapi.responses
|
||||
import prisma.enums
|
||||
from fastapi import Query, Security
|
||||
from pydantic import BaseModel
|
||||
|
||||
import backend.data.graph
|
||||
import backend.util.json
|
||||
from backend.util.exceptions import NotFoundError
|
||||
from backend.util.models import Pagination
|
||||
|
||||
from . import cache as store_cache
|
||||
@@ -34,22 +35,15 @@ router = fastapi.APIRouter()
|
||||
"/profile",
|
||||
summary="Get user profile",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
response_model=store_model.ProfileDetails,
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def get_profile(
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
):
|
||||
"""
|
||||
Get the profile details for the authenticated user.
|
||||
Cached for 1 hour per user.
|
||||
"""
|
||||
user_id: str = Security(autogpt_libs.auth.get_user_id),
|
||||
) -> store_model.ProfileDetails:
|
||||
"""Get the profile details for the authenticated user."""
|
||||
profile = await store_db.get_user_profile(user_id)
|
||||
if profile is None:
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=404,
|
||||
content={"detail": "Profile not found"},
|
||||
)
|
||||
raise NotFoundError("User does not have a profile yet")
|
||||
return profile
|
||||
|
||||
|
||||
@@ -57,98 +51,17 @@ async def get_profile(
|
||||
"/profile",
|
||||
summary="Update user profile",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
response_model=store_model.CreatorDetails,
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def update_or_create_profile(
|
||||
profile: store_model.Profile,
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
):
|
||||
"""
|
||||
Update the store profile for the authenticated user.
|
||||
|
||||
Args:
|
||||
profile (Profile): The updated profile details
|
||||
user_id (str): ID of the authenticated user
|
||||
|
||||
Returns:
|
||||
CreatorDetails: The updated profile
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is an error updating the profile
|
||||
"""
|
||||
user_id: str = Security(autogpt_libs.auth.get_user_id),
|
||||
) -> store_model.ProfileDetails:
|
||||
"""Update the store profile for the authenticated user."""
|
||||
updated_profile = await store_db.update_profile(user_id=user_id, profile=profile)
|
||||
return updated_profile
|
||||
|
||||
|
||||
##############################################
|
||||
############### Agent Endpoints ##############
|
||||
##############################################
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agents",
|
||||
summary="List store agents",
|
||||
tags=["store", "public"],
|
||||
response_model=store_model.StoreAgentsResponse,
|
||||
)
|
||||
async def get_agents(
|
||||
featured: bool = False,
|
||||
creator: str | None = None,
|
||||
sorted_by: Literal["rating", "runs", "name", "updated_at"] | None = None,
|
||||
search_query: str | None = None,
|
||||
category: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
):
|
||||
"""
|
||||
Get a paginated list of agents from the store with optional filtering and sorting.
|
||||
|
||||
Args:
|
||||
featured (bool, optional): Filter to only show featured agents. Defaults to False.
|
||||
creator (str | None, optional): Filter agents by creator username. Defaults to None.
|
||||
sorted_by (str | None, optional): Sort agents by "runs" or "rating". Defaults to None.
|
||||
search_query (str | None, optional): Search agents by name, subheading and description. Defaults to None.
|
||||
category (str | None, optional): Filter agents by category. Defaults to None.
|
||||
page (int, optional): Page number for pagination. Defaults to 1.
|
||||
page_size (int, optional): Number of agents per page. Defaults to 20.
|
||||
|
||||
Returns:
|
||||
StoreAgentsResponse: Paginated list of agents matching the filters
|
||||
|
||||
Raises:
|
||||
HTTPException: If page or page_size are less than 1
|
||||
|
||||
Used for:
|
||||
- Home Page Featured Agents
|
||||
- Home Page Top Agents
|
||||
- Search Results
|
||||
- Agent Details - Other Agents By Creator
|
||||
- Agent Details - Similar Agents
|
||||
- Creator Details - Agents By Creator
|
||||
"""
|
||||
if page < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page must be greater than 0"
|
||||
)
|
||||
|
||||
if page_size < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page size must be greater than 0"
|
||||
)
|
||||
|
||||
agents = await store_cache._get_cached_store_agents(
|
||||
featured=featured,
|
||||
creator=creator,
|
||||
sorted_by=sorted_by,
|
||||
search_query=search_query,
|
||||
category=category,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return agents
|
||||
|
||||
|
||||
##############################################
|
||||
############### Search Endpoints #############
|
||||
##############################################
|
||||
@@ -158,60 +71,30 @@ async def get_agents(
|
||||
"/search",
|
||||
summary="Unified search across all content types",
|
||||
tags=["store", "public"],
|
||||
response_model=store_model.UnifiedSearchResponse,
|
||||
)
|
||||
async def unified_search(
|
||||
query: str,
|
||||
content_types: list[str] | None = fastapi.Query(
|
||||
content_types: list[prisma.enums.ContentType] | None = Query(
|
||||
default=None,
|
||||
description="Content types to search: STORE_AGENT, BLOCK, DOCUMENTATION. If not specified, searches all.",
|
||||
description="Content types to search. If not specified, searches all.",
|
||||
),
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
user_id: str | None = fastapi.Security(
|
||||
page: int = Query(ge=1, default=1),
|
||||
page_size: int = Query(ge=1, default=20),
|
||||
user_id: str | None = Security(
|
||||
autogpt_libs.auth.get_optional_user_id, use_cache=False
|
||||
),
|
||||
):
|
||||
) -> store_model.UnifiedSearchResponse:
|
||||
"""
|
||||
Search across all content types (store agents, blocks, documentation) using hybrid search.
|
||||
Search across all content types (marketplace agents, blocks, documentation)
|
||||
using hybrid search.
|
||||
|
||||
Combines semantic (embedding-based) and lexical (text-based) search for best results.
|
||||
|
||||
Args:
|
||||
query: The search query string
|
||||
content_types: Optional list of content types to filter by (STORE_AGENT, BLOCK, DOCUMENTATION)
|
||||
page: Page number for pagination (default 1)
|
||||
page_size: Number of results per page (default 20)
|
||||
user_id: Optional authenticated user ID (for user-scoped content in future)
|
||||
|
||||
Returns:
|
||||
UnifiedSearchResponse: Paginated list of search results with relevance scores
|
||||
"""
|
||||
if page < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page must be greater than 0"
|
||||
)
|
||||
|
||||
if page_size < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page size must be greater than 0"
|
||||
)
|
||||
|
||||
# Convert string content types to enum
|
||||
content_type_enums: list[prisma.enums.ContentType] | None = None
|
||||
if content_types:
|
||||
try:
|
||||
content_type_enums = [prisma.enums.ContentType(ct) for ct in content_types]
|
||||
except ValueError as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Invalid content type. Valid values: STORE_AGENT, BLOCK, DOCUMENTATION. Error: {e}",
|
||||
)
|
||||
|
||||
# Perform unified hybrid search
|
||||
results, total = await store_hybrid_search.unified_hybrid_search(
|
||||
query=query,
|
||||
content_types=content_type_enums,
|
||||
content_types=content_types,
|
||||
user_id=user_id,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
@@ -245,22 +128,69 @@ async def unified_search(
|
||||
)
|
||||
|
||||
|
||||
##############################################
|
||||
############### Agent Endpoints ##############
|
||||
##############################################
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agents",
|
||||
summary="List store agents",
|
||||
tags=["store", "public"],
|
||||
)
|
||||
async def get_agents(
|
||||
featured: bool = Query(
|
||||
default=False, description="Filter to only show featured agents"
|
||||
),
|
||||
creator: str | None = Query(
|
||||
default=None, description="Filter agents by creator username"
|
||||
),
|
||||
category: str | None = Query(default=None, description="Filter agents by category"),
|
||||
search_query: str | None = Query(
|
||||
default=None, description="Literal + semantic search on names and descriptions"
|
||||
),
|
||||
sorted_by: store_db.StoreAgentsSortOptions | None = Query(
|
||||
default=None,
|
||||
description="Property to sort results by. Ignored if search_query is provided.",
|
||||
),
|
||||
page: int = Query(ge=1, default=1),
|
||||
page_size: int = Query(ge=1, default=20),
|
||||
) -> store_model.StoreAgentsResponse:
|
||||
"""
|
||||
Get a paginated list of agents from the marketplace,
|
||||
with optional filtering and sorting.
|
||||
|
||||
Used for:
|
||||
- Home Page Featured Agents
|
||||
- Home Page Top Agents
|
||||
- Search Results
|
||||
- Agent Details - Other Agents By Creator
|
||||
- Agent Details - Similar Agents
|
||||
- Creator Details - Agents By Creator
|
||||
"""
|
||||
agents = await store_cache._get_cached_store_agents(
|
||||
featured=featured,
|
||||
creator=creator,
|
||||
sorted_by=sorted_by,
|
||||
search_query=search_query,
|
||||
category=category,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return agents
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agents/{username}/{agent_name}",
|
||||
summary="Get specific agent",
|
||||
tags=["store", "public"],
|
||||
response_model=store_model.StoreAgentDetails,
|
||||
)
|
||||
async def get_agent(
|
||||
async def get_agent_by_name(
|
||||
username: str,
|
||||
agent_name: str,
|
||||
include_changelog: bool = fastapi.Query(default=False),
|
||||
):
|
||||
"""
|
||||
This is only used on the AgentDetails Page.
|
||||
|
||||
It returns the store listing agents details.
|
||||
"""
|
||||
include_changelog: bool = Query(default=False),
|
||||
) -> store_model.StoreAgentDetails:
|
||||
"""Get details of a marketplace agent"""
|
||||
username = urllib.parse.unquote(username).lower()
|
||||
# URL decode the agent name since it comes from the URL path
|
||||
agent_name = urllib.parse.unquote(agent_name).lower()
|
||||
@@ -270,76 +200,82 @@ async def get_agent(
|
||||
return agent
|
||||
|
||||
|
||||
@router.get(
|
||||
"/graph/{store_listing_version_id}",
|
||||
summary="Get agent graph",
|
||||
tags=["store"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def get_graph_meta_by_store_listing_version_id(
|
||||
store_listing_version_id: str,
|
||||
) -> backend.data.graph.GraphModelWithoutNodes:
|
||||
"""
|
||||
Get Agent Graph from Store Listing Version ID.
|
||||
"""
|
||||
graph = await store_db.get_available_graph(store_listing_version_id)
|
||||
return graph
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agents/{store_listing_version_id}",
|
||||
summary="Get agent by version",
|
||||
tags=["store"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
response_model=store_model.StoreAgentDetails,
|
||||
)
|
||||
async def get_store_agent(store_listing_version_id: str):
|
||||
"""
|
||||
Get Store Agent Details from Store Listing Version ID.
|
||||
"""
|
||||
agent = await store_db.get_store_agent_by_version_id(store_listing_version_id)
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
@router.post(
|
||||
"/agents/{username}/{agent_name}/review",
|
||||
summary="Create agent review",
|
||||
tags=["store"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
response_model=store_model.StoreReview,
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def create_review(
|
||||
async def post_user_review_for_agent(
|
||||
username: str,
|
||||
agent_name: str,
|
||||
review: store_model.StoreReviewCreate,
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
):
|
||||
"""
|
||||
Create a review for a store agent.
|
||||
|
||||
Args:
|
||||
username: Creator's username
|
||||
agent_name: Name/slug of the agent
|
||||
review: Review details including score and optional comments
|
||||
user_id: ID of authenticated user creating the review
|
||||
|
||||
Returns:
|
||||
The created review
|
||||
"""
|
||||
user_id: str = Security(autogpt_libs.auth.get_user_id),
|
||||
) -> store_model.StoreReview:
|
||||
"""Post a user review on a marketplace agent listing"""
|
||||
username = urllib.parse.unquote(username).lower()
|
||||
agent_name = urllib.parse.unquote(agent_name).lower()
|
||||
# Create the review
|
||||
|
||||
created_review = await store_db.create_store_review(
|
||||
user_id=user_id,
|
||||
store_listing_version_id=review.store_listing_version_id,
|
||||
score=review.score,
|
||||
comments=review.comments,
|
||||
)
|
||||
|
||||
return created_review
|
||||
|
||||
|
||||
@router.get(
|
||||
"/listings/versions/{store_listing_version_id}",
|
||||
summary="Get agent by version",
|
||||
tags=["store"],
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def get_agent_by_listing_version(
|
||||
store_listing_version_id: str,
|
||||
) -> store_model.StoreAgentDetails:
|
||||
agent = await store_db.get_store_agent_by_version_id(store_listing_version_id)
|
||||
return agent
|
||||
|
||||
|
||||
@router.get(
|
||||
"/listings/versions/{store_listing_version_id}/graph",
|
||||
summary="Get agent graph",
|
||||
tags=["store"],
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def get_graph_meta_by_store_listing_version_id(
|
||||
store_listing_version_id: str,
|
||||
) -> backend.data.graph.GraphModelWithoutNodes:
|
||||
"""Get outline of graph belonging to a specific marketplace listing version"""
|
||||
graph = await store_db.get_available_graph(store_listing_version_id)
|
||||
return graph
|
||||
|
||||
|
||||
@router.get(
|
||||
"/listings/versions/{store_listing_version_id}/graph/download",
|
||||
summary="Download agent file",
|
||||
tags=["store", "public"],
|
||||
)
|
||||
async def download_agent_file(
|
||||
store_listing_version_id: str,
|
||||
) -> fastapi.responses.FileResponse:
|
||||
"""Download agent graph file for a specific marketplace listing version"""
|
||||
graph_data = await store_db.get_agent(store_listing_version_id)
|
||||
file_name = f"agent_{graph_data.id}_v{graph_data.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(backend.util.json.dumps(graph_data))
|
||||
tmp_file.flush()
|
||||
|
||||
return fastapi.responses.FileResponse(
|
||||
tmp_file.name, filename=file_name, media_type="application/json"
|
||||
)
|
||||
|
||||
|
||||
##############################################
|
||||
############# Creator Endpoints #############
|
||||
##############################################
|
||||
@@ -349,37 +285,19 @@ async def create_review(
|
||||
"/creators",
|
||||
summary="List store creators",
|
||||
tags=["store", "public"],
|
||||
response_model=store_model.CreatorsResponse,
|
||||
)
|
||||
async def get_creators(
|
||||
featured: bool = False,
|
||||
search_query: str | None = None,
|
||||
sorted_by: Literal["agent_rating", "agent_runs", "num_agents"] | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
):
|
||||
"""
|
||||
This is needed for:
|
||||
- Home Page Featured Creators
|
||||
- Search Results Page
|
||||
|
||||
---
|
||||
|
||||
To support this functionality we need:
|
||||
- featured: bool - to limit the list to just featured agents
|
||||
- search_query: str - vector search based on the creators profile description.
|
||||
- sorted_by: [agent_rating, agent_runs] -
|
||||
"""
|
||||
if page < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page must be greater than 0"
|
||||
)
|
||||
|
||||
if page_size < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page size must be greater than 0"
|
||||
)
|
||||
|
||||
featured: bool = Query(
|
||||
default=False, description="Filter to only show featured creators"
|
||||
),
|
||||
search_query: str | None = Query(
|
||||
default=None, description="Literal + semantic search on names and descriptions"
|
||||
),
|
||||
sorted_by: store_db.StoreCreatorsSortOptions | None = None,
|
||||
page: int = Query(ge=1, default=1),
|
||||
page_size: int = Query(ge=1, default=20),
|
||||
) -> store_model.CreatorsResponse:
|
||||
"""List or search marketplace creators"""
|
||||
creators = await store_cache._get_cached_store_creators(
|
||||
featured=featured,
|
||||
search_query=search_query,
|
||||
@@ -391,18 +309,12 @@ async def get_creators(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/creator/{username}",
|
||||
"/creators/{username}",
|
||||
summary="Get creator details",
|
||||
tags=["store", "public"],
|
||||
response_model=store_model.CreatorDetails,
|
||||
)
|
||||
async def get_creator(
|
||||
username: str,
|
||||
):
|
||||
"""
|
||||
Get the details of a creator.
|
||||
- Creator Details Page
|
||||
"""
|
||||
async def get_creator(username: str) -> store_model.CreatorDetails:
|
||||
"""Get details on a marketplace creator"""
|
||||
username = urllib.parse.unquote(username).lower()
|
||||
creator = await store_cache._get_cached_creator_details(username=username)
|
||||
return creator
|
||||
@@ -414,20 +326,17 @@ async def get_creator(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/myagents",
|
||||
"/my-unpublished-agents",
|
||||
summary="Get my agents",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
response_model=store_model.MyAgentsResponse,
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def get_my_agents(
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
page: typing.Annotated[int, fastapi.Query(ge=1)] = 1,
|
||||
page_size: typing.Annotated[int, fastapi.Query(ge=1)] = 20,
|
||||
):
|
||||
"""
|
||||
Get user's own agents.
|
||||
"""
|
||||
async def get_my_unpublished_agents(
|
||||
user_id: str = Security(autogpt_libs.auth.get_user_id),
|
||||
page: int = Query(ge=1, default=1),
|
||||
page_size: int = Query(ge=1, default=20),
|
||||
) -> store_model.MyUnpublishedAgentsResponse:
|
||||
"""List the authenticated user's unpublished agents"""
|
||||
agents = await store_db.get_my_agents(user_id, page=page, page_size=page_size)
|
||||
return agents
|
||||
|
||||
@@ -436,28 +345,17 @@ async def get_my_agents(
|
||||
"/submissions/{submission_id}",
|
||||
summary="Delete store submission",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
response_model=bool,
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def delete_submission(
|
||||
submission_id: str,
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
):
|
||||
"""
|
||||
Delete a store listing submission.
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the authenticated user
|
||||
submission_id (str): ID of the submission to be deleted
|
||||
|
||||
Returns:
|
||||
bool: True if the submission was successfully deleted, False otherwise
|
||||
"""
|
||||
user_id: str = Security(autogpt_libs.auth.get_user_id),
|
||||
) -> bool:
|
||||
"""Delete a marketplace listing submission"""
|
||||
result = await store_db.delete_store_submission(
|
||||
user_id=user_id,
|
||||
submission_id=submission_id,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -465,37 +363,14 @@ async def delete_submission(
|
||||
"/submissions",
|
||||
summary="List my submissions",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
response_model=store_model.StoreSubmissionsResponse,
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def get_submissions(
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
):
|
||||
"""
|
||||
Get a paginated list of store submissions for the authenticated user.
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the authenticated user
|
||||
page (int, optional): Page number for pagination. Defaults to 1.
|
||||
page_size (int, optional): Number of submissions per page. Defaults to 20.
|
||||
|
||||
Returns:
|
||||
StoreListingsResponse: Paginated list of store submissions
|
||||
|
||||
Raises:
|
||||
HTTPException: If page or page_size are less than 1
|
||||
"""
|
||||
if page < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page must be greater than 0"
|
||||
)
|
||||
|
||||
if page_size < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page size must be greater than 0"
|
||||
)
|
||||
user_id: str = Security(autogpt_libs.auth.get_user_id),
|
||||
page: int = Query(ge=1, default=1),
|
||||
page_size: int = Query(ge=1, default=20),
|
||||
) -> store_model.StoreSubmissionsResponse:
|
||||
"""List the authenticated user's marketplace listing submissions"""
|
||||
listings = await store_db.get_store_submissions(
|
||||
user_id=user_id,
|
||||
page=page,
|
||||
@@ -508,30 +383,17 @@ async def get_submissions(
|
||||
"/submissions",
|
||||
summary="Create store submission",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
response_model=store_model.StoreSubmission,
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def create_submission(
|
||||
submission_request: store_model.StoreSubmissionRequest,
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
):
|
||||
"""
|
||||
Create a new store listing submission.
|
||||
|
||||
Args:
|
||||
submission_request (StoreSubmissionRequest): The submission details
|
||||
user_id (str): ID of the authenticated user submitting the listing
|
||||
|
||||
Returns:
|
||||
StoreSubmission: The created store submission
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is an error creating the submission
|
||||
"""
|
||||
user_id: str = Security(autogpt_libs.auth.get_user_id),
|
||||
) -> store_model.StoreSubmission:
|
||||
"""Submit a new marketplace listing for review"""
|
||||
result = await store_db.create_store_submission(
|
||||
user_id=user_id,
|
||||
agent_id=submission_request.agent_id,
|
||||
agent_version=submission_request.agent_version,
|
||||
graph_id=submission_request.graph_id,
|
||||
graph_version=submission_request.graph_version,
|
||||
slug=submission_request.slug,
|
||||
name=submission_request.name,
|
||||
video_url=submission_request.video_url,
|
||||
@@ -544,7 +406,6 @@ async def create_submission(
|
||||
changes_summary=submission_request.changes_summary or "Initial Submission",
|
||||
recommended_schedule_cron=submission_request.recommended_schedule_cron,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -552,28 +413,14 @@ async def create_submission(
|
||||
"/submissions/{store_listing_version_id}",
|
||||
summary="Edit store submission",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
response_model=store_model.StoreSubmission,
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def edit_submission(
|
||||
store_listing_version_id: str,
|
||||
submission_request: store_model.StoreSubmissionEditRequest,
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
):
|
||||
"""
|
||||
Edit an existing store listing submission.
|
||||
|
||||
Args:
|
||||
store_listing_version_id (str): ID of the store listing version to edit
|
||||
submission_request (StoreSubmissionRequest): The updated submission details
|
||||
user_id (str): ID of the authenticated user editing the listing
|
||||
|
||||
Returns:
|
||||
StoreSubmission: The updated store submission
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is an error editing the submission
|
||||
"""
|
||||
user_id: str = Security(autogpt_libs.auth.get_user_id),
|
||||
) -> store_model.StoreSubmission:
|
||||
"""Update a pending marketplace listing submission"""
|
||||
result = await store_db.edit_store_submission(
|
||||
user_id=user_id,
|
||||
store_listing_version_id=store_listing_version_id,
|
||||
@@ -588,7 +435,6 @@ async def edit_submission(
|
||||
changes_summary=submission_request.changes_summary,
|
||||
recommended_schedule_cron=submission_request.recommended_schedule_cron,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -596,115 +442,61 @@ async def edit_submission(
|
||||
"/submissions/media",
|
||||
summary="Upload submission media",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def upload_submission_media(
|
||||
file: fastapi.UploadFile,
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
):
|
||||
"""
|
||||
Upload media (images/videos) for a store listing submission.
|
||||
|
||||
Args:
|
||||
file (UploadFile): The media file to upload
|
||||
user_id (str): ID of the authenticated user uploading the media
|
||||
|
||||
Returns:
|
||||
str: URL of the uploaded media file
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is an error uploading the media
|
||||
"""
|
||||
user_id: str = Security(autogpt_libs.auth.get_user_id),
|
||||
) -> str:
|
||||
"""Upload media for a marketplace listing submission"""
|
||||
media_url = await store_media.upload_media(user_id=user_id, file=file)
|
||||
return media_url
|
||||
|
||||
|
||||
class ImageURLResponse(BaseModel):
|
||||
image_url: str
|
||||
|
||||
|
||||
@router.post(
|
||||
"/submissions/generate_image",
|
||||
summary="Generate submission image",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Security(autogpt_libs.auth.requires_user)],
|
||||
dependencies=[Security(autogpt_libs.auth.requires_user)],
|
||||
)
|
||||
async def generate_image(
|
||||
agent_id: str,
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
) -> fastapi.responses.Response:
|
||||
graph_id: str,
|
||||
user_id: str = Security(autogpt_libs.auth.get_user_id),
|
||||
) -> ImageURLResponse:
|
||||
"""
|
||||
Generate an image for a store listing submission.
|
||||
|
||||
Args:
|
||||
agent_id (str): ID of the agent to generate an image for
|
||||
user_id (str): ID of the authenticated user
|
||||
|
||||
Returns:
|
||||
JSONResponse: JSON containing the URL of the generated image
|
||||
Generate an image for a marketplace listing submission based on the properties
|
||||
of a given graph.
|
||||
"""
|
||||
agent = await backend.data.graph.get_graph(
|
||||
graph_id=agent_id, version=None, user_id=user_id
|
||||
graph = await backend.data.graph.get_graph(
|
||||
graph_id=graph_id, version=None, user_id=user_id
|
||||
)
|
||||
|
||||
if not agent:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404, detail=f"Agent with ID {agent_id} not found"
|
||||
)
|
||||
if not graph:
|
||||
raise NotFoundError(f"Agent graph #{graph_id} not found")
|
||||
# Use .jpeg here since we are generating JPEG images
|
||||
filename = f"agent_{agent_id}.jpeg"
|
||||
filename = f"agent_{graph_id}.jpeg"
|
||||
|
||||
existing_url = await store_media.check_media_exists(user_id, filename)
|
||||
if existing_url:
|
||||
logger.info(f"Using existing image for agent {agent_id}")
|
||||
return fastapi.responses.JSONResponse(content={"image_url": existing_url})
|
||||
logger.info(f"Using existing image for agent graph {graph_id}")
|
||||
return ImageURLResponse(image_url=existing_url)
|
||||
# Generate agent image as JPEG
|
||||
image = await store_image_gen.generate_agent_image(agent=agent)
|
||||
image = await store_image_gen.generate_agent_image(agent=graph)
|
||||
|
||||
# Create UploadFile with the correct filename and content_type
|
||||
image_file = fastapi.UploadFile(
|
||||
file=image,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
image_url = await store_media.upload_media(
|
||||
user_id=user_id, file=image_file, use_file_name=True
|
||||
)
|
||||
|
||||
return fastapi.responses.JSONResponse(content={"image_url": image_url})
|
||||
|
||||
|
||||
@router.get(
|
||||
"/download/agents/{store_listing_version_id}",
|
||||
summary="Download agent file",
|
||||
tags=["store", "public"],
|
||||
)
|
||||
async def download_agent_file(
|
||||
store_listing_version_id: str = fastapi.Path(
|
||||
..., description="The ID of the agent to download"
|
||||
),
|
||||
) -> fastapi.responses.FileResponse:
|
||||
"""
|
||||
Download the agent file by streaming its content.
|
||||
|
||||
Args:
|
||||
store_listing_version_id (str): The ID 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 store_db.get_agent(store_listing_version_id)
|
||||
file_name = f"agent_{graph_data.id}_v{graph_data.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(backend.util.json.dumps(graph_data))
|
||||
tmp_file.flush()
|
||||
|
||||
return fastapi.responses.FileResponse(
|
||||
tmp_file.name, filename=file_name, media_type="application/json"
|
||||
)
|
||||
return ImageURLResponse(image_url=image_url)
|
||||
|
||||
|
||||
##############################################
|
||||
|
||||
@@ -8,6 +8,8 @@ import pytest
|
||||
import pytest_mock
|
||||
from pytest_snapshot.plugin import Snapshot
|
||||
|
||||
from backend.api.features.store.db import StoreAgentsSortOptions
|
||||
|
||||
from . import model as store_model
|
||||
from . import routes as store_routes
|
||||
|
||||
@@ -196,7 +198,7 @@ def test_get_agents_sorted(
|
||||
mock_db_call.assert_called_once_with(
|
||||
featured=False,
|
||||
creators=None,
|
||||
sorted_by="runs",
|
||||
sorted_by=StoreAgentsSortOptions.RUNS,
|
||||
search_query=None,
|
||||
category=None,
|
||||
page=1,
|
||||
@@ -380,9 +382,11 @@ def test_get_agent_details(
|
||||
runs=100,
|
||||
rating=4.5,
|
||||
versions=["1.0.0", "1.1.0"],
|
||||
agentGraphVersions=["1", "2"],
|
||||
agentGraphId="test-graph-id",
|
||||
graph_versions=["1", "2"],
|
||||
graph_id="test-graph-id",
|
||||
last_updated=FIXED_NOW,
|
||||
active_version_id="test-version-id",
|
||||
has_approved_version=True,
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.api.features.store.db.get_store_agent_details")
|
||||
mock_db_call.return_value = mocked_value
|
||||
@@ -435,15 +439,17 @@ def test_get_creators_pagination(
|
||||
) -> None:
|
||||
mocked_value = store_model.CreatorsResponse(
|
||||
creators=[
|
||||
store_model.Creator(
|
||||
store_model.CreatorDetails(
|
||||
name=f"Creator {i}",
|
||||
username=f"creator{i}",
|
||||
description=f"Creator {i} description",
|
||||
avatar_url=f"avatar{i}.jpg",
|
||||
num_agents=1,
|
||||
agent_rating=4.5,
|
||||
agent_runs=100,
|
||||
description=f"Creator {i} description",
|
||||
links=[f"user{i}.link.com"],
|
||||
is_featured=False,
|
||||
num_agents=1,
|
||||
agent_runs=100,
|
||||
agent_rating=4.5,
|
||||
top_categories=["cat1", "cat2", "cat3"],
|
||||
)
|
||||
for i in range(5)
|
||||
],
|
||||
@@ -496,19 +502,19 @@ def test_get_creator_details(
|
||||
mocked_value = store_model.CreatorDetails(
|
||||
name="Test User",
|
||||
username="creator1",
|
||||
avatar_url="avatar.jpg",
|
||||
description="Test creator description",
|
||||
links=["link1.com", "link2.com"],
|
||||
avatar_url="avatar.jpg",
|
||||
agent_rating=4.8,
|
||||
is_featured=True,
|
||||
num_agents=5,
|
||||
agent_runs=1000,
|
||||
agent_rating=4.8,
|
||||
top_categories=["category1", "category2"],
|
||||
)
|
||||
mock_db_call = mocker.patch(
|
||||
"backend.api.features.store.db.get_store_creator_details"
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.api.features.store.db.get_store_creator")
|
||||
mock_db_call.return_value = mocked_value
|
||||
|
||||
response = client.get("/creator/creator1")
|
||||
response = client.get("/creators/creator1")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = store_model.CreatorDetails.model_validate(response.json())
|
||||
@@ -528,19 +534,26 @@ def test_get_submissions_success(
|
||||
submissions=[
|
||||
store_model.StoreSubmission(
|
||||
listing_id="test-listing-id",
|
||||
name="Test Agent",
|
||||
description="Test agent description",
|
||||
image_urls=["test.jpg"],
|
||||
date_submitted=FIXED_NOW,
|
||||
status=prisma.enums.SubmissionStatus.APPROVED,
|
||||
runs=50,
|
||||
rating=4.2,
|
||||
agent_id="test-agent-id",
|
||||
agent_version=1,
|
||||
sub_heading="Test agent subheading",
|
||||
user_id="test-user-id",
|
||||
slug="test-agent",
|
||||
video_url="test.mp4",
|
||||
listing_version_id="test-version-id",
|
||||
listing_version=1,
|
||||
graph_id="test-agent-id",
|
||||
graph_version=1,
|
||||
name="Test Agent",
|
||||
sub_heading="Test agent subheading",
|
||||
description="Test agent description",
|
||||
instructions="Click the button!",
|
||||
categories=["test-category"],
|
||||
image_urls=["test.jpg"],
|
||||
video_url="test.mp4",
|
||||
agent_output_demo_url="demo_video.mp4",
|
||||
submitted_at=FIXED_NOW,
|
||||
changes_summary="Initial Submission",
|
||||
status=prisma.enums.SubmissionStatus.APPROVED,
|
||||
run_count=50,
|
||||
review_count=5,
|
||||
review_avg_rating=4.2,
|
||||
)
|
||||
],
|
||||
pagination=store_model.Pagination(
|
||||
|
||||
@@ -11,6 +11,7 @@ import pytest
|
||||
from backend.util.models import Pagination
|
||||
|
||||
from . import cache as store_cache
|
||||
from .db import StoreAgentsSortOptions
|
||||
from .model import StoreAgent, StoreAgentsResponse
|
||||
|
||||
|
||||
@@ -215,7 +216,7 @@ class TestCacheDeletion:
|
||||
await store_cache._get_cached_store_agents(
|
||||
featured=True,
|
||||
creator="testuser",
|
||||
sorted_by="rating",
|
||||
sorted_by=StoreAgentsSortOptions.RATING,
|
||||
search_query="AI assistant",
|
||||
category="productivity",
|
||||
page=2,
|
||||
@@ -227,7 +228,7 @@ class TestCacheDeletion:
|
||||
deleted = store_cache._get_cached_store_agents.cache_delete(
|
||||
featured=True,
|
||||
creator="testuser",
|
||||
sorted_by="rating",
|
||||
sorted_by=StoreAgentsSortOptions.RATING,
|
||||
search_query="AI assistant",
|
||||
category="productivity",
|
||||
page=2,
|
||||
@@ -239,7 +240,7 @@ class TestCacheDeletion:
|
||||
deleted = store_cache._get_cached_store_agents.cache_delete(
|
||||
featured=True,
|
||||
creator="testuser",
|
||||
sorted_by="rating",
|
||||
sorted_by=StoreAgentsSortOptions.RATING,
|
||||
search_query="AI assistant",
|
||||
category="productivity",
|
||||
page=2,
|
||||
|
||||
@@ -449,7 +449,6 @@ async def execute_graph_block(
|
||||
async def upload_file(
|
||||
user_id: Annotated[str, Security(get_user_id)],
|
||||
file: UploadFile = File(...),
|
||||
provider: str = "gcs",
|
||||
expiration_hours: int = 24,
|
||||
) -> UploadFileResponse:
|
||||
"""
|
||||
@@ -512,7 +511,6 @@ async def upload_file(
|
||||
storage_path = await cloud_storage.store_file(
|
||||
content=content,
|
||||
filename=file_name,
|
||||
provider=provider,
|
||||
expiration_hours=expiration_hours,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
@@ -515,7 +515,6 @@ async def test_upload_file_success(test_user_id: str):
|
||||
result = await upload_file(
|
||||
file=upload_file_mock,
|
||||
user_id=test_user_id,
|
||||
provider="gcs",
|
||||
expiration_hours=24,
|
||||
)
|
||||
|
||||
@@ -533,7 +532,6 @@ async def test_upload_file_success(test_user_id: str):
|
||||
mock_handler.store_file.assert_called_once_with(
|
||||
content=file_content,
|
||||
filename="test.txt",
|
||||
provider="gcs",
|
||||
expiration_hours=24,
|
||||
user_id=test_user_id,
|
||||
)
|
||||
|
||||
@@ -55,6 +55,7 @@ from backend.util.exceptions import (
|
||||
MissingConfigError,
|
||||
NotAuthorizedError,
|
||||
NotFoundError,
|
||||
PreconditionFailed,
|
||||
)
|
||||
from backend.util.feature_flag import initialize_launchdarkly, shutdown_launchdarkly
|
||||
from backend.util.service import UnhealthyServiceError
|
||||
@@ -275,6 +276,7 @@ app.add_exception_handler(RequestValidationError, validation_error_handler)
|
||||
app.add_exception_handler(pydantic.ValidationError, validation_error_handler)
|
||||
app.add_exception_handler(MissingConfigError, handle_internal_http_error(503))
|
||||
app.add_exception_handler(ValueError, handle_internal_http_error(400))
|
||||
app.add_exception_handler(PreconditionFailed, handle_internal_http_error(428))
|
||||
app.add_exception_handler(Exception, handle_internal_http_error(500))
|
||||
|
||||
app.include_router(backend.api.features.v1.v1_router, tags=["v1"], prefix="/api")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.api.features.store.db import StoreAgentsSortOptions
|
||||
from backend.blocks._base import (
|
||||
Block,
|
||||
BlockCategory,
|
||||
@@ -176,8 +176,8 @@ class SearchStoreAgentsBlock(Block):
|
||||
category: str | None = SchemaField(
|
||||
description="Filter by category", default=None
|
||||
)
|
||||
sort_by: Literal["rating", "runs", "name", "updated_at"] = SchemaField(
|
||||
description="How to sort the results", default="rating"
|
||||
sort_by: StoreAgentsSortOptions = SchemaField(
|
||||
description="How to sort the results", default=StoreAgentsSortOptions.RATING
|
||||
)
|
||||
limit: int = SchemaField(
|
||||
description="Maximum number of results to return", default=10, ge=1, le=100
|
||||
@@ -278,7 +278,7 @@ class SearchStoreAgentsBlock(Block):
|
||||
self,
|
||||
query: str | None = None,
|
||||
category: str | None = None,
|
||||
sort_by: Literal["rating", "runs", "name", "updated_at"] = "rating",
|
||||
sort_by: StoreAgentsSortOptions = StoreAgentsSortOptions.RATING,
|
||||
limit: int = 10,
|
||||
) -> SearchAgentsResponse:
|
||||
"""
|
||||
|
||||
@@ -2,6 +2,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.api.features.store.db import StoreAgentsSortOptions
|
||||
from backend.blocks.system.library_operations import (
|
||||
AddToLibraryFromStoreBlock,
|
||||
LibraryAgent,
|
||||
@@ -121,7 +122,10 @@ async def test_search_store_agents_block(mocker):
|
||||
)
|
||||
|
||||
input_data = block.Input(
|
||||
query="test", category="productivity", sort_by="rating", limit=10
|
||||
query="test",
|
||||
category="productivity",
|
||||
sort_by=StoreAgentsSortOptions.RATING, # type: ignore[reportArgumentType]
|
||||
limit=10,
|
||||
)
|
||||
|
||||
outputs = {}
|
||||
|
||||
@@ -151,8 +151,8 @@ async def setup_test_data(server):
|
||||
unique_slug = f"test-agent-{str(uuid.uuid4())[:8]}"
|
||||
store_submission = await store_db.create_store_submission(
|
||||
user_id=user.id,
|
||||
agent_id=created_graph.id,
|
||||
agent_version=created_graph.version,
|
||||
graph_id=created_graph.id,
|
||||
graph_version=created_graph.version,
|
||||
slug=unique_slug,
|
||||
name="Test Agent",
|
||||
description="A simple test agent",
|
||||
@@ -161,10 +161,10 @@ async def setup_test_data(server):
|
||||
image_urls=["https://example.com/image.jpg"],
|
||||
)
|
||||
|
||||
assert store_submission.store_listing_version_id is not None
|
||||
assert store_submission.listing_version_id is not None
|
||||
# 4. Approve the store listing version
|
||||
await store_db.review_store_submission(
|
||||
store_listing_version_id=store_submission.store_listing_version_id,
|
||||
store_listing_version_id=store_submission.listing_version_id,
|
||||
is_approved=True,
|
||||
external_comments="Approved for testing",
|
||||
internal_comments="Test approval",
|
||||
@@ -321,8 +321,8 @@ async def setup_llm_test_data(server):
|
||||
unique_slug = f"llm-test-agent-{str(uuid.uuid4())[:8]}"
|
||||
store_submission = await store_db.create_store_submission(
|
||||
user_id=user.id,
|
||||
agent_id=created_graph.id,
|
||||
agent_version=created_graph.version,
|
||||
graph_id=created_graph.id,
|
||||
graph_version=created_graph.version,
|
||||
slug=unique_slug,
|
||||
name="LLM Test Agent",
|
||||
description="An agent with LLM capabilities",
|
||||
@@ -330,9 +330,9 @@ async def setup_llm_test_data(server):
|
||||
categories=["testing", "ai"],
|
||||
image_urls=["https://example.com/image.jpg"],
|
||||
)
|
||||
assert store_submission.store_listing_version_id is not None
|
||||
assert store_submission.listing_version_id is not None
|
||||
await store_db.review_store_submission(
|
||||
store_listing_version_id=store_submission.store_listing_version_id,
|
||||
store_listing_version_id=store_submission.listing_version_id,
|
||||
is_approved=True,
|
||||
external_comments="Approved for testing",
|
||||
internal_comments="Test approval for LLM agent",
|
||||
@@ -476,8 +476,8 @@ async def setup_firecrawl_test_data(server):
|
||||
unique_slug = f"firecrawl-test-agent-{str(uuid.uuid4())[:8]}"
|
||||
store_submission = await store_db.create_store_submission(
|
||||
user_id=user.id,
|
||||
agent_id=created_graph.id,
|
||||
agent_version=created_graph.version,
|
||||
graph_id=created_graph.id,
|
||||
graph_version=created_graph.version,
|
||||
slug=unique_slug,
|
||||
name="Firecrawl Test Agent",
|
||||
description="An agent with Firecrawl integration (no credentials)",
|
||||
@@ -485,9 +485,9 @@ async def setup_firecrawl_test_data(server):
|
||||
categories=["testing", "scraping"],
|
||||
image_urls=["https://example.com/image.jpg"],
|
||||
)
|
||||
assert store_submission.store_listing_version_id is not None
|
||||
assert store_submission.listing_version_id is not None
|
||||
await store_db.review_store_submission(
|
||||
store_listing_version_id=store_submission.store_listing_version_id,
|
||||
store_listing_version_id=store_submission.listing_version_id,
|
||||
is_approved=True,
|
||||
external_comments="Approved for testing",
|
||||
internal_comments="Test approval for Firecrawl agent",
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from backend.api.features.store.exceptions import AgentNotFoundError
|
||||
from backend.copilot.model import ChatSession
|
||||
from backend.data.db_accessors import store_db as get_store_db
|
||||
from backend.util.exceptions import NotFoundError
|
||||
|
||||
from .agent_generator import (
|
||||
AgentGeneratorNotConfiguredError,
|
||||
@@ -140,7 +140,7 @@ class CustomizeAgentTool(BaseTool):
|
||||
agent_details = await store_db.get_store_agent_details(
|
||||
username=creator_username, agent_name=agent_slug
|
||||
)
|
||||
except AgentNotFoundError:
|
||||
except NotFoundError:
|
||||
return ErrorResponse(
|
||||
message=(
|
||||
f"Could not find marketplace agent '{agent_id}'. "
|
||||
|
||||
@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch
|
||||
from uuid import UUID
|
||||
|
||||
import fastapi.exceptions
|
||||
import prisma
|
||||
import pytest
|
||||
from pytest_snapshot.plugin import Snapshot
|
||||
|
||||
@@ -250,8 +251,8 @@ async def test_clean_graph(server: SpinTestServer):
|
||||
"_test_id": "node_with_secrets",
|
||||
"input": "normal_value",
|
||||
"control_test_input": "should be preserved",
|
||||
"api_key": "secret_api_key_123", # Should be filtered
|
||||
"password": "secret_password_456", # Should be filtered
|
||||
"api_key": "secret_api_key_123", # Should be filtered # pragma: allowlist secret # noqa
|
||||
"password": "secret_password_456", # Should be filtered # pragma: allowlist secret # noqa
|
||||
"token": "secret_token_789", # Should be filtered
|
||||
"credentials": { # Should be filtered
|
||||
"id": "fake-github-credentials-id",
|
||||
@@ -354,9 +355,24 @@ async def test_access_store_listing_graph(server: SpinTestServer):
|
||||
create_graph, DEFAULT_USER_ID
|
||||
)
|
||||
|
||||
# Ensure the default user has a Profile (required for store submissions)
|
||||
existing_profile = await prisma.models.Profile.prisma().find_first(
|
||||
where={"userId": DEFAULT_USER_ID}
|
||||
)
|
||||
if not existing_profile:
|
||||
await prisma.models.Profile.prisma().create(
|
||||
data=prisma.types.ProfileCreateInput(
|
||||
userId=DEFAULT_USER_ID,
|
||||
name="Default User",
|
||||
username=f"default-user-{DEFAULT_USER_ID[:8]}",
|
||||
description="Default test user profile",
|
||||
links=[],
|
||||
)
|
||||
)
|
||||
|
||||
store_submission_request = store.StoreSubmissionRequest(
|
||||
agent_id=created_graph.id,
|
||||
agent_version=created_graph.version,
|
||||
graph_id=created_graph.id,
|
||||
graph_version=created_graph.version,
|
||||
slug=created_graph.id,
|
||||
name="Test name",
|
||||
sub_heading="Test sub heading",
|
||||
@@ -385,8 +401,8 @@ async def test_access_store_listing_graph(server: SpinTestServer):
|
||||
assert False, "Failed to create store listing"
|
||||
|
||||
slv_id = (
|
||||
store_listing.store_listing_version_id
|
||||
if store_listing.store_listing_version_id is not None
|
||||
store_listing.listing_version_id
|
||||
if store_listing.listing_version_id is not None
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,14 @@ from prisma.types import (
|
||||
)
|
||||
|
||||
# from backend.notifications.models import NotificationEvent
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
EmailStr,
|
||||
Field,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from backend.util.exceptions import DatabaseError
|
||||
from backend.util.json import SafeJson
|
||||
@@ -175,10 +182,26 @@ class RefundRequestData(BaseNotificationData):
|
||||
balance: int
|
||||
|
||||
|
||||
class AgentApprovalData(BaseNotificationData):
|
||||
class _LegacyAgentFieldsMixin:
|
||||
"""Temporary patch to handle existing queued payloads"""
|
||||
|
||||
# FIXME: remove in next release
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _map_legacy_agent_fields(cls, values: Any):
|
||||
if isinstance(values, dict):
|
||||
if "graph_id" not in values and "agent_id" in values:
|
||||
values["graph_id"] = values.pop("agent_id")
|
||||
if "graph_version" not in values and "agent_version" in values:
|
||||
values["graph_version"] = values.pop("agent_version")
|
||||
return values
|
||||
|
||||
|
||||
class AgentApprovalData(_LegacyAgentFieldsMixin, BaseNotificationData):
|
||||
agent_name: str
|
||||
agent_id: str
|
||||
agent_version: int
|
||||
graph_id: str
|
||||
graph_version: int
|
||||
reviewer_name: str
|
||||
reviewer_email: str
|
||||
comments: str
|
||||
@@ -193,10 +216,10 @@ class AgentApprovalData(BaseNotificationData):
|
||||
return value
|
||||
|
||||
|
||||
class AgentRejectionData(BaseNotificationData):
|
||||
class AgentRejectionData(_LegacyAgentFieldsMixin, BaseNotificationData):
|
||||
agent_name: str
|
||||
agent_id: str
|
||||
agent_version: int
|
||||
graph_id: str
|
||||
graph_version: int
|
||||
reviewer_name: str
|
||||
reviewer_email: str
|
||||
comments: str
|
||||
|
||||
@@ -15,8 +15,8 @@ class TestAgentApprovalData:
|
||||
"""Test creating valid AgentApprovalData."""
|
||||
data = AgentApprovalData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
graph_id="test-agent-123",
|
||||
graph_version=1,
|
||||
reviewer_name="John Doe",
|
||||
reviewer_email="john@example.com",
|
||||
comments="Great agent, approved!",
|
||||
@@ -25,8 +25,8 @@ class TestAgentApprovalData:
|
||||
)
|
||||
|
||||
assert data.agent_name == "Test Agent"
|
||||
assert data.agent_id == "test-agent-123"
|
||||
assert data.agent_version == 1
|
||||
assert data.graph_id == "test-agent-123"
|
||||
assert data.graph_version == 1
|
||||
assert data.reviewer_name == "John Doe"
|
||||
assert data.reviewer_email == "john@example.com"
|
||||
assert data.comments == "Great agent, approved!"
|
||||
@@ -40,8 +40,8 @@ class TestAgentApprovalData:
|
||||
):
|
||||
AgentApprovalData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
graph_id="test-agent-123",
|
||||
graph_version=1,
|
||||
reviewer_name="John Doe",
|
||||
reviewer_email="john@example.com",
|
||||
comments="Great agent, approved!",
|
||||
@@ -53,8 +53,8 @@ class TestAgentApprovalData:
|
||||
"""Test AgentApprovalData with empty comments."""
|
||||
data = AgentApprovalData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
graph_id="test-agent-123",
|
||||
graph_version=1,
|
||||
reviewer_name="John Doe",
|
||||
reviewer_email="john@example.com",
|
||||
comments="", # Empty comments
|
||||
@@ -72,8 +72,8 @@ class TestAgentRejectionData:
|
||||
"""Test creating valid AgentRejectionData."""
|
||||
data = AgentRejectionData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
graph_id="test-agent-123",
|
||||
graph_version=1,
|
||||
reviewer_name="Jane Doe",
|
||||
reviewer_email="jane@example.com",
|
||||
comments="Please fix the security issues before resubmitting.",
|
||||
@@ -82,8 +82,8 @@ class TestAgentRejectionData:
|
||||
)
|
||||
|
||||
assert data.agent_name == "Test Agent"
|
||||
assert data.agent_id == "test-agent-123"
|
||||
assert data.agent_version == 1
|
||||
assert data.graph_id == "test-agent-123"
|
||||
assert data.graph_version == 1
|
||||
assert data.reviewer_name == "Jane Doe"
|
||||
assert data.reviewer_email == "jane@example.com"
|
||||
assert data.comments == "Please fix the security issues before resubmitting."
|
||||
@@ -97,8 +97,8 @@ class TestAgentRejectionData:
|
||||
):
|
||||
AgentRejectionData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
graph_id="test-agent-123",
|
||||
graph_version=1,
|
||||
reviewer_name="Jane Doe",
|
||||
reviewer_email="jane@example.com",
|
||||
comments="Please fix the security issues.",
|
||||
@@ -111,8 +111,8 @@ class TestAgentRejectionData:
|
||||
long_comment = "A" * 1000 # Very long comment
|
||||
data = AgentRejectionData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
graph_id="test-agent-123",
|
||||
graph_version=1,
|
||||
reviewer_name="Jane Doe",
|
||||
reviewer_email="jane@example.com",
|
||||
comments=long_comment,
|
||||
@@ -126,8 +126,8 @@ class TestAgentRejectionData:
|
||||
"""Test that models can be serialized and deserialized."""
|
||||
original_data = AgentRejectionData(
|
||||
agent_name="Test Agent",
|
||||
agent_id="test-agent-123",
|
||||
agent_version=1,
|
||||
graph_id="test-agent-123",
|
||||
graph_version=1,
|
||||
reviewer_name="Jane Doe",
|
||||
reviewer_email="jane@example.com",
|
||||
comments="Please fix the issues.",
|
||||
@@ -142,8 +142,8 @@ class TestAgentRejectionData:
|
||||
restored_data = AgentRejectionData.model_validate(data_dict)
|
||||
|
||||
assert restored_data.agent_name == original_data.agent_name
|
||||
assert restored_data.agent_id == original_data.agent_id
|
||||
assert restored_data.agent_version == original_data.agent_version
|
||||
assert restored_data.graph_id == original_data.graph_id
|
||||
assert restored_data.graph_version == original_data.graph_version
|
||||
assert restored_data.reviewer_name == original_data.reviewer_name
|
||||
assert restored_data.reviewer_email == original_data.reviewer_email
|
||||
assert restored_data.comments == original_data.comments
|
||||
|
||||
@@ -244,7 +244,10 @@ def _clean_and_split(text: str) -> list[str]:
|
||||
|
||||
|
||||
def _calculate_points(
|
||||
agent, categories: list[str], custom: list[str], integrations: list[str]
|
||||
agent: prisma.models.StoreAgent,
|
||||
categories: list[str],
|
||||
custom: list[str],
|
||||
integrations: list[str],
|
||||
) -> int:
|
||||
"""
|
||||
Calculates the total points for an agent based on the specified criteria.
|
||||
@@ -397,7 +400,7 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
storeAgents = await prisma.models.StoreAgent.prisma().find_many(
|
||||
where={
|
||||
"is_available": True,
|
||||
"useForOnboarding": True,
|
||||
"use_for_onboarding": True,
|
||||
},
|
||||
order=[
|
||||
{"featured": "desc"},
|
||||
@@ -407,7 +410,7 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
take=100,
|
||||
)
|
||||
|
||||
# If not enough agents found, relax the useForOnboarding filter
|
||||
# If not enough agents found, relax the use_for_onboarding filter
|
||||
if len(storeAgents) < 2:
|
||||
storeAgents = await prisma.models.StoreAgent.prisma().find_many(
|
||||
where=prisma.types.StoreAgentWhereInput(**where_clause),
|
||||
@@ -420,7 +423,7 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
)
|
||||
|
||||
# Calculate points for the first X agents and choose the top 2
|
||||
agent_points = []
|
||||
agent_points: list[tuple[prisma.models.StoreAgent, int]] = []
|
||||
for agent in storeAgents[:POINTS_AGENT_COUNT]:
|
||||
points = _calculate_points(
|
||||
agent, categories, custom, user_onboarding.integrations
|
||||
@@ -430,28 +433,7 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
agent_points.sort(key=lambda x: x[1], reverse=True)
|
||||
recommended_agents = [agent for agent, _ in agent_points[:2]]
|
||||
|
||||
return [
|
||||
StoreAgentDetails(
|
||||
store_listing_version_id=agent.storeListingVersionId,
|
||||
slug=agent.slug,
|
||||
agent_name=agent.agent_name,
|
||||
agent_video=agent.agent_video or "",
|
||||
agent_output_demo=agent.agent_output_demo or "",
|
||||
agent_image=agent.agent_image,
|
||||
creator=agent.creator_username,
|
||||
creator_avatar=agent.creator_avatar,
|
||||
sub_heading=agent.sub_heading,
|
||||
description=agent.description,
|
||||
categories=agent.categories,
|
||||
runs=agent.runs,
|
||||
rating=agent.rating,
|
||||
versions=agent.versions,
|
||||
agentGraphVersions=agent.agentGraphVersions,
|
||||
agentGraphId=agent.agentGraphId,
|
||||
last_updated=agent.updated_at,
|
||||
)
|
||||
for agent in recommended_agents
|
||||
]
|
||||
return [StoreAgentDetails.from_db(agent) for agent in recommended_agents]
|
||||
|
||||
|
||||
@cached(maxsize=1, ttl_seconds=300) # Cache for 5 minutes since this rarely changes
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import fastapi.responses
|
||||
import prisma
|
||||
import pytest
|
||||
|
||||
import backend.api.features.library.model
|
||||
@@ -497,9 +498,24 @@ async def test_store_listing_graph(server: SpinTestServer):
|
||||
test_user = await create_test_user()
|
||||
test_graph = await create_graph(server, create_test_graph(), test_user)
|
||||
|
||||
# Ensure the test user has a Profile (required for store submissions)
|
||||
existing_profile = await prisma.models.Profile.prisma().find_first(
|
||||
where={"userId": test_user.id}
|
||||
)
|
||||
if not existing_profile:
|
||||
await prisma.models.Profile.prisma().create(
|
||||
data=prisma.types.ProfileCreateInput(
|
||||
userId=test_user.id,
|
||||
name=test_user.name or "Test User",
|
||||
username=f"test-user-{test_user.id[:8]}",
|
||||
description="Test user profile",
|
||||
links=[],
|
||||
)
|
||||
)
|
||||
|
||||
store_submission_request = backend.api.features.store.model.StoreSubmissionRequest(
|
||||
agent_id=test_graph.id,
|
||||
agent_version=test_graph.version,
|
||||
graph_id=test_graph.id,
|
||||
graph_version=test_graph.version,
|
||||
slug=test_graph.id,
|
||||
name="Test name",
|
||||
sub_heading="Test sub heading",
|
||||
@@ -517,8 +533,8 @@ async def test_store_listing_graph(server: SpinTestServer):
|
||||
assert False, "Failed to create store listing"
|
||||
|
||||
slv_id = (
|
||||
store_listing.store_listing_version_id
|
||||
if store_listing.store_listing_version_id is not None
|
||||
store_listing.listing_version_id
|
||||
if store_listing.listing_version_id is not None
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
{#
|
||||
Template variables:
|
||||
data.agent_name: the name of the approved agent
|
||||
data.agent_id: the ID of the agent
|
||||
data.agent_version: the version of the agent
|
||||
data.graph_id: the ID of the agent
|
||||
data.graph_version: the version of the agent
|
||||
data.reviewer_name: the name of the reviewer who approved it
|
||||
data.reviewer_email: the email of the reviewer
|
||||
data.comments: comments from the reviewer
|
||||
@@ -70,4 +70,4 @@
|
||||
Thank you for contributing to the AutoGPT ecosystem! 🚀
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
{#
|
||||
Template variables:
|
||||
data.agent_name: the name of the rejected agent
|
||||
data.agent_id: the ID of the agent
|
||||
data.agent_version: the version of the agent
|
||||
data.graph_id: the ID of the agent
|
||||
data.graph_version: the version of the agent
|
||||
data.reviewer_name: the name of the reviewer who rejected it
|
||||
data.reviewer_email: the email of the reviewer
|
||||
data.comments: comments from the reviewer explaining the rejection
|
||||
@@ -74,4 +74,4 @@
|
||||
We're excited to see your improved agent submission! 🚀
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -64,6 +64,10 @@ class GraphNotInLibraryError(GraphNotAccessibleError):
|
||||
"""Raised when attempting to execute a graph that is not / no longer in the user's library."""
|
||||
|
||||
|
||||
class PreconditionFailed(Exception):
|
||||
"""The user must do something else first before trying the current operation"""
|
||||
|
||||
|
||||
class InsufficientBalanceError(ValueError):
|
||||
user_id: str
|
||||
message: str
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
BEGIN;
|
||||
|
||||
-- Drop illogical column StoreListing.agentGraphVersion;
|
||||
ALTER TABLE "StoreListing" DROP CONSTRAINT "StoreListing_agentGraphId_agentGraphVersion_fkey";
|
||||
DROP INDEX "StoreListing_agentGraphId_agentGraphVersion_idx";
|
||||
ALTER TABLE "StoreListing" DROP COLUMN "agentGraphVersion";
|
||||
|
||||
-- Add uniqueness constraint to Profile.userId and remove invalid data
|
||||
--
|
||||
-- Delete any profiles with null userId (which is invalid and doesn't occur in theory)
|
||||
DELETE FROM "Profile" WHERE "userId" IS NULL;
|
||||
--
|
||||
-- Delete duplicate profiles per userId, keeping the most recently updated one
|
||||
DELETE FROM "Profile"
|
||||
WHERE "id" IN (
|
||||
SELECT "id" FROM (
|
||||
SELECT "id", ROW_NUMBER() OVER (
|
||||
PARTITION BY "userId" ORDER BY "updatedAt" DESC, "id" DESC
|
||||
) AS rn
|
||||
FROM "Profile"
|
||||
) ranked
|
||||
WHERE rn > 1
|
||||
);
|
||||
--
|
||||
-- Add userId uniqueness constraint
|
||||
ALTER TABLE "Profile" ALTER COLUMN "userId" SET NOT NULL;
|
||||
CREATE UNIQUE INDEX "Profile_userId_key" ON "Profile"("userId");
|
||||
|
||||
-- Add formal relation StoreListing.owningUserId -> Profile.userId
|
||||
ALTER TABLE "StoreListing" ADD CONSTRAINT "StoreListing_owner_Profile_fkey" FOREIGN KEY ("owningUserId") REFERENCES "Profile"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,219 @@
|
||||
-- Update the StoreSubmission and StoreAgent views with additional fields, clearer field names, and faster joins.
|
||||
-- Steps:
|
||||
-- 1. Update `mv_agent_run_counts` to exclude runs by the agent's creator
|
||||
-- a. Drop dependent views `StoreAgent` and `Creator`
|
||||
-- b. Update `mv_agent_run_counts` and its index
|
||||
-- c. Recreate `StoreAgent` view (with updates)
|
||||
-- d. Restore `Creator` view
|
||||
-- 2. Update `StoreSubmission` view
|
||||
-- 3. Update `StoreListingReview` indices to make `StoreSubmission` query more efficient
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Drop views that are dependent on mv_agent_run_counts
|
||||
DROP VIEW IF EXISTS "StoreAgent";
|
||||
DROP VIEW IF EXISTS "Creator";
|
||||
|
||||
-- Update materialized view for agent run counts to exclude runs by the agent's creator
|
||||
DROP INDEX IF EXISTS "idx_mv_agent_run_counts";
|
||||
DROP MATERIALIZED VIEW IF EXISTS "mv_agent_run_counts";
|
||||
CREATE MATERIALIZED VIEW "mv_agent_run_counts" AS
|
||||
SELECT
|
||||
run."agentGraphId" AS graph_id,
|
||||
COUNT(*) AS run_count
|
||||
FROM "AgentGraphExecution" run
|
||||
JOIN "AgentGraph" graph ON graph.id = run."agentGraphId"
|
||||
-- Exclude runs by the agent's creator to avoid inflating run counts
|
||||
WHERE graph."userId" != run."userId"
|
||||
GROUP BY run."agentGraphId";
|
||||
|
||||
-- Recreate index
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "idx_mv_agent_run_counts" ON "mv_agent_run_counts"("graph_id");
|
||||
|
||||
-- Re-populate the materialized view
|
||||
REFRESH MATERIALIZED VIEW "mv_agent_run_counts";
|
||||
|
||||
|
||||
-- Recreate the StoreAgent view with the following changes
|
||||
-- (compared to 20260115210000_remove_storelistingversion_search):
|
||||
-- - Narrow to *explicitly active* version (sl.activeVersionId) instead of MAX(version)
|
||||
-- - Add `recommended_schedule_cron` column
|
||||
-- - Rename `"storeListingVersionId"` -> `listing_version_id`
|
||||
-- - Rename `"agentGraphVersions"` -> `graph_versions`
|
||||
-- - Rename `"agentGraphId"` -> `graph_id`
|
||||
-- - Rename `"useForOnboarding"` -> `use_for_onboarding`
|
||||
CREATE OR REPLACE VIEW "StoreAgent" AS
|
||||
WITH store_agent_versions AS (
|
||||
SELECT
|
||||
"storeListingId",
|
||||
array_agg(DISTINCT version::text ORDER BY version::text) AS versions
|
||||
FROM "StoreListingVersion"
|
||||
WHERE "submissionStatus" = 'APPROVED'
|
||||
GROUP BY "storeListingId"
|
||||
),
|
||||
agent_graph_versions AS (
|
||||
SELECT
|
||||
"storeListingId",
|
||||
array_agg(DISTINCT "agentGraphVersion"::text ORDER BY "agentGraphVersion"::text) AS graph_versions
|
||||
FROM "StoreListingVersion"
|
||||
WHERE "submissionStatus" = 'APPROVED'
|
||||
GROUP BY "storeListingId"
|
||||
)
|
||||
SELECT
|
||||
sl.id AS listing_id,
|
||||
slv.id AS listing_version_id,
|
||||
slv."createdAt" AS updated_at,
|
||||
sl.slug,
|
||||
COALESCE(slv.name, '') AS agent_name,
|
||||
slv."videoUrl" AS agent_video,
|
||||
slv."agentOutputDemoUrl" AS agent_output_demo,
|
||||
COALESCE(slv."imageUrls", ARRAY[]::text[]) AS agent_image,
|
||||
slv."isFeatured" AS featured,
|
||||
cp.username AS creator_username,
|
||||
cp."avatarUrl" AS creator_avatar,
|
||||
slv."subHeading" AS sub_heading,
|
||||
slv.description,
|
||||
slv.categories,
|
||||
COALESCE(arc.run_count, 0::bigint) AS runs,
|
||||
COALESCE(reviews.avg_rating, 0.0)::double precision AS rating,
|
||||
COALESCE(sav.versions, ARRAY[slv.version::text]) AS versions,
|
||||
slv."agentGraphId" AS graph_id,
|
||||
COALESCE(
|
||||
agv.graph_versions,
|
||||
ARRAY[slv."agentGraphVersion"::text]
|
||||
) AS graph_versions,
|
||||
slv."isAvailable" AS is_available,
|
||||
COALESCE(sl."useForOnboarding", false) AS use_for_onboarding,
|
||||
slv."recommendedScheduleCron" AS recommended_schedule_cron
|
||||
FROM "StoreListing" AS sl
|
||||
JOIN "StoreListingVersion" AS slv
|
||||
ON slv."storeListingId" = sl.id
|
||||
AND slv.id = sl."activeVersionId"
|
||||
AND slv."submissionStatus" = 'APPROVED'
|
||||
JOIN "AgentGraph" AS ag
|
||||
ON slv."agentGraphId" = ag.id
|
||||
AND slv."agentGraphVersion" = ag.version
|
||||
LEFT JOIN "Profile" AS cp
|
||||
ON sl."owningUserId" = cp."userId"
|
||||
LEFT JOIN "mv_review_stats" AS reviews
|
||||
ON sl.id = reviews."storeListingId"
|
||||
LEFT JOIN "mv_agent_run_counts" AS arc
|
||||
ON ag.id = arc.graph_id
|
||||
LEFT JOIN store_agent_versions AS sav
|
||||
ON sl.id = sav."storeListingId"
|
||||
LEFT JOIN agent_graph_versions AS agv
|
||||
ON sl.id = agv."storeListingId"
|
||||
WHERE sl."isDeleted" = false
|
||||
AND sl."hasApprovedVersion" = true;
|
||||
|
||||
|
||||
-- Restore Creator view as last updated in 20250604130249_optimise_store_agent_and_creator_views,
|
||||
-- with minor changes:
|
||||
-- - Ensure top_categories always TEXT[]
|
||||
-- - Filter out empty ('') categories
|
||||
CREATE OR REPLACE VIEW "Creator" AS
|
||||
WITH creator_listings AS (
|
||||
SELECT
|
||||
sl."owningUserId",
|
||||
sl.id AS listing_id,
|
||||
slv."agentGraphId",
|
||||
slv.categories,
|
||||
sr.score,
|
||||
ar.run_count
|
||||
FROM "StoreListing" sl
|
||||
JOIN "StoreListingVersion" slv
|
||||
ON slv."storeListingId" = sl.id
|
||||
AND slv."submissionStatus" = 'APPROVED'
|
||||
LEFT JOIN "StoreListingReview" sr
|
||||
ON sr."storeListingVersionId" = slv.id
|
||||
LEFT JOIN "mv_agent_run_counts" ar
|
||||
ON ar.graph_id = slv."agentGraphId"
|
||||
WHERE sl."isDeleted" = false
|
||||
AND sl."hasApprovedVersion" = true
|
||||
),
|
||||
creator_stats AS (
|
||||
SELECT
|
||||
cl."owningUserId",
|
||||
COUNT(DISTINCT cl.listing_id) AS num_agents,
|
||||
AVG(COALESCE(cl.score, 0)::numeric) AS agent_rating,
|
||||
SUM(COALESCE(cl.run_count, 0)) AS agent_runs,
|
||||
array_agg(DISTINCT cat ORDER BY cat)
|
||||
FILTER (WHERE cat IS NOT NULL AND cat != '') AS all_categories
|
||||
FROM creator_listings cl
|
||||
LEFT JOIN LATERAL unnest(COALESCE(cl.categories, ARRAY[]::text[])) AS cat ON true
|
||||
GROUP BY cl."owningUserId"
|
||||
)
|
||||
SELECT
|
||||
p.username,
|
||||
p.name,
|
||||
p."avatarUrl" AS avatar_url,
|
||||
p.description,
|
||||
COALESCE(cs.all_categories, ARRAY[]::text[]) AS top_categories,
|
||||
p.links,
|
||||
p."isFeatured" AS is_featured,
|
||||
COALESCE(cs.num_agents, 0::bigint) AS num_agents,
|
||||
COALESCE(cs.agent_rating, 0.0) AS agent_rating,
|
||||
COALESCE(cs.agent_runs, 0::numeric) AS agent_runs
|
||||
FROM "Profile" p
|
||||
LEFT JOIN creator_stats cs ON cs."owningUserId" = p."userId";
|
||||
|
||||
|
||||
-- Recreate the StoreSubmission view with updated fields & query strategy:
|
||||
-- - Uses mv_agent_run_counts instead of full AgentGraphExecution table scan + aggregation
|
||||
-- - Renamed agent_id, agent_version -> graph_id, graph_version
|
||||
-- - Renamed store_listing_version_id -> listing_version_id
|
||||
-- - Renamed date_submitted -> submitted_at
|
||||
-- - Renamed runs, rating -> run_count, review_avg_rating
|
||||
-- - Added fields: instructions, agent_output_demo_url, review_count, is_deleted
|
||||
DROP VIEW IF EXISTS "StoreSubmission";
|
||||
CREATE OR REPLACE VIEW "StoreSubmission" AS
|
||||
WITH review_stats AS (
|
||||
SELECT
|
||||
"storeListingVersionId" AS version_id, -- more specific than mv_review_stats
|
||||
avg(score) AS avg_rating,
|
||||
count(*) AS review_count
|
||||
FROM "StoreListingReview"
|
||||
GROUP BY "storeListingVersionId"
|
||||
)
|
||||
SELECT
|
||||
sl.id AS listing_id,
|
||||
sl."owningUserId" AS user_id,
|
||||
sl.slug AS slug,
|
||||
|
||||
slv.id AS listing_version_id,
|
||||
slv.version AS listing_version,
|
||||
slv."agentGraphId" AS graph_id,
|
||||
slv."agentGraphVersion" AS graph_version,
|
||||
slv.name AS name,
|
||||
slv."subHeading" AS sub_heading,
|
||||
slv.description AS description,
|
||||
slv.instructions AS instructions,
|
||||
slv.categories AS categories,
|
||||
slv."imageUrls" AS image_urls,
|
||||
slv."videoUrl" AS video_url,
|
||||
slv."agentOutputDemoUrl" AS agent_output_demo_url,
|
||||
slv."submittedAt" AS submitted_at,
|
||||
slv."changesSummary" AS changes_summary,
|
||||
slv."submissionStatus" AS status,
|
||||
slv."reviewedAt" AS reviewed_at,
|
||||
slv."reviewerId" AS reviewer_id,
|
||||
slv."reviewComments" AS review_comments,
|
||||
slv."internalComments" AS internal_comments,
|
||||
slv."isDeleted" AS is_deleted,
|
||||
|
||||
COALESCE(run_stats.run_count, 0::bigint) AS run_count,
|
||||
COALESCE(review_stats.review_count, 0::bigint) AS review_count,
|
||||
COALESCE(review_stats.avg_rating, 0.0)::double precision AS review_avg_rating
|
||||
FROM "StoreListing" sl
|
||||
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
|
||||
LEFT JOIN review_stats ON review_stats.version_id = slv.id
|
||||
LEFT JOIN mv_agent_run_counts run_stats ON run_stats.graph_id = slv."agentGraphId"
|
||||
WHERE sl."isDeleted" = false;
|
||||
|
||||
|
||||
-- Drop unused index on StoreListingReview.reviewByUserId
|
||||
DROP INDEX IF EXISTS "StoreListingReview_reviewByUserId_idx";
|
||||
-- Add index on storeListingVersionId to make StoreSubmission query faster
|
||||
CREATE INDEX "StoreListingReview_storeListingVersionId_idx" ON "StoreListingReview"("storeListingVersionId");
|
||||
|
||||
COMMIT;
|
||||
@@ -281,7 +281,6 @@ model AgentGraph {
|
||||
|
||||
Presets AgentPreset[]
|
||||
LibraryAgents LibraryAgent[]
|
||||
StoreListings StoreListing[]
|
||||
StoreListingVersions StoreListingVersion[]
|
||||
|
||||
@@id(name: "graphVersionId", [id, version])
|
||||
@@ -814,10 +813,8 @@ model Profile {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
// Only 1 of user or group can be set.
|
||||
// The user this profile belongs to, if any.
|
||||
userId String?
|
||||
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @unique
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
name String
|
||||
username String @unique
|
||||
@@ -830,6 +827,7 @@ model Profile {
|
||||
isFeatured Boolean @default(false)
|
||||
|
||||
LibraryAgents LibraryAgent[]
|
||||
StoreListings StoreListing[]
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
@@ -860,9 +858,9 @@ view Creator {
|
||||
}
|
||||
|
||||
view StoreAgent {
|
||||
listing_id String @id
|
||||
storeListingVersionId String
|
||||
updated_at DateTime
|
||||
listing_id String @id
|
||||
listing_version_id String
|
||||
updated_at DateTime
|
||||
|
||||
slug String
|
||||
agent_name String
|
||||
@@ -879,10 +877,12 @@ view StoreAgent {
|
||||
runs Int
|
||||
rating Float
|
||||
versions String[]
|
||||
agentGraphVersions String[]
|
||||
agentGraphId String
|
||||
graph_id String
|
||||
graph_versions String[]
|
||||
is_available Boolean @default(true)
|
||||
useForOnboarding Boolean @default(false)
|
||||
use_for_onboarding Boolean @default(false)
|
||||
|
||||
recommended_schedule_cron String?
|
||||
|
||||
// Materialized views used (refreshed every 15 minutes via pg_cron):
|
||||
// - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId
|
||||
@@ -896,41 +896,52 @@ view StoreAgent {
|
||||
}
|
||||
|
||||
view StoreSubmission {
|
||||
listing_id String @id
|
||||
user_id String
|
||||
slug String
|
||||
name String
|
||||
sub_heading String
|
||||
description String
|
||||
image_urls String[]
|
||||
date_submitted DateTime
|
||||
status SubmissionStatus
|
||||
runs Int
|
||||
rating Float
|
||||
agent_id String
|
||||
agent_version Int
|
||||
store_listing_version_id String
|
||||
reviewer_id String?
|
||||
review_comments String?
|
||||
internal_comments String?
|
||||
reviewed_at DateTime?
|
||||
changes_summary String?
|
||||
video_url String?
|
||||
categories String[]
|
||||
// From StoreListing:
|
||||
listing_id String
|
||||
user_id String
|
||||
slug String
|
||||
|
||||
// Index or unique are not applied to views
|
||||
// From StoreListingVersion:
|
||||
listing_version_id String @id
|
||||
listing_version Int
|
||||
graph_id String
|
||||
graph_version Int
|
||||
|
||||
name String
|
||||
sub_heading String
|
||||
description String
|
||||
instructions String?
|
||||
categories String[]
|
||||
image_urls String[]
|
||||
video_url String?
|
||||
agent_output_demo_url String?
|
||||
|
||||
submitted_at DateTime?
|
||||
changes_summary String?
|
||||
status SubmissionStatus
|
||||
reviewed_at DateTime?
|
||||
reviewer_id String?
|
||||
review_comments String?
|
||||
internal_comments String?
|
||||
|
||||
is_deleted Boolean
|
||||
|
||||
// Aggregated from AgentGraphExecutions and StoreListingReviews:
|
||||
run_count Int
|
||||
review_count Int
|
||||
review_avg_rating Float
|
||||
}
|
||||
|
||||
// Note: This is actually a MATERIALIZED VIEW in the database
|
||||
// Refreshed automatically every 15 minutes via pg_cron (with fallback to manual refresh)
|
||||
view mv_agent_run_counts {
|
||||
agentGraphId String @unique
|
||||
run_count Int
|
||||
graph_id String @unique
|
||||
run_count Int // excluding runs by the graph's creator
|
||||
|
||||
// Pre-aggregated count of AgentGraphExecution records by agentGraphId
|
||||
// Used by StoreAgent and Creator views for performance optimization
|
||||
// Unique index created automatically on agentGraphId for fast lookups
|
||||
// Refresh uses CONCURRENTLY to avoid blocking reads
|
||||
// Pre-aggregated count of AgentGraphExecution records by agentGraphId.
|
||||
// Used by StoreAgent, Creator, and StoreSubmission views for performance optimization.
|
||||
// - Should have a unique index on graph_id for fast lookups
|
||||
// - Refresh should use CONCURRENTLY to avoid blocking reads
|
||||
}
|
||||
|
||||
// Note: This is actually a MATERIALIZED VIEW in the database
|
||||
@@ -979,22 +990,18 @@ model StoreListing {
|
||||
ActiveVersion StoreListingVersion? @relation("ActiveVersion", fields: [activeVersionId], references: [id])
|
||||
|
||||
// The agent link here is only so we can do lookup on agentId
|
||||
agentGraphId String
|
||||
agentGraphVersion Int
|
||||
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Cascade)
|
||||
agentGraphId String @unique
|
||||
|
||||
owningUserId String
|
||||
OwningUser User @relation(fields: [owningUserId], references: [id])
|
||||
owningUserId String
|
||||
OwningUser User @relation(fields: [owningUserId], references: [id])
|
||||
CreatorProfile Profile @relation(fields: [owningUserId], references: [userId], map: "StoreListing_owner_Profile_fkey", onDelete: Cascade)
|
||||
|
||||
// Relations
|
||||
Versions StoreListingVersion[] @relation("ListingVersions")
|
||||
|
||||
// Unique index on agentId to ensure only one listing per agent, regardless of number of versions the agent has.
|
||||
@@unique([agentGraphId])
|
||||
@@unique([owningUserId, slug])
|
||||
// Used in the view query
|
||||
@@index([isDeleted, hasApprovedVersion])
|
||||
@@index([agentGraphId, agentGraphVersion])
|
||||
}
|
||||
|
||||
model StoreListingVersion {
|
||||
@@ -1089,16 +1096,16 @@ model UnifiedContentEmbedding {
|
||||
// Search data
|
||||
embedding Unsupported("vector(1536)") // pgvector embedding (extension in platform schema)
|
||||
searchableText String // Combined text for search and fallback
|
||||
search Unsupported("tsvector")? @default(dbgenerated("''::tsvector")) // Full-text search (auto-populated by trigger)
|
||||
search Unsupported("tsvector")? @default(dbgenerated("''::tsvector")) // Full-text search (auto-populated by trigger)
|
||||
metadata Json @default("{}") // Content-specific metadata
|
||||
// NO @@index for search - GIN index "UnifiedContentEmbedding_search_idx" created via SQL migration
|
||||
// Prisma may generate DROP INDEX on migrate dev - that's okay, migration recreates it
|
||||
|
||||
@@unique([contentType, contentId, userId], map: "UnifiedContentEmbedding_contentType_contentId_userId_key")
|
||||
@@index([contentType])
|
||||
@@index([userId])
|
||||
@@index([contentType, userId])
|
||||
@@index([embedding], map: "UnifiedContentEmbedding_embedding_idx")
|
||||
// NO @@index for search - GIN index "UnifiedContentEmbedding_search_idx" created via SQL migration
|
||||
// Prisma may generate DROP INDEX on migrate dev - that's okay, migration recreates it
|
||||
}
|
||||
|
||||
model StoreListingReview {
|
||||
@@ -1115,8 +1122,9 @@ model StoreListingReview {
|
||||
score Int
|
||||
comments String?
|
||||
|
||||
// Enforce one review per user per listing version
|
||||
@@unique([storeListingVersionId, reviewByUserId])
|
||||
@@index([reviewByUserId])
|
||||
@@index([storeListingVersionId])
|
||||
}
|
||||
|
||||
enum SubmissionStatus {
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
"1.0.0",
|
||||
"1.1.0"
|
||||
],
|
||||
"agentGraphVersions": [
|
||||
"graph_id": "test-graph-id",
|
||||
"graph_versions": [
|
||||
"1",
|
||||
"2"
|
||||
],
|
||||
"agentGraphId": "test-graph-id",
|
||||
"last_updated": "2023-01-01T00:00:00",
|
||||
"recommended_schedule_cron": null,
|
||||
"active_version_id": null,
|
||||
"has_approved_version": false,
|
||||
"active_version_id": "test-version-id",
|
||||
"has_approved_version": true,
|
||||
"changelog": null
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "Test User",
|
||||
"username": "creator1",
|
||||
"name": "Test User",
|
||||
"description": "Test creator description",
|
||||
"avatar_url": "avatar.jpg",
|
||||
"links": [
|
||||
"link1.com",
|
||||
"link2.com"
|
||||
],
|
||||
"avatar_url": "avatar.jpg",
|
||||
"agent_rating": 4.8,
|
||||
"is_featured": true,
|
||||
"num_agents": 5,
|
||||
"agent_runs": 1000,
|
||||
"agent_rating": 4.8,
|
||||
"top_categories": [
|
||||
"category1",
|
||||
"category2"
|
||||
|
||||
@@ -1,54 +1,94 @@
|
||||
{
|
||||
"creators": [
|
||||
{
|
||||
"name": "Creator 0",
|
||||
"username": "creator0",
|
||||
"name": "Creator 0",
|
||||
"description": "Creator 0 description",
|
||||
"avatar_url": "avatar0.jpg",
|
||||
"links": [
|
||||
"user0.link.com"
|
||||
],
|
||||
"is_featured": false,
|
||||
"num_agents": 1,
|
||||
"agent_rating": 4.5,
|
||||
"agent_runs": 100,
|
||||
"is_featured": false
|
||||
"agent_rating": 4.5,
|
||||
"top_categories": [
|
||||
"cat1",
|
||||
"cat2",
|
||||
"cat3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Creator 1",
|
||||
"username": "creator1",
|
||||
"name": "Creator 1",
|
||||
"description": "Creator 1 description",
|
||||
"avatar_url": "avatar1.jpg",
|
||||
"links": [
|
||||
"user1.link.com"
|
||||
],
|
||||
"is_featured": false,
|
||||
"num_agents": 1,
|
||||
"agent_rating": 4.5,
|
||||
"agent_runs": 100,
|
||||
"is_featured": false
|
||||
"agent_rating": 4.5,
|
||||
"top_categories": [
|
||||
"cat1",
|
||||
"cat2",
|
||||
"cat3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Creator 2",
|
||||
"username": "creator2",
|
||||
"name": "Creator 2",
|
||||
"description": "Creator 2 description",
|
||||
"avatar_url": "avatar2.jpg",
|
||||
"links": [
|
||||
"user2.link.com"
|
||||
],
|
||||
"is_featured": false,
|
||||
"num_agents": 1,
|
||||
"agent_rating": 4.5,
|
||||
"agent_runs": 100,
|
||||
"is_featured": false
|
||||
"agent_rating": 4.5,
|
||||
"top_categories": [
|
||||
"cat1",
|
||||
"cat2",
|
||||
"cat3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Creator 3",
|
||||
"username": "creator3",
|
||||
"name": "Creator 3",
|
||||
"description": "Creator 3 description",
|
||||
"avatar_url": "avatar3.jpg",
|
||||
"links": [
|
||||
"user3.link.com"
|
||||
],
|
||||
"is_featured": false,
|
||||
"num_agents": 1,
|
||||
"agent_rating": 4.5,
|
||||
"agent_runs": 100,
|
||||
"is_featured": false
|
||||
"agent_rating": 4.5,
|
||||
"top_categories": [
|
||||
"cat1",
|
||||
"cat2",
|
||||
"cat3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Creator 4",
|
||||
"username": "creator4",
|
||||
"name": "Creator 4",
|
||||
"description": "Creator 4 description",
|
||||
"avatar_url": "avatar4.jpg",
|
||||
"links": [
|
||||
"user4.link.com"
|
||||
],
|
||||
"is_featured": false,
|
||||
"num_agents": 1,
|
||||
"agent_rating": 4.5,
|
||||
"agent_runs": 100,
|
||||
"is_featured": false
|
||||
"agent_rating": 4.5,
|
||||
"top_categories": [
|
||||
"cat1",
|
||||
"cat2",
|
||||
"cat3"
|
||||
]
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
|
||||
@@ -2,32 +2,33 @@
|
||||
"submissions": [
|
||||
{
|
||||
"listing_id": "test-listing-id",
|
||||
"agent_id": "test-agent-id",
|
||||
"agent_version": 1,
|
||||
"user_id": "test-user-id",
|
||||
"slug": "test-agent",
|
||||
"listing_version_id": "test-version-id",
|
||||
"listing_version": 1,
|
||||
"graph_id": "test-agent-id",
|
||||
"graph_version": 1,
|
||||
"name": "Test Agent",
|
||||
"sub_heading": "Test agent subheading",
|
||||
"slug": "test-agent",
|
||||
"description": "Test agent description",
|
||||
"instructions": null,
|
||||
"instructions": "Click the button!",
|
||||
"categories": [
|
||||
"test-category"
|
||||
],
|
||||
"image_urls": [
|
||||
"test.jpg"
|
||||
],
|
||||
"date_submitted": "2023-01-01T00:00:00",
|
||||
"video_url": "test.mp4",
|
||||
"agent_output_demo_url": "demo_video.mp4",
|
||||
"submitted_at": "2023-01-01T00:00:00",
|
||||
"changes_summary": "Initial Submission",
|
||||
"status": "APPROVED",
|
||||
"runs": 50,
|
||||
"rating": 4.2,
|
||||
"store_listing_version_id": null,
|
||||
"version": null,
|
||||
"reviewed_at": null,
|
||||
"reviewer_id": null,
|
||||
"review_comments": null,
|
||||
"internal_comments": null,
|
||||
"reviewed_at": null,
|
||||
"changes_summary": null,
|
||||
"video_url": "test.mp4",
|
||||
"agent_output_demo_url": null,
|
||||
"categories": [
|
||||
"test-category"
|
||||
]
|
||||
"run_count": 50,
|
||||
"review_count": 5,
|
||||
"review_avg_rating": 4.2
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
|
||||
@@ -128,7 +128,7 @@ class TestDataCreator:
|
||||
email = "test123@gmail.com"
|
||||
else:
|
||||
email = faker.unique.email()
|
||||
password = "testpassword123" # Standard test password
|
||||
password = "testpassword123" # Standard test password # pragma: allowlist secret # noqa
|
||||
user_id = f"test-user-{i}-{faker.uuid4()}"
|
||||
|
||||
# Create user in Supabase Auth (if needed)
|
||||
@@ -571,8 +571,8 @@ class TestDataCreator:
|
||||
if test_user and self.agent_graphs:
|
||||
test_submission_data = {
|
||||
"user_id": test_user["id"],
|
||||
"agent_id": self.agent_graphs[0]["id"],
|
||||
"agent_version": 1,
|
||||
"graph_id": self.agent_graphs[0]["id"],
|
||||
"graph_version": 1,
|
||||
"slug": "test-agent-submission",
|
||||
"name": "Test Agent Submission",
|
||||
"sub_heading": "A test agent for frontend testing",
|
||||
@@ -593,9 +593,9 @@ class TestDataCreator:
|
||||
print("✅ Created special test store submission for test123@gmail.com")
|
||||
|
||||
# ALWAYS approve and feature the test submission
|
||||
if test_submission.store_listing_version_id:
|
||||
if test_submission.listing_version_id:
|
||||
approved_submission = await review_store_submission(
|
||||
store_listing_version_id=test_submission.store_listing_version_id,
|
||||
store_listing_version_id=test_submission.listing_version_id,
|
||||
is_approved=True,
|
||||
external_comments="Test submission approved",
|
||||
internal_comments="Auto-approved test submission",
|
||||
@@ -605,7 +605,7 @@ class TestDataCreator:
|
||||
print("✅ Approved test store submission")
|
||||
|
||||
await prisma.storelistingversion.update(
|
||||
where={"id": test_submission.store_listing_version_id},
|
||||
where={"id": test_submission.listing_version_id},
|
||||
data={"isFeatured": True},
|
||||
)
|
||||
featured_count += 1
|
||||
@@ -640,8 +640,8 @@ class TestDataCreator:
|
||||
|
||||
submission = await create_store_submission(
|
||||
user_id=user["id"],
|
||||
agent_id=graph["id"],
|
||||
agent_version=graph.get("version", 1),
|
||||
graph_id=graph["id"],
|
||||
graph_version=graph.get("version", 1),
|
||||
slug=faker.slug(),
|
||||
name=graph.get("name", faker.sentence(nb_words=3)),
|
||||
sub_heading=faker.sentence(),
|
||||
@@ -654,7 +654,7 @@ class TestDataCreator:
|
||||
submissions.append(submission.model_dump())
|
||||
print(f"✅ Created store submission: {submission.name}")
|
||||
|
||||
if submission.store_listing_version_id:
|
||||
if submission.listing_version_id:
|
||||
# DETERMINISTIC: First N submissions are always approved
|
||||
# First GUARANTEED_FEATURED_AGENTS of those are always featured
|
||||
should_approve = (
|
||||
@@ -667,7 +667,7 @@ class TestDataCreator:
|
||||
try:
|
||||
reviewer_id = random.choice(self.users)["id"]
|
||||
approved_submission = await review_store_submission(
|
||||
store_listing_version_id=submission.store_listing_version_id,
|
||||
store_listing_version_id=submission.listing_version_id,
|
||||
is_approved=True,
|
||||
external_comments="Auto-approved for E2E testing",
|
||||
internal_comments="Automatically approved by E2E test data script",
|
||||
@@ -683,9 +683,7 @@ class TestDataCreator:
|
||||
if should_feature:
|
||||
try:
|
||||
await prisma.storelistingversion.update(
|
||||
where={
|
||||
"id": submission.store_listing_version_id
|
||||
},
|
||||
where={"id": submission.listing_version_id},
|
||||
data={"isFeatured": True},
|
||||
)
|
||||
featured_count += 1
|
||||
@@ -699,9 +697,7 @@ class TestDataCreator:
|
||||
elif random.random() < 0.2:
|
||||
try:
|
||||
await prisma.storelistingversion.update(
|
||||
where={
|
||||
"id": submission.store_listing_version_id
|
||||
},
|
||||
where={"id": submission.listing_version_id},
|
||||
data={"isFeatured": True},
|
||||
)
|
||||
featured_count += 1
|
||||
@@ -721,7 +717,7 @@ class TestDataCreator:
|
||||
try:
|
||||
reviewer_id = random.choice(self.users)["id"]
|
||||
await review_store_submission(
|
||||
store_listing_version_id=submission.store_listing_version_id,
|
||||
store_listing_version_id=submission.listing_version_id,
|
||||
is_approved=False,
|
||||
external_comments="Submission rejected - needs improvements",
|
||||
internal_comments="Automatically rejected by E2E test data script",
|
||||
|
||||
@@ -394,7 +394,6 @@ async def main():
|
||||
listing = await db.storelisting.create(
|
||||
data={
|
||||
"agentGraphId": graph.id,
|
||||
"agentGraphVersion": graph.version,
|
||||
"owningUserId": user.id,
|
||||
"hasApprovedVersion": random.choice([True, False]),
|
||||
"slug": slug,
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import BackendApi from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
StoreListingsWithVersionsResponse,
|
||||
SubmissionStatus,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
getV2GetAdminListingsHistory,
|
||||
postV2ReviewStoreSubmission,
|
||||
getV2AdminDownloadAgentFile,
|
||||
} from "@/app/api/__generated__/endpoints/admin/admin";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
|
||||
|
||||
export async function approveAgent(formData: FormData) {
|
||||
const data = {
|
||||
store_listing_version_id: formData.get("id") as string,
|
||||
const storeListingVersionId = formData.get("id") as string;
|
||||
const comments = formData.get("comments") as string;
|
||||
|
||||
await postV2ReviewStoreSubmission(storeListingVersionId, {
|
||||
store_listing_version_id: storeListingVersionId,
|
||||
is_approved: true,
|
||||
comments: formData.get("comments") as string,
|
||||
};
|
||||
const api = new BackendApi();
|
||||
await api.reviewSubmissionAdmin(data.store_listing_version_id, data);
|
||||
comments,
|
||||
});
|
||||
|
||||
revalidatePath("/admin/marketplace");
|
||||
}
|
||||
|
||||
export async function rejectAgent(formData: FormData) {
|
||||
const data = {
|
||||
store_listing_version_id: formData.get("id") as string,
|
||||
const storeListingVersionId = formData.get("id") as string;
|
||||
const comments = formData.get("comments") as string;
|
||||
const internal_comments =
|
||||
(formData.get("internal_comments") as string) || undefined;
|
||||
|
||||
await postV2ReviewStoreSubmission(storeListingVersionId, {
|
||||
store_listing_version_id: storeListingVersionId,
|
||||
is_approved: false,
|
||||
comments: formData.get("comments") as string,
|
||||
internal_comments: formData.get("internal_comments") as string,
|
||||
};
|
||||
const api = new BackendApi();
|
||||
await api.reviewSubmissionAdmin(data.store_listing_version_id, data);
|
||||
comments,
|
||||
internal_comments,
|
||||
});
|
||||
|
||||
revalidatePath("/admin/marketplace");
|
||||
}
|
||||
@@ -37,26 +43,18 @@ export async function getAdminListingsWithVersions(
|
||||
search?: string,
|
||||
page: number = 1,
|
||||
pageSize: number = 20,
|
||||
): Promise<StoreListingsWithVersionsResponse> {
|
||||
const data: Record<string, any> = {
|
||||
) {
|
||||
const response = await getV2GetAdminListingsHistory({
|
||||
status,
|
||||
search,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
};
|
||||
});
|
||||
|
||||
if (status) {
|
||||
data.status = status;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
data.search = search;
|
||||
}
|
||||
const api = new BackendApi();
|
||||
const response = await api.getAdminListingsWithVersions(data);
|
||||
return response;
|
||||
return okData(response);
|
||||
}
|
||||
|
||||
export async function downloadAsAdmin(storeListingVersion: string) {
|
||||
const api = new BackendApi();
|
||||
const file = await api.downloadStoreAgentAdmin(storeListingVersion);
|
||||
return file;
|
||||
const response = await getV2AdminDownloadAgentFile(storeListingVersion);
|
||||
return okData(response);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/__legacy__/ui/table";
|
||||
import {
|
||||
StoreSubmission,
|
||||
SubmissionStatus,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import type { StoreSubmissionAdminView } from "@/app/api/__generated__/models/storeSubmissionAdminView";
|
||||
import type { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
|
||||
import { PaginationControls } from "../../../../../components/__legacy__/ui/pagination-controls";
|
||||
import { getAdminListingsWithVersions } from "@/app/(platform)/admin/marketplace/actions";
|
||||
import { ExpandableRow } from "./ExpandleRow";
|
||||
@@ -17,12 +15,14 @@ import { SearchAndFilterAdminMarketplace } from "./SearchFilterForm";
|
||||
|
||||
// Helper function to get the latest version by version number
|
||||
const getLatestVersionByNumber = (
|
||||
versions: StoreSubmission[],
|
||||
): StoreSubmission | null => {
|
||||
versions: StoreSubmissionAdminView[] | undefined,
|
||||
): StoreSubmissionAdminView | null => {
|
||||
if (!versions || versions.length === 0) return null;
|
||||
return versions.reduce(
|
||||
(latest, current) =>
|
||||
(current.version ?? 0) > (latest.version ?? 1) ? current : latest,
|
||||
(current.listing_version ?? 0) > (latest.listing_version ?? 1)
|
||||
? current
|
||||
: latest,
|
||||
versions[0],
|
||||
);
|
||||
};
|
||||
@@ -37,12 +37,14 @@ export async function AdminAgentsDataTable({
|
||||
initialSearch?: string;
|
||||
}) {
|
||||
// Server-side data fetching
|
||||
const { listings, pagination } = await getAdminListingsWithVersions(
|
||||
const data = await getAdminListingsWithVersions(
|
||||
initialStatus,
|
||||
initialSearch,
|
||||
initialPage,
|
||||
10,
|
||||
);
|
||||
const listings = data?.listings ?? [];
|
||||
const pagination = data?.pagination;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -92,7 +94,7 @@ export async function AdminAgentsDataTable({
|
||||
|
||||
<PaginationControls
|
||||
currentPage={initialPage}
|
||||
totalPages={pagination.total_pages}
|
||||
totalPages={pagination?.total_pages ?? 1}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/components/__legacy__/ui/dialog";
|
||||
import { Label } from "@/components/__legacy__/ui/label";
|
||||
import { Textarea } from "@/components/__legacy__/ui/textarea";
|
||||
import type { StoreSubmission } from "@/lib/autogpt-server-api/types";
|
||||
import type { StoreSubmissionAdminView } from "@/app/api/__generated__/models/storeSubmissionAdminView";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
approveAgent,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
export function ApproveRejectButtons({
|
||||
version,
|
||||
}: {
|
||||
version: StoreSubmission;
|
||||
version: StoreSubmissionAdminView;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [isApproveDialogOpen, setIsApproveDialogOpen] = useState(false);
|
||||
@@ -95,7 +95,7 @@ export function ApproveRejectButtons({
|
||||
<input
|
||||
type="hidden"
|
||||
name="id"
|
||||
value={version.store_listing_version_id || ""}
|
||||
value={version.listing_version_id || ""}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
@@ -142,7 +142,7 @@ export function ApproveRejectButtons({
|
||||
<input
|
||||
type="hidden"
|
||||
name="id"
|
||||
value={version.store_listing_version_id || ""}
|
||||
value={version.listing_version_id || ""}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
|
||||
@@ -12,11 +12,9 @@ import {
|
||||
import { Badge } from "@/components/__legacy__/ui/badge";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
type StoreListingWithVersions,
|
||||
type StoreSubmission,
|
||||
SubmissionStatus,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import type { StoreListingWithVersionsAdminView } from "@/app/api/__generated__/models/storeListingWithVersionsAdminView";
|
||||
import type { StoreSubmissionAdminView } from "@/app/api/__generated__/models/storeSubmissionAdminView";
|
||||
import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
|
||||
import { ApproveRejectButtons } from "./ApproveRejectButton";
|
||||
import { DownloadAgentAdminButton } from "./DownloadAgentButton";
|
||||
|
||||
@@ -38,8 +36,8 @@ export function ExpandableRow({
|
||||
listing,
|
||||
latestVersion,
|
||||
}: {
|
||||
listing: StoreListingWithVersions;
|
||||
latestVersion: StoreSubmission | null;
|
||||
listing: StoreListingWithVersionsAdminView;
|
||||
latestVersion: StoreSubmissionAdminView | null;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
@@ -69,17 +67,17 @@ export function ExpandableRow({
|
||||
{latestVersion?.status && getStatusBadge(latestVersion.status)}
|
||||
</TableCell>
|
||||
<TableCell onClick={() => setExpanded(!expanded)}>
|
||||
{latestVersion?.date_submitted
|
||||
? formatDistanceToNow(new Date(latestVersion.date_submitted), {
|
||||
{latestVersion?.submitted_at
|
||||
? formatDistanceToNow(new Date(latestVersion.submitted_at), {
|
||||
addSuffix: true,
|
||||
})
|
||||
: "Unknown"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
{latestVersion?.store_listing_version_id && (
|
||||
{latestVersion?.listing_version_id && (
|
||||
<DownloadAgentAdminButton
|
||||
storeListingVersionId={latestVersion.store_listing_version_id}
|
||||
storeListingVersionId={latestVersion.listing_version_id}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -115,14 +113,17 @@ export function ExpandableRow({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{listing.versions
|
||||
.sort((a, b) => (b.version ?? 1) - (a.version ?? 0))
|
||||
{(listing.versions ?? [])
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(b.listing_version ?? 1) - (a.listing_version ?? 0),
|
||||
)
|
||||
.map((version) => (
|
||||
<TableRow key={version.store_listing_version_id}>
|
||||
<TableRow key={version.listing_version_id}>
|
||||
<TableCell>
|
||||
v{version.version || "?"}
|
||||
{version.store_listing_version_id ===
|
||||
listing.active_version_id && (
|
||||
v{version.listing_version || "?"}
|
||||
{version.listing_version_id ===
|
||||
listing.active_listing_version_id && (
|
||||
<Badge className="ml-2 bg-blue-500">Active</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
@@ -131,9 +132,9 @@ export function ExpandableRow({
|
||||
{version.changes_summary || "No summary"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{version.date_submitted
|
||||
{version.submitted_at
|
||||
? formatDistanceToNow(
|
||||
new Date(version.date_submitted),
|
||||
new Date(version.submitted_at),
|
||||
{ addSuffix: true },
|
||||
)
|
||||
: "Unknown"}
|
||||
@@ -182,10 +183,10 @@ export function ExpandableRow({
|
||||
{/* <TableCell>{version.categories.join(", ")}</TableCell> */}
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
{version.store_listing_version_id && (
|
||||
{version.listing_version_id && (
|
||||
<DownloadAgentAdminButton
|
||||
storeListingVersionId={
|
||||
version.store_listing_version_id
|
||||
version.listing_version_id
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/__legacy__/ui/select";
|
||||
import { SubmissionStatus } from "@/lib/autogpt-server-api/types";
|
||||
import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
|
||||
|
||||
export function SearchAndFilterAdminMarketplace({
|
||||
initialSearch,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { withRoleAccess } from "@/lib/withRoleAccess";
|
||||
import { Suspense } from "react";
|
||||
import type { SubmissionStatus } from "@/lib/autogpt-server-api/types";
|
||||
import type { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
|
||||
import { AdminAgentsDataTable } from "./components/AdminAgentsDataTable";
|
||||
|
||||
type MarketplaceAdminPageSearchParams = {
|
||||
page?: string;
|
||||
status?: string;
|
||||
status?: SubmissionStatus;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ async function AdminMarketplaceDashboard({
|
||||
searchParams: MarketplaceAdminPageSearchParams;
|
||||
}) {
|
||||
const page = searchParams.page ? Number.parseInt(searchParams.page) : 1;
|
||||
const status = searchParams.status as SubmissionStatus | undefined;
|
||||
const status = searchParams.status;
|
||||
const search = searchParams.search;
|
||||
|
||||
return (
|
||||
|
||||
@@ -42,8 +42,8 @@ export function AgentVersionChangelog({
|
||||
|
||||
// Create version info from available graph versions
|
||||
const storeData = okData(storeAgentData) as StoreAgentDetails | undefined;
|
||||
const agentVersions: VersionInfo[] = storeData?.agentGraphVersions
|
||||
? storeData.agentGraphVersions
|
||||
const agentVersions: VersionInfo[] = storeData?.graph_versions
|
||||
? storeData.graph_versions
|
||||
.map((versionStr: string) => parseInt(versionStr, 10))
|
||||
.sort((a: number, b: number) => b - a) // Sort descending (newest first)
|
||||
.map((version: number) => ({
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { postV1UploadFileToCloudStorage } from "@/app/api/__generated__/endpoints/files/files";
|
||||
import { resolveResponse } from "@/app/api/helpers";
|
||||
import { useState } from "react";
|
||||
|
||||
export function useRunAgentInputs() {
|
||||
const api = new BackendAPI();
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
|
||||
async function handleUploadFile(file: File) {
|
||||
const result = await api.uploadFile(file, "gcs", 24, (progress) =>
|
||||
setUploadProgress(progress),
|
||||
setUploadProgress(0);
|
||||
const result = await resolveResponse(
|
||||
postV1UploadFileToCloudStorage({ file }, { expiration_hours: 24 }),
|
||||
);
|
||||
setUploadProgress(100);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,14 +95,14 @@ export function useMarketplaceUpdate({ agent }: UseMarketplaceUpdateProps) {
|
||||
const submissionsResponse = okData(submissionsData) as any;
|
||||
const agentSubmissions =
|
||||
submissionsResponse?.submissions?.filter(
|
||||
(submission: StoreSubmission) => submission.agent_id === agent.graph_id,
|
||||
(submission: StoreSubmission) => submission.graph_id === agent.graph_id,
|
||||
) || [];
|
||||
|
||||
const highestSubmittedVersion =
|
||||
agentSubmissions.length > 0
|
||||
? Math.max(
|
||||
...agentSubmissions.map(
|
||||
(submission: StoreSubmission) => submission.agent_version,
|
||||
(submission: StoreSubmission) => submission.graph_version,
|
||||
),
|
||||
)
|
||||
: 0;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { backgroundColor } from "./helper";
|
||||
|
||||
interface CreatorCardProps {
|
||||
creatorName: string;
|
||||
creatorImage: string;
|
||||
creatorImage: string | null;
|
||||
bio: string;
|
||||
agentsUploaded: number;
|
||||
onClick: () => void;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { StarRatingIcons } from "@/components/__legacy__/ui/icons";
|
||||
interface CreatorInfoCardProps {
|
||||
username: string;
|
||||
handle: string;
|
||||
avatarSrc: string;
|
||||
avatarSrc: string | null;
|
||||
categories: string[];
|
||||
averageRating: number;
|
||||
totalRuns: number;
|
||||
@@ -29,12 +29,14 @@ export const CreatorInfoCard = ({
|
||||
>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-3.5 sm:h-[218px]">
|
||||
<Avatar className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]">
|
||||
<AvatarImage
|
||||
width={130}
|
||||
height={130}
|
||||
src={avatarSrc}
|
||||
alt={`${username}'s avatar`}
|
||||
/>
|
||||
{avatarSrc && (
|
||||
<AvatarImage
|
||||
width={130}
|
||||
height={130}
|
||||
src={avatarSrc}
|
||||
alt={`${username}'s avatar`}
|
||||
/>
|
||||
)}
|
||||
<AvatarFallback
|
||||
size={130}
|
||||
className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]"
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { CreatorCard } from "../CreatorCard/CreatorCard";
|
||||
import { useFeaturedCreators } from "./useFeaturedCreators";
|
||||
import { Creator } from "@/app/api/__generated__/models/creator";
|
||||
import { CreatorDetails } from "@/app/api/__generated__/models/creatorDetails";
|
||||
|
||||
interface FeaturedCreatorsProps {
|
||||
title?: string;
|
||||
featuredCreators: Creator[];
|
||||
featuredCreators: CreatorDetails[];
|
||||
}
|
||||
|
||||
export const FeaturedCreators = ({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Creator } from "@/app/api/__generated__/models/creator";
|
||||
import { CreatorDetails } from "@/app/api/__generated__/models/creatorDetails";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface useFeaturedCreatorsProps {
|
||||
featuredCreators: Creator[];
|
||||
featuredCreators: CreatorDetails[];
|
||||
}
|
||||
|
||||
export const useFeaturedCreators = ({
|
||||
|
||||
@@ -3,33 +3,23 @@
|
||||
import * as React from "react";
|
||||
import { AgentTableCard } from "../AgentTableCard/AgentTableCard";
|
||||
import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
|
||||
import {
|
||||
AgentTableRow,
|
||||
AgentTableRowProps,
|
||||
} from "../AgentTableRow/AgentTableRow";
|
||||
import { AgentTableRow } from "../AgentTableRow/AgentTableRow";
|
||||
import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest";
|
||||
|
||||
export interface AgentTableProps {
|
||||
agents: Omit<
|
||||
AgentTableRowProps,
|
||||
| "setSelectedAgents"
|
||||
| "selectedAgents"
|
||||
| "onViewSubmission"
|
||||
| "onDeleteSubmission"
|
||||
| "onEditSubmission"
|
||||
>[];
|
||||
storeAgentSubmissions: StoreSubmission[];
|
||||
onViewSubmission: (submission: StoreSubmission) => void;
|
||||
onDeleteSubmission: (submission_id: string) => void;
|
||||
onEditSubmission: (
|
||||
submission: StoreSubmissionEditRequest & {
|
||||
store_listing_version_id: string | undefined;
|
||||
agent_id: string;
|
||||
graph_id: string;
|
||||
},
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const AgentTable: React.FC<AgentTableProps> = ({
|
||||
agents,
|
||||
storeAgentSubmissions,
|
||||
onViewSubmission,
|
||||
onDeleteSubmission,
|
||||
onEditSubmission,
|
||||
@@ -63,19 +53,19 @@ export const AgentTable: React.FC<AgentTableProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Table body */}
|
||||
{agents.length > 0 ? (
|
||||
{storeAgentSubmissions.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
{agents.map((agent) => (
|
||||
<div key={agent.id} className="md:block">
|
||||
{storeAgentSubmissions.map((agentSubmission) => (
|
||||
<div key={agentSubmission.listing_version_id} className="md:block">
|
||||
<AgentTableRow
|
||||
{...agent}
|
||||
storeAgentSubmission={agentSubmission}
|
||||
onViewSubmission={onViewSubmission}
|
||||
onDeleteSubmission={onDeleteSubmission}
|
||||
onEditSubmission={onEditSubmission}
|
||||
/>
|
||||
<div className="block md:hidden">
|
||||
<AgentTableCard
|
||||
{...agent}
|
||||
storeAgentSubmission={agentSubmission}
|
||||
onViewSubmission={onViewSubmission}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,62 +3,38 @@
|
||||
import Image from "next/image";
|
||||
import { IconStarFilled, IconMore } from "@/components/__legacy__/ui/icons";
|
||||
import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
|
||||
import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
|
||||
import { Status } from "@/components/__legacy__/Status";
|
||||
|
||||
export interface AgentTableCardProps {
|
||||
agent_id: string;
|
||||
agent_version: number;
|
||||
agentName: string;
|
||||
sub_heading: string;
|
||||
description: string;
|
||||
imageSrc: string[];
|
||||
dateSubmitted: Date;
|
||||
status: SubmissionStatus;
|
||||
runs: number;
|
||||
rating: number;
|
||||
id: number;
|
||||
listing_id?: string;
|
||||
storeAgentSubmission: StoreSubmission;
|
||||
onViewSubmission: (submission: StoreSubmission) => void;
|
||||
}
|
||||
|
||||
export const AgentTableCard = ({
|
||||
agent_id,
|
||||
agent_version,
|
||||
agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
imageSrc,
|
||||
dateSubmitted,
|
||||
status,
|
||||
runs,
|
||||
rating,
|
||||
listing_id,
|
||||
storeAgentSubmission,
|
||||
onViewSubmission,
|
||||
}: AgentTableCardProps) => {
|
||||
const onView = () => {
|
||||
onViewSubmission({
|
||||
listing_id: listing_id || "",
|
||||
agent_id,
|
||||
agent_version,
|
||||
slug: "",
|
||||
name: agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
image_urls: imageSrc,
|
||||
date_submitted: dateSubmitted,
|
||||
status: status,
|
||||
runs,
|
||||
rating,
|
||||
});
|
||||
onViewSubmission(storeAgentSubmission);
|
||||
};
|
||||
|
||||
const {
|
||||
graph_version,
|
||||
name: agentName,
|
||||
description,
|
||||
image_urls,
|
||||
submitted_at,
|
||||
status,
|
||||
run_count,
|
||||
review_avg_rating: rating,
|
||||
} = storeAgentSubmission;
|
||||
|
||||
return (
|
||||
<div className="border-b border-neutral-300 p-4 dark:border-neutral-700">
|
||||
<div className="flex gap-4">
|
||||
<div className="relative aspect-video w-24 shrink-0 overflow-hidden rounded-lg bg-[#d9d9d9] dark:bg-neutral-800">
|
||||
<Image
|
||||
src={imageSrc?.[0] ?? "/nada.png"}
|
||||
src={image_urls?.[0] ?? "/nada.png"}
|
||||
alt={agentName}
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
@@ -70,7 +46,7 @@ export const AgentTableCard = ({
|
||||
{agentName}
|
||||
</h3>
|
||||
<span className="text-[13px] text-neutral-500 dark:text-neutral-400">
|
||||
v{agent_version}
|
||||
v{graph_version}
|
||||
</span>
|
||||
</div>
|
||||
<p className="line-clamp-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
@@ -88,14 +64,14 @@ export const AgentTableCard = ({
|
||||
<div className="mt-4 flex flex-wrap gap-4">
|
||||
<Status status={status} />
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{dateSubmitted.toLocaleDateString()}
|
||||
{submitted_at && submitted_at.toLocaleDateString()}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{runs.toLocaleString()} runs
|
||||
{(run_count ?? 0).toLocaleString()} runs
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
{rating.toFixed(1)}
|
||||
{(rating ?? 0).toFixed(1)}
|
||||
</span>
|
||||
<IconStarFilled className="h-4 w-4 text-neutral-800 dark:text-neutral-200" />
|
||||
</div>
|
||||
|
||||
@@ -18,92 +18,60 @@ import {
|
||||
import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
|
||||
import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest";
|
||||
|
||||
export interface AgentTableRowProps {
|
||||
agent_id: string;
|
||||
agent_version: number;
|
||||
agentName: string;
|
||||
sub_heading: string;
|
||||
description: string;
|
||||
imageSrc: string[];
|
||||
dateSubmitted: Date;
|
||||
status: SubmissionStatus;
|
||||
runs: number;
|
||||
rating: number;
|
||||
id: number;
|
||||
video_url?: string;
|
||||
categories?: string[];
|
||||
store_listing_version_id?: string;
|
||||
changes_summary?: string;
|
||||
listing_id?: string;
|
||||
export type AgentTableRowProps = {
|
||||
storeAgentSubmission: StoreSubmission;
|
||||
onViewSubmission: (submission: StoreSubmission) => void;
|
||||
onDeleteSubmission: (submission_id: string) => void;
|
||||
onEditSubmission: (
|
||||
submission: StoreSubmissionEditRequest & {
|
||||
store_listing_version_id: string | undefined;
|
||||
agent_id: string;
|
||||
graph_id: string;
|
||||
},
|
||||
) => void;
|
||||
}
|
||||
};
|
||||
|
||||
export const AgentTableRow = ({
|
||||
agent_id,
|
||||
agent_version,
|
||||
agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
imageSrc,
|
||||
dateSubmitted,
|
||||
status,
|
||||
runs,
|
||||
rating,
|
||||
id,
|
||||
video_url,
|
||||
categories,
|
||||
store_listing_version_id,
|
||||
changes_summary,
|
||||
listing_id,
|
||||
storeAgentSubmission,
|
||||
onViewSubmission,
|
||||
onDeleteSubmission,
|
||||
onEditSubmission,
|
||||
}: AgentTableRowProps) => {
|
||||
const { handleView, handleDelete, handleEdit } = useAgentTableRow({
|
||||
id,
|
||||
storeAgentSubmission,
|
||||
onViewSubmission,
|
||||
onDeleteSubmission,
|
||||
onEditSubmission,
|
||||
agent_id,
|
||||
agent_version,
|
||||
agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
imageSrc,
|
||||
dateSubmitted,
|
||||
status,
|
||||
runs,
|
||||
rating,
|
||||
video_url,
|
||||
categories,
|
||||
store_listing_version_id,
|
||||
changes_summary,
|
||||
listing_id,
|
||||
});
|
||||
|
||||
const {
|
||||
listing_version_id,
|
||||
graph_id,
|
||||
graph_version,
|
||||
name: agentName,
|
||||
description,
|
||||
image_urls,
|
||||
submitted_at,
|
||||
status,
|
||||
run_count,
|
||||
review_avg_rating,
|
||||
} = storeAgentSubmission;
|
||||
|
||||
const canModify = status === SubmissionStatus.PENDING;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="agent-table-row"
|
||||
data-agent-id={agent_id}
|
||||
data-submission-id={store_listing_version_id}
|
||||
data-agent-id={graph_id}
|
||||
data-submission-id={listing_version_id}
|
||||
className="hidden items-center border-b border-neutral-300 px-4 py-4 hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800 md:flex"
|
||||
>
|
||||
<div className="grid w-full grid-cols-[minmax(400px,1fr),180px,140px,100px,100px,40px] items-center gap-4">
|
||||
{/* Agent info column */}
|
||||
<div className="flex items-center gap-4">
|
||||
{imageSrc?.[0] ? (
|
||||
{image_urls?.[0] ? (
|
||||
<div className="relative aspect-video w-32 shrink-0 overflow-hidden rounded-[10px] bg-zinc-100">
|
||||
<Image
|
||||
src={imageSrc?.[0] ?? ""}
|
||||
src={image_urls?.[0] ?? ""}
|
||||
alt={agentName}
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
@@ -128,7 +96,7 @@ export const AgentTableRow = ({
|
||||
size="small"
|
||||
className="text-neutral-500 dark:text-neutral-400"
|
||||
>
|
||||
v{agent_version}
|
||||
v{graph_version}
|
||||
</Text>
|
||||
</div>
|
||||
<Text
|
||||
@@ -142,7 +110,7 @@ export const AgentTableRow = ({
|
||||
|
||||
{/* Date column */}
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{dateSubmitted.toLocaleDateString()}
|
||||
{submitted_at && submitted_at.toLocaleDateString()}
|
||||
</div>
|
||||
|
||||
{/* Status column */}
|
||||
@@ -152,14 +120,16 @@ export const AgentTableRow = ({
|
||||
|
||||
{/* Runs column */}
|
||||
<div className="text-right text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{runs?.toLocaleString() ?? "0"}
|
||||
{run_count?.toLocaleString() ?? "0"}
|
||||
</div>
|
||||
|
||||
{/* Reviews column */}
|
||||
<div className="text-right">
|
||||
{rating ? (
|
||||
{review_avg_rating ? (
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<span className="text-sm font-medium">{rating.toFixed(1)}</span>
|
||||
<span className="text-sm font-medium">
|
||||
{review_avg_rating.toFixed(1)}
|
||||
</span>
|
||||
<StarIcon weight="fill" className="h-2 w-2" />
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,97 +1,45 @@
|
||||
import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
|
||||
import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest";
|
||||
import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
|
||||
|
||||
interface useAgentTableRowProps {
|
||||
id: number;
|
||||
storeAgentSubmission: StoreSubmission;
|
||||
onViewSubmission: (submission: StoreSubmission) => void;
|
||||
onDeleteSubmission: (submission_id: string) => void;
|
||||
onEditSubmission: (
|
||||
submission: StoreSubmissionEditRequest & {
|
||||
store_listing_version_id: string | undefined;
|
||||
agent_id: string;
|
||||
graph_id: string;
|
||||
},
|
||||
) => void;
|
||||
agent_id: string;
|
||||
agent_version: number;
|
||||
agentName: string;
|
||||
sub_heading: string;
|
||||
description: string;
|
||||
imageSrc: string[];
|
||||
dateSubmitted: Date;
|
||||
status: SubmissionStatus;
|
||||
runs: number;
|
||||
rating: number;
|
||||
video_url?: string;
|
||||
categories?: string[];
|
||||
store_listing_version_id?: string;
|
||||
changes_summary?: string;
|
||||
listing_id?: string;
|
||||
}
|
||||
|
||||
export const useAgentTableRow = ({
|
||||
storeAgentSubmission,
|
||||
onViewSubmission,
|
||||
onDeleteSubmission,
|
||||
onEditSubmission,
|
||||
agent_id,
|
||||
agent_version,
|
||||
agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
imageSrc,
|
||||
dateSubmitted,
|
||||
status,
|
||||
runs,
|
||||
rating,
|
||||
video_url,
|
||||
categories,
|
||||
store_listing_version_id,
|
||||
changes_summary,
|
||||
listing_id,
|
||||
}: useAgentTableRowProps) => {
|
||||
const handleView = () => {
|
||||
onViewSubmission({
|
||||
listing_id: listing_id || "",
|
||||
agent_id,
|
||||
agent_version,
|
||||
slug: "",
|
||||
name: agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
image_urls: imageSrc,
|
||||
date_submitted: dateSubmitted,
|
||||
status: status,
|
||||
runs,
|
||||
rating,
|
||||
video_url,
|
||||
categories,
|
||||
store_listing_version_id,
|
||||
} satisfies StoreSubmission);
|
||||
onViewSubmission(storeAgentSubmission);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
onEditSubmission({
|
||||
name: agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
image_urls: imageSrc,
|
||||
video_url,
|
||||
categories,
|
||||
changes_summary: changes_summary || "Update Submission",
|
||||
store_listing_version_id,
|
||||
agent_id,
|
||||
name: storeAgentSubmission.name,
|
||||
sub_heading: storeAgentSubmission.sub_heading,
|
||||
description: storeAgentSubmission.description,
|
||||
image_urls: storeAgentSubmission.image_urls,
|
||||
video_url: storeAgentSubmission.video_url,
|
||||
categories: storeAgentSubmission.categories,
|
||||
changes_summary:
|
||||
storeAgentSubmission.changes_summary || "Update Submission",
|
||||
store_listing_version_id: storeAgentSubmission.listing_version_id,
|
||||
graph_id: storeAgentSubmission.graph_id,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
// Backend only accepts StoreListingVersion IDs for deletion
|
||||
if (!store_listing_version_id) {
|
||||
console.error(
|
||||
"Cannot delete submission: store_listing_version_id is required",
|
||||
);
|
||||
return;
|
||||
}
|
||||
onDeleteSubmission(store_listing_version_id);
|
||||
onDeleteSubmission(storeAgentSubmission.listing_version_id);
|
||||
};
|
||||
|
||||
return { handleView, handleDelete, handleEdit };
|
||||
|
||||
@@ -81,26 +81,7 @@ export const MainDashboardPage = () => {
|
||||
<SubmissionsLoading />
|
||||
) : submissions && submissions.submissions.length > 0 ? (
|
||||
<AgentTable
|
||||
agents={submissions.submissions.map((submission, index) => ({
|
||||
id: index,
|
||||
agent_id: submission.agent_id,
|
||||
agent_version: submission.agent_version,
|
||||
sub_heading: submission.sub_heading,
|
||||
agentName: submission.name,
|
||||
description: submission.description,
|
||||
imageSrc: submission.image_urls || [""],
|
||||
dateSubmitted: submission.date_submitted,
|
||||
status: submission.status,
|
||||
runs: submission.runs,
|
||||
rating: submission.rating,
|
||||
video_url: submission.video_url || undefined,
|
||||
categories: submission.categories,
|
||||
slug: submission.slug,
|
||||
store_listing_version_id:
|
||||
submission.store_listing_version_id || undefined,
|
||||
changes_summary: submission.changes_summary || undefined,
|
||||
listing_id: submission.listing_id,
|
||||
}))}
|
||||
storeAgentSubmissions={submissions.submissions}
|
||||
onViewSubmission={onViewSubmission}
|
||||
onDeleteSubmission={onDeleteSubmission}
|
||||
onEditSubmission={onEditSubmission}
|
||||
|
||||
@@ -24,7 +24,7 @@ type EditState = {
|
||||
submission:
|
||||
| (StoreSubmissionEditRequest & {
|
||||
store_listing_version_id: string | undefined;
|
||||
agent_id: string;
|
||||
graph_id: string;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
@@ -79,7 +79,7 @@ export const useMainDashboardPage = () => {
|
||||
const onEditSubmission = (
|
||||
submission: StoreSubmissionEditRequest & {
|
||||
store_listing_version_id: string | undefined;
|
||||
agent_id: string;
|
||||
graph_id: string;
|
||||
},
|
||||
) => {
|
||||
setEditState({
|
||||
@@ -90,7 +90,7 @@ export const useMainDashboardPage = () => {
|
||||
|
||||
const onEditSuccess = async (submission: StoreSubmission) => {
|
||||
try {
|
||||
if (!submission.store_listing_version_id) {
|
||||
if (!submission.listing_version_id) {
|
||||
Sentry.captureException(
|
||||
new Error("No store listing version ID found for submission"),
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/
|
||||
import { ProfileInfoForm } from "@/components/__legacy__/ProfileInfoForm";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers";
|
||||
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
|
||||
import type { ProfileDetails } from "@/app/api/__generated__/models/profileDetails";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { ProfileLoading } from "./ProfileLoading";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/__legacy__/ui/card";
|
||||
import { useState } from "react";
|
||||
import { StoreAgent } from "@/lib/autogpt-server-api";
|
||||
import type { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
||||
|
||||
interface FeaturedStoreCardProps {
|
||||
agent: StoreAgent;
|
||||
|
||||
@@ -6,14 +6,15 @@ import Image from "next/image";
|
||||
|
||||
import { IconPersonFill } from "@/components/__legacy__/ui/icons";
|
||||
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
|
||||
import { postV2UpdateUserProfile } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { postV2UploadSubmissionMedia } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { resolveResponse } from "@/app/api/helpers";
|
||||
import type { ProfileDetails } from "@/app/api/__generated__/models/profileDetails";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [profileData, setProfileData] = useState<ProfileDetails>(profile);
|
||||
const api = useBackendAPI();
|
||||
|
||||
async function submitForm() {
|
||||
try {
|
||||
@@ -28,8 +29,10 @@ export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) {
|
||||
};
|
||||
|
||||
if (!isSubmitting) {
|
||||
const returnedProfile = await api.updateStoreProfile(updatedProfile);
|
||||
setProfileData(returnedProfile);
|
||||
const returnedProfile = await resolveResponse(
|
||||
postV2UpdateUserProfile(updatedProfile),
|
||||
);
|
||||
if (returnedProfile) setProfileData(returnedProfile);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating profile:", error);
|
||||
@@ -40,15 +43,20 @@ export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) {
|
||||
|
||||
async function handleImageUpload(file: File) {
|
||||
try {
|
||||
const mediaUrl = await api.uploadStoreSubmissionMedia(file);
|
||||
const mediaRes = await resolveResponse(
|
||||
postV2UploadSubmissionMedia({ file }),
|
||||
);
|
||||
const mediaUrl = String(mediaRes ?? "");
|
||||
|
||||
const updatedProfile = {
|
||||
...profileData,
|
||||
avatar_url: mediaUrl,
|
||||
};
|
||||
|
||||
const returnedProfile = await api.updateStoreProfile(updatedProfile);
|
||||
setProfileData(returnedProfile);
|
||||
const returnedProfile = await resolveResponse(
|
||||
postV2UpdateUserProfile(updatedProfile),
|
||||
);
|
||||
if (returnedProfile) setProfileData(returnedProfile);
|
||||
} catch (error) {
|
||||
console.error("Error uploading image:", error);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from "react";
|
||||
import { Cross1Icon } from "@radix-ui/react-icons";
|
||||
import { IconStar, IconStarFilled } from "@/components/__legacy__/ui/icons";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { postV2CreateAgentReview } from "@/app/api/__generated__/endpoints/store/store";
|
||||
|
||||
interface RatingCardProps {
|
||||
agentName: string;
|
||||
@@ -17,7 +17,6 @@ export const RatingCard: React.FC<RatingCardProps> = ({
|
||||
const [rating, setRating] = React.useState<number>(0);
|
||||
const [hoveredRating, setHoveredRating] = React.useState<number>(0);
|
||||
const [isVisible, setIsVisible] = React.useState(true);
|
||||
const api = useBackendAPI();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
@@ -28,7 +27,7 @@ export const RatingCard: React.FC<RatingCardProps> = ({
|
||||
const handleSubmit = async (rating: number) => {
|
||||
if (rating > 0) {
|
||||
console.log(`Rating submitted for ${agentName}:`, rating);
|
||||
await api.reviewAgent("--", agentName, {
|
||||
await postV2CreateAgentReview("--", agentName, {
|
||||
store_listing_version_id: storeListingVersionId,
|
||||
score: rating,
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
CarouselIndicator,
|
||||
} from "@/components/__legacy__/ui/carousel";
|
||||
import { useCallback, useState } from "react";
|
||||
import { StoreAgent } from "@/lib/autogpt-server-api";
|
||||
import type { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
|
||||
import Link from "next/link";
|
||||
|
||||
const BACKGROUND_COLORS = [
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface EditAgentModalProps {
|
||||
submission:
|
||||
| (StoreSubmissionEditRequest & {
|
||||
store_listing_version_id: string | undefined;
|
||||
agent_id: string;
|
||||
graph_id: string;
|
||||
})
|
||||
| null;
|
||||
onSuccess: (submission: StoreSubmission) => void;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useEditAgentForm } from "./useEditAgentForm";
|
||||
interface EditAgentFormProps {
|
||||
submission: StoreSubmissionEditRequest & {
|
||||
store_listing_version_id: string | undefined;
|
||||
agent_id: string;
|
||||
graph_id: string;
|
||||
};
|
||||
onClose: () => void;
|
||||
onSuccess: (submission: StoreSubmission) => void;
|
||||
@@ -70,7 +70,7 @@ export function EditAgentForm({
|
||||
/>
|
||||
|
||||
<ThumbnailImages
|
||||
agentId={submission.agent_id}
|
||||
agentId={submission.graph_id}
|
||||
onImagesChange={handleImagesChange}
|
||||
initialImages={Array.from(new Set(submission.image_urls || []))}
|
||||
initialSelectedImage={submission.image_urls?.[0] || null}
|
||||
|
||||
@@ -16,7 +16,7 @@ import z from "zod";
|
||||
interface useEditAgentFormProps {
|
||||
submission: StoreSubmissionEditRequest & {
|
||||
store_listing_version_id: string | undefined;
|
||||
agent_id: string;
|
||||
graph_id: string;
|
||||
};
|
||||
onSuccess: (submission: StoreSubmission) => void;
|
||||
onClose: () => void;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
postV2UploadSubmissionMedia,
|
||||
postV2GenerateSubmissionImage,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { resolveResponse } from "@/app/api/helpers";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
|
||||
interface UseThumbnailImagesProps {
|
||||
@@ -97,12 +101,10 @@ export function useThumbnailImages({
|
||||
|
||||
async function uploadImage(file: File) {
|
||||
try {
|
||||
const api = new BackendAPI();
|
||||
|
||||
const imageUrl = (await api.uploadStoreSubmissionMedia(file)).replace(
|
||||
/^"(.*)"$/,
|
||||
"$1",
|
||||
const mediaRes = await resolveResponse(
|
||||
postV2UploadSubmissionMedia({ file }),
|
||||
);
|
||||
const imageUrl = mediaRes.replace(/^"(.*)"$/, "$1");
|
||||
|
||||
setImagesWithValidation([...images, imageUrl]);
|
||||
if (!selectedImage) {
|
||||
@@ -122,11 +124,12 @@ export function useThumbnailImages({
|
||||
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
const api = new BackendAPI();
|
||||
if (!agentId) {
|
||||
throw new Error("Agent ID is required");
|
||||
}
|
||||
const { image_url } = await api.generateStoreSubmissionImage(agentId);
|
||||
const { image_url } = await resolveResponse(
|
||||
postV2GenerateSubmissionImage({ graph_id: agentId }),
|
||||
);
|
||||
setImagesWithValidation([...images, image_url]);
|
||||
if (!selectedImage) {
|
||||
setSelectedImage(image_url);
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useEffect, useCallback, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { postV2CreateStoreSubmission } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import {
|
||||
PublishAgentFormData,
|
||||
@@ -32,7 +33,6 @@ export function useAgentInfoStep({
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const { toast } = useToast();
|
||||
const api = useBackendAPI();
|
||||
|
||||
const form = useForm<PublishAgentFormData>({
|
||||
resolver: zodResolver(publishAgentSchemaFactory(isMarketplaceUpdate)),
|
||||
@@ -115,21 +115,23 @@ export function useAgentInfoStep({
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await api.createStoreSubmission({
|
||||
name: data.title,
|
||||
sub_heading: data.subheader,
|
||||
description: data.description,
|
||||
instructions: data.instructions || null,
|
||||
image_urls: images,
|
||||
video_url: data.youtubeLink || "",
|
||||
agent_output_demo_url: data.agentOutputDemo || "",
|
||||
agent_id: selectedAgentId,
|
||||
agent_version: selectedAgentVersion,
|
||||
slug: (data.slug || "").replace(/\s+/g, "-"),
|
||||
categories: filteredCategories,
|
||||
recommended_schedule_cron: data.recommendedScheduleCron || null,
|
||||
changes_summary: data.changesSummary || null,
|
||||
} as any);
|
||||
const response = okData(
|
||||
await postV2CreateStoreSubmission({
|
||||
slug: (data.slug || "").replace(/\s+/g, "-"),
|
||||
graph_id: selectedAgentId,
|
||||
graph_version: selectedAgentVersion,
|
||||
name: data.title || "",
|
||||
sub_heading: data.subheader || "",
|
||||
description: data.description,
|
||||
instructions: data.instructions || undefined,
|
||||
categories: filteredCategories,
|
||||
image_urls: images,
|
||||
video_url: data.youtubeLink || undefined,
|
||||
agent_output_demo_url: data.agentOutputDemo || undefined,
|
||||
recommended_schedule_cron: data.recommendedScheduleCron || undefined,
|
||||
changes_summary: data.changesSummary || undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
onSuccess(response);
|
||||
} catch (error) {
|
||||
|
||||
@@ -51,8 +51,8 @@ export function useAgentSelectStep({
|
||||
?.agents.map(
|
||||
(agent): Agent => ({
|
||||
name: agent.agent_name,
|
||||
id: agent.agent_id,
|
||||
version: agent.agent_version,
|
||||
id: agent.graph_id,
|
||||
version: agent.graph_version,
|
||||
lastEdited: agent.last_edited.toLocaleDateString(),
|
||||
imageSrc: agent.agent_image || "https://picsum.photos/300/200",
|
||||
description: agent.description || "",
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getGetV2ListMySubmissionsQueryKey,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import type { MyAgent } from "@/app/api/__generated__/models/myAgent";
|
||||
import type { MyUnpublishedAgent } from "@/app/api/__generated__/models/myUnpublishedAgent";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
|
||||
@@ -114,14 +114,14 @@ export function usePublishAgentModal({
|
||||
!preSelectedAgentVersion
|
||||
)
|
||||
return;
|
||||
const agentsData = okData(myAgents) as any;
|
||||
const submissionsData = okData(mySubmissions) as any;
|
||||
const agentsData = okData(myAgents);
|
||||
const submissionsData = okData(mySubmissions);
|
||||
|
||||
if (!agentsData || !submissionsData) return;
|
||||
|
||||
// Find the agent data
|
||||
const agent = agentsData.agents?.find(
|
||||
(a: MyAgent) => a.agent_id === preSelectedAgentId,
|
||||
(a: MyUnpublishedAgent) => a.graph_id === preSelectedAgentId,
|
||||
);
|
||||
if (!agent) return;
|
||||
|
||||
@@ -129,11 +129,11 @@ export function usePublishAgentModal({
|
||||
const publishedSubmissionData = submissionsData.submissions
|
||||
?.filter(
|
||||
(s: StoreSubmission) =>
|
||||
s.status === "APPROVED" && s.agent_id === preSelectedAgentId,
|
||||
s.status === "APPROVED" && s.graph_id === preSelectedAgentId,
|
||||
)
|
||||
.sort(
|
||||
(a: StoreSubmission, b: StoreSubmission) =>
|
||||
b.agent_version - a.agent_version,
|
||||
b.graph_version - a.graph_version,
|
||||
)[0];
|
||||
|
||||
// Populate initial data (same logic as handleNextFromSelect)
|
||||
|
||||
@@ -6,7 +6,7 @@ export function useRunAgentInputs() {
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
|
||||
async function handleUploadFile(file: File) {
|
||||
const result = await api.uploadFile(file, "gcs", 24, (progress) =>
|
||||
const result = await api.uploadFile(file, 24, (progress) =>
|
||||
setUploadProgress(progress),
|
||||
);
|
||||
return result;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { GraphExecutionMeta as GeneratedGraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
|
||||
import { MyAgent } from "@/app/api/__generated__/models/myAgent";
|
||||
import type { GraphExecution } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
// Time constants
|
||||
@@ -64,33 +63,6 @@ export interface NotificationState {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export function createAgentInfoMap(
|
||||
agents: MyAgent[],
|
||||
): Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
> {
|
||||
const agentMap = new Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>();
|
||||
|
||||
agents.forEach((agent) => {
|
||||
// Ensure we have valid agent data
|
||||
const agentName =
|
||||
agent.agent_name || `Agent ${agent.agent_id?.slice(0, 8)}`;
|
||||
const agentDescription = agent.description || "";
|
||||
|
||||
agentMap.set(agent.agent_id, {
|
||||
name: agentName,
|
||||
description: agentDescription,
|
||||
library_agent_id: undefined, // MyAgent doesn't have library_agent_id
|
||||
});
|
||||
});
|
||||
|
||||
return agentMap;
|
||||
}
|
||||
|
||||
export function enrichExecutionWithAgentInfo(
|
||||
execution: GeneratedGraphExecutionMeta,
|
||||
agentInfoMap: Map<
|
||||
|
||||
@@ -16,8 +16,6 @@ import type {
|
||||
APIKeyPermission,
|
||||
Block,
|
||||
CreateAPIKeyResponse,
|
||||
CreatorDetails,
|
||||
CreatorsResponse,
|
||||
Credentials,
|
||||
CredentialsDeleteNeedConfirmationResponse,
|
||||
CredentialsDeleteResponse,
|
||||
@@ -43,26 +41,15 @@ import type {
|
||||
LibraryAgentPresetUpdatable,
|
||||
LibraryAgentResponse,
|
||||
LibraryAgentSortEnum,
|
||||
MyAgentsResponse,
|
||||
NodeExecutionResult,
|
||||
NotificationPreference,
|
||||
NotificationPreferenceDTO,
|
||||
OttoQuery,
|
||||
OttoResponse,
|
||||
ProfileDetails,
|
||||
RefundRequest,
|
||||
ReviewSubmissionRequest,
|
||||
Schedule,
|
||||
ScheduleCreatable,
|
||||
ScheduleID,
|
||||
StoreAgentsResponse,
|
||||
StoreListingsWithVersionsResponse,
|
||||
StoreReview,
|
||||
StoreReviewCreate,
|
||||
StoreSubmission,
|
||||
StoreSubmissionRequest,
|
||||
StoreSubmissionsResponse,
|
||||
SubmissionStatus,
|
||||
TransactionHistory,
|
||||
User,
|
||||
UserPasswordCredentials,
|
||||
@@ -445,86 +432,8 @@ export default class BackendAPI {
|
||||
return this._request("POST", "/analytics/log_raw_analytics", analytic);
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
///////////// V2 STORE API /////////////
|
||||
////////////////////////////////////////
|
||||
|
||||
async getStoreProfile(): Promise<ProfileDetails | null> {
|
||||
try {
|
||||
return await this._get("/store/profile");
|
||||
} catch (error) {
|
||||
if (!(error instanceof LogoutInterruptError)) {
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getStoreAgents(params?: {
|
||||
featured?: boolean;
|
||||
creator?: string;
|
||||
sorted_by?: string;
|
||||
search_query?: string;
|
||||
category?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<StoreAgentsResponse> {
|
||||
return this._get("/store/agents", params);
|
||||
}
|
||||
|
||||
getGraphMetaByStoreListingVersionID(
|
||||
storeListingVersionID: string,
|
||||
): Promise<GraphMeta> {
|
||||
return this._get(`/store/graph/${storeListingVersionID}`);
|
||||
}
|
||||
|
||||
getStoreCreators(params?: {
|
||||
featured?: boolean;
|
||||
search_query?: string;
|
||||
sorted_by?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<CreatorsResponse> {
|
||||
return this._get("/store/creators", params);
|
||||
}
|
||||
|
||||
getStoreCreator(username: string): Promise<CreatorDetails> {
|
||||
return this._get(`/store/creator/${encodeURIComponent(username)}`);
|
||||
}
|
||||
|
||||
getStoreSubmissions(params?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<StoreSubmissionsResponse> {
|
||||
return this._get("/store/submissions", params);
|
||||
}
|
||||
|
||||
createStoreSubmission(
|
||||
submission: StoreSubmissionRequest,
|
||||
): Promise<StoreSubmission> {
|
||||
return this._request("POST", "/store/submissions", submission);
|
||||
}
|
||||
|
||||
generateStoreSubmissionImage(
|
||||
agent_id: string,
|
||||
): Promise<{ image_url: string }> {
|
||||
return this._request(
|
||||
"POST",
|
||||
"/store/submissions/generate_image?agent_id=" + agent_id,
|
||||
);
|
||||
}
|
||||
|
||||
deleteStoreSubmission(submission_id: string): Promise<boolean> {
|
||||
return this._request("DELETE", `/store/submissions/${submission_id}`);
|
||||
}
|
||||
|
||||
uploadStoreSubmissionMedia(file: File): Promise<string> {
|
||||
return this._uploadFile("/store/submissions/media", file);
|
||||
}
|
||||
|
||||
uploadFile(
|
||||
async uploadFile(
|
||||
file: File,
|
||||
provider: string = "gcs",
|
||||
expiration_hours: number = 24,
|
||||
onProgress?: (progress: number) => void,
|
||||
): Promise<{
|
||||
@@ -534,82 +443,22 @@ export default class BackendAPI {
|
||||
content_type: string;
|
||||
expires_in_hours: number;
|
||||
}> {
|
||||
return this._uploadFileWithProgress(
|
||||
const response = await this._uploadFileWithProgress(
|
||||
"/files/upload",
|
||||
file,
|
||||
{
|
||||
provider,
|
||||
expiration_hours,
|
||||
},
|
||||
{ expiration_hours },
|
||||
onProgress,
|
||||
).then((response) => {
|
||||
if (typeof response === "string") {
|
||||
return JSON.parse(response);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
updateStoreProfile(profile: ProfileDetails): Promise<ProfileDetails> {
|
||||
return this._request("POST", "/store/profile", profile);
|
||||
}
|
||||
|
||||
reviewAgent(
|
||||
username: string,
|
||||
agentName: string,
|
||||
review: StoreReviewCreate,
|
||||
): Promise<StoreReview> {
|
||||
return this._request(
|
||||
"POST",
|
||||
`/store/agents/${encodeURIComponent(username)}/${encodeURIComponent(
|
||||
agentName,
|
||||
)}/review`,
|
||||
review,
|
||||
);
|
||||
}
|
||||
|
||||
getMyAgents(params?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<MyAgentsResponse> {
|
||||
return this._get("/store/myagents", params);
|
||||
}
|
||||
|
||||
downloadStoreAgent(
|
||||
storeListingVersionId: string,
|
||||
version?: number,
|
||||
): Promise<BlobPart> {
|
||||
const url = version
|
||||
? `/store/download/agents/${storeListingVersionId}?version=${version}`
|
||||
: `/store/download/agents/${storeListingVersionId}`;
|
||||
|
||||
return this._get(url);
|
||||
if (typeof response === "string") {
|
||||
return JSON.parse(response);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
/////////// Admin API ///////////////////
|
||||
/////////////////////////////////////////
|
||||
|
||||
getAdminListingsWithVersions(params?: {
|
||||
status?: SubmissionStatus;
|
||||
search?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<StoreListingsWithVersionsResponse> {
|
||||
return this._get("/store/admin/listings", params);
|
||||
}
|
||||
|
||||
reviewSubmissionAdmin(
|
||||
storeListingVersionId: string,
|
||||
review: ReviewSubmissionRequest,
|
||||
): Promise<StoreSubmission> {
|
||||
return this._request(
|
||||
"POST",
|
||||
`/store/admin/submissions/${storeListingVersionId}/review`,
|
||||
review,
|
||||
);
|
||||
}
|
||||
|
||||
addUserCredits(
|
||||
user_id: string,
|
||||
amount: number,
|
||||
@@ -631,12 +480,6 @@ export default class BackendAPI {
|
||||
return this._get("/credits/admin/users_history", params);
|
||||
}
|
||||
|
||||
downloadStoreAgentAdmin(storeListingVersionId: string): Promise<BlobPart> {
|
||||
const url = `/store/admin/submissions/download/${storeListingVersionId}`;
|
||||
|
||||
return this._get(url);
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
//////////// V2 LIBRARY API ////////////
|
||||
////////////////////////////////////////
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
export enum SubmissionStatus {
|
||||
DRAFT = "DRAFT",
|
||||
PENDING = "PENDING",
|
||||
APPROVED = "APPROVED",
|
||||
REJECTED = "REJECTED",
|
||||
}
|
||||
export type ReviewSubmissionRequest = {
|
||||
store_listing_version_id: string;
|
||||
is_approved: boolean;
|
||||
comments: string; // External comments visible to creator
|
||||
internal_comments?: string; // Admin-only comments
|
||||
};
|
||||
export type Category = {
|
||||
category: string;
|
||||
description: string;
|
||||
@@ -778,102 +766,6 @@ export type Pagination = {
|
||||
page_size: number;
|
||||
};
|
||||
|
||||
export type StoreAgent = {
|
||||
slug: string;
|
||||
agent_name: string;
|
||||
agent_image: string;
|
||||
creator: string;
|
||||
creator_avatar: string;
|
||||
sub_heading: string;
|
||||
description: string;
|
||||
runs: number;
|
||||
rating: number;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type StoreAgentsResponse = {
|
||||
agents: StoreAgent[];
|
||||
pagination: Pagination;
|
||||
};
|
||||
|
||||
export type Creator = {
|
||||
name: string;
|
||||
username: string;
|
||||
description: string;
|
||||
avatar_url: string;
|
||||
num_agents: number;
|
||||
agent_rating: number;
|
||||
agent_runs: number;
|
||||
};
|
||||
|
||||
export type CreatorsResponse = {
|
||||
creators: Creator[];
|
||||
pagination: Pagination;
|
||||
};
|
||||
|
||||
export type CreatorDetails = {
|
||||
name: string;
|
||||
username: string;
|
||||
description: string;
|
||||
links: string[];
|
||||
avatar_url: string;
|
||||
agent_rating: number;
|
||||
agent_runs: number;
|
||||
top_categories: string[];
|
||||
};
|
||||
|
||||
export type StoreSubmission = {
|
||||
agent_id: string;
|
||||
agent_version: number;
|
||||
name: string;
|
||||
sub_heading: string;
|
||||
description: string;
|
||||
instructions?: string;
|
||||
image_urls: string[];
|
||||
date_submitted: string;
|
||||
status: SubmissionStatus;
|
||||
runs: number;
|
||||
rating: number;
|
||||
slug: string;
|
||||
store_listing_version_id?: string;
|
||||
version?: number; // Actual version number from the database
|
||||
|
||||
// Review information
|
||||
reviewer_id?: string;
|
||||
review_comments?: string;
|
||||
internal_comments?: string; // Admin-only comments
|
||||
reviewed_at?: string;
|
||||
changes_summary?: string;
|
||||
};
|
||||
|
||||
export type StoreSubmissionsResponse = {
|
||||
submissions: StoreSubmission[];
|
||||
pagination: Pagination;
|
||||
};
|
||||
|
||||
export type StoreSubmissionRequest = {
|
||||
agent_id: string;
|
||||
agent_version: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
sub_heading: string;
|
||||
video_url?: string;
|
||||
image_urls: string[];
|
||||
description: string;
|
||||
instructions?: string | null;
|
||||
categories: string[];
|
||||
changes_summary?: string;
|
||||
recommended_schedule_cron?: string | null;
|
||||
};
|
||||
|
||||
export type ProfileDetails = {
|
||||
name: string;
|
||||
username: string;
|
||||
description: string;
|
||||
links: string[];
|
||||
avatar_url: string;
|
||||
};
|
||||
|
||||
/* Mirror of backend/executor/scheduler.py:GraphExecutionJobInfo */
|
||||
export type Schedule = {
|
||||
id: ScheduleID;
|
||||
@@ -900,32 +792,6 @@ export type ScheduleCreatable = {
|
||||
credentials?: Record<string, CredentialsMetaInput>;
|
||||
};
|
||||
|
||||
export type MyAgent = {
|
||||
agent_id: GraphID;
|
||||
agent_version: number;
|
||||
agent_name: string;
|
||||
agent_image: string | null;
|
||||
last_edited: string;
|
||||
description: string;
|
||||
recommended_schedule_cron: string | null;
|
||||
};
|
||||
|
||||
export type MyAgentsResponse = {
|
||||
agents: MyAgent[];
|
||||
pagination: Pagination;
|
||||
};
|
||||
|
||||
export type StoreReview = {
|
||||
score: number;
|
||||
comments?: string;
|
||||
};
|
||||
|
||||
export type StoreReviewCreate = {
|
||||
store_listing_version_id: string;
|
||||
score: number;
|
||||
comments?: string;
|
||||
};
|
||||
|
||||
// API Key Types
|
||||
|
||||
export enum APIKeyPermission {
|
||||
@@ -1081,46 +947,6 @@ export interface OttoQuery {
|
||||
graph_id?: string;
|
||||
}
|
||||
|
||||
export interface StoreListingWithVersions {
|
||||
listing_id: string;
|
||||
slug: string;
|
||||
agent_id: string;
|
||||
agent_version: number;
|
||||
active_version_id: string | null;
|
||||
has_approved_version: boolean;
|
||||
creator_email: string | null;
|
||||
latest_version: StoreSubmission | null;
|
||||
versions: StoreSubmission[];
|
||||
}
|
||||
|
||||
export interface StoreListingsWithVersionsResponse {
|
||||
listings: StoreListingWithVersions[];
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
||||
// Admin API Types
|
||||
export type AdminSubmissionsRequest = {
|
||||
status?: SubmissionStatus;
|
||||
search?: string;
|
||||
page: number;
|
||||
page_size: number;
|
||||
};
|
||||
|
||||
export type AdminListingHistoryRequest = {
|
||||
listing_id: string;
|
||||
page: number;
|
||||
page_size: number;
|
||||
};
|
||||
|
||||
export type AdminSubmissionDetailsRequest = {
|
||||
store_listing_version_id: string;
|
||||
};
|
||||
|
||||
export type AdminPendingSubmissionsRequest = {
|
||||
page: number;
|
||||
page_size: number;
|
||||
};
|
||||
|
||||
export enum CreditTransactionType {
|
||||
TOP_UP = "TOP_UP",
|
||||
USAGE = "USAGE",
|
||||
|
||||
@@ -54,7 +54,7 @@ Search for agents in the store
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block searches the AutoGPT agent store using a query string. Filter results by category and sort by rating, runs, or name. Limit controls the maximum number of results returned.
|
||||
This block searches the AutoGPT agent store using a query string. Filter results by category and sort by rating, runs, name, or recency (`updated_at`). Limit controls the maximum number of results returned.
|
||||
|
||||
Results include basic agent information and are output both as a list and individually for workflow iteration.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
Reference in New Issue
Block a user