next interation on failure (#4192)
This commit is contained in:
@@ -23,6 +23,8 @@ export const baseHelpTooltipContent = {
|
||||
"If you are running multiple workflows at once, you will need to give the block an identifier to know that this TOTP goes with this block.",
|
||||
continueOnFailure:
|
||||
"Allow the workflow to continue if it encounters a failure.",
|
||||
nextIterationOnFailure:
|
||||
"When inside a for loop, continue to the next iteration if this block fails.",
|
||||
includeActionHistoryInVerification:
|
||||
"Include the action history in the completion verification.",
|
||||
} as const;
|
||||
@@ -72,6 +74,8 @@ export const helpTooltips = {
|
||||
...baseHelpTooltipContent,
|
||||
loopValue:
|
||||
"Define the values to iterate over. Use a parameter reference or natural language (e.g., 'Extract links of the top 2 posts'). Natural language automatically creates an extraction block that generates a list of string values. Use {{ current_value }} in the loop to get the current iteration value.",
|
||||
nextIterationOnFailure:
|
||||
"When enabled, if any block inside the loop fails, the loop will immediately jump to the next iteration instead of stopping.",
|
||||
},
|
||||
sendEmail: {
|
||||
...baseHelpTooltipContent,
|
||||
|
||||
@@ -23,7 +23,10 @@ import { useRerender } from "@/hooks/useRerender";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { AppNode } from "..";
|
||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import {
|
||||
getAvailableOutputParameterKeys,
|
||||
isNodeInsideForLoop,
|
||||
} from "../../workflowEditorUtils";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||
@@ -37,6 +40,7 @@ import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuer
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||
|
||||
const urlTooltip =
|
||||
"The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off.";
|
||||
@@ -64,6 +68,7 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const update = useUpdate<ActionNode["data"]>({ id, editable });
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -248,28 +253,19 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["action"]["continueOnFailure"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
continueOnFailure={data.continueOnFailure}
|
||||
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||
editable={editable}
|
||||
isInsideForLoop={isInsideForLoop}
|
||||
blockType="action"
|
||||
onContinueOnFailureChange={(checked) => {
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
onNextIterationOnFailureChange={(checked) => {
|
||||
update({ nextIterationOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
<DisableCache
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { dataSchemaExampleValue } from "../types";
|
||||
@@ -20,7 +19,10 @@ import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTexta
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { AppNode } from "..";
|
||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import {
|
||||
getAvailableOutputParameterKeys,
|
||||
isNodeInsideForLoop,
|
||||
} from "../../workflowEditorUtils";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
@@ -37,6 +39,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||
|
||||
function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
@@ -58,6 +61,7 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
const rerender = useRerender({ prefix: "accordian" });
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<ExtractionNode["data"]>({ id, editable });
|
||||
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -209,30 +213,19 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips["extraction"]["continueOnFailure"]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
continueOnFailure={data.continueOnFailure}
|
||||
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||
editable={editable}
|
||||
isInsideForLoop={isInsideForLoop}
|
||||
blockType="extraction"
|
||||
onContinueOnFailureChange={(checked) => {
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
onNextIterationOnFailureChange={(checked) => {
|
||||
update({ nextIterationOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
<DisableCache
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
@@ -22,7 +21,10 @@ import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import type { FileDownloadNode } from "./types";
|
||||
import { AppNode } from "..";
|
||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import {
|
||||
getAvailableOutputParameterKeys,
|
||||
isNodeInsideForLoop,
|
||||
} from "../../workflowEditorUtils";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||
@@ -37,6 +39,7 @@ import { useRerender } from "@/hooks/useRerender";
|
||||
import { BROWSER_DOWNLOAD_TIMEOUT_SECONDS } from "@/api/types";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||
|
||||
const urlTooltip =
|
||||
@@ -65,6 +68,7 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<FileDownloadNode["data"]>({ id, editable });
|
||||
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -279,25 +283,19 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["download"]["continueOnFailure"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
continueOnFailure={data.continueOnFailure}
|
||||
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||
editable={editable}
|
||||
isInsideForLoop={isInsideForLoop}
|
||||
blockType="download"
|
||||
onContinueOnFailureChange={(checked) => {
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
onNextIterationOnFailureChange={(checked) => {
|
||||
update({ nextIterationOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
<DisableCache
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
@@ -22,7 +21,10 @@ import { errorMappingExampleValue } from "../types";
|
||||
import type { LoginNode } from "./types";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { AppNode } from "..";
|
||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import {
|
||||
getAvailableOutputParameterKeys,
|
||||
isNodeInsideForLoop,
|
||||
} from "../../workflowEditorUtils";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { LoginBlockCredentialSelector } from "./LoginBlockCredentialSelector";
|
||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||
@@ -36,6 +38,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||
|
||||
function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
@@ -56,6 +59,7 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<LoginNode["data"]>({ id, editable });
|
||||
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||
|
||||
// Manage flippable facing state
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
@@ -277,25 +281,19 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["login"]["continueOnFailure"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
continueOnFailure={data.continueOnFailure}
|
||||
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||
editable={editable}
|
||||
isInsideForLoop={isInsideForLoop}
|
||||
blockType="login"
|
||||
onContinueOnFailureChange={(checked) => {
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
onNextIterationOnFailureChange={(checked) => {
|
||||
update({ nextIterationOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
<DisableCache
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
|
||||
@@ -159,6 +159,26 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
<HelpTooltip content="When checked, the loop will continue executing even if one of its iterations fails" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={data.nextIterationOnFailure ?? false}
|
||||
disabled={!data.editable}
|
||||
onCheckedChange={(checked) => {
|
||||
update({
|
||||
nextIterationOnFailure:
|
||||
checked === "indeterminate" ? false : checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Label className="text-xs text-slate-300">
|
||||
Next Loop on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["loop"]["nextIterationOnFailure"]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ export type LoopNodeData = NodeBaseData & {
|
||||
loopVariableReference: string;
|
||||
completeIfEmpty: boolean;
|
||||
continueOnFailure: boolean;
|
||||
nextIterationOnFailure?: boolean;
|
||||
};
|
||||
|
||||
export type LoopNode = Node<LoopNodeData, "loop">;
|
||||
@@ -19,6 +20,7 @@ export const loopNodeDefaultData: LoopNodeData = {
|
||||
loopVariableReference: "",
|
||||
completeIfEmpty: false,
|
||||
continueOnFailure: false,
|
||||
nextIterationOnFailure: false,
|
||||
model: null,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -25,7 +25,10 @@ import { errorMappingExampleValue } from "../types";
|
||||
import type { NavigationNode } from "./types";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { AppNode } from "..";
|
||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import {
|
||||
getAvailableOutputParameterKeys,
|
||||
isNodeInsideForLoop,
|
||||
} from "../../workflowEditorUtils";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
@@ -37,6 +40,7 @@ import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuer
|
||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||
|
||||
function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
@@ -58,6 +62,7 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<NavigationNode["data"]>({ id, editable });
|
||||
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -287,51 +292,32 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Include Action History
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips["navigation"][
|
||||
"includeActionHistoryInVerification"
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.includeActionHistoryInVerification}
|
||||
onCheckedChange={(checked) => {
|
||||
update({
|
||||
includeActionHistoryInVerification: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips["navigation"]["continueOnFailure"]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
continueOnFailure={data.continueOnFailure}
|
||||
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||
includeActionHistoryInVerification={
|
||||
data.includeActionHistoryInVerification
|
||||
}
|
||||
editable={editable}
|
||||
isInsideForLoop={isInsideForLoop}
|
||||
blockType="navigation"
|
||||
showOptions={{
|
||||
continueOnFailure: true,
|
||||
nextIterationOnFailure: true,
|
||||
includeActionHistoryInVerification: true,
|
||||
}}
|
||||
onContinueOnFailureChange={(checked) => {
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
onNextIterationOnFailureChange={(checked) => {
|
||||
update({ nextIterationOnFailure: checked });
|
||||
}}
|
||||
onIncludeActionHistoryInVerificationChange={(checked) => {
|
||||
update({
|
||||
includeActionHistoryInVerification: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<DisableCache
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
|
||||
@@ -22,7 +22,10 @@ import { useState } from "react";
|
||||
import { AppNode } from "..";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import {
|
||||
getAvailableOutputParameterKeys,
|
||||
isNodeInsideForLoop,
|
||||
} from "../../workflowEditorUtils";
|
||||
import { dataSchemaExampleValue, errorMappingExampleValue } from "../types";
|
||||
import { ParametersMultiSelect } from "./ParametersMultiSelect";
|
||||
import type { TaskNode } from "./types";
|
||||
@@ -39,6 +42,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||
|
||||
function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
@@ -59,6 +63,7 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<TaskNode["data"]>({ id, editable });
|
||||
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -303,49 +308,32 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Include Action History
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips["task"][
|
||||
"includeActionHistoryInVerification"
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.includeActionHistoryInVerification}
|
||||
onCheckedChange={(checked) => {
|
||||
update({
|
||||
includeActionHistoryInVerification: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["task"]["continueOnFailure"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
continueOnFailure={data.continueOnFailure}
|
||||
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||
includeActionHistoryInVerification={
|
||||
data.includeActionHistoryInVerification
|
||||
}
|
||||
editable={editable}
|
||||
isInsideForLoop={isInsideForLoop}
|
||||
blockType="task"
|
||||
showOptions={{
|
||||
continueOnFailure: true,
|
||||
nextIterationOnFailure: true,
|
||||
includeActionHistoryInVerification: true,
|
||||
}}
|
||||
onContinueOnFailureChange={(checked) => {
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
onNextIterationOnFailureChange={(checked) => {
|
||||
update({ nextIterationOnFailure: checked });
|
||||
}}
|
||||
onIncludeActionHistoryInVerificationChange={(checked) => {
|
||||
update({
|
||||
includeActionHistoryInVerification: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<DisableCache
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
@@ -21,7 +20,10 @@ import { helpTooltips } from "../../helpContent";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import type { ValidationNode } from "./types";
|
||||
import { AppNode } from "..";
|
||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import {
|
||||
getAvailableOutputParameterKeys,
|
||||
isNodeInsideForLoop,
|
||||
} from "../../workflowEditorUtils";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
@@ -34,6 +36,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
|
||||
import { DisableCache } from "../DisableCache";
|
||||
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||
|
||||
function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
||||
@@ -55,6 +58,7 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
const update = useUpdate<ValidationNode["data"]>({ id, editable });
|
||||
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
@@ -212,30 +216,19 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips["validation"]["continueOnFailure"]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
continueOnFailure={data.continueOnFailure}
|
||||
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||
editable={editable}
|
||||
isInsideForLoop={isInsideForLoop}
|
||||
blockType="validation"
|
||||
onContinueOnFailureChange={(checked) => {
|
||||
update({ continueOnFailure: checked });
|
||||
}}
|
||||
onNextIterationOnFailureChange={(checked) => {
|
||||
update({ nextIterationOnFailure: checked });
|
||||
}}
|
||||
/>
|
||||
<DisableCache
|
||||
disableCache={data.disableCache}
|
||||
editable={editable}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
|
||||
interface BlockExecutionOptionsProps {
|
||||
continueOnFailure: boolean;
|
||||
nextIterationOnFailure?: boolean;
|
||||
includeActionHistoryInVerification?: boolean;
|
||||
editable: boolean;
|
||||
isInsideForLoop: boolean;
|
||||
blockType: string;
|
||||
onContinueOnFailureChange: (checked: boolean) => void;
|
||||
onNextIterationOnFailureChange: (checked: boolean) => void;
|
||||
onIncludeActionHistoryInVerificationChange?: (checked: boolean) => void;
|
||||
showOptions?: {
|
||||
continueOnFailure?: boolean;
|
||||
nextIterationOnFailure?: boolean;
|
||||
includeActionHistoryInVerification?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function BlockExecutionOptions({
|
||||
continueOnFailure,
|
||||
nextIterationOnFailure = false,
|
||||
includeActionHistoryInVerification = false,
|
||||
editable,
|
||||
isInsideForLoop,
|
||||
blockType,
|
||||
onContinueOnFailureChange,
|
||||
onNextIterationOnFailureChange,
|
||||
onIncludeActionHistoryInVerificationChange,
|
||||
showOptions = {
|
||||
continueOnFailure: true,
|
||||
nextIterationOnFailure: true,
|
||||
includeActionHistoryInVerification: false,
|
||||
},
|
||||
}: BlockExecutionOptionsProps) {
|
||||
const showContinueOnFailure = showOptions.continueOnFailure ?? true;
|
||||
const showNextIterationOnFailure = showOptions.nextIterationOnFailure ?? true;
|
||||
const showIncludeActionHistory =
|
||||
showOptions.includeActionHistoryInVerification ?? false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Separator />
|
||||
{showIncludeActionHistory &&
|
||||
onIncludeActionHistoryInVerificationChange && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Include Action History
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips[blockType as keyof typeof helpTooltips]?.[
|
||||
"includeActionHistoryInVerification"
|
||||
] ||
|
||||
helpTooltips["task"]["includeActionHistoryInVerification"]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={includeActionHistoryInVerification}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
onIncludeActionHistoryInVerificationChange(checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showContinueOnFailure && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips[blockType as keyof typeof helpTooltips]?.[
|
||||
"continueOnFailure"
|
||||
] || helpTooltips["task"]["continueOnFailure"]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
onContinueOnFailureChange(checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showNextIterationOnFailure && isInsideForLoop && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Next Loop on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips[blockType as keyof typeof helpTooltips]?.[
|
||||
"nextIterationOnFailure"
|
||||
] || helpTooltips["task"]["nextIterationOnFailure"]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={nextIterationOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
onNextIterationOnFailureChange(checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Separator />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export type NodeBaseData = {
|
||||
debuggable: boolean;
|
||||
label: string;
|
||||
continueOnFailure: boolean;
|
||||
nextIterationOnFailure?: boolean;
|
||||
editable: boolean;
|
||||
model: WorkflowModel | null;
|
||||
showCode?: boolean;
|
||||
|
||||
@@ -223,6 +223,7 @@ function convertToNode(
|
||||
debuggable: debuggableWorkflowBlockTypes.has(block.block_type),
|
||||
label: block.label,
|
||||
continueOnFailure: block.continue_on_failure,
|
||||
nextIterationOnFailure: block.next_iteration_on_failure,
|
||||
editable,
|
||||
model: block.model,
|
||||
};
|
||||
@@ -489,6 +490,7 @@ function convertToNode(
|
||||
loopValue: block.loop_over?.key ?? "",
|
||||
loopVariableReference: loopVariableReference,
|
||||
completeIfEmpty: block.complete_if_empty,
|
||||
nextIterationOnFailure: block.next_iteration_on_failure,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1073,6 +1075,7 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
||||
const base = {
|
||||
label: node.data.label,
|
||||
continue_on_failure: node.data.continueOnFailure,
|
||||
next_iteration_on_failure: node.data.nextIterationOnFailure,
|
||||
model: node.data.model,
|
||||
};
|
||||
switch (node.type) {
|
||||
@@ -1422,6 +1425,7 @@ function getOrderedChildrenBlocks(
|
||||
block_type: "for_loop",
|
||||
label: currentNode.data.label,
|
||||
continue_on_failure: currentNode.data.continueOnFailure,
|
||||
next_iteration_on_failure: currentNode.data.nextIterationOnFailure,
|
||||
loop_blocks: loopChildren,
|
||||
loop_variable_reference: currentNode.data.loopVariableReference,
|
||||
complete_if_empty: currentNode.data.completeIfEmpty,
|
||||
@@ -1452,6 +1456,7 @@ function getWorkflowBlocksUtil(
|
||||
block_type: "for_loop",
|
||||
label: node.data.label,
|
||||
continue_on_failure: node.data.continueOnFailure,
|
||||
next_iteration_on_failure: node.data.nextIterationOnFailure,
|
||||
loop_blocks: getOrderedChildrenBlocks(nodes, edges, node.id),
|
||||
loop_variable_reference: node.data.loopVariableReference,
|
||||
complete_if_empty: node.data.completeIfEmpty,
|
||||
@@ -1952,6 +1957,7 @@ function convertBlocksToBlockYAML(
|
||||
const base = {
|
||||
label: block.label,
|
||||
continue_on_failure: block.continue_on_failure,
|
||||
next_iteration_on_failure: block.next_iteration_on_failure,
|
||||
next_block_label: block.next_block_label,
|
||||
};
|
||||
switch (block.block_type) {
|
||||
@@ -2455,6 +2461,23 @@ function getLabelForWorkflowParameterType(type: WorkflowParameterValueType) {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node is inside a for loop block
|
||||
* @param nodes - Array of all nodes in the workflow
|
||||
* @param nodeId - ID of the node to check
|
||||
* @returns true if the node is inside a for loop block, false otherwise
|
||||
*/
|
||||
function isNodeInsideForLoop(nodes: Array<AppNode>, nodeId: string): boolean {
|
||||
const currentNode = nodes.find((n) => n.id === nodeId);
|
||||
if (!currentNode) {
|
||||
return false;
|
||||
}
|
||||
const parentNode = currentNode.parentId
|
||||
? nodes.find((n) => n.id === currentNode.parentId)
|
||||
: null;
|
||||
return parentNode?.type === "loop";
|
||||
}
|
||||
|
||||
export {
|
||||
convert,
|
||||
convertEchoParameters,
|
||||
@@ -2479,6 +2502,7 @@ export {
|
||||
getUpdatedParametersAfterLabelUpdateForSourceParameterKey,
|
||||
getWorkflowBlocks,
|
||||
getWorkflowErrors,
|
||||
isNodeInsideForLoop,
|
||||
isOutputParameterKey,
|
||||
layout,
|
||||
};
|
||||
|
||||
@@ -287,6 +287,7 @@ export type WorkflowBlockBase = {
|
||||
block_type: WorkflowBlockType;
|
||||
output_parameter: OutputParameter;
|
||||
continue_on_failure: boolean;
|
||||
next_iteration_on_failure?: boolean;
|
||||
model: WorkflowModel | null;
|
||||
next_block_label?: string | null;
|
||||
};
|
||||
|
||||
@@ -145,6 +145,7 @@ export type BlockYAMLBase = {
|
||||
block_type: WorkflowBlockType;
|
||||
label: string;
|
||||
continue_on_failure?: boolean;
|
||||
next_iteration_on_failure?: boolean;
|
||||
next_block_label?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -135,6 +135,10 @@ class Block(BaseModel, abc.ABC):
|
||||
model: dict[str, Any] | None = None
|
||||
disable_cache: bool = False
|
||||
|
||||
# Only valid for blocks inside a for loop block
|
||||
# Whether to continue to the next iteration when the block fails
|
||||
next_iteration_on_failure: bool = False
|
||||
|
||||
@property
|
||||
def override_llm_key(self) -> str | None:
|
||||
"""
|
||||
@@ -1386,15 +1390,15 @@ class ForLoopBlock(Block):
|
||||
organization_id=organization_id,
|
||||
)
|
||||
block_outputs.append(failure_block_result)
|
||||
# If continue_on_failure is False, stop the entire loop
|
||||
if not self.continue_on_failure:
|
||||
# If next_iteration_on_failure is False, stop the entire loop
|
||||
if not self.next_iteration_on_failure:
|
||||
outputs_with_loop_values.append(each_loop_output_values)
|
||||
return LoopBlockExecutedResult(
|
||||
outputs_with_loop_values=outputs_with_loop_values,
|
||||
block_outputs=block_outputs,
|
||||
last_block=current_block,
|
||||
)
|
||||
# If continue_on_failure is True, break out of the block loop for this iteration
|
||||
# If next_iteration_on_failure is True, break out of the block loop for this iteration
|
||||
break
|
||||
|
||||
if block_output.status == BlockStatus.canceled:
|
||||
@@ -1412,7 +1416,12 @@ class ForLoopBlock(Block):
|
||||
last_block=current_block,
|
||||
)
|
||||
|
||||
if not block_output.success and not loop_block.continue_on_failure:
|
||||
if (
|
||||
not block_output.success
|
||||
and not loop_block.continue_on_failure
|
||||
and not loop_block.next_iteration_on_failure
|
||||
and not self.next_iteration_on_failure
|
||||
):
|
||||
LOG.info(
|
||||
f"ForLoopBlock: Encountered a failure processing block {block_idx} during loop {loop_idx}, terminating early",
|
||||
block_outputs=block_outputs,
|
||||
@@ -1421,6 +1430,8 @@ class ForLoopBlock(Block):
|
||||
loop_over_value=loop_over_value,
|
||||
loop_block_continue_on_failure=loop_block.continue_on_failure,
|
||||
failure_reason=block_output.failure_reason,
|
||||
next_iteration_on_failure=loop_block.next_iteration_on_failure
|
||||
or self.next_iteration_on_failure,
|
||||
)
|
||||
outputs_with_loop_values.append(each_loop_output_values)
|
||||
return LoopBlockExecutedResult(
|
||||
@@ -1429,6 +1440,21 @@ class ForLoopBlock(Block):
|
||||
last_block=current_block,
|
||||
)
|
||||
|
||||
if block_output.success or loop_block.continue_on_failure:
|
||||
continue
|
||||
|
||||
if loop_block.next_iteration_on_failure or self.next_iteration_on_failure:
|
||||
LOG.info(
|
||||
f"ForLoopBlock: Block {block_idx} during loop {loop_idx} failed but will continue to next iteration",
|
||||
block_outputs=block_outputs,
|
||||
loop_idx=loop_idx,
|
||||
block_idx=block_idx,
|
||||
loop_over_value=loop_over_value,
|
||||
loop_block_next_iteration_on_failure=loop_block.next_iteration_on_failure
|
||||
or self.next_iteration_on_failure,
|
||||
)
|
||||
break
|
||||
|
||||
outputs_with_loop_values.append(each_loop_output_values)
|
||||
|
||||
return LoopBlockExecutedResult(
|
||||
|
||||
@@ -2951,6 +2951,7 @@ class WorkflowService:
|
||||
"next_block_label": block_yaml.next_block_label,
|
||||
"output_parameter": output_parameter,
|
||||
"continue_on_failure": block_yaml.continue_on_failure,
|
||||
"next_iteration_on_failure": block_yaml.next_iteration_on_failure,
|
||||
"model": block_yaml.model,
|
||||
}
|
||||
|
||||
|
||||
@@ -206,6 +206,9 @@ class BlockYAML(BaseModel, abc.ABC):
|
||||
)
|
||||
continue_on_failure: bool = False
|
||||
model: dict[str, Any] | None = None
|
||||
# Only valid for blocks inside a for loop block
|
||||
# Whether to continue to the next iteration when the block fails
|
||||
next_iteration_on_failure: bool = False
|
||||
|
||||
@field_validator("label")
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user