feat(platform/builder): Hide action buttons on triggered graphs (#10218)

- Resolves #10217


https://github.com/user-attachments/assets/26a402f5-6f43-453b-8c83-481380bde853

### Changes 🏗️

Frontend:
- Show message instead of action buttons ("Run" etc) when graph has
webhook node(s)
- Fix check for webhook nodes used in `BlocksControl` and `FlowEditor`
- Clean up `PrimaryActionBar` implementation
  - Add `accent` variant to `ui/button:Button`

API:
- Add `GET /library/agents/by-graph/{graph_id}` endpoint

### 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:
  - Go to Builder
  - Add a trigger block
  - [x] -> action buttons disappear; message shows in their place
  - Save the graph
  - Click the "Agent Library" link in the message
- [x] -> app navigates to `/library/agents/[id]` for the newly created
agent
This commit is contained in:
Reinier van der Leer
2025-06-30 09:33:33 +01:00
committed by GitHub
parent b5c7f381c1
commit f3202fa776
8 changed files with 210 additions and 117 deletions

View File

@@ -215,6 +215,32 @@ async def get_library_agent_by_store_version_id(
return None
async def get_library_agent_by_graph_id(
user_id: str,
graph_id: str,
graph_version: Optional[int] = None,
) -> library_model.LibraryAgent | None:
try:
filter: prisma.types.LibraryAgentWhereInput = {
"agentGraphId": graph_id,
"userId": user_id,
"isDeleted": False,
}
if graph_version is not None:
filter["agentGraphVersion"] = graph_version
agent = await prisma.models.LibraryAgent.prisma().find_first(
where=filter,
include=library_agent_include(user_id),
)
if not agent:
return None
return library_model.LibraryAgent.from_db(agent)
except prisma.errors.PrismaError as e:
logger.error(f"Database error fetching library agent by graph ID: {e}")
raise store_exceptions.DatabaseError("Failed to fetch library agent") from e
async def add_generated_agent_image(
graph: graph_db.GraphModel,
library_agent_id: str,

View File

@@ -92,6 +92,23 @@ async def get_library_agent(
return await library_db.get_library_agent(id=library_agent_id, user_id=user_id)
@router.get("/by-graph/{graph_id}")
async def get_library_agent_by_graph_id(
graph_id: str,
version: Optional[int] = Query(default=None),
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
) -> library_model.LibraryAgent:
library_agent = await library_db.get_library_agent_by_graph_id(
user_id, graph_id, version
)
if not library_agent:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Library agent for graph #{graph_id} and user #{user_id} not found",
)
return library_agent
@router.get(
"/marketplace/{store_listing_version_id}",
summary="Get Agent By Store ID",

View File

@@ -14,11 +14,13 @@ export default function BuilderPage() {
completeStep("BUILDER_OPEN");
}, [completeStep]);
const _graphVersion = query.get("flowVersion");
const graphVersion = _graphVersion ? parseInt(_graphVersion) : undefined
return (
<FlowEditor
className="flow-container"
flowID={query.get("flowID") as GraphID | null ?? undefined}
flowVersion={query.get("flowVersion") ?? undefined}
flowVersion={graphVersion}
/>
);
}

View File

@@ -4,10 +4,12 @@ import React, {
useState,
useCallback,
useEffect,
useMemo,
useRef,
MouseEvent,
Suspense,
} from "react";
import Link from "next/link";
import {
ReactFlow,
ReactFlowProvider,
@@ -32,7 +34,9 @@ import {
formatEdgeID,
GraphExecutionID,
GraphID,
LibraryAgent,
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { getTypeColor, findNewlyAddedBlockCoordinates } from "@/lib/utils";
import { history } from "./history";
import { CustomEdge } from "./CustomEdge";
@@ -41,6 +45,7 @@ import { Control, ControlPanel } from "@/components/edit/control/ControlPanel";
import { SaveControl } from "@/components/edit/control/SaveControl";
import { BlocksControl } from "@/components/edit/control/BlocksControl";
import { IconUndo2, IconRedo2 } from "@/components/ui/icons";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { startTutorial } from "./tutorial";
import useAgentGraph from "@/hooks/useAgentGraph";
import { v4 as uuidv4 } from "uuid";
@@ -48,11 +53,11 @@ import { useRouter, usePathname, useSearchParams } from "next/navigation";
import RunnerUIWrapper, {
RunnerUIWrapperRef,
} from "@/components/RunnerUIWrapper";
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
import PrimaryActionBar from "@/components/PrimaryActionButton";
import OttoChatWidget from "@/components/OttoChatWidget";
import { useToast } from "@/components/ui/use-toast";
import { useCopyPaste } from "../hooks/useCopyPaste";
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
// This is for the history, this is the minimum distance a block must move before it is logged
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
@@ -77,7 +82,7 @@ export const FlowContext = createContext<FlowContextType | null>(null);
const FlowEditor: React.FC<{
flowID?: GraphID;
flowVersion?: string;
flowVersion?: number;
className?: string;
}> = ({ flowID, flowVersion, className }) => {
const {
@@ -118,10 +123,24 @@ const FlowEditor: React.FC<{
setEdges,
} = useAgentGraph(
flowID,
flowVersion ? parseInt(flowVersion) : undefined,
flowVersion,
flowExecutionID,
visualizeBeads !== "no",
);
const api = useBackendAPI();
const [libraryAgent, setLibraryAgent] = useState<LibraryAgent | null>(null);
useEffect(() => {
if (!flowID) return;
api
.getLibraryAgentByGraphID(flowID, flowVersion)
.then((libraryAgent) => setLibraryAgent(libraryAgent))
.catch((error) => {
console.warn(
`Failed to fetch LibraryAgent for graph #${flowID} v${flowVersion}`,
error,
);
});
}, [api, flowID, flowVersion]);
const router = useRouter();
const pathname = usePathname();
@@ -154,6 +173,16 @@ const FlowEditor: React.FC<{
: `Builder - AutoGPT Platform`;
}, [savedAgent]);
const graphHasWebhookNodes = useMemo(
() =>
nodes.some((n) =>
[BlockUIType.WEBHOOK, BlockUIType.WEBHOOK_MANUAL].includes(
n.data.uiType,
),
),
[nodes],
);
useEffect(() => {
if (params.get("resetTutorial") === "true") {
localStorage.removeItem(TUTORIAL_STORAGE_KEY);
@@ -707,36 +736,62 @@ const FlowEditor: React.FC<{
/>
}
></ControlPanel>
<PrimaryActionBar
className="absolute bottom-0 left-1/2 z-20 -translate-x-1/2"
onClickAgentOutputs={() => runnerUIRef.current?.openRunnerOutput()}
onClickRunAgent={() => {
if (!savedAgent) {
toast({
title: `Please save the agent using the button in the left sidebar before running it.`,
duration: 2000,
});
return;
}
if (!isRunning) {
runnerUIRef.current?.runOrOpenInput();
} else {
requestStopRun();
}
}}
onClickScheduleButton={handleScheduleButton}
isScheduling={isScheduling}
isDisabled={!savedAgent}
isRunning={isRunning}
requestStopRun={requestStopRun}
runAgentTooltip={!isRunning ? "Run Agent" : "Stop Agent"}
/>
<CronSchedulerDialog
open={openCron}
setOpen={setOpenCron}
afterCronCreation={afterCronCreation}
defaultScheduleName={agentName}
/>
{!graphHasWebhookNodes ? (
<>
<PrimaryActionBar
className="absolute bottom-0 left-1/2 z-20 -translate-x-1/2"
onClickAgentOutputs={() =>
runnerUIRef.current?.openRunnerOutput()
}
onClickRunAgent={() => {
if (isRunning) return;
if (!savedAgent) {
toast({
title: `Please save the agent using the button in the left sidebar before running it.`,
duration: 2000,
});
return;
}
runnerUIRef.current?.runOrOpenInput();
}}
onClickStopRun={requestStopRun}
onClickScheduleButton={handleScheduleButton}
isScheduling={isScheduling}
isDisabled={!savedAgent}
isRunning={isRunning}
/>
<CronSchedulerDialog
afterCronCreation={afterCronCreation}
open={openCron}
setOpen={setOpenCron}
defaultScheduleName={agentName}
/>
</>
) : (
<Alert className="absolute bottom-4 left-1/2 z-20 w-auto -translate-x-1/2 select-none">
<AlertTitle>You are building a Trigger Agent</AlertTitle>
<AlertDescription>
Your agent{" "}
{savedAgent?.nodes.some((node) => node.webhook)
? "is listening"
: "will listen"}{" "}
for its trigger and will run when the time is right.
<br />
You can view its activity in your
<Link
href={
libraryAgent
? `/library/agents/${libraryAgent.id}`
: "/library"
}
className="underline"
>
Agent Library
</Link>
.
</AlertDescription>
</Alert>
)}
</ReactFlow>
</div>
<RunnerUIWrapper

View File

@@ -4,41 +4,30 @@ import { Button } from "@/components/ui/button";
import { FaSpinner } from "react-icons/fa";
import { Clock, LogOut } from "lucide-react";
import { IconPlay, IconSquare } from "@/components/ui/icons";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface PrimaryActionBarProps {
onClickAgentOutputs: () => void;
onClickRunAgent: () => void;
onClickScheduleButton: () => void;
onClickRunAgent?: () => void;
onClickStopRun: () => void;
onClickScheduleButton?: () => void;
isRunning: boolean;
isDisabled: boolean;
isScheduling: boolean;
requestStopRun: () => void;
runAgentTooltip: string;
className?: string;
}
const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
onClickAgentOutputs,
onClickRunAgent,
onClickStopRun,
onClickScheduleButton,
isRunning,
isDisabled,
isScheduling,
requestStopRun,
runAgentTooltip,
className,
}) => {
const runButtonLabel = !isRunning ? "Run" : "Stop";
const runButtonIcon = !isRunning ? <IconPlay /> : <IconSquare />;
const runButtonOnClick = !isRunning ? onClickRunAgent : requestStopRun;
const buttonClasses =
"flex items-center gap-2 text-sm font-medium md:text-lg";
return (
<div
className={cn(
@@ -47,70 +36,64 @@ const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
)}
>
<div className="flex gap-1 md:gap-4">
<Tooltip key="ViewOutputs" delayDuration={500}>
<TooltipTrigger asChild>
<Button
className="flex items-center gap-2"
onClick={onClickAgentOutputs}
size="primary"
variant="outline"
>
<LogOut className="hidden h-5 w-5 md:flex" />
<span className="text-sm font-medium md:text-lg">
Agent Outputs{" "}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>View agent outputs</p>
</TooltipContent>
</Tooltip>
<Tooltip key="RunAgent" delayDuration={500}>
<TooltipTrigger asChild>
<Button
className="flex items-center gap-2"
onClick={runButtonOnClick}
size="primary"
style={{
background: isRunning ? "#DF4444" : "#7544DF",
opacity: isDisabled ? 0.5 : 1,
}}
data-id="primary-action-run-agent"
>
{runButtonIcon}
<span className="text-sm font-medium md:text-lg">
{runButtonLabel}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{runAgentTooltip}</p>
</TooltipContent>
</Tooltip>
<Tooltip key="ScheduleAgent" delayDuration={500}>
<TooltipTrigger asChild>
<Button
className="flex items-center gap-2"
onClick={onClickScheduleButton}
size="primary"
disabled={isScheduling}
variant="outline"
data-id="primary-action-schedule-agent"
>
{isScheduling ? (
<FaSpinner className="animate-spin" />
) : (
<Clock className="hidden h-5 w-5 md:flex" />
)}
<span className="text-sm font-medium md:text-lg">
Schedule Run
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Schedule this Agent</p>
</TooltipContent>
</Tooltip>
<Button
className={buttonClasses}
variant="outline"
size="primary"
onClick={onClickAgentOutputs}
title="View agent outputs"
>
<LogOut className="hidden size-5 md:flex" /> Agent Outputs
</Button>
{!isRunning ? (
<Button
className={cn(
buttonClasses,
onClickRunAgent && isDisabled
? "cursor-default opacity-50 hover:bg-accent"
: "",
)}
variant="accent"
size="primary"
onClick={onClickRunAgent}
disabled={!onClickRunAgent}
title="Run the agent"
data-id="primary-action-run-agent"
>
<IconPlay /> Run
</Button>
) : (
<Button
className={buttonClasses}
variant="destructive"
size="primary"
onClick={onClickStopRun}
title="Stop the agent"
data-id="primary-action-stop-agent"
>
<IconSquare /> Stop
</Button>
)}
{onClickScheduleButton && (
<Button
className={buttonClasses}
variant="outline"
size="primary"
onClick={onClickScheduleButton}
disabled={isScheduling}
title="Set up a run schedule for the agent"
data-id="primary-action-schedule-agent"
>
{isScheduling ? (
<FaSpinner className="animate-spin" />
) : (
<Clock className="hidden h-5 w-5 md:flex" />
)}
Schedule Run
</Button>
)}
</div>
</div>
);

View File

@@ -55,8 +55,8 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const graphHasWebhookNodes = nodes.some(
(n) => n.data.uiType == BlockUIType.WEBHOOK,
const graphHasWebhookNodes = nodes.some((n) =>
[BlockUIType.WEBHOOK, BlockUIType.WEBHOOK_MANUAL].includes(n.data.uiType),
);
const graphHasInputNodes = nodes.some(
(n) => n.data.uiType == BlockUIType.INPUT,

View File

@@ -13,6 +13,7 @@ const buttonVariants = cva(
"bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90",
destructive:
"bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90",
accent: "bg-accent text-accent-foreground hover:bg-violet-500",
outline:
"border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
secondary:

View File

@@ -635,6 +635,15 @@ export default class BackendAPI {
return this._get(`/library/agents/marketplace/${storeListingVersionId}`);
}
getLibraryAgentByGraphID(
graphID: GraphID,
graphVersion?: number,
): Promise<LibraryAgent> {
return this._get(`/library/agents/by-graph/${graphID}`, {
version: graphVersion,
});
}
addMarketplaceAgentToLibrary(
storeListingVersionID: string,
): Promise<LibraryAgent> {