From 6dc767aafaaa24d87f232cf07135ae6cf97c805f Mon Sep 17 00:00:00 2001 From: Bentlybro Date: Wed, 7 Jan 2026 14:17:37 +0000 Subject: [PATCH] Improve admin LLM registry UX and error handling Adds user feedback and error handling to LLM registry modals (add/edit creator, model, provider) in the admin UI, including loading states and error messages. Ensures atomic updates for model costs in the backend using transactions. Improves display of creator website URLs and handles the case where no LLM models are available in analytics config. Updates icon usage and removes unnecessary 'use server' directive. --- .../admin/execution_analytics_routes.py | 15 ++++++ .../backend/executor/llm_registry_init.py | 7 +-- .../backend/backend/server/v2/llm/db.py | 32 ++++++----- .../src/app/(platform)/admin/layout.tsx | 7 ++- .../admin/llms/components/AddCreatorModal.tsx | 46 ++++++++++++---- .../admin/llms/components/AddModelModal.tsx | 47 ++++++++++++---- .../llms/components/AddProviderModal.tsx | 48 +++++++++++++---- .../admin/llms/components/CreatorsTable.tsx | 54 ++++++++++++++----- .../llms/components/DeleteModelModal.tsx | 1 + .../src/app/(platform)/admin/llms/page.tsx | 1 - 10 files changed, 187 insertions(+), 71 deletions(-) diff --git a/autogpt_platform/backend/backend/api/features/admin/execution_analytics_routes.py b/autogpt_platform/backend/backend/api/features/admin/execution_analytics_routes.py index 13d5662d82..6fbc087826 100644 --- a/autogpt_platform/backend/backend/api/features/admin/execution_analytics_routes.py +++ b/autogpt_platform/backend/backend/api/features/admin/execution_analytics_routes.py @@ -201,6 +201,21 @@ async def get_execution_analytics_config( # Sort models by provider and name for better UX available_models.sort(key=lambda x: (x.provider, x.label)) + # Handle case where no models are available + if not available_models: + logger.warning( + "No enabled LLM models found in registry. " + "Ensure models are configured and enabled in the LLM Registry." + ) + # Provide a placeholder entry so admins see meaningful feedback + available_models.append( + ModelInfo( + value="", + label="No models available - configure in LLM Registry", + provider="none", + ) + ) + return ExecutionAnalyticsConfig( available_models=available_models, default_system_prompt=DEFAULT_SYSTEM_PROMPT, diff --git a/autogpt_platform/backend/backend/executor/llm_registry_init.py b/autogpt_platform/backend/backend/executor/llm_registry_init.py index 92b8dc01bf..0779e932da 100644 --- a/autogpt_platform/backend/backend/executor/llm_registry_init.py +++ b/autogpt_platform/backend/backend/executor/llm_registry_init.py @@ -27,12 +27,7 @@ async def initialize_registry_for_executor() -> None: await db.connect() logger.info("[GraphExecutor] Connected to database for registry refresh") - # Refresh LLM registry before initializing blocks - await llm_registry.refresh_llm_registry() - refresh_llm_costs() - logger.info("[GraphExecutor] LLM registry refreshed") - - # Initialize blocks (also refreshes registry, but we do it explicitly above) + # Initialize blocks (internally refreshes LLM registry and costs) await initialize_blocks() logger.info("[GraphExecutor] Blocks initialized") except Exception as exc: diff --git a/autogpt_platform/backend/backend/server/v2/llm/db.py b/autogpt_platform/backend/backend/server/v2/llm/db.py index ad3ee30082..894fcbf256 100644 --- a/autogpt_platform/backend/backend/server/v2/llm/db.py +++ b/autogpt_platform/backend/backend/server/v2/llm/db.py @@ -252,24 +252,22 @@ async def update_model( # If we have costs to update, we need to handle them separately # because nested writes have different constraints if request.costs is not None: - # First update scalar fields - if scalar_data: - await prisma.models.LlmModel.prisma().update( - where={"id": model_id}, - data=scalar_data, - ) - # Then handle costs: delete existing and create new - await prisma.models.LlmModelCost.prisma().delete_many( - where={"llmModelId": model_id} - ) - if request.costs: - cost_payload = _cost_create_payload(request.costs) - for cost_item in cost_payload["create"]: - cost_item["llmModelId"] = model_id - await prisma.models.LlmModelCost.prisma().create( - data=cast(Any, cost_item) + # Wrap cost replacement in a transaction for atomicity + async with transaction() as tx: + # First update scalar fields + if scalar_data: + await tx.llmmodel.update( + where={"id": model_id}, + data=scalar_data, ) - # Fetch the updated record + # Then handle costs: delete existing and create new + await tx.llmmodelcost.delete_many(where={"llmModelId": model_id}) + if request.costs: + cost_payload = _cost_create_payload(request.costs) + for cost_item in cost_payload["create"]: + cost_item["llmModelId"] = model_id + await tx.llmmodelcost.create(data=cast(Any, cost_item)) + # Fetch the updated record (outside transaction) record = await prisma.models.LlmModel.prisma().find_unique( where={"id": model_id}, include={"Costs": True, "Creator": True}, diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx index 52778393ca..c024b45540 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx @@ -1,5 +1,8 @@ +"use client"; + import { Sidebar } from "@/components/__legacy__/Sidebar"; -import { Users, DollarSign, UserSearch, FileText, Cpu } from "lucide-react"; +import { Users, DollarSign, UserSearch, FileText } from "lucide-react"; +import { Cpu } from "@phosphor-icons/react"; import { IconSliders } from "@/components/__legacy__/ui/icons"; @@ -29,7 +32,7 @@ const sidebarLinkGroups = [ { text: "LLM Registry", href: "/admin/llms", - icon: , + icon: , }, { text: "Admin User Management", diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddCreatorModal.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddCreatorModal.tsx index 81914b0196..ab84c07092 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddCreatorModal.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddCreatorModal.tsx @@ -8,8 +8,24 @@ import { useRouter } from "next/navigation"; export function AddCreatorModal() { const [open, setOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); const router = useRouter(); + async function handleSubmit(formData: FormData) { + setIsSubmitting(true); + setError(null); + try { + await createLlmCreatorAction(formData); + setOpen(false); + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create creator"); + } finally { + setIsSubmitting(false); + } + } + return ( -
{ - await createLlmCreatorAction(formData); - setOpen(false); - router.refresh(); - }} - className="space-y-4" - > +
+ {error && ( +
+ {error} +
+ )} + - diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddModelModal.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddModelModal.tsx index cdecf3fd32..9d94e9850c 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddModelModal.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddModelModal.tsx @@ -16,8 +16,24 @@ interface Props { export function AddModelModal({ providers, creators }: Props) { const [open, setOpen] = useState(false); const [selectedCreatorId, setSelectedCreatorId] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); const router = useRouter(); + async function handleSubmit(formData: FormData) { + setIsSubmitting(true); + setError(null); + try { + await createLlmModelAction(formData); + setOpen(false); + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create model"); + } finally { + setIsSubmitting(false); + } + } + // When provider changes, auto-select matching creator if one exists function handleProviderChange(providerId: string) { const provider = providers.find((p) => p.id === providerId); @@ -49,14 +65,7 @@ export function AddModelModal({ providers, creators }: Props) { Register a new model slug, metadata, and pricing.
-
{ - await createLlmModelAction(formData); - setOpen(false); - router.refresh(); - }} - className="space-y-6" - > + {/* Basic Information */}
@@ -239,6 +248,7 @@ export function AddModelModal({ providers, creators }: Props) { required type="number" name="credit_cost" + step="1" className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" placeholder="5" min={0} @@ -269,17 +279,32 @@ export function AddModelModal({ providers, creators }: Props) {
+ {error && ( +
+ {error} +
+ )} + - diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddProviderModal.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddProviderModal.tsx index cb664b298d..4e771b1d81 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddProviderModal.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/AddProviderModal.tsx @@ -8,8 +8,26 @@ import { useRouter } from "next/navigation"; export function AddProviderModal() { const [open, setOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); const router = useRouter(); + async function handleSubmit(formData: FormData) { + setIsSubmitting(true); + setError(null); + try { + await createLlmProviderAction(formData); + setOpen(false); + router.refresh(); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to create provider", + ); + } finally { + setIsSubmitting(false); + } + } + return (
-
{ - await createLlmProviderAction(formData); - setOpen(false); - router.refresh(); - }} - className="space-y-6" - > + {/* Basic Information */}
@@ -222,17 +233,32 @@ export function AddProviderModal() {
+ {error && ( +
+ {error} +
+ )} + -
diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/CreatorsTable.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/CreatorsTable.tsx index de6ad5a116..e66992e8a7 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/CreatorsTable.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/CreatorsTable.tsx @@ -58,7 +58,13 @@ export function CreatorsTable({ creators }: { creators: LlmModelCreator[] }) { rel="noopener noreferrer" className="text-sm text-primary hover:underline" > - {new URL(creator.website_url).hostname} + {(() => { + try { + return new URL(creator.website_url).hostname; + } catch { + return creator.website_url; + } + })()} ) : ( @@ -80,8 +86,24 @@ export function CreatorsTable({ creators }: { creators: LlmModelCreator[] }) { function EditCreatorModal({ creator }: { creator: LlmModelCreator }) { const [open, setOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); const router = useRouter(); + async function handleSubmit(formData: FormData) { + setIsSubmitting(true); + setError(null); + try { + await updateLlmCreatorAction(formData); + setOpen(false); + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to update creator"); + } finally { + setIsSubmitting(false); + } + } + return ( -
{ - await updateLlmCreatorAction(formData); - setOpen(false); - router.refresh(); - }} - className="space-y-4" - > +
@@ -145,17 +160,32 @@ function EditCreatorModal({ creator }: { creator: LlmModelCreator }) { />
+ {error && ( +
+ {error} +
+ )} + -
diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/DeleteModelModal.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/DeleteModelModal.tsx index 8b975d89b2..ef762f5251 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/DeleteModelModal.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/admin/llms/components/DeleteModelModal.tsx @@ -147,6 +147,7 @@ export function DeleteModelModal({