feat(library): Create presets from runs (#10823)

- Resolves #9307

### Changes 🏗️

- feat(library): Create presets from runs
  - Prevent creating preset from run with unknown credentials
- Fix running presets with credentials
  - Add `credential_inputs` parameter to `execute_preset` endpoint

API:
- Return `GraphExecutionMeta` from `*/execute` endpoints

### 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 `/library/agents/[id]` for an agent that *does not* require
credentials
- Click the menu on any run and select "Pin as a preset"; fill out the
dialog and submit
    - [x] -> UI works
    - [x] -> Operation succeeds and dialog closes
    - [x] -> New preset is shown at the top of the runs list
- Go to `/library/agents/[id]` for an agent that *does* require
credentials
- Click the menu on any run and select "Pin as a preset"; fill out the
dialog and submit
    - [x] -> UI works
    - [x] -> Error toast appears with descriptive message
- Initiate a new run; once finished, click "Create preset from run";
fill out the dialog and submit
    - [x] -> UI works
    - [x] -> Operation succeeds and dialog closes
    - [x] -> New preset is shown at the top of the runs list
This commit is contained in:
Reinier van der Leer
2025-09-03 02:26:12 +01:00
committed by GitHub
parent e16e69ca55
commit 4928ce3f90
23 changed files with 312 additions and 120 deletions

View File

@@ -35,20 +35,19 @@ async def execute_graph(
logger.info("Input data: %s", input_data)
# --- Test adding new executions --- #
response = await agent_server.test_execute_graph(
graph_exec = await agent_server.test_execute_graph(
user_id=test_user.id,
graph_id=test_graph.id,
graph_version=test_graph.version,
node_input=input_data,
)
graph_exec_id = response.graph_exec_id
logger.info("Created execution with ID: %s", graph_exec_id)
logger.info("Created execution with ID: %s", graph_exec.id)
# Execution queue should be empty
logger.info("Waiting for execution to complete...")
result = await wait_execution(test_user.id, graph_exec_id, 30)
result = await wait_execution(test_user.id, graph_exec.id, 30)
logger.info("Execution completed with %d results", len(result))
return graph_exec_id
return graph_exec.id
@pytest.mark.asyncio(loop_scope="session")

View File

@@ -35,21 +35,20 @@ async def execute_graph(
logger.info(f"Input data: {input_data}")
# --- Test adding new executions --- #
response = await agent_server.test_execute_graph(
graph_exec = await agent_server.test_execute_graph(
user_id=test_user.id,
graph_id=test_graph.id,
graph_version=test_graph.version,
node_input=input_data,
)
graph_exec_id = response.graph_exec_id
logger.info(f"Created execution with ID: {graph_exec_id}")
logger.info(f"Created execution with ID: {graph_exec.id}")
# Execution queue should be empty
logger.info("Waiting for execution to complete...")
result = await wait_execution(test_user.id, graph_exec_id, 30)
result = await wait_execution(test_user.id, graph_exec.id, 30)
logger.info(f"Execution completed with {len(result)} results")
assert len(result) == num_execs
return graph_exec_id
return graph_exec.id
async def assert_sample_graph_executions(
@@ -379,7 +378,7 @@ async def test_execute_preset(server: SpinTestServer):
# Verify execution
assert result is not None
graph_exec_id = result["id"]
graph_exec_id = result.id
# Wait for execution to complete
executions = await wait_execution(test_user.id, graph_exec_id)
@@ -468,7 +467,7 @@ async def test_execute_preset_with_clash(server: SpinTestServer):
# Verify execution
assert result is not None, "Result must not be None"
graph_exec_id = result["id"]
graph_exec_id = result.id
# Wait for execution to complete
executions = await wait_execution(test_user.id, graph_exec_id)

View File

@@ -34,10 +34,6 @@ class WSSubscribeGraphExecutionsRequest(pydantic.BaseModel):
graph_id: str
class ExecuteGraphResponse(pydantic.BaseModel):
graph_exec_id: str
class CreateGraph(pydantic.BaseModel):
graph: Graph

View File

@@ -363,6 +363,7 @@ class AgentServer(backend.util.service.AppProcess):
preset_id=preset_id,
user_id=user_id,
inputs=inputs or {},
credential_inputs={},
)
@staticmethod

View File

@@ -78,7 +78,6 @@ from backend.server.model import (
CreateAPIKeyRequest,
CreateAPIKeyResponse,
CreateGraph,
ExecuteGraphResponse,
RequestTopUp,
SetGraphActiveVersion,
TimezoneResponse,
@@ -783,7 +782,7 @@ async def execute_graph(
],
graph_version: Optional[int] = None,
preset_id: Optional[str] = None,
) -> ExecuteGraphResponse:
) -> execution_db.GraphExecutionMeta:
current_balance = await _user_credit_model.get_credits(user_id)
if current_balance <= 0:
raise HTTPException(
@@ -792,7 +791,7 @@ async def execute_graph(
)
try:
graph_exec = await execution_utils.add_graph_execution(
return await execution_utils.add_graph_execution(
graph_id=graph_id,
user_id=user_id,
inputs=inputs,
@@ -800,7 +799,6 @@ async def execute_graph(
graph_version=graph_version,
graph_credentials_inputs=credentials_inputs,
)
return ExecuteGraphResponse(graph_exec_id=graph_exec.id)
except GraphValidationError as e:
# Return structured validation errors that the frontend can parse
raise HTTPException(

View File

@@ -747,6 +747,25 @@ async def create_preset_from_graph_execution(
if not graph_execution:
raise NotFoundError(f"Graph execution #{graph_exec_id} not found")
# Sanity check: credential inputs must be available if required for this preset
if graph_execution.credential_inputs is None:
graph = await graph_db.get_graph(
graph_id=graph_execution.graph_id,
version=graph_execution.graph_version,
user_id=graph_execution.user_id,
include_subgraphs=True,
)
if not graph:
raise NotFoundError(
f"Graph #{graph_execution.graph_id} not found or accessible"
)
elif len(graph.aggregate_credentials_inputs()) > 0:
raise ValueError(
f"Graph execution #{graph_exec_id} can't be turned into a preset "
"because it was run before this feature existed "
"and so the input credentials were not saved."
)
logger.debug(
f"Creating preset for user #{user_id} from graph execution #{graph_exec_id}",
)
@@ -754,7 +773,7 @@ async def create_preset_from_graph_execution(
user_id=user_id,
preset=library_model.LibraryAgentPresetCreatable(
inputs=graph_execution.inputs,
credentials={}, # FIXME
credentials=graph_execution.credential_inputs or {},
graph_id=graph_execution.graph_id,
graph_version=graph_execution.graph_version,
name=create_request.name,

View File

@@ -6,8 +6,10 @@ from fastapi import APIRouter, Body, HTTPException, Query, Security, status
import backend.server.v2.library.db as db
import backend.server.v2.library.model as models
from backend.data.execution import GraphExecutionMeta
from backend.data.graph import get_graph
from backend.data.integrations import get_webhook
from backend.data.model import CredentialsMetaInput
from backend.executor.utils import add_graph_execution, make_node_credentials_input_map
from backend.integrations.creds_manager import IntegrationCredentialsManager
from backend.integrations.webhooks import get_webhook_manager
@@ -369,48 +371,41 @@ async def execute_preset(
preset_id: str,
user_id: str = Security(autogpt_auth_lib.get_user_id),
inputs: dict[str, Any] = Body(..., embed=True, default_factory=dict),
) -> dict[str, Any]: # FIXME: add proper return type
credential_inputs: dict[str, CredentialsMetaInput] = Body(
..., embed=True, default_factory=dict
),
) -> GraphExecutionMeta:
"""
Execute a preset given graph parameters, returning the execution ID on success.
Args:
preset_id (str): ID of the preset to execute.
user_id (str): ID of the authenticated user.
inputs (dict[str, Any]): Optionally, additional input data for the graph execution.
preset_id: ID of the preset to execute.
user_id: ID of the authenticated user.
inputs: Optionally, inputs to override the preset for execution.
credential_inputs: Optionally, credentials to override the preset for execution.
Returns:
{id: graph_exec_id}: A response containing the execution ID.
GraphExecutionMeta: Object representing the created execution.
Raises:
HTTPException: If the preset is not found or an error occurs while executing the preset.
"""
try:
preset = await db.get_preset(user_id, preset_id)
if not preset:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Preset #{preset_id} not found",
)
# Merge input overrides with preset inputs
merged_node_input = preset.inputs | inputs
execution = await add_graph_execution(
user_id=user_id,
graph_id=preset.graph_id,
graph_version=preset.graph_version,
preset_id=preset_id,
inputs=merged_node_input,
)
logger.debug(f"Execution added: {execution} with input: {merged_node_input}")
return {"id": execution.id}
except HTTPException:
raise
except Exception as e:
logger.exception("Preset execution failed for user %s: %s", user_id, e)
preset = await db.get_preset(user_id, preset_id)
if not preset:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Preset #{preset_id} not found",
)
# Merge input overrides with preset inputs
merged_node_input = preset.inputs | inputs
merged_credential_inputs = preset.credentials | credential_inputs
return await add_graph_execution(
user_id=user_id,
graph_id=preset.graph_id,
graph_version=preset.graph_version,
preset_id=preset_id,
inputs=merged_node_input,
graph_credentials_inputs=merged_credential_inputs,
)

View File

@@ -251,14 +251,14 @@ async def block_autogen_agent():
test_user = await create_test_user()
test_graph = await create_graph(create_test_graph(), user_id=test_user.id)
input_data = {"input": "Write me a block that writes a string into a file."}
response = await server.agent_server.test_execute_graph(
graph_exec = await server.agent_server.test_execute_graph(
graph_id=test_graph.id,
user_id=test_user.id,
node_input=input_data,
)
print(response)
print(graph_exec)
result = await wait_execution(
graph_exec_id=response.graph_exec_id,
graph_exec_id=graph_exec.id,
timeout=1200,
user_id=test_user.id,
)

View File

@@ -155,13 +155,13 @@ async def reddit_marketing_agent():
test_user = await create_test_user()
test_graph = await create_graph(create_test_graph(), user_id=test_user.id)
input_data = {"subreddit": "AutoGPT"}
response = await server.agent_server.test_execute_graph(
graph_exec = await server.agent_server.test_execute_graph(
graph_id=test_graph.id,
user_id=test_user.id,
node_input=input_data,
)
print(response)
result = await wait_execution(test_user.id, response.graph_exec_id, 120)
print(graph_exec)
result = await wait_execution(test_user.id, graph_exec.id, 120)
print(result)

View File

@@ -88,12 +88,12 @@ async def sample_agent():
test_user = await create_test_user()
test_graph = await create_graph(create_test_graph(), test_user.id)
input_data = {"input_1": "Hello", "input_2": "World"}
response = await server.agent_server.test_execute_graph(
graph_exec = await server.agent_server.test_execute_graph(
graph_id=test_graph.id,
user_id=test_user.id,
node_input=input_data,
)
await wait_execution(test_user.id, response.graph_exec_id, 10)
await wait_execution(test_user.id, graph_exec.id, 10)
if __name__ == "__main__":

View File

@@ -89,13 +89,13 @@ export default function Page() {
const libraryAgent = await api.addMarketplaceAgentToLibrary(
storeAgent?.store_listing_version_id || "",
);
const { graph_exec_id } = await api.executeGraph(
const { id: runID } = await api.executeGraph(
libraryAgent.graph_id,
libraryAgent.graph_version,
state?.agentInput || {},
);
updateState({
onboardingAgentExecutionId: graph_exec_id,
onboardingAgentExecutionId: runID,
agentRuns: (state?.agentRuns || 0) + 1,
});
router.push("/onboarding/6-congrats");

View File

@@ -5,7 +5,7 @@ import { isEmpty } from "@/lib/utils";
import { usePostV1ExecuteGraphAgent } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { usePostV1CreateExecutionSchedule as useCreateSchedule } from "@/app/api/__generated__/endpoints/schedules/schedules";
import { usePostV2SetupTrigger } from "@/app/api/__generated__/endpoints/presets/presets";
import { ExecuteGraphResponse } from "@/app/api/__generated__/models/executeGraphResponse";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
@@ -16,7 +16,7 @@ export type RunVariant =
| "manual-trigger";
interface UseAgentRunModalCallbacks {
onRun?: (execution: ExecuteGraphResponse) => void;
onRun?: (execution: GraphExecutionMeta) => void;
onCreateSchedule?: (schedule: GraphExecutionJobInfo) => void;
onSetupTrigger?: (preset: LibraryAgentPreset) => void;
}

View File

@@ -38,9 +38,13 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import LoadingBox, { LoadingSpinner } from "@/components/ui/loading";
import { useToast } from "@/components/molecules/Toast/use-toast";
import {
useToast,
useToastOnFail,
} from "@/components/molecules/Toast/use-toast";
import { AgentRunDetailsView } from "./components/agent-run-details-view";
import { AgentRunDraftView } from "./components/agent-run-draft-view";
import { CreatePresetDialog } from "./components/create-preset-dialog";
import { useAgentRunsInfinite } from "./use-agent-runs";
import { AgentRunsSelectorList } from "./components/agent-runs-selector-list";
import { AgentScheduleDetailsView } from "./components/agent-schedule-details-view";
@@ -48,6 +52,7 @@ import { AgentScheduleDetailsView } from "./components/agent-schedule-details-vi
export function OldAgentLibraryView() {
const { id: agentID }: { id: LibraryAgentID } = useParams();
const [executionId, setExecutionId] = useQueryState("executionId");
const toastOnFail = useToastOnFail();
const { toast } = useToast();
const router = useRouter();
const api = useBackendAPI();
@@ -85,6 +90,8 @@ export function OldAgentLibraryView() {
incrementRuns,
} = useOnboarding();
const [copyAgentDialogOpen, setCopyAgentDialogOpen] = useState(false);
const [creatingPresetFromExecutionID, setCreatingPresetFromExecutionID] =
useState<GraphExecutionID | null>(null);
// Set page title with agent name
useEffect(() => {
@@ -384,6 +391,26 @@ export function OldAgentLibraryView() {
[schedules, api],
);
const handleCreatePresetFromRun = useCallback(
async (name: string, description: string) => {
if (!creatingPresetFromExecutionID) return;
await api
.createLibraryAgentPreset({
name,
description,
graph_execution_id: creatingPresetFromExecutionID,
})
.then((preset) => {
setAgentPresets((prev) => [...prev, preset]);
selectPreset(preset.id);
setCreatingPresetFromExecutionID(null);
})
.catch(toastOnFail("create a preset"));
},
[api, creatingPresetFromExecutionID, selectPreset, toast],
);
const downloadGraph = useCallback(
async () =>
agent &&
@@ -488,6 +515,7 @@ export function OldAgentLibraryView() {
doDeleteRun={setConfirmingDeleteAgentRun}
doDeletePreset={setConfirmingDeleteAgentPreset}
doDeleteSchedule={deleteSchedule}
doCreatePresetFromRun={setCreatingPresetFromExecutionID}
/>
<div className="flex-1">
@@ -512,7 +540,10 @@ export function OldAgentLibraryView() {
run={selectedRun}
agentActions={agentActions}
onRun={selectRun}
deleteRun={() => setConfirmingDeleteAgentRun(selectedRun)}
doDeleteRun={() => setConfirmingDeleteAgentRun(selectedRun)}
doCreatePresetFromRun={() =>
setCreatingPresetFromExecutionID(selectedRun.id)
}
/>
) : null
) : selectedView.type == "run" ? (
@@ -608,6 +639,11 @@ export function OldAgentLibraryView() {
</DialogFooter>
</DialogContent>
</Dialog>
<CreatePresetDialog
open={!!creatingPresetFromExecutionID}
onOpenChange={() => setCreatingPresetFromExecutionID(null)}
onConfirm={handleCreatePresetFromRun}
/>
</div>
</div>
);

View File

@@ -38,14 +38,16 @@ export function AgentRunDetailsView({
run,
agentActions,
onRun,
deleteRun,
doDeleteRun,
doCreatePresetFromRun,
}: {
agent: LibraryAgent;
graph: Graph;
run: GraphExecution | GraphExecutionMeta;
agentActions: ButtonAction[];
onRun: (runID: GraphExecutionID) => void;
deleteRun: () => void;
doDeleteRun: () => void;
doCreatePresetFromRun: () => void;
}): React.ReactNode {
const api = useBackendAPI();
const { formatCredits } = useCredits();
@@ -107,23 +109,36 @@ export function AgentRunDetailsView({
);
}, [graph, run]);
const runAgain = useCallback(
() =>
run.inputs &&
graph.credentials_input_schema.required.every(
const runAgain = useCallback(() => {
if (
!run.inputs ||
!(graph.credentials_input_schema.required ?? []).every(
(k) => k in (run.credential_inputs ?? {}),
) &&
api
.executeGraph(
graph.id,
graph.version,
run.inputs,
run.credential_inputs ?? undefined,
)
)
return;
if (run.preset_id) {
return api
.executeLibraryAgentPreset(
run.preset_id,
run.inputs!,
run.credential_inputs!,
)
.then(({ graph_exec_id }) => onRun(graph_exec_id))
.catch(toastOnFail("execute agent")),
[api, graph, agentRunInputs, onRun, toastOnFail],
);
.then(({ id }) => onRun(id))
.catch(toastOnFail("execute agent preset"));
}
return api
.executeGraph(
graph.id,
graph.version,
run.inputs!,
run.credential_inputs!,
)
.then(({ id }) => onRun(id))
.catch(toastOnFail("execute agent"));
}, [api, graph, run, onRun, toastOnFail]);
const stopRun = useCallback(
() => api.stopGraphExecution(graph.id, run.id),
@@ -178,7 +193,7 @@ export function AgentRunDetailsView({
: []),
...(["success", "failed", "stopped"].includes(runStatus) &&
!graph.has_external_trigger &&
graph.credentials_input_schema.required.every(
(graph.credentials_input_schema.required ?? []).every(
(k) => k in (run.credential_inputs ?? {}),
)
? [
@@ -202,13 +217,15 @@ export function AgentRunDetailsView({
},
]
: []),
{ label: "Delete run", variant: "secondary", callback: deleteRun },
{ label: "Create preset from run", callback: doCreatePresetFromRun },
{ label: "Delete run", variant: "secondary", callback: doDeleteRun },
],
[
runStatus,
runAgain,
stopRun,
deleteRun,
doDeleteRun,
doCreatePresetFromRun,
graph.has_external_trigger,
graph.credentials_input_schema.required,
agent.can_access_graph,

View File

@@ -174,7 +174,7 @@ export function AgentRunDraftView({
.executeGraph(graph.id, graph.version, inputValues, inputCredentials)
.catch(toastOnFail("execute agent"));
if (newRun && onRun) onRun(newRun.graph_exec_id);
if (newRun && onRun) onRun(newRun.id);
} else {
await api
.executeLibraryAgentPreset(agentPreset.id)

View File

@@ -15,6 +15,7 @@ import {
import { AgentStatus, AgentStatusChip } from "./agent-status-chip";
import { AgentRunStatus, AgentRunStatusChip } from "./agent-run-status-chip";
import { PushPinSimpleIcon } from "@phosphor-icons/react";
export type AgentRunSummaryProps = (
| {
@@ -23,6 +24,10 @@ export type AgentRunSummaryProps = (
}
| {
type: "preset";
status?: undefined;
}
| {
type: "preset.triggered";
status: AgentStatus;
}
| {
@@ -36,6 +41,7 @@ export type AgentRunSummaryProps = (
onClick?: () => void;
// onRename: () => void;
onDelete: () => void;
onPinAsPreset?: () => void;
className?: string;
};
@@ -48,6 +54,7 @@ export function AgentRunSummaryCard({
onClick,
// onRename,
onDelete,
onPinAsPreset,
className,
}: AgentRunSummaryProps): React.ReactElement {
return (
@@ -64,14 +71,19 @@ export function AgentRunSummaryCard({
<AgentRunStatusChip status={status} />
)}
{type == "preset" && (
<div className="flex items-center text-sm font-medium text-neutral-700">
<PushPinSimpleIcon className="mr-1 size-4 text-foreground" /> Preset
</div>
)}
{type == "preset.triggered" && (
<div className="flex items-center justify-between">
<AgentStatusChip status={status} />
<div className="flex items-center text-sm text-zinc-400">
<div className="flex items-center text-sm font-medium text-neutral-700">
{status == "inactive" ? (
<Link2OffIcon className="mr-1 size-4" />
<Link2OffIcon className="mr-1 size-4 text-foreground" />
) : (
<Link2Icon className="mr-1 size-4" />
<Link2Icon className="mr-1 size-4 text-foreground" />
)}{" "}
Trigger
</div>
@@ -90,11 +102,11 @@ export function AgentRunSummaryCard({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{/* {onPinAsPreset && (
{onPinAsPreset && (
<DropdownMenuItem onClick={onPinAsPreset}>
Pin as a preset
</DropdownMenuItem>
)} */}
</DropdownMenuItem>
)}
{/* <DropdownMenuItem onClick={onRename}>Rename</DropdownMenuItem> */}

View File

@@ -41,6 +41,7 @@ interface AgentRunsSelectorListProps {
doDeleteRun: (id: GraphExecutionMeta) => void;
doDeletePreset: (id: LibraryAgentPresetID) => void;
doDeleteSchedule: (id: ScheduleID) => void;
doCreatePresetFromRun?: (id: GraphExecutionID) => void;
className?: string;
}
@@ -64,6 +65,7 @@ export function AgentRunsSelectorList({
doDeleteRun,
doDeletePreset,
doDeleteSchedule,
doCreatePresetFromRun,
className,
}: AgentRunsSelectorListProps): React.ReactElement {
const [activeListTab, setActiveListTab] = useState<"runs" | "scheduled">(
@@ -159,6 +161,25 @@ export function AgentRunsSelectorList({
{activeListTab === "runs" ? (
<>
{agentPresets
.filter((preset) => preset.webhook) // Triggers
.toSorted(
(a, b) => b.updated_at.getTime() - a.updated_at.getTime(),
)
.map((preset) => (
<AgentRunSummaryCard
className={cn(listItemClasses, "lg:h-auto")}
key={preset.id}
type="preset.triggered"
status={preset.is_active ? "active" : "inactive"}
title={preset.name}
// timestamp={preset.last_run_time} // TODO: implement this
selected={selectedView.id === preset.id}
onClick={() => onSelectPreset(preset.id)}
onDelete={() => doDeletePreset(preset.id)}
/>
))}
{agentPresets
.filter((preset) => !preset.webhook) // Presets
.toSorted(
(a, b) => b.updated_at.getTime() - a.updated_at.getTime(),
)
@@ -167,7 +188,6 @@ export function AgentRunsSelectorList({
className={cn(listItemClasses, "lg:h-auto")}
key={preset.id}
type="preset"
status={preset.is_active ? "active" : "inactive"}
title={preset.name}
// timestamp={preset.last_run_time} // TODO: implement this
selected={selectedView.id === preset.id}
@@ -196,6 +216,11 @@ export function AgentRunsSelectorList({
selected={selectedView.id === run.id}
onClick={() => onSelectRun(run.id)}
onDelete={() => doDeleteRun(run)}
onPinAsPreset={
doCreatePresetFromRun
? () => doCreatePresetFromRun(run.id)
: undefined
}
/>
))}
</>

View File

@@ -96,7 +96,7 @@ export function AgentScheduleDetailsView({
schedule.input_data,
schedule.input_credentials,
)
.then((run) => onForcedRun(run.graph_exec_id))
.then((run) => onForcedRun(run.id))
.catch(toastOnFail("execute agent")),
[api, graph, schedule, onForcedRun, toastOnFail],
);

View File

@@ -0,0 +1,100 @@
"use client";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
interface CreatePresetDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: (name: string, description: string) => Promise<void> | void;
}
export function CreatePresetDialog({
open,
onOpenChange,
onConfirm,
}: CreatePresetDialogProps) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const handleSubmit = async () => {
if (name.trim()) {
await onConfirm(name.trim(), description.trim());
setName("");
setDescription("");
onOpenChange(false);
}
};
const handleCancel = () => {
setName("");
setDescription("");
onOpenChange(false);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
handleSubmit();
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create Preset</DialogTitle>
<DialogDescription>
Give your preset a name and description to help identify it later.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<label htmlFor="preset-name" className="text-sm font-medium">
Name *
</label>
<Input
id="preset-name"
placeholder="Enter preset name"
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={handleKeyDown}
autoFocus
/>
</div>
<div className="grid gap-2">
<label htmlFor="preset-description" className="text-sm font-medium">
Description
</label>
<Textarea
id="preset-description"
placeholder="Optional description"
value={description}
onChange={(e) => setDescription(e.target.value)}
onKeyDown={handleKeyDown}
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!name.trim()}>
Create Preset
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1729,9 +1729,7 @@
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExecuteGraphResponse"
}
"schema": { "$ref": "#/components/schemas/GraphExecutionMeta" }
}
}
},
@@ -3924,11 +3922,7 @@
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true,
"title": "Response Postv2Execute A Preset"
}
"schema": { "$ref": "#/components/schemas/GraphExecutionMeta" }
}
}
},
@@ -4863,6 +4857,13 @@
"additionalProperties": true,
"type": "object",
"title": "Inputs"
},
"credential_inputs": {
"additionalProperties": {
"$ref": "#/components/schemas/CredentialsMetaInput"
},
"type": "object",
"title": "Credential Inputs"
}
},
"type": "object",
@@ -5143,14 +5144,6 @@
"required": ["url", "relevance_score"],
"title": "Document"
},
"ExecuteGraphResponse": {
"properties": {
"graph_exec_id": { "type": "string", "title": "Graph Exec Id" }
},
"type": "object",
"required": ["graph_exec_id"],
"title": "ExecuteGraphResponse"
},
"Graph": {
"properties": {
"id": { "type": "string", "title": "Id" },

View File

@@ -782,13 +782,13 @@ export default function useAgentGraph(
credentialsInputs,
);
setActiveExecutionID(graphExecution.graph_exec_id);
setActiveExecutionID(graphExecution.id);
// Update URL params
const path = new URLSearchParams(searchParams);
path.set("flowID", savedAgent.id);
path.set("flowVersion", savedAgent.version.toString());
path.set("flowExecutionID", graphExecution.graph_exec_id);
path.set("flowExecutionID", graphExecution.id);
router.push(`${pathname}?${path.toString()}`);
if (state?.completedSteps.includes("BUILDER_SAVE_AGENT")) {

View File

@@ -273,7 +273,7 @@ export default class BackendAPI {
version: number,
inputs: { [key: string]: any } = {},
credentials_inputs: { [key: string]: CredentialsMetaInput } = {},
): Promise<{ graph_exec_id: GraphExecutionID }> {
): Promise<GraphExecutionMeta> {
return this._request("POST", `/graphs/${id}/execute/${version}`, {
inputs,
credentials_inputs,
@@ -772,10 +772,12 @@ export default class BackendAPI {
executeLibraryAgentPreset(
presetID: LibraryAgentPresetID,
inputs?: { [key: string]: any },
): Promise<{ id: GraphExecutionID }> {
inputs?: Record<string, any>,
credential_inputs?: Record<string, CredentialsMetaInput>,
): Promise<GraphExecutionMeta> {
return this._request("POST", `/library/presets/${presetID}/execute`, {
inputs,
credential_inputs,
});
}

View File

@@ -335,7 +335,7 @@ export type GraphIOSubSchema = Omit<
export type CredentialsInputSchema = {
type: "object";
properties: Record<string, BlockIOCredentialsSubSchema>;
required: (keyof CredentialsInputSchema["properties"])[];
required?: (keyof CredentialsInputSchema["properties"])[];
};
/* Mirror of backend/data/graph.py:GraphTriggerInfo */
@@ -493,7 +493,7 @@ export type LibraryAgentPresetCreatable = Omit<
export type LibraryAgentPresetCreatableFromGraphExecution = Omit<
LibraryAgentPresetCreatable,
"graph_id" | "graph_version" | "inputs"
"graph_id" | "graph_version" | "inputs" | "credentials"
> & {
graph_execution_id: GraphExecutionID;
};