Compare commits

..

3 Commits
v0.6.44 ... dev

Author SHA1 Message Date
Nicholas Tindle
e0dfae5732 fix(platform): evaluate chat flag after auth for correct redirect (#11873)
Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 14:58:02 -06:00
Zamil Majdy
7df867d645 Merge branch 'master' of github.com:Significant-Gravitas/AutoGPT into dev 2026-01-28 12:29:41 -06:00
Zamil Majdy
d855f79874 fix(platform): reduce Sentry alert spam for expected errors (#11872)
## Summary
- Add `InvalidInputError` for validation errors (search term too long,
invalid pagination) - returns 400 instead of 500
- Remove redundant try/catch blocks in library routes - global exception
handlers already handle `ValueError`→400 and `NotFoundError`→404
- Aggregate embedding backfill errors and log once at the end instead of
per content type to prevent Sentry issue spam

## Test plan
- [x] Verify validation errors (search term >100 chars) return 400 Bad
Request
- [x] Verify NotFoundError still returns 404
- [x] Verify embedding errors are logged once at the end with aggregated
counts

Fixes AUTOGPT-SERVER-7K5, BUILDER-6NC

---------

Co-authored-by: Swifty <craigswift13@gmail.com>
2026-01-29 01:28:27 +07:00
14 changed files with 142 additions and 321 deletions

View File

@@ -21,7 +21,7 @@ from backend.data.model import CredentialsMetaInput
from backend.integrations.creds_manager import IntegrationCredentialsManager from backend.integrations.creds_manager import IntegrationCredentialsManager
from backend.integrations.webhooks.graph_lifecycle_hooks import on_graph_activate from backend.integrations.webhooks.graph_lifecycle_hooks import on_graph_activate
from backend.util.clients import get_scheduler_client from backend.util.clients import get_scheduler_client
from backend.util.exceptions import DatabaseError, NotFoundError from backend.util.exceptions import DatabaseError, InvalidInputError, NotFoundError
from backend.util.json import SafeJson from backend.util.json import SafeJson
from backend.util.models import Pagination from backend.util.models import Pagination
from backend.util.settings import Config from backend.util.settings import Config
@@ -64,11 +64,11 @@ async def list_library_agents(
if page < 1 or page_size < 1: if page < 1 or page_size < 1:
logger.warning(f"Invalid pagination: page={page}, page_size={page_size}") logger.warning(f"Invalid pagination: page={page}, page_size={page_size}")
raise DatabaseError("Invalid pagination input") raise InvalidInputError("Invalid pagination input")
if search_term and len(search_term.strip()) > 100: if search_term and len(search_term.strip()) > 100:
logger.warning(f"Search term too long: {repr(search_term)}") logger.warning(f"Search term too long: {repr(search_term)}")
raise DatabaseError("Search term is too long") raise InvalidInputError("Search term is too long")
where_clause: prisma.types.LibraryAgentWhereInput = { where_clause: prisma.types.LibraryAgentWhereInput = {
"userId": user_id, "userId": user_id,
@@ -175,7 +175,7 @@ async def list_favorite_library_agents(
if page < 1 or page_size < 1: if page < 1 or page_size < 1:
logger.warning(f"Invalid pagination: page={page}, page_size={page_size}") logger.warning(f"Invalid pagination: page={page}, page_size={page_size}")
raise DatabaseError("Invalid pagination input") raise InvalidInputError("Invalid pagination input")
where_clause: prisma.types.LibraryAgentWhereInput = { where_clause: prisma.types.LibraryAgentWhereInput = {
"userId": user_id, "userId": user_id,

View File

@@ -1,4 +1,3 @@
import logging
from typing import Literal, Optional from typing import Literal, Optional
import autogpt_libs.auth as autogpt_auth_lib import autogpt_libs.auth as autogpt_auth_lib
@@ -6,15 +5,11 @@ from fastapi import APIRouter, Body, HTTPException, Query, Security, status
from fastapi.responses import Response from fastapi.responses import Response
from prisma.enums import OnboardingStep from prisma.enums import OnboardingStep
import backend.api.features.store.exceptions as store_exceptions
from backend.data.onboarding import complete_onboarding_step from backend.data.onboarding import complete_onboarding_step
from backend.util.exceptions import DatabaseError, NotFoundError
from .. import db as library_db from .. import db as library_db
from .. import model as library_model from .. import model as library_model
logger = logging.getLogger(__name__)
router = APIRouter( router = APIRouter(
prefix="/agents", prefix="/agents",
tags=["library", "private"], tags=["library", "private"],
@@ -26,10 +21,6 @@ router = APIRouter(
"", "",
summary="List Library Agents", summary="List Library Agents",
response_model=library_model.LibraryAgentResponse, response_model=library_model.LibraryAgentResponse,
responses={
200: {"description": "List of library agents"},
500: {"description": "Server error", "content": {"application/json": {}}},
},
) )
async def list_library_agents( async def list_library_agents(
user_id: str = Security(autogpt_auth_lib.get_user_id), user_id: str = Security(autogpt_auth_lib.get_user_id),
@@ -53,43 +44,19 @@ async def list_library_agents(
) -> library_model.LibraryAgentResponse: ) -> library_model.LibraryAgentResponse:
""" """
Get all agents in the user's library (both created and saved). Get all agents in the user's library (both created and saved).
Args:
user_id: ID of the authenticated user.
search_term: Optional search term to filter agents by name/description.
filter_by: List of filters to apply (favorites, created by user).
sort_by: List of sorting criteria (created date, updated date).
page: Page number to retrieve.
page_size: Number of agents per page.
Returns:
A LibraryAgentResponse containing agents and pagination metadata.
Raises:
HTTPException: If a server/database error occurs.
""" """
try: return await library_db.list_library_agents(
return await library_db.list_library_agents( user_id=user_id,
user_id=user_id, search_term=search_term,
search_term=search_term, sort_by=sort_by,
sort_by=sort_by, page=page,
page=page, page_size=page_size,
page_size=page_size, )
)
except Exception as e:
logger.error(f"Could not list library agents for user #{user_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
) from e
@router.get( @router.get(
"/favorites", "/favorites",
summary="List Favorite Library Agents", summary="List Favorite Library Agents",
responses={
500: {"description": "Server error", "content": {"application/json": {}}},
},
) )
async def list_favorite_library_agents( async def list_favorite_library_agents(
user_id: str = Security(autogpt_auth_lib.get_user_id), user_id: str = Security(autogpt_auth_lib.get_user_id),
@@ -106,30 +73,12 @@ async def list_favorite_library_agents(
) -> library_model.LibraryAgentResponse: ) -> library_model.LibraryAgentResponse:
""" """
Get all favorite agents in the user's library. Get all favorite agents in the user's library.
Args:
user_id: ID of the authenticated user.
page: Page number to retrieve.
page_size: Number of agents per page.
Returns:
A LibraryAgentResponse containing favorite agents and pagination metadata.
Raises:
HTTPException: If a server/database error occurs.
""" """
try: return await library_db.list_favorite_library_agents(
return await library_db.list_favorite_library_agents( user_id=user_id,
user_id=user_id, page=page,
page=page, page_size=page_size,
page_size=page_size, )
)
except Exception as e:
logger.error(f"Could not list favorite library agents for user #{user_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
) from e
@router.get("/{library_agent_id}", summary="Get Library Agent") @router.get("/{library_agent_id}", summary="Get Library Agent")
@@ -162,10 +111,6 @@ async def get_library_agent_by_graph_id(
summary="Get Agent By Store ID", summary="Get Agent By Store ID",
tags=["store", "library"], tags=["store", "library"],
response_model=library_model.LibraryAgent | None, response_model=library_model.LibraryAgent | None,
responses={
200: {"description": "Library agent found"},
404: {"description": "Agent not found"},
},
) )
async def get_library_agent_by_store_listing_version_id( async def get_library_agent_by_store_listing_version_id(
store_listing_version_id: str, store_listing_version_id: str,
@@ -174,32 +119,15 @@ async def get_library_agent_by_store_listing_version_id(
""" """
Get Library Agent from Store Listing Version ID. Get Library Agent from Store Listing Version ID.
""" """
try: return await library_db.get_library_agent_by_store_version_id(
return await library_db.get_library_agent_by_store_version_id( store_listing_version_id, user_id
store_listing_version_id, user_id )
)
except NotFoundError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
)
except Exception as e:
logger.error(f"Could not fetch library agent from store version ID: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
) from e
@router.post( @router.post(
"", "",
summary="Add Marketplace Agent", summary="Add Marketplace Agent",
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={
201: {"description": "Agent added successfully"},
404: {"description": "Store listing version not found"},
500: {"description": "Server error"},
},
) )
async def add_marketplace_agent_to_library( async def add_marketplace_agent_to_library(
store_listing_version_id: str = Body(embed=True), store_listing_version_id: str = Body(embed=True),
@@ -210,59 +138,19 @@ async def add_marketplace_agent_to_library(
) -> library_model.LibraryAgent: ) -> library_model.LibraryAgent:
""" """
Add an agent from the marketplace to the user's library. Add an agent from the marketplace to the user's library.
Args:
store_listing_version_id: ID of the store listing version to add.
user_id: ID of the authenticated user.
Returns:
library_model.LibraryAgent: Agent added to the library
Raises:
HTTPException(404): If the listing version is not found.
HTTPException(500): If a server/database error occurs.
""" """
try: agent = await library_db.add_store_agent_to_library(
agent = await library_db.add_store_agent_to_library( store_listing_version_id=store_listing_version_id,
store_listing_version_id=store_listing_version_id, user_id=user_id,
user_id=user_id, )
) if source != "onboarding":
if source != "onboarding": await complete_onboarding_step(user_id, OnboardingStep.MARKETPLACE_ADD_AGENT)
await complete_onboarding_step( return agent
user_id, OnboardingStep.MARKETPLACE_ADD_AGENT
)
return agent
except store_exceptions.AgentNotFoundError as e:
logger.warning(
f"Could not find store listing version {store_listing_version_id} "
"to add to library"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except DatabaseError as e:
logger.error(f"Database error while adding agent to library: {e}", e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"message": str(e), "hint": "Inspect DB logs for details."},
) from e
except Exception as e:
logger.error(f"Unexpected error while adding agent to library: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"message": str(e),
"hint": "Check server logs for more information.",
},
) from e
@router.patch( @router.patch(
"/{library_agent_id}", "/{library_agent_id}",
summary="Update Library Agent", summary="Update Library Agent",
responses={
200: {"description": "Agent updated successfully"},
500: {"description": "Server error"},
},
) )
async def update_library_agent( async def update_library_agent(
library_agent_id: str, library_agent_id: str,
@@ -271,52 +159,21 @@ async def update_library_agent(
) -> library_model.LibraryAgent: ) -> library_model.LibraryAgent:
""" """
Update the library agent with the given fields. Update the library agent with the given fields.
Args:
library_agent_id: ID of the library agent to update.
payload: Fields to update (auto_update_version, is_favorite, etc.).
user_id: ID of the authenticated user.
Raises:
HTTPException(500): If a server/database error occurs.
""" """
try: return await library_db.update_library_agent(
return await library_db.update_library_agent( library_agent_id=library_agent_id,
library_agent_id=library_agent_id, user_id=user_id,
user_id=user_id, auto_update_version=payload.auto_update_version,
auto_update_version=payload.auto_update_version, graph_version=payload.graph_version,
graph_version=payload.graph_version, is_favorite=payload.is_favorite,
is_favorite=payload.is_favorite, is_archived=payload.is_archived,
is_archived=payload.is_archived, settings=payload.settings,
settings=payload.settings, )
)
except NotFoundError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
) from e
except DatabaseError as e:
logger.error(f"Database error while updating library agent: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"message": str(e), "hint": "Verify DB connection."},
) from e
except Exception as e:
logger.error(f"Unexpected error while updating library agent: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"message": str(e), "hint": "Check server logs."},
) from e
@router.delete( @router.delete(
"/{library_agent_id}", "/{library_agent_id}",
summary="Delete Library Agent", summary="Delete Library Agent",
responses={
204: {"description": "Agent deleted successfully"},
404: {"description": "Agent not found"},
500: {"description": "Server error"},
},
) )
async def delete_library_agent( async def delete_library_agent(
library_agent_id: str, library_agent_id: str,
@@ -324,28 +181,11 @@ async def delete_library_agent(
) -> Response: ) -> Response:
""" """
Soft-delete the specified library agent. Soft-delete the specified library agent.
Args:
library_agent_id: ID of the library agent to delete.
user_id: ID of the authenticated user.
Returns:
204 No Content if successful.
Raises:
HTTPException(404): If the agent does not exist.
HTTPException(500): If a server/database error occurs.
""" """
try: await library_db.delete_library_agent(
await library_db.delete_library_agent( library_agent_id=library_agent_id, user_id=user_id
library_agent_id=library_agent_id, user_id=user_id )
) return Response(status_code=status.HTTP_204_NO_CONTENT)
return Response(status_code=status.HTTP_204_NO_CONTENT)
except NotFoundError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
) from e
@router.post("/{library_agent_id}/fork", summary="Fork Library Agent") @router.post("/{library_agent_id}/fork", summary="Fork Library Agent")

View File

@@ -118,21 +118,6 @@ async def test_get_library_agents_success(
) )
def test_get_library_agents_error(mocker: pytest_mock.MockFixture, test_user_id: str):
mock_db_call = mocker.patch("backend.api.features.library.db.list_library_agents")
mock_db_call.side_effect = Exception("Test error")
response = client.get("/agents?search_term=test")
assert response.status_code == 500
mock_db_call.assert_called_once_with(
user_id=test_user_id,
search_term="test",
sort_by=library_model.LibraryAgentSort.UPDATED_AT,
page=1,
page_size=15,
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_favorite_library_agents_success( async def test_get_favorite_library_agents_success(
mocker: pytest_mock.MockFixture, mocker: pytest_mock.MockFixture,
@@ -190,23 +175,6 @@ async def test_get_favorite_library_agents_success(
) )
def test_get_favorite_library_agents_error(
mocker: pytest_mock.MockFixture, test_user_id: str
):
mock_db_call = mocker.patch(
"backend.api.features.library.db.list_favorite_library_agents"
)
mock_db_call.side_effect = Exception("Test error")
response = client.get("/agents/favorites")
assert response.status_code == 500
mock_db_call.assert_called_once_with(
user_id=test_user_id,
page=1,
page_size=15,
)
def test_add_agent_to_library_success( def test_add_agent_to_library_success(
mocker: pytest_mock.MockFixture, test_user_id: str mocker: pytest_mock.MockFixture, test_user_id: str
): ):
@@ -258,19 +226,3 @@ def test_add_agent_to_library_success(
store_listing_version_id="test-version-id", user_id=test_user_id store_listing_version_id="test-version-id", user_id=test_user_id
) )
mock_complete_onboarding.assert_awaited_once() mock_complete_onboarding.assert_awaited_once()
def test_add_agent_to_library_error(mocker: pytest_mock.MockFixture, test_user_id: str):
mock_db_call = mocker.patch(
"backend.api.features.library.db.add_store_agent_to_library"
)
mock_db_call.side_effect = Exception("Test error")
response = client.post(
"/agents", json={"store_listing_version_id": "test-version-id"}
)
assert response.status_code == 500
assert "detail" in response.json() # Verify error response structure
mock_db_call.assert_called_once_with(
store_listing_version_id="test-version-id", user_id=test_user_id
)

View File

@@ -454,6 +454,7 @@ async def backfill_all_content_types(batch_size: int = 10) -> dict[str, Any]:
total_processed = 0 total_processed = 0
total_success = 0 total_success = 0
total_failed = 0 total_failed = 0
all_errors: dict[str, int] = {} # Aggregate errors across all content types
# Process content types in explicit order # Process content types in explicit order
processing_order = [ processing_order = [
@@ -499,23 +500,12 @@ async def backfill_all_content_types(batch_size: int = 10) -> dict[str, Any]:
success = sum(1 for result in results if result is True) success = sum(1 for result in results if result is True)
failed = len(results) - success failed = len(results) - success
# Aggregate unique errors to avoid Sentry spam # Aggregate errors across all content types
if failed > 0: if failed > 0:
# Group errors by type and message
error_summary: dict[str, int] = {}
for result in results: for result in results:
if isinstance(result, Exception): if isinstance(result, Exception):
error_key = f"{type(result).__name__}: {str(result)}" error_key = f"{type(result).__name__}: {str(result)}"
error_summary[error_key] = error_summary.get(error_key, 0) + 1 all_errors[error_key] = all_errors.get(error_key, 0) + 1
# Log aggregated error summary
error_details = ", ".join(
f"{error} ({count}x)" for error, count in error_summary.items()
)
logger.error(
f"{content_type.value}: {failed}/{len(results)} embeddings failed. "
f"Errors: {error_details}"
)
results_by_type[content_type.value] = { results_by_type[content_type.value] = {
"processed": len(missing_items), "processed": len(missing_items),
@@ -542,6 +532,13 @@ async def backfill_all_content_types(batch_size: int = 10) -> dict[str, Any]:
"error": str(e), "error": str(e),
} }
# Log aggregated errors once at the end
if all_errors:
error_details = ", ".join(
f"{error} ({count}x)" for error, count in all_errors.items()
)
logger.error(f"Embedding backfill errors: {error_details}")
return { return {
"by_type": results_by_type, "by_type": results_by_type,
"totals": { "totals": {

View File

@@ -261,18 +261,36 @@ async def get_onboarding_agents(
return await get_recommended_agents(user_id) return await get_recommended_agents(user_id)
class OnboardingStatusResponse(pydantic.BaseModel):
"""Response for onboarding status check."""
is_onboarding_enabled: bool
is_chat_enabled: bool
@v1_router.get( @v1_router.get(
"/onboarding/enabled", "/onboarding/enabled",
summary="Is onboarding enabled", summary="Is onboarding enabled",
tags=["onboarding", "public"], tags=["onboarding", "public"],
response_model=OnboardingStatusResponse,
) )
async def is_onboarding_enabled( async def is_onboarding_enabled(
user_id: Annotated[str, Security(get_user_id)], user_id: Annotated[str, Security(get_user_id)],
) -> bool: ) -> OnboardingStatusResponse:
# If chat is enabled for user, skip legacy onboarding # Check if chat is enabled for user
if await is_feature_enabled(Flag.CHAT, user_id, False): is_chat_enabled = await is_feature_enabled(Flag.CHAT, user_id, False)
return False
return await onboarding_enabled() # If chat is enabled, skip legacy onboarding
if is_chat_enabled:
return OnboardingStatusResponse(
is_onboarding_enabled=False,
is_chat_enabled=True,
)
return OnboardingStatusResponse(
is_onboarding_enabled=await onboarding_enabled(),
is_chat_enabled=False,
)
@v1_router.post( @v1_router.post(

View File

@@ -135,6 +135,12 @@ class GraphValidationError(ValueError):
) )
class InvalidInputError(ValueError):
"""Raised when user input validation fails (e.g., search term too long)"""
pass
class DatabaseError(Exception): class DatabaseError(Exception):
"""Raised when there is an error interacting with the database""" """Raised when there is an error interacting with the database"""

View File

@@ -2,8 +2,9 @@
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { resolveResponse, shouldShowOnboarding } from "@/app/api/helpers"; import { resolveResponse, getOnboardingStatus } from "@/app/api/helpers";
import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding"; import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
import { getHomepageRoute } from "@/lib/constants";
export default function OnboardingPage() { export default function OnboardingPage() {
const router = useRouter(); const router = useRouter();
@@ -11,10 +12,13 @@ export default function OnboardingPage() {
useEffect(() => { useEffect(() => {
async function redirectToStep() { async function redirectToStep() {
try { try {
// Check if onboarding is enabled // Check if onboarding is enabled (also gets chat flag for redirect)
const isEnabled = await shouldShowOnboarding(); const { shouldShowOnboarding, isChatEnabled } =
if (!isEnabled) { await getOnboardingStatus();
router.replace("/"); const homepageRoute = getHomepageRoute(isChatEnabled);
if (!shouldShowOnboarding) {
router.replace(homepageRoute);
return; return;
} }
@@ -22,7 +26,7 @@ export default function OnboardingPage() {
// Handle completed onboarding // Handle completed onboarding
if (onboarding.completedSteps.includes("GET_RESULTS")) { if (onboarding.completedSteps.includes("GET_RESULTS")) {
router.replace("/"); router.replace(homepageRoute);
return; return;
} }

View File

@@ -1,8 +1,9 @@
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { getHomepageRoute } from "@/lib/constants";
import BackendAPI from "@/lib/autogpt-server-api"; import BackendAPI from "@/lib/autogpt-server-api";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { shouldShowOnboarding } from "@/app/api/helpers"; import { getOnboardingStatus } from "@/app/api/helpers";
// Handle the callback to complete the user session login // Handle the callback to complete the user session login
export async function GET(request: Request) { export async function GET(request: Request) {
@@ -25,11 +26,15 @@ export async function GET(request: Request) {
const api = new BackendAPI(); const api = new BackendAPI();
await api.createUser(); await api.createUser();
if (await shouldShowOnboarding()) { // Get onboarding status from backend (includes chat flag evaluated for this user)
const { shouldShowOnboarding, isChatEnabled } =
await getOnboardingStatus();
if (shouldShowOnboarding) {
next = "/onboarding"; next = "/onboarding";
revalidatePath("/onboarding", "layout"); revalidatePath("/onboarding", "layout");
} else { } else {
revalidatePath("/", "layout"); next = getHomepageRoute(isChatEnabled);
revalidatePath(next, "layout");
} }
} catch (createUserError) { } catch (createUserError) {
console.error("Error creating user:", createUserError); console.error("Error creating user:", createUserError);

View File

@@ -1,10 +1,11 @@
"use server"; "use server";
import { getHomepageRoute } from "@/lib/constants";
import BackendAPI from "@/lib/autogpt-server-api"; import BackendAPI from "@/lib/autogpt-server-api";
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { loginFormSchema } from "@/types/auth"; import { loginFormSchema } from "@/types/auth";
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
import { shouldShowOnboarding } from "../../api/helpers"; import { getOnboardingStatus } from "../../api/helpers";
export async function login(email: string, password: string) { export async function login(email: string, password: string) {
try { try {
@@ -36,11 +37,15 @@ export async function login(email: string, password: string) {
const api = new BackendAPI(); const api = new BackendAPI();
await api.createUser(); await api.createUser();
const onboarding = await shouldShowOnboarding(); // Get onboarding status from backend (includes chat flag evaluated for this user)
const { shouldShowOnboarding, isChatEnabled } = await getOnboardingStatus();
const next = shouldShowOnboarding
? "/onboarding"
: getHomepageRoute(isChatEnabled);
return { return {
success: true, success: true,
onboarding, next,
}; };
} catch (err) { } catch (err) {
Sentry.captureException(err); Sentry.captureException(err);

View File

@@ -97,13 +97,8 @@ export function useLoginPage() {
throw new Error(result.error || "Login failed"); throw new Error(result.error || "Login failed");
} }
if (nextUrl) { // Prefer URL's next parameter, then use backend-determined route
router.replace(nextUrl); router.replace(nextUrl || result.next || homepageRoute);
} else if (result.onboarding) {
router.replace("/onboarding");
} else {
router.replace(homepageRoute);
}
} catch (error) { } catch (error) {
toast({ toast({
title: title:

View File

@@ -5,14 +5,13 @@ import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { signupFormSchema } from "@/types/auth"; import { signupFormSchema } from "@/types/auth";
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
import { isWaitlistError, logWaitlistError } from "../../api/auth/utils"; import { isWaitlistError, logWaitlistError } from "../../api/auth/utils";
import { shouldShowOnboarding } from "../../api/helpers"; import { getOnboardingStatus } from "../../api/helpers";
export async function signup( export async function signup(
email: string, email: string,
password: string, password: string,
confirmPassword: string, confirmPassword: string,
agreeToTerms: boolean, agreeToTerms: boolean,
isChatEnabled: boolean,
) { ) {
try { try {
const parsed = signupFormSchema.safeParse({ const parsed = signupFormSchema.safeParse({
@@ -59,8 +58,9 @@ export async function signup(
await supabase.auth.setSession(data.session); await supabase.auth.setSession(data.session);
} }
const isOnboardingEnabled = await shouldShowOnboarding(); // Get onboarding status from backend (includes chat flag evaluated for this user)
const next = isOnboardingEnabled const { shouldShowOnboarding, isChatEnabled } = await getOnboardingStatus();
const next = shouldShowOnboarding
? "/onboarding" ? "/onboarding"
: getHomepageRoute(isChatEnabled); : getHomepageRoute(isChatEnabled);

View File

@@ -108,7 +108,6 @@ export function useSignupPage() {
data.password, data.password,
data.confirmPassword, data.confirmPassword,
data.agreeToTerms, data.agreeToTerms,
isChatEnabled === true,
); );
setIsLoading(false); setIsLoading(false);

View File

@@ -175,9 +175,12 @@ export async function resolveResponse<
return res.data; return res.data;
} }
export async function shouldShowOnboarding() { export async function getOnboardingStatus() {
const isEnabled = await resolveResponse(getV1IsOnboardingEnabled()); const status = await resolveResponse(getV1IsOnboardingEnabled());
const onboarding = await resolveResponse(getV1OnboardingState()); const onboarding = await resolveResponse(getV1OnboardingState());
const isCompleted = onboarding.completedSteps.includes("CONGRATS"); const isCompleted = onboarding.completedSteps.includes("CONGRATS");
return isEnabled && !isCompleted; return {
shouldShowOnboarding: status.is_onboarding_enabled && !isCompleted,
isChatEnabled: status.is_chat_enabled,
};
} }

View File

@@ -3339,7 +3339,7 @@
"get": { "get": {
"tags": ["v2", "library", "private"], "tags": ["v2", "library", "private"],
"summary": "List Library Agents", "summary": "List Library Agents",
"description": "Get all agents in the user's library (both created and saved).\n\nArgs:\n user_id: ID of the authenticated user.\n search_term: Optional search term to filter agents by name/description.\n filter_by: List of filters to apply (favorites, created by user).\n sort_by: List of sorting criteria (created date, updated date).\n page: Page number to retrieve.\n page_size: Number of agents per page.\n\nReturns:\n A LibraryAgentResponse containing agents and pagination metadata.\n\nRaises:\n HTTPException: If a server/database error occurs.", "description": "Get all agents in the user's library (both created and saved).",
"operationId": "getV2List library agents", "operationId": "getV2List library agents",
"security": [{ "HTTPBearerJWT": [] }], "security": [{ "HTTPBearerJWT": [] }],
"parameters": [ "parameters": [
@@ -3394,7 +3394,7 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "List of library agents", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -3413,17 +3413,13 @@
"schema": { "$ref": "#/components/schemas/HTTPValidationError" } "schema": { "$ref": "#/components/schemas/HTTPValidationError" }
} }
} }
},
"500": {
"description": "Server error",
"content": { "application/json": {} }
} }
} }
}, },
"post": { "post": {
"tags": ["v2", "library", "private"], "tags": ["v2", "library", "private"],
"summary": "Add Marketplace Agent", "summary": "Add Marketplace Agent",
"description": "Add an agent from the marketplace to the user's library.\n\nArgs:\n store_listing_version_id: ID of the store listing version to add.\n user_id: ID of the authenticated user.\n\nReturns:\n library_model.LibraryAgent: Agent added to the library\n\nRaises:\n HTTPException(404): If the listing version is not found.\n HTTPException(500): If a server/database error occurs.", "description": "Add an agent from the marketplace to the user's library.",
"operationId": "postV2Add marketplace agent", "operationId": "postV2Add marketplace agent",
"security": [{ "HTTPBearerJWT": [] }], "security": [{ "HTTPBearerJWT": [] }],
"requestBody": { "requestBody": {
@@ -3438,7 +3434,7 @@
}, },
"responses": { "responses": {
"201": { "201": {
"description": "Agent added successfully", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "$ref": "#/components/schemas/LibraryAgent" } "schema": { "$ref": "#/components/schemas/LibraryAgent" }
@@ -3448,7 +3444,6 @@
"401": { "401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError" "$ref": "#/components/responses/HTTP401NotAuthenticatedError"
}, },
"404": { "description": "Store listing version not found" },
"422": { "422": {
"description": "Validation Error", "description": "Validation Error",
"content": { "content": {
@@ -3456,8 +3451,7 @@
"schema": { "$ref": "#/components/schemas/HTTPValidationError" } "schema": { "$ref": "#/components/schemas/HTTPValidationError" }
} }
} }
}, }
"500": { "description": "Server error" }
} }
} }
}, },
@@ -3511,7 +3505,7 @@
"get": { "get": {
"tags": ["v2", "library", "private"], "tags": ["v2", "library", "private"],
"summary": "List Favorite Library Agents", "summary": "List Favorite Library Agents",
"description": "Get all favorite agents in the user's library.\n\nArgs:\n user_id: ID of the authenticated user.\n page: Page number to retrieve.\n page_size: Number of agents per page.\n\nReturns:\n A LibraryAgentResponse containing favorite agents and pagination metadata.\n\nRaises:\n HTTPException: If a server/database error occurs.", "description": "Get all favorite agents in the user's library.",
"operationId": "getV2List favorite library agents", "operationId": "getV2List favorite library agents",
"security": [{ "HTTPBearerJWT": [] }], "security": [{ "HTTPBearerJWT": [] }],
"parameters": [ "parameters": [
@@ -3563,10 +3557,6 @@
"schema": { "$ref": "#/components/schemas/HTTPValidationError" } "schema": { "$ref": "#/components/schemas/HTTPValidationError" }
} }
} }
},
"500": {
"description": "Server error",
"content": { "application/json": {} }
} }
} }
} }
@@ -3588,7 +3578,7 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Library agent found", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -3604,7 +3594,6 @@
"401": { "401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError" "$ref": "#/components/responses/HTTP401NotAuthenticatedError"
}, },
"404": { "description": "Agent not found" },
"422": { "422": {
"description": "Validation Error", "description": "Validation Error",
"content": { "content": {
@@ -3620,7 +3609,7 @@
"delete": { "delete": {
"tags": ["v2", "library", "private"], "tags": ["v2", "library", "private"],
"summary": "Delete Library Agent", "summary": "Delete Library Agent",
"description": "Soft-delete the specified library agent.\n\nArgs:\n library_agent_id: ID of the library agent to delete.\n user_id: ID of the authenticated user.\n\nReturns:\n 204 No Content if successful.\n\nRaises:\n HTTPException(404): If the agent does not exist.\n HTTPException(500): If a server/database error occurs.", "description": "Soft-delete the specified library agent.",
"operationId": "deleteV2Delete library agent", "operationId": "deleteV2Delete library agent",
"security": [{ "HTTPBearerJWT": [] }], "security": [{ "HTTPBearerJWT": [] }],
"parameters": [ "parameters": [
@@ -3636,11 +3625,9 @@
"description": "Successful Response", "description": "Successful Response",
"content": { "application/json": { "schema": {} } } "content": { "application/json": { "schema": {} } }
}, },
"204": { "description": "Agent deleted successfully" },
"401": { "401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError" "$ref": "#/components/responses/HTTP401NotAuthenticatedError"
}, },
"404": { "description": "Agent not found" },
"422": { "422": {
"description": "Validation Error", "description": "Validation Error",
"content": { "content": {
@@ -3648,8 +3635,7 @@
"schema": { "$ref": "#/components/schemas/HTTPValidationError" } "schema": { "$ref": "#/components/schemas/HTTPValidationError" }
} }
} }
}, }
"500": { "description": "Server error" }
} }
}, },
"get": { "get": {
@@ -3690,7 +3676,7 @@
"patch": { "patch": {
"tags": ["v2", "library", "private"], "tags": ["v2", "library", "private"],
"summary": "Update Library Agent", "summary": "Update Library Agent",
"description": "Update the library agent with the given fields.\n\nArgs:\n library_agent_id: ID of the library agent to update.\n payload: Fields to update (auto_update_version, is_favorite, etc.).\n user_id: ID of the authenticated user.\n\nRaises:\n HTTPException(500): If a server/database error occurs.", "description": "Update the library agent with the given fields.",
"operationId": "patchV2Update library agent", "operationId": "patchV2Update library agent",
"security": [{ "HTTPBearerJWT": [] }], "security": [{ "HTTPBearerJWT": [] }],
"parameters": [ "parameters": [
@@ -3713,7 +3699,7 @@
}, },
"responses": { "responses": {
"200": { "200": {
"description": "Agent updated successfully", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "$ref": "#/components/schemas/LibraryAgent" } "schema": { "$ref": "#/components/schemas/LibraryAgent" }
@@ -3730,8 +3716,7 @@
"schema": { "$ref": "#/components/schemas/HTTPValidationError" } "schema": { "$ref": "#/components/schemas/HTTPValidationError" }
} }
} }
}, }
"500": { "description": "Server error" }
} }
} }
}, },
@@ -4540,8 +4525,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "boolean", "$ref": "#/components/schemas/OnboardingStatusResponse"
"title": "Response Getv1Is Onboarding Enabled"
} }
} }
} }
@@ -8745,6 +8729,19 @@
"title": "OAuthApplicationPublicInfo", "title": "OAuthApplicationPublicInfo",
"description": "Public information about an OAuth application (for consent screen)" "description": "Public information about an OAuth application (for consent screen)"
}, },
"OnboardingStatusResponse": {
"properties": {
"is_onboarding_enabled": {
"type": "boolean",
"title": "Is Onboarding Enabled"
},
"is_chat_enabled": { "type": "boolean", "title": "Is Chat Enabled" }
},
"type": "object",
"required": ["is_onboarding_enabled", "is_chat_enabled"],
"title": "OnboardingStatusResponse",
"description": "Response for onboarding status check."
},
"OnboardingStep": { "OnboardingStep": {
"type": "string", "type": "string",
"enum": [ "enum": [