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.",
|
"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:
|
continueOnFailure:
|
||||||
"Allow the workflow to continue if it encounters a failure.",
|
"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:
|
includeActionHistoryInVerification:
|
||||||
"Include the action history in the completion verification.",
|
"Include the action history in the completion verification.",
|
||||||
} as const;
|
} as const;
|
||||||
@@ -72,6 +74,8 @@ export const helpTooltips = {
|
|||||||
...baseHelpTooltipContent,
|
...baseHelpTooltipContent,
|
||||||
loopValue:
|
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.",
|
"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: {
|
sendEmail: {
|
||||||
...baseHelpTooltipContent,
|
...baseHelpTooltipContent,
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ import { useRerender } from "@/hooks/useRerender";
|
|||||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||||
import { AppNode } from "..";
|
import { AppNode } from "..";
|
||||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
import {
|
||||||
|
getAvailableOutputParameterKeys,
|
||||||
|
isNodeInsideForLoop,
|
||||||
|
} from "../../workflowEditorUtils";
|
||||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||||
@@ -37,6 +40,7 @@ import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuer
|
|||||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||||
|
|
||||||
import { DisableCache } from "../DisableCache";
|
import { DisableCache } from "../DisableCache";
|
||||||
|
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||||
|
|
||||||
const urlTooltip =
|
const urlTooltip =
|
||||||
"The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off.";
|
"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 outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||||
const update = useUpdate<ActionNode["data"]>({ id, editable });
|
const update = useUpdate<ActionNode["data"]>({ id, editable });
|
||||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||||
|
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFacing(data.showCode ? "back" : "front");
|
setFacing(data.showCode ? "back" : "front");
|
||||||
@@ -248,28 +253,19 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<BlockExecutionOptions
|
||||||
<div className="flex items-center justify-between">
|
continueOnFailure={data.continueOnFailure}
|
||||||
<div className="flex gap-2">
|
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
editable={editable}
|
||||||
Continue on Failure
|
isInsideForLoop={isInsideForLoop}
|
||||||
</Label>
|
blockType="action"
|
||||||
<HelpTooltip
|
onContinueOnFailureChange={(checked) => {
|
||||||
content={helpTooltips["action"]["continueOnFailure"]}
|
update({ continueOnFailure: checked });
|
||||||
/>
|
}}
|
||||||
</div>
|
onNextIterationOnFailureChange={(checked) => {
|
||||||
<div className="w-52">
|
update({ nextIterationOnFailure: checked });
|
||||||
<Switch
|
}}
|
||||||
checked={data.continueOnFailure}
|
/>
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
if (!editable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
update({ continueOnFailure: checked });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DisableCache
|
<DisableCache
|
||||||
disableCache={data.disableCache}
|
disableCache={data.disableCache}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { dataSchemaExampleValue } from "../types";
|
import { dataSchemaExampleValue } from "../types";
|
||||||
@@ -20,7 +19,10 @@ import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTexta
|
|||||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||||
import { helpTooltips, placeholders } from "../../helpContent";
|
import { helpTooltips, placeholders } from "../../helpContent";
|
||||||
import { AppNode } from "..";
|
import { AppNode } from "..";
|
||||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
import {
|
||||||
|
getAvailableOutputParameterKeys,
|
||||||
|
isNodeInsideForLoop,
|
||||||
|
} from "../../workflowEditorUtils";
|
||||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||||
import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup";
|
import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup";
|
||||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||||
@@ -37,6 +39,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
|||||||
import { useRerender } from "@/hooks/useRerender";
|
import { useRerender } from "@/hooks/useRerender";
|
||||||
|
|
||||||
import { DisableCache } from "../DisableCache";
|
import { DisableCache } from "../DisableCache";
|
||||||
|
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||||
|
|
||||||
function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||||
@@ -58,6 +61,7 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
|||||||
const rerender = useRerender({ prefix: "accordian" });
|
const rerender = useRerender({ prefix: "accordian" });
|
||||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||||
const update = useUpdate<ExtractionNode["data"]>({ id, editable });
|
const update = useUpdate<ExtractionNode["data"]>({ id, editable });
|
||||||
|
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFacing(data.showCode ? "back" : "front");
|
setFacing(data.showCode ? "back" : "front");
|
||||||
@@ -209,30 +213,19 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<BlockExecutionOptions
|
||||||
<div className="flex items-center justify-between">
|
continueOnFailure={data.continueOnFailure}
|
||||||
<div className="flex gap-2">
|
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
editable={editable}
|
||||||
Continue on Failure
|
isInsideForLoop={isInsideForLoop}
|
||||||
</Label>
|
blockType="extraction"
|
||||||
<HelpTooltip
|
onContinueOnFailureChange={(checked) => {
|
||||||
content={
|
update({ continueOnFailure: checked });
|
||||||
helpTooltips["extraction"]["continueOnFailure"]
|
}}
|
||||||
}
|
onNextIterationOnFailureChange={(checked) => {
|
||||||
/>
|
update({ nextIterationOnFailure: checked });
|
||||||
</div>
|
}}
|
||||||
<div className="w-52">
|
/>
|
||||||
<Switch
|
|
||||||
checked={data.continueOnFailure}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
if (!editable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
update({ continueOnFailure: checked });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DisableCache
|
<DisableCache
|
||||||
disableCache={data.disableCache}
|
disableCache={data.disableCache}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
@@ -22,7 +21,10 @@ import { helpTooltips, placeholders } from "../../helpContent";
|
|||||||
import { errorMappingExampleValue } from "../types";
|
import { errorMappingExampleValue } from "../types";
|
||||||
import type { FileDownloadNode } from "./types";
|
import type { FileDownloadNode } from "./types";
|
||||||
import { AppNode } from "..";
|
import { AppNode } from "..";
|
||||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
import {
|
||||||
|
getAvailableOutputParameterKeys,
|
||||||
|
isNodeInsideForLoop,
|
||||||
|
} from "../../workflowEditorUtils";
|
||||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||||
@@ -37,6 +39,7 @@ import { useRerender } from "@/hooks/useRerender";
|
|||||||
import { BROWSER_DOWNLOAD_TIMEOUT_SECONDS } from "@/api/types";
|
import { BROWSER_DOWNLOAD_TIMEOUT_SECONDS } from "@/api/types";
|
||||||
|
|
||||||
import { DisableCache } from "../DisableCache";
|
import { DisableCache } from "../DisableCache";
|
||||||
|
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||||
|
|
||||||
const urlTooltip =
|
const urlTooltip =
|
||||||
@@ -65,6 +68,7 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
|||||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||||
const update = useUpdate<FileDownloadNode["data"]>({ id, editable });
|
const update = useUpdate<FileDownloadNode["data"]>({ id, editable });
|
||||||
|
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFacing(data.showCode ? "back" : "front");
|
setFacing(data.showCode ? "back" : "front");
|
||||||
@@ -279,25 +283,19 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<BlockExecutionOptions
|
||||||
<div className="flex items-center justify-between">
|
continueOnFailure={data.continueOnFailure}
|
||||||
<div className="flex gap-2">
|
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
editable={editable}
|
||||||
Continue on Failure
|
isInsideForLoop={isInsideForLoop}
|
||||||
</Label>
|
blockType="download"
|
||||||
<HelpTooltip
|
onContinueOnFailureChange={(checked) => {
|
||||||
content={helpTooltips["download"]["continueOnFailure"]}
|
update({ continueOnFailure: checked });
|
||||||
/>
|
}}
|
||||||
</div>
|
onNextIterationOnFailureChange={(checked) => {
|
||||||
<div className="w-52">
|
update({ nextIterationOnFailure: checked });
|
||||||
<Switch
|
}}
|
||||||
checked={data.continueOnFailure}
|
/>
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
update({ continueOnFailure: checked });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DisableCache
|
<DisableCache
|
||||||
disableCache={data.disableCache}
|
disableCache={data.disableCache}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
@@ -22,7 +21,10 @@ import { errorMappingExampleValue } from "../types";
|
|||||||
import type { LoginNode } from "./types";
|
import type { LoginNode } from "./types";
|
||||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||||
import { AppNode } from "..";
|
import { AppNode } from "..";
|
||||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
import {
|
||||||
|
getAvailableOutputParameterKeys,
|
||||||
|
isNodeInsideForLoop,
|
||||||
|
} from "../../workflowEditorUtils";
|
||||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||||
import { LoginBlockCredentialSelector } from "./LoginBlockCredentialSelector";
|
import { LoginBlockCredentialSelector } from "./LoginBlockCredentialSelector";
|
||||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||||
@@ -36,6 +38,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
|||||||
import { useRerender } from "@/hooks/useRerender";
|
import { useRerender } from "@/hooks/useRerender";
|
||||||
|
|
||||||
import { DisableCache } from "../DisableCache";
|
import { DisableCache } from "../DisableCache";
|
||||||
|
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||||
|
|
||||||
function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
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 outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||||
const update = useUpdate<LoginNode["data"]>({ id, editable });
|
const update = useUpdate<LoginNode["data"]>({ id, editable });
|
||||||
|
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||||
|
|
||||||
// Manage flippable facing state
|
// Manage flippable facing state
|
||||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||||
@@ -277,25 +281,19 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<BlockExecutionOptions
|
||||||
<div className="flex items-center justify-between">
|
continueOnFailure={data.continueOnFailure}
|
||||||
<div className="flex gap-2">
|
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
editable={editable}
|
||||||
Continue on Failure
|
isInsideForLoop={isInsideForLoop}
|
||||||
</Label>
|
blockType="login"
|
||||||
<HelpTooltip
|
onContinueOnFailureChange={(checked) => {
|
||||||
content={helpTooltips["login"]["continueOnFailure"]}
|
update({ continueOnFailure: checked });
|
||||||
/>
|
}}
|
||||||
</div>
|
onNextIterationOnFailureChange={(checked) => {
|
||||||
<div className="w-52">
|
update({ nextIterationOnFailure: checked });
|
||||||
<Switch
|
}}
|
||||||
checked={data.continueOnFailure}
|
/>
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
update({ continueOnFailure: checked });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DisableCache
|
<DisableCache
|
||||||
disableCache={data.disableCache}
|
disableCache={data.disableCache}
|
||||||
editable={editable}
|
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" />
|
<HelpTooltip content="When checked, the loop will continue executing even if one of its iterations fails" />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type LoopNodeData = NodeBaseData & {
|
|||||||
loopVariableReference: string;
|
loopVariableReference: string;
|
||||||
completeIfEmpty: boolean;
|
completeIfEmpty: boolean;
|
||||||
continueOnFailure: boolean;
|
continueOnFailure: boolean;
|
||||||
|
nextIterationOnFailure?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoopNode = Node<LoopNodeData, "loop">;
|
export type LoopNode = Node<LoopNodeData, "loop">;
|
||||||
@@ -19,6 +20,7 @@ export const loopNodeDefaultData: LoopNodeData = {
|
|||||||
loopVariableReference: "",
|
loopVariableReference: "",
|
||||||
completeIfEmpty: false,
|
completeIfEmpty: false,
|
||||||
continueOnFailure: false,
|
continueOnFailure: false,
|
||||||
|
nextIterationOnFailure: false,
|
||||||
model: null,
|
model: null,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ import { errorMappingExampleValue } from "../types";
|
|||||||
import type { NavigationNode } from "./types";
|
import type { NavigationNode } from "./types";
|
||||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||||
import { AppNode } from "..";
|
import { AppNode } from "..";
|
||||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
import {
|
||||||
|
getAvailableOutputParameterKeys,
|
||||||
|
isNodeInsideForLoop,
|
||||||
|
} from "../../workflowEditorUtils";
|
||||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||||
import { ModelSelector } from "@/components/ModelSelector";
|
import { ModelSelector } from "@/components/ModelSelector";
|
||||||
@@ -37,6 +40,7 @@ import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuer
|
|||||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||||
|
|
||||||
import { DisableCache } from "../DisableCache";
|
import { DisableCache } from "../DisableCache";
|
||||||
|
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||||
|
|
||||||
function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
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 outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||||
const update = useUpdate<NavigationNode["data"]>({ id, editable });
|
const update = useUpdate<NavigationNode["data"]>({ id, editable });
|
||||||
|
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFacing(data.showCode ? "back" : "front");
|
setFacing(data.showCode ? "back" : "front");
|
||||||
@@ -287,51 +292,32 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<BlockExecutionOptions
|
||||||
<div className="flex items-center justify-between">
|
continueOnFailure={data.continueOnFailure}
|
||||||
<div className="flex gap-2">
|
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
includeActionHistoryInVerification={
|
||||||
Include Action History
|
data.includeActionHistoryInVerification
|
||||||
</Label>
|
}
|
||||||
<HelpTooltip
|
editable={editable}
|
||||||
content={
|
isInsideForLoop={isInsideForLoop}
|
||||||
helpTooltips["navigation"][
|
blockType="navigation"
|
||||||
"includeActionHistoryInVerification"
|
showOptions={{
|
||||||
]
|
continueOnFailure: true,
|
||||||
}
|
nextIterationOnFailure: true,
|
||||||
/>
|
includeActionHistoryInVerification: true,
|
||||||
</div>
|
}}
|
||||||
<div className="w-52">
|
onContinueOnFailureChange={(checked) => {
|
||||||
<Switch
|
update({ continueOnFailure: checked });
|
||||||
checked={data.includeActionHistoryInVerification}
|
}}
|
||||||
onCheckedChange={(checked) => {
|
onNextIterationOnFailureChange={(checked) => {
|
||||||
update({
|
update({ nextIterationOnFailure: checked });
|
||||||
includeActionHistoryInVerification: checked,
|
}}
|
||||||
});
|
onIncludeActionHistoryInVerificationChange={(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>
|
|
||||||
<DisableCache
|
<DisableCache
|
||||||
disableCache={data.disableCache}
|
disableCache={data.disableCache}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ import { useState } from "react";
|
|||||||
import { AppNode } from "..";
|
import { AppNode } from "..";
|
||||||
import { helpTooltips, placeholders } from "../../helpContent";
|
import { helpTooltips, placeholders } from "../../helpContent";
|
||||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
import {
|
||||||
|
getAvailableOutputParameterKeys,
|
||||||
|
isNodeInsideForLoop,
|
||||||
|
} from "../../workflowEditorUtils";
|
||||||
import { dataSchemaExampleValue, errorMappingExampleValue } from "../types";
|
import { dataSchemaExampleValue, errorMappingExampleValue } from "../types";
|
||||||
import { ParametersMultiSelect } from "./ParametersMultiSelect";
|
import { ParametersMultiSelect } from "./ParametersMultiSelect";
|
||||||
import type { TaskNode } from "./types";
|
import type { TaskNode } from "./types";
|
||||||
@@ -39,6 +42,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
|||||||
import { useRerender } from "@/hooks/useRerender";
|
import { useRerender } from "@/hooks/useRerender";
|
||||||
|
|
||||||
import { DisableCache } from "../DisableCache";
|
import { DisableCache } from "../DisableCache";
|
||||||
|
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||||
|
|
||||||
function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
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 outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||||
const update = useUpdate<TaskNode["data"]>({ id, editable });
|
const update = useUpdate<TaskNode["data"]>({ id, editable });
|
||||||
|
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFacing(data.showCode ? "back" : "front");
|
setFacing(data.showCode ? "back" : "front");
|
||||||
@@ -303,49 +308,32 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<BlockExecutionOptions
|
||||||
<div className="flex items-center justify-between">
|
continueOnFailure={data.continueOnFailure}
|
||||||
<div className="flex gap-2">
|
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
includeActionHistoryInVerification={
|
||||||
Include Action History
|
data.includeActionHistoryInVerification
|
||||||
</Label>
|
}
|
||||||
<HelpTooltip
|
editable={editable}
|
||||||
content={
|
isInsideForLoop={isInsideForLoop}
|
||||||
helpTooltips["task"][
|
blockType="task"
|
||||||
"includeActionHistoryInVerification"
|
showOptions={{
|
||||||
]
|
continueOnFailure: true,
|
||||||
}
|
nextIterationOnFailure: true,
|
||||||
/>
|
includeActionHistoryInVerification: true,
|
||||||
</div>
|
}}
|
||||||
<div className="w-52">
|
onContinueOnFailureChange={(checked) => {
|
||||||
<Switch
|
update({ continueOnFailure: checked });
|
||||||
checked={data.includeActionHistoryInVerification}
|
}}
|
||||||
onCheckedChange={(checked) => {
|
onNextIterationOnFailureChange={(checked) => {
|
||||||
update({
|
update({ nextIterationOnFailure: checked });
|
||||||
includeActionHistoryInVerification: checked,
|
}}
|
||||||
});
|
onIncludeActionHistoryInVerificationChange={(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>
|
|
||||||
<DisableCache
|
<DisableCache
|
||||||
disableCache={data.disableCache}
|
disableCache={data.disableCache}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
@@ -21,7 +20,10 @@ import { helpTooltips } from "../../helpContent";
|
|||||||
import { errorMappingExampleValue } from "../types";
|
import { errorMappingExampleValue } from "../types";
|
||||||
import type { ValidationNode } from "./types";
|
import type { ValidationNode } from "./types";
|
||||||
import { AppNode } from "..";
|
import { AppNode } from "..";
|
||||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
import {
|
||||||
|
getAvailableOutputParameterKeys,
|
||||||
|
isNodeInsideForLoop,
|
||||||
|
} from "../../workflowEditorUtils";
|
||||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||||
import { ModelSelector } from "@/components/ModelSelector";
|
import { ModelSelector } from "@/components/ModelSelector";
|
||||||
@@ -34,6 +36,7 @@ import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
|||||||
import { useRerender } from "@/hooks/useRerender";
|
import { useRerender } from "@/hooks/useRerender";
|
||||||
|
|
||||||
import { DisableCache } from "../DisableCache";
|
import { DisableCache } from "../DisableCache";
|
||||||
|
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||||
|
|
||||||
function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
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 outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||||
const update = useUpdate<ValidationNode["data"]>({ id, editable });
|
const update = useUpdate<ValidationNode["data"]>({ id, editable });
|
||||||
|
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFacing(data.showCode ? "back" : "front");
|
setFacing(data.showCode ? "back" : "front");
|
||||||
@@ -212,30 +216,19 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<BlockExecutionOptions
|
||||||
<div className="flex items-center justify-between">
|
continueOnFailure={data.continueOnFailure}
|
||||||
<div className="flex gap-2">
|
nextIterationOnFailure={data.nextIterationOnFailure}
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
editable={editable}
|
||||||
Continue on Failure
|
isInsideForLoop={isInsideForLoop}
|
||||||
</Label>
|
blockType="validation"
|
||||||
<HelpTooltip
|
onContinueOnFailureChange={(checked) => {
|
||||||
content={
|
update({ continueOnFailure: checked });
|
||||||
helpTooltips["validation"]["continueOnFailure"]
|
}}
|
||||||
}
|
onNextIterationOnFailureChange={(checked) => {
|
||||||
/>
|
update({ nextIterationOnFailure: checked });
|
||||||
</div>
|
}}
|
||||||
<div className="w-52">
|
/>
|
||||||
<Switch
|
|
||||||
checked={data.continueOnFailure}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
if (!editable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
update({ continueOnFailure: checked });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DisableCache
|
<DisableCache
|
||||||
disableCache={data.disableCache}
|
disableCache={data.disableCache}
|
||||||
editable={editable}
|
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;
|
debuggable: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
continueOnFailure: boolean;
|
continueOnFailure: boolean;
|
||||||
|
nextIterationOnFailure?: boolean;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
model: WorkflowModel | null;
|
model: WorkflowModel | null;
|
||||||
showCode?: boolean;
|
showCode?: boolean;
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ function convertToNode(
|
|||||||
debuggable: debuggableWorkflowBlockTypes.has(block.block_type),
|
debuggable: debuggableWorkflowBlockTypes.has(block.block_type),
|
||||||
label: block.label,
|
label: block.label,
|
||||||
continueOnFailure: block.continue_on_failure,
|
continueOnFailure: block.continue_on_failure,
|
||||||
|
nextIterationOnFailure: block.next_iteration_on_failure,
|
||||||
editable,
|
editable,
|
||||||
model: block.model,
|
model: block.model,
|
||||||
};
|
};
|
||||||
@@ -489,6 +490,7 @@ function convertToNode(
|
|||||||
loopValue: block.loop_over?.key ?? "",
|
loopValue: block.loop_over?.key ?? "",
|
||||||
loopVariableReference: loopVariableReference,
|
loopVariableReference: loopVariableReference,
|
||||||
completeIfEmpty: block.complete_if_empty,
|
completeIfEmpty: block.complete_if_empty,
|
||||||
|
nextIterationOnFailure: block.next_iteration_on_failure,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1073,6 +1075,7 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
|||||||
const base = {
|
const base = {
|
||||||
label: node.data.label,
|
label: node.data.label,
|
||||||
continue_on_failure: node.data.continueOnFailure,
|
continue_on_failure: node.data.continueOnFailure,
|
||||||
|
next_iteration_on_failure: node.data.nextIterationOnFailure,
|
||||||
model: node.data.model,
|
model: node.data.model,
|
||||||
};
|
};
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
@@ -1422,6 +1425,7 @@ function getOrderedChildrenBlocks(
|
|||||||
block_type: "for_loop",
|
block_type: "for_loop",
|
||||||
label: currentNode.data.label,
|
label: currentNode.data.label,
|
||||||
continue_on_failure: currentNode.data.continueOnFailure,
|
continue_on_failure: currentNode.data.continueOnFailure,
|
||||||
|
next_iteration_on_failure: currentNode.data.nextIterationOnFailure,
|
||||||
loop_blocks: loopChildren,
|
loop_blocks: loopChildren,
|
||||||
loop_variable_reference: currentNode.data.loopVariableReference,
|
loop_variable_reference: currentNode.data.loopVariableReference,
|
||||||
complete_if_empty: currentNode.data.completeIfEmpty,
|
complete_if_empty: currentNode.data.completeIfEmpty,
|
||||||
@@ -1452,6 +1456,7 @@ function getWorkflowBlocksUtil(
|
|||||||
block_type: "for_loop",
|
block_type: "for_loop",
|
||||||
label: node.data.label,
|
label: node.data.label,
|
||||||
continue_on_failure: node.data.continueOnFailure,
|
continue_on_failure: node.data.continueOnFailure,
|
||||||
|
next_iteration_on_failure: node.data.nextIterationOnFailure,
|
||||||
loop_blocks: getOrderedChildrenBlocks(nodes, edges, node.id),
|
loop_blocks: getOrderedChildrenBlocks(nodes, edges, node.id),
|
||||||
loop_variable_reference: node.data.loopVariableReference,
|
loop_variable_reference: node.data.loopVariableReference,
|
||||||
complete_if_empty: node.data.completeIfEmpty,
|
complete_if_empty: node.data.completeIfEmpty,
|
||||||
@@ -1952,6 +1957,7 @@ function convertBlocksToBlockYAML(
|
|||||||
const base = {
|
const base = {
|
||||||
label: block.label,
|
label: block.label,
|
||||||
continue_on_failure: block.continue_on_failure,
|
continue_on_failure: block.continue_on_failure,
|
||||||
|
next_iteration_on_failure: block.next_iteration_on_failure,
|
||||||
next_block_label: block.next_block_label,
|
next_block_label: block.next_block_label,
|
||||||
};
|
};
|
||||||
switch (block.block_type) {
|
switch (block.block_type) {
|
||||||
@@ -2455,6 +2461,23 @@ function getLabelForWorkflowParameterType(type: WorkflowParameterValueType) {
|
|||||||
return type;
|
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 {
|
export {
|
||||||
convert,
|
convert,
|
||||||
convertEchoParameters,
|
convertEchoParameters,
|
||||||
@@ -2479,6 +2502,7 @@ export {
|
|||||||
getUpdatedParametersAfterLabelUpdateForSourceParameterKey,
|
getUpdatedParametersAfterLabelUpdateForSourceParameterKey,
|
||||||
getWorkflowBlocks,
|
getWorkflowBlocks,
|
||||||
getWorkflowErrors,
|
getWorkflowErrors,
|
||||||
|
isNodeInsideForLoop,
|
||||||
isOutputParameterKey,
|
isOutputParameterKey,
|
||||||
layout,
|
layout,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ export type WorkflowBlockBase = {
|
|||||||
block_type: WorkflowBlockType;
|
block_type: WorkflowBlockType;
|
||||||
output_parameter: OutputParameter;
|
output_parameter: OutputParameter;
|
||||||
continue_on_failure: boolean;
|
continue_on_failure: boolean;
|
||||||
|
next_iteration_on_failure?: boolean;
|
||||||
model: WorkflowModel | null;
|
model: WorkflowModel | null;
|
||||||
next_block_label?: string | null;
|
next_block_label?: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export type BlockYAMLBase = {
|
|||||||
block_type: WorkflowBlockType;
|
block_type: WorkflowBlockType;
|
||||||
label: string;
|
label: string;
|
||||||
continue_on_failure?: boolean;
|
continue_on_failure?: boolean;
|
||||||
|
next_iteration_on_failure?: boolean;
|
||||||
next_block_label?: string | null;
|
next_block_label?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ class Block(BaseModel, abc.ABC):
|
|||||||
model: dict[str, Any] | None = None
|
model: dict[str, Any] | None = None
|
||||||
disable_cache: bool = False
|
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
|
@property
|
||||||
def override_llm_key(self) -> str | None:
|
def override_llm_key(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
@@ -1386,15 +1390,15 @@ class ForLoopBlock(Block):
|
|||||||
organization_id=organization_id,
|
organization_id=organization_id,
|
||||||
)
|
)
|
||||||
block_outputs.append(failure_block_result)
|
block_outputs.append(failure_block_result)
|
||||||
# If continue_on_failure is False, stop the entire loop
|
# If next_iteration_on_failure is False, stop the entire loop
|
||||||
if not self.continue_on_failure:
|
if not self.next_iteration_on_failure:
|
||||||
outputs_with_loop_values.append(each_loop_output_values)
|
outputs_with_loop_values.append(each_loop_output_values)
|
||||||
return LoopBlockExecutedResult(
|
return LoopBlockExecutedResult(
|
||||||
outputs_with_loop_values=outputs_with_loop_values,
|
outputs_with_loop_values=outputs_with_loop_values,
|
||||||
block_outputs=block_outputs,
|
block_outputs=block_outputs,
|
||||||
last_block=current_block,
|
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
|
break
|
||||||
|
|
||||||
if block_output.status == BlockStatus.canceled:
|
if block_output.status == BlockStatus.canceled:
|
||||||
@@ -1412,7 +1416,12 @@ class ForLoopBlock(Block):
|
|||||||
last_block=current_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(
|
LOG.info(
|
||||||
f"ForLoopBlock: Encountered a failure processing block {block_idx} during loop {loop_idx}, terminating early",
|
f"ForLoopBlock: Encountered a failure processing block {block_idx} during loop {loop_idx}, terminating early",
|
||||||
block_outputs=block_outputs,
|
block_outputs=block_outputs,
|
||||||
@@ -1421,6 +1430,8 @@ class ForLoopBlock(Block):
|
|||||||
loop_over_value=loop_over_value,
|
loop_over_value=loop_over_value,
|
||||||
loop_block_continue_on_failure=loop_block.continue_on_failure,
|
loop_block_continue_on_failure=loop_block.continue_on_failure,
|
||||||
failure_reason=block_output.failure_reason,
|
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)
|
outputs_with_loop_values.append(each_loop_output_values)
|
||||||
return LoopBlockExecutedResult(
|
return LoopBlockExecutedResult(
|
||||||
@@ -1429,6 +1440,21 @@ class ForLoopBlock(Block):
|
|||||||
last_block=current_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)
|
outputs_with_loop_values.append(each_loop_output_values)
|
||||||
|
|
||||||
return LoopBlockExecutedResult(
|
return LoopBlockExecutedResult(
|
||||||
|
|||||||
@@ -2951,6 +2951,7 @@ class WorkflowService:
|
|||||||
"next_block_label": block_yaml.next_block_label,
|
"next_block_label": block_yaml.next_block_label,
|
||||||
"output_parameter": output_parameter,
|
"output_parameter": output_parameter,
|
||||||
"continue_on_failure": block_yaml.continue_on_failure,
|
"continue_on_failure": block_yaml.continue_on_failure,
|
||||||
|
"next_iteration_on_failure": block_yaml.next_iteration_on_failure,
|
||||||
"model": block_yaml.model,
|
"model": block_yaml.model,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,6 +206,9 @@ class BlockYAML(BaseModel, abc.ABC):
|
|||||||
)
|
)
|
||||||
continue_on_failure: bool = False
|
continue_on_failure: bool = False
|
||||||
model: dict[str, Any] | None = None
|
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")
|
@field_validator("label")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
Reference in New Issue
Block a user