Add extraction node (#1259)
This commit is contained in:
40
skyvern-frontend/src/components/icons/ExtractIcon.tsx
Normal file
40
skyvern-frontend/src/components/icons/ExtractIcon.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ExtractIcon({ className }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V16.2C3 17.8802 3 18.7202 3.32698 19.362C3.6146 19.9265 4.07354 20.3854 4.63803 20.673C5.27976 21 6.11984 21 7.8 21H16.2C17.8802 21 18.7202 21 19.362 20.673C19.9265 20.3854 20.3854 19.9265 20.673 19.362C21 18.7202 21 17.8802 21 16.2V14"
|
||||||
|
stroke="#F8FAFC"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 14V11.4286C8 10.9739 8.1756 10.5379 8.48816 10.2164C8.80072 9.8949 9.22464 9.71429 9.66667 9.71429H16.3333C16.7754 9.71429 17.1993 9.53367 17.5118 9.21218C17.8244 8.89069 18 8.45466 18 8V2"
|
||||||
|
stroke="#F8FAFC"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M21 5L18 2L15 5"
|
||||||
|
stroke="#F8FAFC"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ExtractIcon };
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
|
import { getClient } from "@/api/AxiosClient";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { toast } from "@/components/ui/use-toast";
|
||||||
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
|
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
||||||
import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext";
|
import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext";
|
||||||
|
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||||
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
||||||
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Background,
|
Background,
|
||||||
BackgroundVariant,
|
BackgroundVariant,
|
||||||
@@ -12,8 +28,11 @@ import {
|
|||||||
useNodesState,
|
useNodesState,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useBlocker, useParams } from "react-router-dom";
|
||||||
|
import { stringify as convertToYAML } from "yaml";
|
||||||
import {
|
import {
|
||||||
AWSSecretParameter,
|
AWSSecretParameter,
|
||||||
WorkflowApiResponse,
|
WorkflowApiResponse,
|
||||||
@@ -31,12 +50,7 @@ import {
|
|||||||
import { WorkflowHeader } from "./WorkflowHeader";
|
import { WorkflowHeader } from "./WorkflowHeader";
|
||||||
import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
|
import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
|
||||||
import { edgeTypes } from "./edges";
|
import { edgeTypes } from "./edges";
|
||||||
import {
|
import { AppNode, nodeTypes, WorkflowBlockNode } from "./nodes";
|
||||||
AppNode,
|
|
||||||
isWorkflowBlockNode,
|
|
||||||
nodeTypes,
|
|
||||||
WorkflowBlockNode,
|
|
||||||
} from "./nodes";
|
|
||||||
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
||||||
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
||||||
import "./reactFlowOverrideStyles.css";
|
import "./reactFlowOverrideStyles.css";
|
||||||
@@ -48,33 +62,11 @@ import {
|
|||||||
getAdditionalParametersForEmailBlock,
|
getAdditionalParametersForEmailBlock,
|
||||||
getOutputParameterKey,
|
getOutputParameterKey,
|
||||||
getWorkflowBlocks,
|
getWorkflowBlocks,
|
||||||
|
getWorkflowErrors,
|
||||||
layout,
|
layout,
|
||||||
nodeAdderNode,
|
nodeAdderNode,
|
||||||
startNode,
|
startNode,
|
||||||
} from "./workflowEditorUtils";
|
} from "./workflowEditorUtils";
|
||||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
|
||||||
import { useBlocker, useParams } from "react-router-dom";
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { getClient } from "@/api/AxiosClient";
|
|
||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
|
||||||
import { stringify as convertToYAML } from "yaml";
|
|
||||||
import { toast } from "@/components/ui/use-toast";
|
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
|
||||||
import { isLoopNode, LoopNode } from "./nodes/LoopNode/types";
|
|
||||||
import { isTaskNode } from "./nodes/TaskNode/types";
|
|
||||||
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
|
||||||
import { isValidationNode } from "./nodes/ValidationNode/types";
|
|
||||||
import { isActionNode } from "./nodes/ActionNode/types";
|
|
||||||
|
|
||||||
function convertToParametersYAML(
|
function convertToParametersYAML(
|
||||||
parameters: ParametersState,
|
parameters: ParametersState,
|
||||||
@@ -456,80 +448,6 @@ function FlowRenderer({
|
|||||||
doLayout(newNodesWithUpdatedParameters, newEdges);
|
doLayout(newNodesWithUpdatedParameters, newEdges);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWorkflowErrors(): Array<string> {
|
|
||||||
const errors: Array<string> = [];
|
|
||||||
|
|
||||||
const workflowBlockNodes = nodes.filter(isWorkflowBlockNode);
|
|
||||||
if (
|
|
||||||
workflowBlockNodes.length > 0 &&
|
|
||||||
workflowBlockNodes[0]!.type === "validation"
|
|
||||||
) {
|
|
||||||
const label = workflowBlockNodes[0]!.data.label;
|
|
||||||
errors.push(
|
|
||||||
`${label}: Validation block can't be the first block in a workflow.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionNodes = nodes.filter(isActionNode);
|
|
||||||
actionNodes.forEach((node) => {
|
|
||||||
if (node.data.navigationGoal.length === 0) {
|
|
||||||
errors.push(`${node.data.label}: Action Instruction is required.`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
JSON.parse(node.data.errorCodeMapping);
|
|
||||||
} catch {
|
|
||||||
errors.push(`${node.data.label}: Error messages is not valid JSON.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// check loop node parameters
|
|
||||||
const loopNodes: Array<LoopNode> = nodes.filter(isLoopNode);
|
|
||||||
const emptyLoopNodes = loopNodes.filter(
|
|
||||||
(node: LoopNode) => node.data.loopValue === "",
|
|
||||||
);
|
|
||||||
if (emptyLoopNodes.length > 0) {
|
|
||||||
emptyLoopNodes.forEach((node) => {
|
|
||||||
errors.push(
|
|
||||||
`${node.data.label}: Loop value parameter must be selected.`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// check task node json fields
|
|
||||||
const taskNodes = nodes.filter(isTaskNode);
|
|
||||||
taskNodes.forEach((node) => {
|
|
||||||
try {
|
|
||||||
JSON.parse(node.data.dataSchema);
|
|
||||||
} catch {
|
|
||||||
errors.push(`${node.data.label}: Data schema is not valid JSON.`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
JSON.parse(node.data.errorCodeMapping);
|
|
||||||
} catch {
|
|
||||||
errors.push(`${node.data.label}: Error messages is not valid JSON.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const validationNodes = nodes.filter(isValidationNode);
|
|
||||||
validationNodes.forEach((node) => {
|
|
||||||
try {
|
|
||||||
JSON.parse(node.data.errorCodeMapping);
|
|
||||||
} catch {
|
|
||||||
errors.push(`${node.data.label}: Error messages is not valid JSON`);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
node.data.completeCriterion.length === 0 &&
|
|
||||||
node.data.terminateCriterion.length === 0
|
|
||||||
) {
|
|
||||||
errors.push(
|
|
||||||
`${node.data.label}: At least one of completion or termination criteria must be provided`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -649,7 +567,7 @@ function FlowRenderer({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSave={async () => {
|
onSave={async () => {
|
||||||
const errors = getWorkflowErrors();
|
const errors = getWorkflowErrors(nodes);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
toast({
|
toast({
|
||||||
title: "Can not save workflow because of errors:",
|
title: "Can not save workflow because of errors:",
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||||
|
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||||
|
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
import { NodeActionMenu } from "../NodeActionMenu";
|
||||||
|
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { dataSchemaExampleValue } from "../types";
|
||||||
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import type { ExtractionNode } from "./types";
|
||||||
|
import {
|
||||||
|
commonFieldPlaceholders,
|
||||||
|
commonHelpTooltipContent,
|
||||||
|
} from "../../constants";
|
||||||
|
import { ExtractIcon } from "@/components/icons/ExtractIcon";
|
||||||
|
|
||||||
|
const dataExtractionGoalTooltip =
|
||||||
|
"Tell Skyvern what data you would like to scrape.";
|
||||||
|
const dataSchemaTooltip = "Specify a format for extracted data in JSON.";
|
||||||
|
const dataExtractionGoalPlaceholder = "What data do you need to extract?";
|
||||||
|
|
||||||
|
function ExtractionNode({ id, data }: NodeProps<ExtractionNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
const { editable } = data;
|
||||||
|
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||||
|
id,
|
||||||
|
initialValue: data.label,
|
||||||
|
});
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
url: data.url,
|
||||||
|
dataExtractionGoal: data.dataExtractionGoal,
|
||||||
|
dataSchema: data.dataSchema,
|
||||||
|
maxRetries: data.maxRetries,
|
||||||
|
maxStepsOverride: data.maxStepsOverride,
|
||||||
|
continueOnFailure: data.continueOnFailure,
|
||||||
|
cacheActions: data.cacheActions,
|
||||||
|
});
|
||||||
|
const deleteNodeCallback = useDeleteNodeCallback();
|
||||||
|
|
||||||
|
function handleChange(key: string, value: unknown) {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputs({ ...inputs, [key]: value });
|
||||||
|
updateNodeData(id, { [key]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="a"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
id="b"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||||
|
<header className="flex h-[2.75rem] justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||||
|
<ExtractIcon className="size-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<EditableNodeTitle
|
||||||
|
value={label}
|
||||||
|
editable={editable}
|
||||||
|
onChange={setLabel}
|
||||||
|
titleClassName="text-base"
|
||||||
|
inputClassName="text-base"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-400">Extraction Block</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NodeActionMenu
|
||||||
|
onDelete={() => {
|
||||||
|
deleteNodeCallback(id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">
|
||||||
|
Data Extraction Goal
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip content={dataExtractionGoalTooltip} />
|
||||||
|
</div>
|
||||||
|
<AutoResizingTextarea
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("dataExtractionGoal", event.target.value);
|
||||||
|
}}
|
||||||
|
value={inputs.dataExtractionGoal}
|
||||||
|
placeholder={dataExtractionGoalPlaceholder}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">Data Schema</Label>
|
||||||
|
<HelpTooltip content={dataSchemaTooltip} />
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
checked={inputs.dataSchema !== "null"}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange(
|
||||||
|
"dataSchema",
|
||||||
|
checked
|
||||||
|
? JSON.stringify(dataSchemaExampleValue, null, 2)
|
||||||
|
: "null",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{inputs.dataSchema !== "null" && (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
value={inputs.dataSchema}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("dataSchema", value);
|
||||||
|
}}
|
||||||
|
className="nowheel nopan"
|
||||||
|
fontSize={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<Accordion type="single" collapsible>
|
||||||
|
<AccordionItem value="advanced" className="border-b-0">
|
||||||
|
<AccordionTrigger className="py-0">
|
||||||
|
Advanced Settings
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="pl-6 pr-1 pt-1">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Max Retries
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={commonHelpTooltipContent["maxRetries"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={commonFieldPlaceholders["maxRetries"]}
|
||||||
|
className="nopan w-52 text-xs"
|
||||||
|
min="0"
|
||||||
|
value={inputs.maxRetries ?? ""}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value =
|
||||||
|
event.target.value === ""
|
||||||
|
? null
|
||||||
|
: Number(event.target.value);
|
||||||
|
handleChange("maxRetries", value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Max Steps Override
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={commonHelpTooltipContent["maxStepsOverride"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={commonFieldPlaceholders["maxStepsOverride"]}
|
||||||
|
className="nopan w-52 text-xs"
|
||||||
|
min="0"
|
||||||
|
value={inputs.maxStepsOverride ?? ""}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value =
|
||||||
|
event.target.value === ""
|
||||||
|
? null
|
||||||
|
: Number(event.target.value);
|
||||||
|
handleChange("maxStepsOverride", value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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={commonHelpTooltipContent["continueOnFailure"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-52">
|
||||||
|
<Switch
|
||||||
|
checked={inputs.continueOnFailure}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("continueOnFailure", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Cache Actions
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={commonHelpTooltipContent["cacheActions"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-52">
|
||||||
|
<Switch
|
||||||
|
checked={inputs.cacheActions}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("cacheActions", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ExtractionNode };
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { Node } from "@xyflow/react";
|
||||||
|
import { NodeBaseData } from "../types";
|
||||||
|
|
||||||
|
export type ExtractionNodeData = NodeBaseData & {
|
||||||
|
url: string;
|
||||||
|
dataExtractionGoal: string;
|
||||||
|
dataSchema: string;
|
||||||
|
maxRetries: number | null;
|
||||||
|
maxStepsOverride: number | null;
|
||||||
|
parameterKeys: Array<string>;
|
||||||
|
cacheActions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtractionNode = Node<ExtractionNodeData, "extraction">;
|
||||||
|
|
||||||
|
export const extractionNodeDefaultData: ExtractionNodeData = {
|
||||||
|
label: "",
|
||||||
|
url: "",
|
||||||
|
dataExtractionGoal: "",
|
||||||
|
dataSchema: "null",
|
||||||
|
maxRetries: null,
|
||||||
|
maxStepsOverride: null,
|
||||||
|
editable: true,
|
||||||
|
parameterKeys: [],
|
||||||
|
continueOnFailure: false,
|
||||||
|
cacheActions: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function isExtractionNode(node: Node): node is ExtractionNode {
|
||||||
|
return node.type === "extraction";
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@ import type { ActionNode } from "./ActionNode/types";
|
|||||||
import { ActionNode as ActionNodeComponent } from "./ActionNode/ActionNode";
|
import { ActionNode as ActionNodeComponent } from "./ActionNode/ActionNode";
|
||||||
import { NavigationNode } from "./NavigationNode/types";
|
import { NavigationNode } from "./NavigationNode/types";
|
||||||
import { NavigationNode as NavigationNodeComponent } from "./NavigationNode/NavigationNode";
|
import { NavigationNode as NavigationNodeComponent } from "./NavigationNode/NavigationNode";
|
||||||
|
import { ExtractionNode } from "./ExtractionNode/types";
|
||||||
|
import { ExtractionNode as ExtractionNodeComponent } from "./ExtractionNode/ExtractionNode";
|
||||||
|
|
||||||
export type UtilityNode = StartNode | NodeAdderNode;
|
export type UtilityNode = StartNode | NodeAdderNode;
|
||||||
|
|
||||||
@@ -39,7 +41,8 @@ export type WorkflowBlockNode =
|
|||||||
| DownloadNode
|
| DownloadNode
|
||||||
| ValidationNode
|
| ValidationNode
|
||||||
| ActionNode
|
| ActionNode
|
||||||
| NavigationNode;
|
| NavigationNode
|
||||||
|
| ExtractionNode;
|
||||||
|
|
||||||
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
||||||
return node.type === "nodeAdder" || node.type === "start";
|
return node.type === "nodeAdder" || node.type === "start";
|
||||||
@@ -65,4 +68,5 @@ export const nodeTypes = {
|
|||||||
validation: memo(ValidationNodeComponent),
|
validation: memo(ValidationNodeComponent),
|
||||||
action: memo(ActionNodeComponent),
|
action: memo(ActionNodeComponent),
|
||||||
navigation: memo(NavigationNodeComponent),
|
navigation: memo(NavigationNodeComponent),
|
||||||
|
extraction: memo(ExtractionNodeComponent),
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { AddNodeProps } from "../FlowRenderer";
|
|||||||
import { ClickIcon } from "@/components/icons/ClickIcon";
|
import { ClickIcon } from "@/components/icons/ClickIcon";
|
||||||
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
||||||
import { RobotIcon } from "@/components/icons/RobotIcon";
|
import { RobotIcon } from "@/components/icons/RobotIcon";
|
||||||
|
import { ExtractIcon } from "@/components/icons/ExtractIcon";
|
||||||
|
|
||||||
const nodeLibraryItems: Array<{
|
const nodeLibraryItems: Array<{
|
||||||
nodeType: NonNullable<WorkflowBlockNode["type"]>;
|
nodeType: NonNullable<WorkflowBlockNode["type"]>;
|
||||||
@@ -90,6 +91,12 @@ const nodeLibraryItems: Array<{
|
|||||||
title: "Navigation Block",
|
title: "Navigation Block",
|
||||||
description: "Navigate on the page",
|
description: "Navigate on the page",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
nodeType: "extraction",
|
||||||
|
icon: <ExtractIcon className="size-6" />,
|
||||||
|
title: "Extraction Block",
|
||||||
|
description: "Extract data from the page",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
ValidationBlockYAML,
|
ValidationBlockYAML,
|
||||||
NavigationBlockYAML,
|
NavigationBlockYAML,
|
||||||
WorkflowCreateYAMLRequest,
|
WorkflowCreateYAMLRequest,
|
||||||
|
ExtractionBlockYAML,
|
||||||
} from "../types/workflowYamlTypes";
|
} from "../types/workflowYamlTypes";
|
||||||
import {
|
import {
|
||||||
EMAIL_BLOCK_SENDER,
|
EMAIL_BLOCK_SENDER,
|
||||||
@@ -43,17 +44,31 @@ import { AppNode, isWorkflowBlockNode, WorkflowBlockNode } from "./nodes";
|
|||||||
import { codeBlockNodeDefaultData } from "./nodes/CodeBlockNode/types";
|
import { codeBlockNodeDefaultData } from "./nodes/CodeBlockNode/types";
|
||||||
import { downloadNodeDefaultData } from "./nodes/DownloadNode/types";
|
import { downloadNodeDefaultData } from "./nodes/DownloadNode/types";
|
||||||
import { fileParserNodeDefaultData } from "./nodes/FileParserNode/types";
|
import { fileParserNodeDefaultData } from "./nodes/FileParserNode/types";
|
||||||
import { LoopNode, loopNodeDefaultData } from "./nodes/LoopNode/types";
|
import {
|
||||||
|
isLoopNode,
|
||||||
|
LoopNode,
|
||||||
|
loopNodeDefaultData,
|
||||||
|
} from "./nodes/LoopNode/types";
|
||||||
import { NodeAdderNode } from "./nodes/NodeAdderNode/types";
|
import { NodeAdderNode } from "./nodes/NodeAdderNode/types";
|
||||||
import { sendEmailNodeDefaultData } from "./nodes/SendEmailNode/types";
|
import { sendEmailNodeDefaultData } from "./nodes/SendEmailNode/types";
|
||||||
import { StartNode } from "./nodes/StartNode/types";
|
import { StartNode } from "./nodes/StartNode/types";
|
||||||
import { taskNodeDefaultData } from "./nodes/TaskNode/types";
|
import { isTaskNode, taskNodeDefaultData } from "./nodes/TaskNode/types";
|
||||||
import { textPromptNodeDefaultData } from "./nodes/TextPromptNode/types";
|
import { textPromptNodeDefaultData } from "./nodes/TextPromptNode/types";
|
||||||
import { NodeBaseData } from "./nodes/types";
|
import { NodeBaseData } from "./nodes/types";
|
||||||
import { uploadNodeDefaultData } from "./nodes/UploadNode/types";
|
import { uploadNodeDefaultData } from "./nodes/UploadNode/types";
|
||||||
import { validationNodeDefaultData } from "./nodes/ValidationNode/types";
|
import {
|
||||||
import { actionNodeDefaultData } from "./nodes/ActionNode/types";
|
isValidationNode,
|
||||||
import { navigationNodeDefaultData } from "./nodes/NavigationNode/types";
|
validationNodeDefaultData,
|
||||||
|
} from "./nodes/ValidationNode/types";
|
||||||
|
import { actionNodeDefaultData, isActionNode } from "./nodes/ActionNode/types";
|
||||||
|
import {
|
||||||
|
isNavigationNode,
|
||||||
|
navigationNodeDefaultData,
|
||||||
|
} from "./nodes/NavigationNode/types";
|
||||||
|
import {
|
||||||
|
extractionNodeDefaultData,
|
||||||
|
isExtractionNode,
|
||||||
|
} from "./nodes/ExtractionNode/types";
|
||||||
|
|
||||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
export const NEW_NODE_LABEL_PREFIX = "block_";
|
||||||
|
|
||||||
@@ -220,6 +235,23 @@ function convertToNode(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "extraction": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "extraction",
|
||||||
|
data: {
|
||||||
|
...commonData,
|
||||||
|
url: block.url ?? "",
|
||||||
|
dataExtractionGoal: block.data_extraction_goal ?? "",
|
||||||
|
dataSchema: JSON.stringify(block.data_schema, null, 2),
|
||||||
|
parameterKeys: block.parameters.map((p) => p.key),
|
||||||
|
maxRetries: block.max_retries ?? null,
|
||||||
|
maxStepsOverride: block.max_steps_per_run ?? null,
|
||||||
|
cacheActions: block.cache_actions,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
case "code": {
|
case "code": {
|
||||||
return {
|
return {
|
||||||
...identifiers,
|
...identifiers,
|
||||||
@@ -543,6 +575,17 @@ function createNode(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "extraction": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "extraction",
|
||||||
|
data: {
|
||||||
|
...extractionNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
case "loop": {
|
case "loop": {
|
||||||
return {
|
return {
|
||||||
...identifiers,
|
...identifiers,
|
||||||
@@ -679,6 +722,7 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
|||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
block_type: "action",
|
block_type: "action",
|
||||||
|
title: node.data.label,
|
||||||
navigation_goal: node.data.navigationGoal,
|
navigation_goal: node.data.navigationGoal,
|
||||||
error_code_mapping: JSONParseSafe(node.data.errorCodeMapping) as Record<
|
error_code_mapping: JSONParseSafe(node.data.errorCodeMapping) as Record<
|
||||||
string,
|
string,
|
||||||
@@ -700,6 +744,7 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
|||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
block_type: "navigation",
|
block_type: "navigation",
|
||||||
|
title: node.data.label,
|
||||||
navigation_goal: node.data.navigationGoal,
|
navigation_goal: node.data.navigationGoal,
|
||||||
error_code_mapping: JSONParseSafe(node.data.errorCodeMapping) as Record<
|
error_code_mapping: JSONParseSafe(node.data.errorCodeMapping) as Record<
|
||||||
string,
|
string,
|
||||||
@@ -718,6 +763,22 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
|||||||
cache_actions: node.data.cacheActions,
|
cache_actions: node.data.cacheActions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "extraction": {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
block_type: "extraction",
|
||||||
|
url: node.data.url,
|
||||||
|
title: node.data.label,
|
||||||
|
data_extraction_goal: node.data.dataExtractionGoal,
|
||||||
|
data_schema: JSONParseSafe(node.data.dataSchema),
|
||||||
|
...(node.data.maxRetries !== null && {
|
||||||
|
max_retries: node.data.maxRetries,
|
||||||
|
}),
|
||||||
|
max_steps_per_run: node.data.maxStepsOverride,
|
||||||
|
parameter_keys: node.data.parameterKeys,
|
||||||
|
cache_actions: node.data.cacheActions,
|
||||||
|
};
|
||||||
|
}
|
||||||
case "sendEmail": {
|
case "sendEmail": {
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@@ -1174,6 +1235,7 @@ function convertBlocksToBlockYAML(
|
|||||||
const blockYaml: TaskBlockYAML = {
|
const blockYaml: TaskBlockYAML = {
|
||||||
...base,
|
...base,
|
||||||
block_type: "task",
|
block_type: "task",
|
||||||
|
title: block.title,
|
||||||
url: block.url,
|
url: block.url,
|
||||||
navigation_goal: block.navigation_goal,
|
navigation_goal: block.navigation_goal,
|
||||||
data_extraction_goal: block.data_extraction_goal,
|
data_extraction_goal: block.data_extraction_goal,
|
||||||
@@ -1206,6 +1268,7 @@ function convertBlocksToBlockYAML(
|
|||||||
...base,
|
...base,
|
||||||
block_type: "action",
|
block_type: "action",
|
||||||
url: block.url,
|
url: block.url,
|
||||||
|
title: block.title,
|
||||||
navigation_goal: block.navigation_goal,
|
navigation_goal: block.navigation_goal,
|
||||||
error_code_mapping: block.error_code_mapping,
|
error_code_mapping: block.error_code_mapping,
|
||||||
max_retries: block.max_retries,
|
max_retries: block.max_retries,
|
||||||
@@ -1223,6 +1286,7 @@ function convertBlocksToBlockYAML(
|
|||||||
...base,
|
...base,
|
||||||
block_type: "navigation",
|
block_type: "navigation",
|
||||||
url: block.url,
|
url: block.url,
|
||||||
|
title: block.title,
|
||||||
navigation_goal: block.navigation_goal,
|
navigation_goal: block.navigation_goal,
|
||||||
error_code_mapping: block.error_code_mapping,
|
error_code_mapping: block.error_code_mapping,
|
||||||
max_retries: block.max_retries,
|
max_retries: block.max_retries,
|
||||||
@@ -1236,6 +1300,21 @@ function convertBlocksToBlockYAML(
|
|||||||
};
|
};
|
||||||
return blockYaml;
|
return blockYaml;
|
||||||
}
|
}
|
||||||
|
case "extraction": {
|
||||||
|
const blockYaml: ExtractionBlockYAML = {
|
||||||
|
...base,
|
||||||
|
block_type: "extraction",
|
||||||
|
url: block.url,
|
||||||
|
title: block.title,
|
||||||
|
data_extraction_goal: block.data_extraction_goal,
|
||||||
|
data_schema: block.data_schema,
|
||||||
|
max_retries: block.max_retries,
|
||||||
|
max_steps_per_run: block.max_steps_per_run,
|
||||||
|
parameter_keys: block.parameters.map((p) => p.key),
|
||||||
|
cache_actions: block.cache_actions,
|
||||||
|
};
|
||||||
|
return blockYaml;
|
||||||
|
}
|
||||||
case "for_loop": {
|
case "for_loop": {
|
||||||
const blockYaml: ForLoopBlockYAML = {
|
const blockYaml: ForLoopBlockYAML = {
|
||||||
...base,
|
...base,
|
||||||
@@ -1327,6 +1406,92 @@ function convert(workflow: WorkflowApiResponse): WorkflowCreateYAMLRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWorkflowErrors(nodes: Array<AppNode>): Array<string> {
|
||||||
|
const errors: Array<string> = [];
|
||||||
|
|
||||||
|
const workflowBlockNodes = nodes.filter(isWorkflowBlockNode);
|
||||||
|
if (
|
||||||
|
workflowBlockNodes.length > 0 &&
|
||||||
|
workflowBlockNodes[0]!.type === "validation"
|
||||||
|
) {
|
||||||
|
const label = workflowBlockNodes[0]!.data.label;
|
||||||
|
errors.push(
|
||||||
|
`${label}: Validation block can't be the first block in a workflow.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionNodes = nodes.filter(isActionNode);
|
||||||
|
actionNodes.forEach((node) => {
|
||||||
|
if (node.data.navigationGoal.length === 0) {
|
||||||
|
errors.push(`${node.data.label}: Action Instruction is required.`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSON.parse(node.data.errorCodeMapping);
|
||||||
|
} catch {
|
||||||
|
errors.push(`${node.data.label}: Error messages is not valid JSON.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// check loop node parameters
|
||||||
|
const loopNodes: Array<LoopNode> = nodes.filter(isLoopNode);
|
||||||
|
const emptyLoopNodes = loopNodes.filter(
|
||||||
|
(node: LoopNode) => node.data.loopValue === "",
|
||||||
|
);
|
||||||
|
if (emptyLoopNodes.length > 0) {
|
||||||
|
emptyLoopNodes.forEach((node) => {
|
||||||
|
errors.push(`${node.data.label}: Loop value parameter must be selected.`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// check task node json fields
|
||||||
|
const taskNodes = nodes.filter(isTaskNode);
|
||||||
|
taskNodes.forEach((node) => {
|
||||||
|
try {
|
||||||
|
JSON.parse(node.data.dataSchema);
|
||||||
|
} catch {
|
||||||
|
errors.push(`${node.data.label}: Data schema is not valid JSON.`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSON.parse(node.data.errorCodeMapping);
|
||||||
|
} catch {
|
||||||
|
errors.push(`${node.data.label}: Error messages is not valid JSON.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const validationNodes = nodes.filter(isValidationNode);
|
||||||
|
validationNodes.forEach((node) => {
|
||||||
|
try {
|
||||||
|
JSON.parse(node.data.errorCodeMapping);
|
||||||
|
} catch {
|
||||||
|
errors.push(`${node.data.label}: Error messages is not valid JSON`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
node.data.completeCriterion.length === 0 &&
|
||||||
|
node.data.terminateCriterion.length === 0
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
`${node.data.label}: At least one of completion or termination criteria must be provided`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const navigationNodes = nodes.filter(isNavigationNode);
|
||||||
|
navigationNodes.forEach((node) => {
|
||||||
|
if (node.data.navigationGoal.length === 0) {
|
||||||
|
errors.push(`${node.data.label}: Navigation goal is required.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const extractionNodes = nodes.filter(isExtractionNode);
|
||||||
|
extractionNodes.forEach((node) => {
|
||||||
|
if (node.data.dataExtractionGoal.length === 0) {
|
||||||
|
errors.push(`${node.data.label}: Data extraction goal is required.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
convert,
|
convert,
|
||||||
convertEchoParameters,
|
convertEchoParameters,
|
||||||
@@ -1344,6 +1509,7 @@ export {
|
|||||||
getUpdatedNodesAfterLabelUpdateForParameterKeys,
|
getUpdatedNodesAfterLabelUpdateForParameterKeys,
|
||||||
getUpdatedParametersAfterLabelUpdateForSourceParameterKey,
|
getUpdatedParametersAfterLabelUpdateForSourceParameterKey,
|
||||||
getWorkflowBlocks,
|
getWorkflowBlocks,
|
||||||
|
getWorkflowErrors,
|
||||||
isOutputParameterKey,
|
isOutputParameterKey,
|
||||||
layout,
|
layout,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -101,6 +101,20 @@ export type Parameter =
|
|||||||
| BitwardenSensitiveInformationParameter
|
| BitwardenSensitiveInformationParameter
|
||||||
| AWSSecretParameter;
|
| AWSSecretParameter;
|
||||||
|
|
||||||
|
export type WorkflowBlock =
|
||||||
|
| TaskBlock
|
||||||
|
| ForLoopBlock
|
||||||
|
| TextPromptBlock
|
||||||
|
| CodeBlock
|
||||||
|
| UploadToS3Block
|
||||||
|
| DownloadToS3Block
|
||||||
|
| SendEmailBlock
|
||||||
|
| FileURLParserBlock
|
||||||
|
| ValidationBlock
|
||||||
|
| ActionBlock
|
||||||
|
| NavigationBlock
|
||||||
|
| ExtractionBlock;
|
||||||
|
|
||||||
export const WorkflowBlockType = {
|
export const WorkflowBlockType = {
|
||||||
Task: "task",
|
Task: "task",
|
||||||
ForLoop: "for_loop",
|
ForLoop: "for_loop",
|
||||||
@@ -113,6 +127,7 @@ export const WorkflowBlockType = {
|
|||||||
Validation: "validation",
|
Validation: "validation",
|
||||||
Action: "action",
|
Action: "action",
|
||||||
Navigation: "navigation",
|
Navigation: "navigation",
|
||||||
|
Extraction: "extraction",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type WorkflowBlockType =
|
export type WorkflowBlockType =
|
||||||
@@ -232,18 +247,17 @@ export type NavigationBlock = WorkflowBlockBase & {
|
|||||||
cache_actions: boolean;
|
cache_actions: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowBlock =
|
export type ExtractionBlock = WorkflowBlockBase & {
|
||||||
| TaskBlock
|
block_type: "extraction";
|
||||||
| ForLoopBlock
|
data_extraction_goal: string | null;
|
||||||
| TextPromptBlock
|
url: string | null;
|
||||||
| CodeBlock
|
title: string;
|
||||||
| UploadToS3Block
|
data_schema: Record<string, unknown> | null;
|
||||||
| DownloadToS3Block
|
max_retries?: number;
|
||||||
| SendEmailBlock
|
max_steps_per_run?: number | null;
|
||||||
| FileURLParserBlock
|
parameters: Array<WorkflowParameter>;
|
||||||
| ValidationBlock
|
cache_actions: boolean;
|
||||||
| ActionBlock
|
};
|
||||||
| NavigationBlock;
|
|
||||||
|
|
||||||
export type WorkflowDefinition = {
|
export type WorkflowDefinition = {
|
||||||
parameters: Array<Parameter>;
|
parameters: Array<Parameter>;
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ const BlockTypes = {
|
|||||||
VALIDATION: "validation",
|
VALIDATION: "validation",
|
||||||
ACTION: "action",
|
ACTION: "action",
|
||||||
NAVIGATION: "navigation",
|
NAVIGATION: "navigation",
|
||||||
|
EXTRACTION: "extraction",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
|
export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
|
||||||
@@ -93,7 +94,8 @@ export type BlockYAML =
|
|||||||
| ForLoopBlockYAML
|
| ForLoopBlockYAML
|
||||||
| ValidationBlockYAML
|
| ValidationBlockYAML
|
||||||
| ActionBlockYAML
|
| ActionBlockYAML
|
||||||
| NavigationBlockYAML;
|
| NavigationBlockYAML
|
||||||
|
| ExtractionBlockYAML;
|
||||||
|
|
||||||
export type BlockYAMLBase = {
|
export type BlockYAMLBase = {
|
||||||
block_type: BlockType;
|
block_type: BlockType;
|
||||||
@@ -130,6 +132,7 @@ export type ValidationBlockYAML = BlockYAMLBase & {
|
|||||||
export type ActionBlockYAML = BlockYAMLBase & {
|
export type ActionBlockYAML = BlockYAMLBase & {
|
||||||
block_type: "action";
|
block_type: "action";
|
||||||
url: string | null;
|
url: string | null;
|
||||||
|
title?: string;
|
||||||
navigation_goal: string | null;
|
navigation_goal: string | null;
|
||||||
error_code_mapping: Record<string, string> | null;
|
error_code_mapping: Record<string, string> | null;
|
||||||
max_retries?: number;
|
max_retries?: number;
|
||||||
@@ -144,6 +147,7 @@ export type ActionBlockYAML = BlockYAMLBase & {
|
|||||||
export type NavigationBlockYAML = BlockYAMLBase & {
|
export type NavigationBlockYAML = BlockYAMLBase & {
|
||||||
block_type: "navigation";
|
block_type: "navigation";
|
||||||
url: string | null;
|
url: string | null;
|
||||||
|
title?: string;
|
||||||
navigation_goal: string | null;
|
navigation_goal: string | null;
|
||||||
error_code_mapping: Record<string, string> | null;
|
error_code_mapping: Record<string, string> | null;
|
||||||
max_retries?: number;
|
max_retries?: number;
|
||||||
@@ -156,6 +160,18 @@ export type NavigationBlockYAML = BlockYAMLBase & {
|
|||||||
cache_actions: boolean;
|
cache_actions: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ExtractionBlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "extraction";
|
||||||
|
url: string | null;
|
||||||
|
title?: string;
|
||||||
|
data_extraction_goal: string | null;
|
||||||
|
data_schema: Record<string, unknown> | null;
|
||||||
|
max_retries?: number;
|
||||||
|
max_steps_per_run?: number | null;
|
||||||
|
parameter_keys?: Array<string> | null;
|
||||||
|
cache_actions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type CodeBlockYAML = BlockYAMLBase & {
|
export type CodeBlockYAML = BlockYAMLBase & {
|
||||||
block_type: "code";
|
block_type: "code";
|
||||||
code: string;
|
code: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user