From eb1e53aba36d7f6dda39f7f40eb1783749c28106 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Mon, 4 Nov 2024 20:33:31 +0700 Subject: [PATCH] feat(platform): Display error message on save failure --- .../backend/backend/server/rest_api.py | 44 ++++++++++++------- .../frontend/src/components/ui/toast.tsx | 2 +- .../frontend/src/hooks/useAgentGraph.ts | 22 +++++++++- .../src/lib/autogpt-server-api/baseClient.ts | 2 +- 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/autogpt_platform/backend/backend/server/rest_api.py b/autogpt_platform/backend/backend/server/rest_api.py index 77484419da..07d55a0582 100644 --- a/autogpt_platform/backend/backend/server/rest_api.py +++ b/autogpt_platform/backend/backend/server/rest_api.py @@ -1,9 +1,10 @@ import contextlib +import logging import typing import fastapi -import fastapi.middleware.cors import fastapi.responses +import starlette.middleware.cors import uvicorn import backend.data.block @@ -14,6 +15,7 @@ import backend.util.service import backend.util.settings settings = backend.util.settings.Settings() +logger = logging.getLogger(__name__) @contextlib.asynccontextmanager @@ -25,6 +27,21 @@ async def lifespan_context(app: fastapi.FastAPI): await backend.data.db.disconnect() +def handle_internal_http_error(status_code: int = 500, log_error: bool = True): + def handler(request: fastapi.Request, exc: Exception): + if log_error: + logger.exception(f"{request.method} {request.url.path} failed: {exc}") + return fastapi.responses.JSONResponse( + content={ + "message": f"{request.method} {request.url.path} failed", + "detail": str(exc), + }, + status_code=status_code, + ) + + return handler + + docs_url = ( "/docs" if settings.config.app_env == backend.util.settings.AppEnvironment.LOCAL @@ -43,14 +60,9 @@ app = fastapi.FastAPI( docs_url=docs_url, ) +app.add_exception_handler(ValueError, handle_internal_http_error(400)) +app.add_exception_handler(500, handle_internal_http_error(500)) app.include_router(backend.server.routers.v1.v1_router, tags=["v1"]) -app.add_middleware( - fastapi.middleware.cors.CORSMiddleware, - allow_origins=settings.config.backend_cors_allow_origins, - allow_credentials=True, - allow_methods=["*"], # Allows all methods - allow_headers=["*"], # Allows all headers -) @app.get(path="/health", tags=["health"], dependencies=[]) @@ -58,15 +70,13 @@ async def health(): return {"status": "healthy"} -@app.exception_handler(Exception) -def handle_internal_http_error(request: fastapi.Request, exc: Exception): - return fastapi.responses.JSONResponse( - content={ - "message": f"{request.method} {request.url.path} failed", - "error": str(exc), - }, - status_code=500, - ) +app = starlette.middleware.cors.CORSMiddleware( + app=app, + allow_origins=settings.config.backend_cors_allow_origins, + allow_credentials=True, + allow_methods=["*"], # Allows all methods + allow_headers=["*"], # Allows all headers +) class AgentServer(backend.util.service.AppProcess): diff --git a/autogpt_platform/frontend/src/components/ui/toast.tsx b/autogpt_platform/frontend/src/components/ui/toast.tsx index d651f71a53..78a6407e4a 100644 --- a/autogpt_platform/frontend/src/components/ui/toast.tsx +++ b/autogpt_platform/frontend/src/components/ui/toast.tsx @@ -25,7 +25,7 @@ const ToastViewport = React.forwardRef< ToastViewport.displayName = ToastPrimitives.Viewport.displayName; const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-neutral-200 p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-neutral-800", + "whitespace-pre-line group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-neutral-200 p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-neutral-800", { variants: { variant: { diff --git a/autogpt_platform/frontend/src/hooks/useAgentGraph.ts b/autogpt_platform/frontend/src/hooks/useAgentGraph.ts index 5898b71bdd..641b1c2e5e 100644 --- a/autogpt_platform/frontend/src/hooks/useAgentGraph.ts +++ b/autogpt_platform/frontend/src/hooks/useAgentGraph.ts @@ -17,6 +17,7 @@ import { Connection, MarkerType } from "@xyflow/react"; import Ajv from "ajv"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useRouter, useSearchParams, usePathname } from "next/navigation"; +import { useToast } from "@/components/ui/use-toast"; const ajv = new Ajv({ strict: false, allErrors: true }); @@ -25,6 +26,7 @@ export default function useAgentGraph( template?: boolean, passDataToBeads?: boolean, ) { + const { toast } = useToast(); const [router, searchParams, pathname] = [ useRouter(), useSearchParams(), @@ -588,7 +590,8 @@ export default function useAgentGraph( [availableNodes], ); - const saveAgent = useCallback( + const _saveAgent = ( + () => async (asTemplate: boolean = false) => { //FIXME frontend ids should be resolved better (e.g. returned from the server) // currently this relays on block_id and position @@ -742,6 +745,23 @@ export default function useAgentGraph( }, })); }); + } + )(); + + const saveAgent = useCallback( + async (asTemplate: boolean = false) => { + try { + await _saveAgent(asTemplate); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error("Error saving agent", error); + toast({ + variant: "destructive", + title: "Error saving agent", + description: errorMessage, + }); + } }, [ api, diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/baseClient.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/baseClient.ts index 3d42ab1cc9..99c4102e34 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/baseClient.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/baseClient.ts @@ -287,7 +287,7 @@ export default class BaseAutoGPTServerAPI { errorDetail = response.statusText; } - throw new Error(`HTTP error ${response.status}! ${errorDetail}`); + throw new Error(errorDetail); } // Handle responses with no content (like DELETE requests)