wip: adding waitlist

This commit is contained in:
Nicholas Tindle
2026-01-06 22:13:35 -07:00
parent cb08def96c
commit 2c60aa64ef
5 changed files with 521 additions and 1 deletions

View File

@@ -1957,3 +1957,104 @@ async def get_agent_as_admin(
)
return graph
async def get_waitlist() -> list[store_model.StoreWaitlistEntry]:
"""Get all waitlists."""
try:
waitlists = await prisma.models.WaitlistEntry.prisma().find_many(
where={"isDeleted": False},
order=[{"createdAt": "desc"}],
)
# order them by votes before returning without vote counts to the frontend
sorted_list = sorted(waitlists, key=lambda x: x.votes, reverse=True)
lists = [
store_model.StoreWaitlistEntry(
name=waitlist.name,
description=waitlist.description,
waitlist_id=waitlist.waitlistId,
slug=waitlist.slug,
subHeading=waitlist.subHeading,
imageUrls=waitlist.imageUrls or [],
categories=waitlist.categories,
)
for waitlist in sorted_list
if waitlist.status != prisma.enums.WaitlistExternalStatus.CANCELED
and waitlist.status != prisma.enums.WaitlistExternalStatus.DONE
]
return lists
except Exception as e:
logger.error(f"Error fetching waitlists: {e}")
raise DatabaseError("Failed to fetch waitlists") from e
async def add_user_to_waitlist(
waitlist_id: str, user_id: str | None, email: str | None
):
"""Add a user to a waitlist."""
logger.debug(f"Adding user {user_id} to waitlist {waitlist_id}")
try:
waitlist = await prisma.models.WaitlistEntry.prisma().find_unique(
where={"id": waitlist_id}
)
if not waitlist:
raise ValueError(f"Waitlist {waitlist_id} not found")
# Check if user is already in the waitlist
existing_entry = None
if user_id:
existing_entry = await prisma.models.WaitlistEntry.prisma().find_first(
where={
"waitlistId": waitlist_id,
"userId": user_id,
"isDeleted": False,
}
)
elif email:
existing_entry = await prisma.models.WaitlistEntry.prisma().find_first(
where={
"waitlistId": waitlist_id,
"email": email,
"isDeleted": False,
}
)
if existing_entry:
# convert emails to user if appropriate
raise NotImplementedError(
"Implment the capibility to convert email based waitlist entries to user based ones and remove the email based waitlist entry."
)
# Create new waitlist entry
new_entry = await prisma.models.WaitlistEntry.prisma().create(
data=prisma.types.WaitlistEntryCreateInput(
waitlistId=waitlist_id,
userId=user_id,
email=email,
name="",
description="",
slug="",
subHeading="",
imageUrls=[],
categories=[],
)
)
return store_model.StoreWaitlistEntry(
name=new_entry.name,
description=new_entry.description,
waitlist_id=new_entry.waitlistId,
slug=new_entry.slug,
subHeading=new_entry.subHeading,
imageUrls=new_entry.imageUrls or [],
categories=new_entry.categories,
)
except Exception as e:
logger.error(f"Error adding user to waitlist: {e}")
raise DatabaseError("Failed to add user to waitlist") from e

View File

@@ -1,6 +1,7 @@
import datetime
from typing import List
from backend.data.model import User
import prisma.enums
import pydantic
@@ -216,3 +217,23 @@ class ReviewSubmissionRequest(pydantic.BaseModel):
is_approved: bool
comments: str # External comments visible to creator
internal_comments: str | None = None # Private admin notes
class StoreWaitlistEntry(pydantic.BaseModel):
waitlist_id: str
storeListing: StoreListingWithVersions | None = None
owner: User | None = None
slug: str
# Content fields
name: str
subHeading: str
videoUrl: str | None = None
agentOutputDemoUrl: str | None = None
imageUrls: list[str]
description: str
categories: list[str]
class StoreWaitlistsAllResponse(pydantic.BaseModel):
listings: list[StoreWaitlistEntry]

View File

@@ -5,6 +5,7 @@ import urllib.parse
from typing import Literal
import autogpt_libs.auth
from autogpt_libs.auth.dependencies import get_optional_user_id
import fastapi
import fastapi.responses
@@ -78,6 +79,45 @@ async def update_or_create_profile(
return updated_profile
##############################################
############## Waitlist Endpoints ############
##############################################
@router.get(
"/waitlist",
summary="Get the agent waitlist",
tags=["store", "public"],
response_model=store_model.StoreWaitlistsAllResponse,
)
async def get_waitlist():
"""
Get the agent waitlist details.
"""
waitlist = await store_db.get_waitlist()
return waitlist
@router.post(
path="/waitlist/{waitlist_id}/join",
summary="Add self to the agent waitlist",
tags=["store", "public"],
response_model=store_model.StoreWaitlistEntry,
)
async def add_self_to_waitlist(
user_id: str | None = fastapi.Security(get_optional_user_id),
waitlist_id: str = fastapi.Path(..., description="The ID of the waitlist to join"),
email: str | None = fastapi.Body(
default=None, description="Email address for unauthenticated users"
),
):
"""
Add the current user to the agent waitlist.
"""
waitlist_entry = await store_db.add_user_to_waitlist(
waitlist_id=waitlist_id, user_id=user_id, email=email
)
return waitlist_entry
##############################################
############### Agent Endpoints ##############
##############################################

View File

@@ -67,6 +67,10 @@ model User {
OAuthAuthorizationCodes OAuthAuthorizationCode[]
OAuthAccessTokens OAuthAccessToken[]
OAuthRefreshTokens OAuthRefreshToken[]
// Waitlist relations
waitlistEntries WaitlistEntry[]
joinedWaitlists WaitlistEntry[] @relation("joinedWaitlists")
}
enum OnboardingStep {
@@ -834,7 +838,8 @@ model StoreListing {
OwningUser User @relation(fields: [owningUserId], references: [id])
// Relations
Versions StoreListingVersion[] @relation("ListingVersions")
Versions StoreListingVersion[] @relation("ListingVersions")
waitlistEntries WaitlistEntry[]
// Unique index on agentId to ensure only one listing per agent, regardless of number of versions the agent has.
@@unique([agentGraphId])
@@ -924,6 +929,45 @@ model StoreListingReview {
@@index([reviewByUserId])
}
enum WaitlistExternalStatus {
DONE
NOT_STARTED
CANCELED
WORK_IN_PROGRESS
}
model WaitlistEntry {
id String @id @default(uuid())
storeListingId String?
StoreListing StoreListing? @relation(fields: [storeListingId], references: [id], onDelete: Cascade)
owningUserId String
OwningUser User @relation(fields: [owningUserId], references: [id])
slug String
search Unsupported("tsvector")? @default(dbgenerated("''::tsvector"))
// Content fields
name String
subHeading String
videoUrl String?
agentOutputDemoUrl String?
imageUrls String[]
description String
categories String[]
//Waitlist specific fields
status WaitlistExternalStatus @default(NOT_STARTED)
votes Int @default(0) // Hide from frontend api
joinedUsers User[] @relation("joinedWaitlists")
// NOTE: DO NOT DOUBLE SEND TO THESE USERS, IF THEY HAVE SIGNED UP SINCE THEY MAY HAVE ALREADY RECEIVED AN EMAIL
// DOUBLE CHECK WHEN SENDING THAT THEY ARE NOT IN THE JOINED USERS LIST ALSO
unafilliatedEmailUsers String[] @default([])
isDeleted Boolean @default(false)
}
enum SubmissionStatus {
DRAFT // Being prepared, not yet submitted
PENDING // Submitted, awaiting review

View File

@@ -4965,6 +4965,135 @@
}
}
},
"/api/store/profile": {
"get": {
"tags": ["v2", "store", "private"],
"summary": "Get user profile",
"description": "Get the profile details for the authenticated user.\nCached for 1 hour per user.",
"operationId": "getV2Get user profile",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ProfileDetails" }
}
}
},
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
}
},
"security": [{ "HTTPBearerJWT": [] }]
},
"post": {
"tags": ["v2", "store", "private"],
"summary": "Update user profile",
"description": "Update the store profile for the authenticated user.\n\nArgs:\n profile (Profile): The updated profile details\n user_id (str): ID of the authenticated user\n\nReturns:\n CreatorDetails: The updated profile\n\nRaises:\n HTTPException: If there is an error updating the profile",
"operationId": "postV2Update user profile",
"requestBody": {
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Profile" }
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/CreatorDetails" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
},
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
}
},
"security": [{ "HTTPBearerJWT": [] }]
}
},
"/api/store/waitlist": {
"get": {
"tags": ["v2", "store", "public"],
"summary": "Get the agent waitlist",
"description": "Get the agent waitlist details.",
"operationId": "getV2Get the agent waitlist",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StoreWaitlistsAllResponse"
}
}
}
}
}
}
},
"/api/store/waitlist/{waitlist_id}/join": {
"post": {
"tags": ["v2", "store", "public"],
"summary": "Add self to the agent waitlist",
"description": "Add the current user to the agent waitlist.",
"operationId": "postV2Add self to the agent waitlist",
"security": [{ "HTTPBearer": [] }],
"parameters": [
{
"name": "waitlist_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"description": "The ID of the waitlist to join",
"title": "Waitlist Id"
},
"description": "The ID of the waitlist to join"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"description": "Email address for unauthenticated users",
"title": "Email"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/StoreWaitlistEntry" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/store/agents": {
"get": {
"tags": ["v2", "store", "public"],
@@ -9885,6 +10014,68 @@
"required": ["submissions", "pagination"],
"title": "StoreSubmissionsResponse"
},
"StoreWaitlistEntry": {
"properties": {
"waitlist_id": { "type": "string", "title": "Waitlist Id" },
"storeListing": {
"anyOf": [
{ "$ref": "#/components/schemas/StoreListingWithVersions" },
{ "type": "null" }
]
},
"owner": {
"anyOf": [
{ "$ref": "#/components/schemas/User" },
{ "type": "null" }
]
},
"slug": { "type": "string", "title": "Slug" },
"name": { "type": "string", "title": "Name" },
"subHeading": { "type": "string", "title": "Subheading" },
"videoUrl": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Videourl"
},
"agentOutputDemoUrl": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Agentoutputdemourl"
},
"imageUrls": {
"items": { "type": "string" },
"type": "array",
"title": "Imageurls"
},
"description": { "type": "string", "title": "Description" },
"categories": {
"items": { "type": "string" },
"type": "array",
"title": "Categories"
}
},
"type": "object",
"required": [
"waitlist_id",
"slug",
"name",
"subHeading",
"imageUrls",
"description",
"categories"
],
"title": "StoreWaitlistEntry"
},
"StoreWaitlistsAllResponse": {
"properties": {
"listings": {
"items": { "$ref": "#/components/schemas/StoreWaitlistEntry" },
"type": "array",
"title": "Listings"
}
},
"type": "object",
"required": ["listings"],
"title": "StoreWaitlistsAllResponse"
},
"SubmissionStatus": {
"type": "string",
"enum": ["DRAFT", "PENDING", "APPROVED", "REJECTED"],
@@ -11368,6 +11559,128 @@
],
"title": "UploadFileResponse"
},
"User": {
"properties": {
"id": { "type": "string", "title": "Id", "description": "User ID" },
"email": {
"type": "string",
"title": "Email",
"description": "User email address"
},
"email_verified": {
"type": "boolean",
"title": "Email Verified",
"description": "Whether email is verified",
"default": true
},
"name": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Name",
"description": "User display name"
},
"created_at": {
"type": "string",
"format": "date-time",
"title": "Created At",
"description": "When user was created"
},
"updated_at": {
"type": "string",
"format": "date-time",
"title": "Updated At",
"description": "When user was last updated"
},
"metadata": {
"additionalProperties": true,
"type": "object",
"title": "Metadata",
"description": "User metadata as dict"
},
"integrations": {
"type": "string",
"title": "Integrations",
"description": "Encrypted integrations data",
"default": ""
},
"stripe_customer_id": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Stripe Customer Id",
"description": "Stripe customer ID"
},
"top_up_config": {
"anyOf": [
{ "$ref": "#/components/schemas/AutoTopUpConfig" },
{ "type": "null" }
],
"description": "Top up configuration"
},
"max_emails_per_day": {
"type": "integer",
"title": "Max Emails Per Day",
"description": "Maximum emails per day",
"default": 3
},
"notify_on_agent_run": {
"type": "boolean",
"title": "Notify On Agent Run",
"description": "Notify on agent run",
"default": true
},
"notify_on_zero_balance": {
"type": "boolean",
"title": "Notify On Zero Balance",
"description": "Notify on zero balance",
"default": true
},
"notify_on_low_balance": {
"type": "boolean",
"title": "Notify On Low Balance",
"description": "Notify on low balance",
"default": true
},
"notify_on_block_execution_failed": {
"type": "boolean",
"title": "Notify On Block Execution Failed",
"description": "Notify on block execution failure",
"default": true
},
"notify_on_continuous_agent_error": {
"type": "boolean",
"title": "Notify On Continuous Agent Error",
"description": "Notify on continuous agent error",
"default": true
},
"notify_on_daily_summary": {
"type": "boolean",
"title": "Notify On Daily Summary",
"description": "Notify on daily summary",
"default": true
},
"notify_on_weekly_summary": {
"type": "boolean",
"title": "Notify On Weekly Summary",
"description": "Notify on weekly summary",
"default": true
},
"notify_on_monthly_summary": {
"type": "boolean",
"title": "Notify On Monthly Summary",
"description": "Notify on monthly summary",
"default": true
},
"timezone": {
"type": "string",
"title": "Timezone",
"description": "User timezone (IANA timezone identifier or 'not-set')",
"default": "not-set"
}
},
"additionalProperties": false,
"type": "object",
"required": ["id", "email", "created_at", "updated_at"],
"title": "User",
"description": "Application-layer User model with snake_case convention."
},
"UserHistoryResponse": {
"properties": {
"history": {
@@ -11674,6 +11987,7 @@
}
},
"securitySchemes": {
"HTTPBearer": { "type": "http", "scheme": "bearer" },
"APIKeyAuthenticator-X-Postmark-Webhook-Token": {
"type": "apiKey",
"in": "header",