@scenario-labs/sdk 3.3.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/client.d.mts +4 -4
- package/client.d.mts.map +1 -1
- package/client.d.ts +4 -4
- package/client.d.ts.map +1 -1
- package/client.js.map +1 -1
- package/client.mjs.map +1 -1
- package/index.d.mts +1 -1
- package/index.d.mts.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +2 -1
- package/index.js.map +1 -1
- package/index.mjs +1 -1
- package/index.mjs.map +1 -1
- package/lib/user_selection_limits.d.mts +13 -0
- package/lib/user_selection_limits.d.mts.map +1 -0
- package/lib/user_selection_limits.d.ts +13 -0
- package/lib/user_selection_limits.d.ts.map +1 -0
- package/lib/user_selection_limits.js +16 -0
- package/lib/user_selection_limits.js.map +1 -0
- package/lib/user_selection_limits.mjs +13 -0
- package/lib/user_selection_limits.mjs.map +1 -0
- package/lib/workflow_converter.d.mts +6 -0
- package/lib/workflow_converter.d.mts.map +1 -1
- package/lib/workflow_converter.d.ts +6 -0
- package/lib/workflow_converter.d.ts.map +1 -1
- package/lib/workflow_converter.js +154 -52
- package/lib/workflow_converter.js.map +1 -1
- package/lib/workflow_converter.mjs +154 -52
- package/lib/workflow_converter.mjs.map +1 -1
- package/lib/workflow_import_validator.d.mts +17 -2
- package/lib/workflow_import_validator.d.mts.map +1 -1
- package/lib/workflow_import_validator.d.ts +17 -2
- package/lib/workflow_import_validator.d.ts.map +1 -1
- package/lib/workflow_import_validator.js +100 -1
- package/lib/workflow_import_validator.js.map +1 -1
- package/lib/workflow_import_validator.mjs +99 -1
- package/lib/workflow_import_validator.mjs.map +1 -1
- package/lib/workflow_validator.d.mts +1 -1
- package/lib/workflow_validator.d.mts.map +1 -1
- package/lib/workflow_validator.d.ts +1 -1
- package/lib/workflow_validator.d.ts.map +1 -1
- package/lib/workflow_validator.js +10 -11
- package/lib/workflow_validator.js.map +1 -1
- package/lib/workflow_validator.mjs +7 -8
- package/lib/workflow_validator.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/assets/assets.d.mts +126 -30
- package/resources/assets/assets.d.mts.map +1 -1
- package/resources/assets/assets.d.ts +126 -30
- package/resources/assets/assets.d.ts.map +1 -1
- package/resources/assets/assets.js +12 -0
- package/resources/assets/assets.js.map +1 -1
- package/resources/assets/assets.mjs +12 -0
- package/resources/assets/assets.mjs.map +1 -1
- package/resources/assets/index.d.mts +1 -1
- package/resources/assets/index.d.mts.map +1 -1
- package/resources/assets/index.d.ts +1 -1
- package/resources/assets/index.d.ts.map +1 -1
- package/resources/assets/index.js.map +1 -1
- package/resources/assets/index.mjs.map +1 -1
- package/resources/generate.d.mts +120 -9
- package/resources/generate.d.mts.map +1 -1
- package/resources/generate.d.ts +120 -9
- package/resources/generate.d.ts.map +1 -1
- package/resources/index.d.mts +2 -2
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +2 -2
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js.map +1 -1
- package/resources/index.mjs.map +1 -1
- package/resources/jobs.d.mts +36 -0
- package/resources/jobs.d.mts.map +1 -1
- package/resources/jobs.d.ts +36 -0
- package/resources/jobs.d.ts.map +1 -1
- package/resources/models/description.d.mts +4 -4
- package/resources/models/description.d.mts.map +1 -1
- package/resources/models/description.d.ts +4 -4
- package/resources/models/description.d.ts.map +1 -1
- package/resources/models/examples.d.mts +40 -6
- package/resources/models/examples.d.mts.map +1 -1
- package/resources/models/examples.d.ts +40 -6
- package/resources/models/examples.d.ts.map +1 -1
- package/resources/models/index.d.mts +1 -1
- package/resources/models/index.d.mts.map +1 -1
- package/resources/models/index.d.ts +1 -1
- package/resources/models/index.d.ts.map +1 -1
- package/resources/models/index.js.map +1 -1
- package/resources/models/index.mjs.map +1 -1
- package/resources/models/models.d.mts +1353 -199
- package/resources/models/models.d.mts.map +1 -1
- package/resources/models/models.d.ts +1353 -199
- package/resources/models/models.d.ts.map +1 -1
- package/resources/models/models.js +13 -0
- package/resources/models/models.js.map +1 -1
- package/resources/models/models.mjs +13 -0
- package/resources/models/models.mjs.map +1 -1
- package/resources/models/train.d.mts +36 -2
- package/resources/models/train.d.mts.map +1 -1
- package/resources/models/train.d.ts +36 -2
- package/resources/models/train.d.ts.map +1 -1
- package/resources/recommendations.d.mts +16 -5
- package/resources/recommendations.d.mts.map +1 -1
- package/resources/recommendations.d.ts +16 -5
- package/resources/recommendations.d.ts.map +1 -1
- package/resources/usages.d.mts +14 -2
- package/resources/usages.d.mts.map +1 -1
- package/resources/usages.d.ts +14 -2
- package/resources/usages.d.ts.map +1 -1
- package/resources/workflows.d.mts +152 -9
- package/resources/workflows.d.mts.map +1 -1
- package/resources/workflows.d.ts +152 -9
- package/resources/workflows.d.ts.map +1 -1
- package/resources/workflows.js +2 -3
- package/resources/workflows.js.map +1 -1
- package/resources/workflows.mjs +2 -3
- package/resources/workflows.mjs.map +1 -1
- package/src/client.ts +8 -0
- package/src/index.ts +1 -0
- package/src/lib/user_selection_limits.ts +14 -0
- package/src/lib/workflow_converter.ts +173 -54
- package/src/lib/workflow_import_validator.ts +116 -1
- package/src/lib/workflow_validator.ts +12 -8
- package/src/resources/assets/assets.ts +164 -4
- package/src/resources/assets/index.ts +2 -0
- package/src/resources/generate.ts +142 -0
- package/src/resources/index.ts +4 -0
- package/src/resources/jobs.ts +42 -0
- package/src/resources/models/description.ts +6 -0
- package/src/resources/models/examples.ts +48 -0
- package/src/resources/models/index.ts +2 -0
- package/src/resources/models/models.ts +1610 -114
- package/src/resources/models/train.ts +42 -0
- package/src/resources/recommendations.ts +18 -3
- package/src/resources/usages.ts +16 -0
- package/src/resources/workflows.ts +176 -9
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Inclusive lower bound for user-selection `min` / `max` and choose counts. */
|
|
2
|
+
export const USER_SELECTION_MIN_SELECT = 1;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Caps how many candidates may be offered (and how high `input.min` may be).
|
|
6
|
+
* Matches the API `MAX_USER_SELECTION_CANDIDATES` limit.
|
|
7
|
+
*/
|
|
8
|
+
export const USER_SELECTION_MAX_CANDIDATES = 50;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Caps how many items the user may pick (`input.max`).
|
|
12
|
+
* Matches the API `USER_SELECTION_DEFAULT_MAX` (also the omitted-`max` default).
|
|
13
|
+
*/
|
|
14
|
+
export const USER_SELECTION_DEFAULT_MAX = 100;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Converts a workflow editor state (nodes + edges) to a flow array suitable for the Scenario API.
|
|
2
2
|
// Ported from fawkes useConvertWorkflowToApp — pure function, no React dependencies.
|
|
3
3
|
|
|
4
|
+
import { USER_SELECTION_MAX_CANDIDATES, USER_SELECTION_MIN_SELECT } from './user_selection_limits';
|
|
5
|
+
|
|
4
6
|
// ---- Public types ----
|
|
5
7
|
|
|
6
8
|
export type WorkflowEditorHandleInput = {
|
|
@@ -47,6 +49,7 @@ export type WorkflowEditorNode =
|
|
|
47
49
|
}
|
|
48
50
|
| { id: string; type: 'transformText'; data: NodeCommonData & { value?: string } }
|
|
49
51
|
| { id: string; type: 'splitText'; data: NodeCommonData & { splitDelimiter?: string } }
|
|
52
|
+
| { id: string; type: 'joinTexts'; data: NodeCommonData & { joinDelimiter?: string } }
|
|
50
53
|
| { id: string; type: 'aspectRatio'; data: NodeCommonData & { output?: string; quality?: string } }
|
|
51
54
|
| { id: string; type: 'groupItems'; data: NodeCommonData & { value?: string } }
|
|
52
55
|
| {
|
|
@@ -136,8 +139,6 @@ type ModelInputNodeData = NodeCommonData & {
|
|
|
136
139
|
|
|
137
140
|
const RUNNABLE_TYPES = new Set(['model', 'llm', 'forEach', 'forEachEnd']);
|
|
138
141
|
const MODEL_CUSTOM_INPUT_HANDLE_SUFFIX = '-source-custom';
|
|
139
|
-
const USER_SELECTION_MIN_SELECT = 1;
|
|
140
|
-
const USER_SELECTION_MAX_SELECT = 100;
|
|
141
142
|
const DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE = 'Select the items to continue';
|
|
142
143
|
|
|
143
144
|
type SliceAssetsMode = 'default' | 'randomize' | 'choose';
|
|
@@ -232,7 +233,12 @@ function toCelIdentifier(name: string): string {
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
function escapeCELString(value: string): string {
|
|
235
|
-
return value
|
|
236
|
+
return value
|
|
237
|
+
.replace(/\\/g, '\\\\')
|
|
238
|
+
.replace(/'/g, "\\'")
|
|
239
|
+
.replace(/\n/g, '\\n')
|
|
240
|
+
.replace(/\r/g, '\\r')
|
|
241
|
+
.replace(/\t/g, '\\t');
|
|
236
242
|
}
|
|
237
243
|
|
|
238
244
|
function formatTextAsCELString(text: string): string {
|
|
@@ -391,6 +397,7 @@ function getForEachFlowListInputDescriptor(args: {
|
|
|
391
397
|
listProvider?.type === 'text' ||
|
|
392
398
|
listProvider?.type === 'llm' ||
|
|
393
399
|
listProvider?.type === 'transformText' ||
|
|
400
|
+
listProvider?.type === 'joinTexts' ||
|
|
394
401
|
(outputHandle?.type === 'text' && !!outputHandle.isArray);
|
|
395
402
|
return isText ?
|
|
396
403
|
{ name: `text${inputIndex}`, type: 'string_array' }
|
|
@@ -592,21 +599,50 @@ function getIsSliceAssetsItemsSourceHandle(nodeId: string, sourceHandle: string
|
|
|
592
599
|
}
|
|
593
600
|
|
|
594
601
|
/**
|
|
595
|
-
*
|
|
596
|
-
* list
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
602
|
+
* True when `expression` treats `varName` as a CEL list via indexing, `size(x)`,
|
|
603
|
+
* or list macros (`.map` / `.filter` / …). This is a converter tolerance for
|
|
604
|
+
* flows that index a list producer (editor First/Last/Nth stores `x[0]` on
|
|
605
|
+
* `isArray` handles; agent/MCP/import flows may index without that flag). It is
|
|
606
|
+
* not Fawkes's preview rule: the webapp binds a list only when the handle has
|
|
607
|
+
* `isArray`. Intentionally narrow so Prompt Builder cannot be steered into
|
|
608
|
+
* list semantics through other CEL list idioms.
|
|
609
|
+
*/
|
|
610
|
+
function expressionUsesListVariable(expression: string, varName: string): boolean {
|
|
611
|
+
const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
612
|
+
return (
|
|
613
|
+
new RegExp(`${escaped}\\s*\\[`).test(expression) ||
|
|
614
|
+
new RegExp(`\\bsize\\s*\\(\\s*${escaped}\\s*\\)`).test(expression) ||
|
|
615
|
+
new RegExp(`${escaped}\\s*\\.\\s*(map|filter|all|exists|exists_one)\\s*\\(`).test(expression)
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Prompt Builder (transformText) binds upstream vars for CEL. List producers
|
|
621
|
+
* bind as `string_array` when the source handle is `isArray`, or when the
|
|
622
|
+
* expression indexes/uses the variable as a list (see
|
|
623
|
+
* `expressionUsesListVariable`). Otherwise bare refs stay `string` so the API
|
|
624
|
+
* collapses the list to its first item.
|
|
625
|
+
*
|
|
626
|
+
* Fawkes today omits `isArray` on Slice/Group Items on purpose: only `isArray`
|
|
627
|
+
* handles get an indexed insert (`x[0]`); every other list insert is bare `x`
|
|
628
|
+
* and must collapse to the first item. Adding `isArray` to those handles later
|
|
629
|
+
* requires migrating stored expressions (`x` → `x[0]`) in the same Fawkes
|
|
630
|
+
* release, or both preview and published flows break on `'…' + x`.
|
|
601
631
|
*/
|
|
602
632
|
function getPromptBuilderInputType(
|
|
603
633
|
targetNode: WorkflowEditorNode,
|
|
604
634
|
outHandle: WorkflowEditorHandleOutput | undefined,
|
|
605
635
|
outHandleName: string,
|
|
636
|
+
expression: string,
|
|
637
|
+
varName: string,
|
|
606
638
|
): 'string' | 'string_array' {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
639
|
+
if (getConcatListSourceDescriptor(targetNode, outHandle, outHandleName).inputType !== 'string_array') {
|
|
640
|
+
return 'string';
|
|
641
|
+
}
|
|
642
|
+
if (outHandle?.isArray || expressionUsesListVariable(expression, varName)) {
|
|
643
|
+
return 'string_array';
|
|
644
|
+
}
|
|
645
|
+
return 'string';
|
|
610
646
|
}
|
|
611
647
|
|
|
612
648
|
function getConcatListSourceDescriptor(
|
|
@@ -903,17 +939,65 @@ function applyAspectRatioInputs(args: {
|
|
|
903
939
|
}
|
|
904
940
|
}
|
|
905
941
|
|
|
942
|
+
/**
|
|
943
|
+
* Neighbours of every editor node, built once per conversion. Edges read
|
|
944
|
+
* `{ source: consumer, target: provider }`: an input handle on `source` reads an
|
|
945
|
+
* output handle on `target`. `consumers` keeps the internal for-each → end edge,
|
|
946
|
+
* which lets the body walk reach a nested loop from its end. `dataProviders` and
|
|
947
|
+
* `dataConsumers` drop it: it carries no data, and it closes a cycle
|
|
948
|
+
* (loop → body → end → loop) that a walk must not follow. They also drop the
|
|
949
|
+
* edge of an approval: it gates the node it is attached to and follows it into
|
|
950
|
+
* a loop body, so it is not a consumer through which a draw escapes the loop.
|
|
951
|
+
*/
|
|
952
|
+
type LoopGraph = {
|
|
953
|
+
nodeById: Map<string, WorkflowEditorNode>;
|
|
954
|
+
consumers: Map<string, string[]>;
|
|
955
|
+
dataProviders: Map<string, string[]>;
|
|
956
|
+
dataConsumers: Map<string, string[]>;
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
function buildLoopGraph(nodes: WorkflowEditorNode[], edges: WorkflowEditorEdge[]): LoopGraph {
|
|
960
|
+
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
961
|
+
const isInternalForEachEdge = (e: WorkflowEditorEdge): boolean => {
|
|
962
|
+
const target = nodeById.get(e.target);
|
|
963
|
+
return (
|
|
964
|
+
nodeById.get(e.source)?.type === 'forEach' &&
|
|
965
|
+
target?.type === 'forEachEnd' &&
|
|
966
|
+
target.data.parentNodeId === e.source
|
|
967
|
+
);
|
|
968
|
+
};
|
|
969
|
+
const push = (map: Map<string, string[]>, key: string, value: string) => {
|
|
970
|
+
const list = map.get(key);
|
|
971
|
+
if (list) list.push(value);
|
|
972
|
+
else map.set(key, [value]);
|
|
973
|
+
};
|
|
974
|
+
const graph: LoopGraph = {
|
|
975
|
+
nodeById,
|
|
976
|
+
consumers: new Map(),
|
|
977
|
+
dataProviders: new Map(),
|
|
978
|
+
dataConsumers: new Map(),
|
|
979
|
+
};
|
|
980
|
+
for (const e of edges) {
|
|
981
|
+
push(graph.consumers, e.target, e.source);
|
|
982
|
+
if (isInternalForEachEdge(e) || nodeById.get(e.source)?.type === 'approval') continue;
|
|
983
|
+
push(graph.dataProviders, e.source, e.target);
|
|
984
|
+
push(graph.dataConsumers, e.target, e.source);
|
|
985
|
+
}
|
|
986
|
+
return graph;
|
|
987
|
+
}
|
|
988
|
+
|
|
906
989
|
function getForEachBodyEditorNodeIds(
|
|
907
990
|
forEachId: string,
|
|
908
991
|
forEachEndId: string,
|
|
909
992
|
edges: WorkflowEditorEdge[],
|
|
910
|
-
|
|
993
|
+
graph: LoopGraph,
|
|
911
994
|
): Set<string> {
|
|
912
995
|
const bodyNodeIds = new Set<string>();
|
|
913
996
|
const queue: string[] = [];
|
|
914
997
|
const escaped = forEachId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
998
|
+
const itemHandle = new RegExp(`^${escaped}-output-\\d+$`);
|
|
915
999
|
for (const edge of edges) {
|
|
916
|
-
if (edge.target === forEachId && edge.targetHandle
|
|
1000
|
+
if (edge.target === forEachId && edge.targetHandle && itemHandle.test(edge.targetHandle)) {
|
|
917
1001
|
queue.push(edge.source);
|
|
918
1002
|
}
|
|
919
1003
|
}
|
|
@@ -921,14 +1005,12 @@ function getForEachBodyEditorNodeIds(
|
|
|
921
1005
|
const nodeId = queue.pop()!;
|
|
922
1006
|
if (nodeId === forEachEndId || bodyNodeIds.has(nodeId)) continue;
|
|
923
1007
|
bodyNodeIds.add(nodeId);
|
|
924
|
-
for (const
|
|
925
|
-
if (
|
|
926
|
-
queue.push(edge.source);
|
|
927
|
-
}
|
|
1008
|
+
for (const consumer of graph.consumers.get(nodeId) ?? []) {
|
|
1009
|
+
if (!bodyNodeIds.has(consumer) && consumer !== forEachEndId) queue.push(consumer);
|
|
928
1010
|
}
|
|
929
1011
|
}
|
|
930
1012
|
|
|
931
|
-
addRandomizeClosureToBody(bodyNodeIds, forEachEndId,
|
|
1013
|
+
addRandomizeClosureToBody(bodyNodeIds, forEachEndId, graph);
|
|
932
1014
|
|
|
933
1015
|
return bodyNodeIds;
|
|
934
1016
|
}
|
|
@@ -944,36 +1026,36 @@ function getForEachBodyEditorNodeIds(
|
|
|
944
1026
|
* for an UNLOCKED randomize node we mutate `bodyNodeIds` in place to also include
|
|
945
1027
|
* that node plus the deterministic chain down to the body, so the runtime clones
|
|
946
1028
|
* it per iteration and each iteration re-draws. We only pull it in when *every*
|
|
947
|
-
* path out of it lands in the body — if it also feeds a
|
|
948
|
-
* cloning it would break that external consumer, so we
|
|
1029
|
+
* path out of it lands in the body or in this loop's results — if it also feeds a
|
|
1030
|
+
* node outside the loop, cloning it would break that external consumer, so we
|
|
1031
|
+
* leave it shared.
|
|
1032
|
+
*
|
|
1033
|
+
* Loop nodes (any for-each or loop end outside the body) stop both walks: a
|
|
1034
|
+
* loop's list is computed once, before the loop runs, so neither a loop nor what
|
|
1035
|
+
* feeds its list may be cloned with another loop's body. A for-each in its own
|
|
1036
|
+
* body would even expand again on every iteration. A node that reaches one stays
|
|
1037
|
+
* shared.
|
|
949
1038
|
*/
|
|
950
|
-
function addRandomizeClosureToBody(
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
): void {
|
|
956
|
-
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
1039
|
+
function addRandomizeClosureToBody(bodyNodeIds: Set<string>, forEachEndId: string, graph: LoopGraph): void {
|
|
1040
|
+
const isLoopNode = (id: string): boolean => {
|
|
1041
|
+
const type = graph.nodeById.get(id)?.type;
|
|
1042
|
+
return type === 'forEach' || type === 'forEachEnd';
|
|
1043
|
+
};
|
|
957
1044
|
const isUnlockedRandomize = (id: string): boolean => {
|
|
958
|
-
const data = nodeById.get(id)?.data as
|
|
1045
|
+
const data = graph.nodeById.get(id)?.data as
|
|
959
1046
|
| { mode?: SliceAssetsMode; isRandomize?: boolean; isSeedLocked?: boolean }
|
|
960
1047
|
| undefined;
|
|
961
1048
|
return !!data && getSliceAssetsMode(data) === 'randomize' && data.isSeedLocked !== true;
|
|
962
1049
|
};
|
|
963
|
-
// edge convention: `{ source: consumer, target: provider }` — an input handle
|
|
964
|
-
// on `source` reads an output handle on `target`.
|
|
965
|
-
const providersOf = (id: string): string[] => edges.filter((e) => e.source === id).map((e) => e.target);
|
|
966
|
-
const consumersOf = (id: string): string[] => edges.filter((e) => e.target === id).map((e) => e.source);
|
|
967
1050
|
|
|
968
|
-
//
|
|
1051
|
+
// The nodes that feed the body without going through a loop node.
|
|
969
1052
|
const upstream = new Set<string>();
|
|
970
1053
|
const upstreamQueue = [...bodyNodeIds];
|
|
971
1054
|
while (upstreamQueue.length > 0) {
|
|
972
|
-
for (const provider of
|
|
973
|
-
if (
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
}
|
|
1055
|
+
for (const provider of graph.dataProviders.get(upstreamQueue.pop()!) ?? []) {
|
|
1056
|
+
if (bodyNodeIds.has(provider) || upstream.has(provider) || isLoopNode(provider)) continue;
|
|
1057
|
+
upstream.add(provider);
|
|
1058
|
+
upstreamQueue.push(provider);
|
|
977
1059
|
}
|
|
978
1060
|
}
|
|
979
1061
|
|
|
@@ -985,16 +1067,16 @@ function addRandomizeClosureToBody(
|
|
|
985
1067
|
let escapesLoop = false;
|
|
986
1068
|
while (chainQueue.length > 0) {
|
|
987
1069
|
const id = chainQueue.pop()!;
|
|
988
|
-
|
|
989
|
-
chain.
|
|
990
|
-
const consumers =
|
|
991
|
-
|
|
992
|
-
|
|
1070
|
+
// The body and this loop's results are where a per-iteration draw belongs.
|
|
1071
|
+
if (bodyNodeIds.has(id) || id === forEachEndId || chain.has(id)) continue;
|
|
1072
|
+
const consumers = graph.dataConsumers.get(id) ?? [];
|
|
1073
|
+
// Another loop, or a dead end outside the loop: the draw must stay shared.
|
|
1074
|
+
if (isLoopNode(id) || consumers.length === 0) {
|
|
1075
|
+
escapesLoop = true;
|
|
993
1076
|
break;
|
|
994
1077
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
}
|
|
1078
|
+
chain.add(id);
|
|
1079
|
+
chainQueue.push(...consumers);
|
|
998
1080
|
}
|
|
999
1081
|
if (!escapesLoop) for (const id of chain) bodyNodeIds.add(id);
|
|
1000
1082
|
}
|
|
@@ -1275,13 +1357,14 @@ export function convertWorkflowEditorToFlow(
|
|
|
1275
1357
|
const forEachLoopFlowIds = new Map<string, string[]>();
|
|
1276
1358
|
const forEachFlowItemRefs = new Map<string, WorkflowEditorFlowItem>();
|
|
1277
1359
|
|
|
1360
|
+
const loopGraph = buildLoopGraph(nodes, edges);
|
|
1278
1361
|
for (const node of sortedNodes) {
|
|
1279
1362
|
if (node.type !== 'forEach') continue;
|
|
1280
1363
|
const endNode = nodes.find(
|
|
1281
1364
|
(n) => n.type === 'forEachEnd' && (n.data as { parentNodeId?: string }).parentNodeId === node.id,
|
|
1282
1365
|
);
|
|
1283
1366
|
if (!endNode) continue;
|
|
1284
|
-
forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges,
|
|
1367
|
+
forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges, loopGraph));
|
|
1285
1368
|
forEachLoopFlowIds.set(node.id, []);
|
|
1286
1369
|
}
|
|
1287
1370
|
|
|
@@ -1660,9 +1743,10 @@ export function convertWorkflowEditorToFlow(
|
|
|
1660
1743
|
const outHandle = targetNode.data.outputHandles?.find((h) => h.id === edge.targetHandle);
|
|
1661
1744
|
const outHandleName = outHandle?.name ?? 'output';
|
|
1662
1745
|
const targetFlowId = nodeIdToFlowIdMap.get(targetNode.id) ?? targetNode.id;
|
|
1746
|
+
const varName = `${targetFlowId}_${outHandleName}`;
|
|
1663
1747
|
flowInputs.push({
|
|
1664
|
-
name:
|
|
1665
|
-
type: getPromptBuilderInputType(targetNode, outHandle, outHandleName),
|
|
1748
|
+
name: varName,
|
|
1749
|
+
type: getPromptBuilderInputType(targetNode, outHandle, outHandleName, value, varName),
|
|
1666
1750
|
ref,
|
|
1667
1751
|
});
|
|
1668
1752
|
}
|
|
@@ -1704,6 +1788,35 @@ export function convertWorkflowEditorToFlow(
|
|
|
1704
1788
|
inputs: flowInputs.length > 0 ? flowInputs : undefined,
|
|
1705
1789
|
});
|
|
1706
1790
|
|
|
1791
|
+
// ------------------------------------
|
|
1792
|
+
// JoinTexts -> join string list into a single string
|
|
1793
|
+
// ------------------------------------
|
|
1794
|
+
} else if (node.type === 'joinTexts') {
|
|
1795
|
+
const flowInputs: WorkflowEditorFlowInput[] = [];
|
|
1796
|
+
const conditionalHandleId = getConditionalInputHandleId(node.id);
|
|
1797
|
+
let listSourceVarName: string | undefined;
|
|
1798
|
+
|
|
1799
|
+
for (const edge of incomingEdges) {
|
|
1800
|
+
if (edge.sourceHandle === conditionalHandleId) continue;
|
|
1801
|
+
const targetNode = nodes.find((n) => n.id === edge.target);
|
|
1802
|
+
if (!targetNode) continue;
|
|
1803
|
+
const ref = getSourceRef(edge, ctx);
|
|
1804
|
+
if (!ref) continue;
|
|
1805
|
+
const varName = toCelIdentifier(targetNode.id);
|
|
1806
|
+
flowInputs.push({ name: varName, type: 'string_array', ref });
|
|
1807
|
+
listSourceVarName = varName;
|
|
1808
|
+
}
|
|
1809
|
+
flowInputs.push(...collectConditionalConnectionInputs(incomingEdges, node.id, ctx));
|
|
1810
|
+
const delim = (node.data as { joinDelimiter?: string }).joinDelimiter ?? ', ';
|
|
1811
|
+
flow.push({
|
|
1812
|
+
id: flowId,
|
|
1813
|
+
type: 'transform',
|
|
1814
|
+
logic: {
|
|
1815
|
+
transform: `${listSourceVarName ?? '[]'}.join(${formatTextAsCELString(delim)})`,
|
|
1816
|
+
},
|
|
1817
|
+
inputs: flowInputs.length > 0 ? flowInputs : undefined,
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1707
1820
|
// ------------------------------------
|
|
1708
1821
|
// GroupItems -> array concatenation
|
|
1709
1822
|
// ------------------------------------
|
|
@@ -1767,7 +1880,10 @@ export function convertWorkflowEditorToFlow(
|
|
|
1767
1880
|
getOutputHandleVarName,
|
|
1768
1881
|
);
|
|
1769
1882
|
const count = clampUserSelectionCount(parseInt(String(sliceData.count ?? ''), 10) || 1);
|
|
1770
|
-
const message =
|
|
1883
|
+
const message =
|
|
1884
|
+
typeof sliceData.message === 'string' ?
|
|
1885
|
+
sliceData.message.trim() || DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE
|
|
1886
|
+
: DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE;
|
|
1771
1887
|
const arrayType = getUserSelectionArrayType(flowInputs);
|
|
1772
1888
|
|
|
1773
1889
|
// Single array-typed source can feed user-selection directly; scalars must be wrapped.
|
|
@@ -1903,6 +2019,9 @@ export function convertWorkflowEditorToFlow(
|
|
|
1903
2019
|
}
|
|
1904
2020
|
}
|
|
1905
2021
|
|
|
2022
|
+
// CEL conditions need hyphen→underscore identifiers, but the shared input-name
|
|
2023
|
+
// map must keep the real workflow input keys (`text-1`, not `text_1`).
|
|
2024
|
+
const celFieldNameMap = new Map<string, string>();
|
|
1906
2025
|
const edgesIntoIfElse = edges.filter((e) => e.target === node.id);
|
|
1907
2026
|
for (const fieldKey of fieldsUsed) {
|
|
1908
2027
|
const resolved = resolveIfElseConditionField(fieldKey, nodes);
|
|
@@ -1917,7 +2036,7 @@ export function convertWorkflowEditorToFlow(
|
|
|
1917
2036
|
: 'output';
|
|
1918
2037
|
|
|
1919
2038
|
const celVarName = getOutputHandleVarName(fieldNodeId, outputHandleName);
|
|
1920
|
-
|
|
2039
|
+
celFieldNameMap.set(fieldKey, celVarName);
|
|
1921
2040
|
|
|
1922
2041
|
const edgeToIfElse =
|
|
1923
2042
|
edgesIntoIfElse
|
|
@@ -1953,7 +2072,7 @@ export function convertWorkflowEditorToFlow(
|
|
|
1953
2072
|
|
|
1954
2073
|
const cases: { condition: string; value: string }[] = [];
|
|
1955
2074
|
for (let i = 0; i < conditionBlocks.length; i++) {
|
|
1956
|
-
const condition = conditionBlockToCEL(conditionBlocks[i]!,
|
|
2075
|
+
const condition = conditionBlockToCEL(conditionBlocks[i]!, celFieldNameMap);
|
|
1957
2076
|
if (condition) cases.push({ condition, value: String(i + 2) });
|
|
1958
2077
|
}
|
|
1959
2078
|
|
|
@@ -2100,7 +2219,7 @@ function getSliceAssetsMode(data: { mode?: SliceAssetsMode; isRandomize?: boolea
|
|
|
2100
2219
|
}
|
|
2101
2220
|
|
|
2102
2221
|
function clampUserSelectionCount(count: number): number {
|
|
2103
|
-
return Math.min(
|
|
2222
|
+
return Math.min(USER_SELECTION_MAX_CANDIDATES, Math.max(USER_SELECTION_MIN_SELECT, count));
|
|
2104
2223
|
}
|
|
2105
2224
|
|
|
2106
2225
|
function getUserSelectionArrayType(flowInputs: WorkflowEditorFlowInput[]): 'file_array' | 'string_array' {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { evaluateCel } from './cel';
|
|
2
|
+
|
|
1
3
|
export class WorkflowImportError extends Error {
|
|
2
4
|
constructor(message: string) {
|
|
3
5
|
super(message);
|
|
@@ -13,6 +15,7 @@ export type WorkflowEditorNodeType =
|
|
|
13
15
|
| 'llm'
|
|
14
16
|
| 'transformText'
|
|
15
17
|
| 'splitText'
|
|
18
|
+
| 'joinTexts'
|
|
16
19
|
| 'ifElse'
|
|
17
20
|
| 'groupItems'
|
|
18
21
|
| 'sliceAssets'
|
|
@@ -30,6 +33,7 @@ export const VALID_EDITOR_NODE_TYPES: readonly WorkflowEditorNodeType[] = [
|
|
|
30
33
|
'llm',
|
|
31
34
|
'transformText',
|
|
32
35
|
'splitText',
|
|
36
|
+
'joinTexts',
|
|
33
37
|
'ifElse',
|
|
34
38
|
'groupItems',
|
|
35
39
|
'sliceAssets',
|
|
@@ -40,6 +44,12 @@ export const VALID_EDITOR_NODE_TYPES: readonly WorkflowEditorNodeType[] = [
|
|
|
40
44
|
'modelInput',
|
|
41
45
|
];
|
|
42
46
|
|
|
47
|
+
export interface WorkflowNodeGroup {
|
|
48
|
+
title: string;
|
|
49
|
+
color: string;
|
|
50
|
+
locked?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
export interface WorkflowExportData {
|
|
44
54
|
version: '1.0';
|
|
45
55
|
name: string;
|
|
@@ -48,6 +58,7 @@ export interface WorkflowExportData {
|
|
|
48
58
|
nodes: Array<{ type: WorkflowEditorNodeType; [key: string]: unknown }>;
|
|
49
59
|
edges: Array<{ [key: string]: unknown }>;
|
|
50
60
|
inputKeys: string[];
|
|
61
|
+
nodeGroups?: Record<string, WorkflowNodeGroup>;
|
|
51
62
|
};
|
|
52
63
|
inputs: unknown[];
|
|
53
64
|
tagSet: string[];
|
|
@@ -67,7 +78,10 @@ export interface WorkflowExportData {
|
|
|
67
78
|
* 2. `version` field is present and equals "1.0"
|
|
68
79
|
* 3. `editorInfo` has non-empty `nodes` and `edges` arrays
|
|
69
80
|
* 4. Each node's `type` is a recognized editor node type
|
|
70
|
-
* 5. `
|
|
81
|
+
* 5. `sliceAssets` nodes with a `data.message` require that field to be a string
|
|
82
|
+
* 6. `inputs` is an array
|
|
83
|
+
* 7. Each `transformText` node's `data.value` is supported by the Prompt
|
|
84
|
+
* Builder visual editor (single-quoted string templates)
|
|
71
85
|
*/
|
|
72
86
|
export function validateEditorInfo(data: unknown): WorkflowExportData {
|
|
73
87
|
if (typeof data !== 'object' || data === null) {
|
|
@@ -104,7 +118,108 @@ export function validateEditorInfo(data: unknown): WorkflowExportData {
|
|
|
104
118
|
if (!(VALID_EDITOR_NODE_TYPES as readonly string[]).includes(node.type)) {
|
|
105
119
|
throw new WorkflowImportError(`Invalid node type: ${node.type}`);
|
|
106
120
|
}
|
|
121
|
+
|
|
122
|
+
if (node.type === 'sliceAssets') {
|
|
123
|
+
const nodeData = node['data'];
|
|
124
|
+
if (
|
|
125
|
+
nodeData !== null &&
|
|
126
|
+
typeof nodeData === 'object' &&
|
|
127
|
+
'message' in nodeData &&
|
|
128
|
+
(nodeData as { message?: unknown }).message != null &&
|
|
129
|
+
typeof (nodeData as { message?: unknown }).message !== 'string'
|
|
130
|
+
) {
|
|
131
|
+
const nodeId = typeof node['id'] === 'string' && node['id'] ? node['id'] : '(unknown)';
|
|
132
|
+
throw new WorkflowImportError(`Invalid sliceAssets node "${nodeId}": data.message must be a string`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (node.type === 'transformText') {
|
|
137
|
+
const value = getTransformTextValue(node);
|
|
138
|
+
if (value === undefined) continue;
|
|
139
|
+
if (typeof value !== 'string' || !getIsPromptBuilderVisualEditorSupported(value)) {
|
|
140
|
+
throw new WorkflowImportError(
|
|
141
|
+
`Node "${getNodeIdLabel(
|
|
142
|
+
node,
|
|
143
|
+
)}": Prompt Builder value is not supported by the visual editor; use a single-quoted template like 'sheep ' + text1_output`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
107
147
|
}
|
|
108
148
|
|
|
109
149
|
return workflow as WorkflowExportData;
|
|
110
150
|
}
|
|
151
|
+
|
|
152
|
+
// ------------------------------------
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* True if `value` is safe for the Prompt Builder chip editor.
|
|
156
|
+
* Only single-quoted string templates like `'lit' + var` are supported;
|
|
157
|
+
* double-quoted strings or CEL lists/numbers/bools will break or be removed.
|
|
158
|
+
*/
|
|
159
|
+
export function getIsPromptBuilderVisualEditorSupported(value: string): boolean {
|
|
160
|
+
const trimmed = value.trim();
|
|
161
|
+
if (trimmed.length === 0 || trimmed === "''") return true;
|
|
162
|
+
|
|
163
|
+
// List / map literals never come from the visual editor and confuse the
|
|
164
|
+
// quote/variable scanner (`["sheep", "on"]`, `{a: 1}`, …).
|
|
165
|
+
if (/^[[{]/.test(trimmed)) return false;
|
|
166
|
+
|
|
167
|
+
if (getHasDoubleQuotedCelStringLiteral(trimmed)) return false;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const result = evaluateCel(trimmed, {});
|
|
171
|
+
if (typeof result !== 'string') return false;
|
|
172
|
+
} catch {
|
|
173
|
+
// Unbound variables (`text1_output`) and similar fail evaluation with an
|
|
174
|
+
// empty binding map — that is still a normal canvas template.
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** True when the expression contains a `"` / `r"..."` string literal outside of `'...'`. */
|
|
181
|
+
function getHasDoubleQuotedCelStringLiteral(input: string): boolean {
|
|
182
|
+
let i = 0;
|
|
183
|
+
while (i < input.length) {
|
|
184
|
+
const ch = input[i];
|
|
185
|
+
|
|
186
|
+
if ((ch === 'r' || ch === 'R') && (input[i + 1] === '"' || input[i + 1] === "'")) {
|
|
187
|
+
if (input[i + 1] === '"') return true;
|
|
188
|
+
i += 2;
|
|
189
|
+
while (i < input.length && input[i] !== "'") i++;
|
|
190
|
+
if (i < input.length) i++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (ch === "'") {
|
|
195
|
+
i++;
|
|
196
|
+
while (i < input.length) {
|
|
197
|
+
if (input[i] === '\\' && i + 1 < input.length) {
|
|
198
|
+
i += 2;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (input[i] === "'") {
|
|
202
|
+
i++;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
i++;
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (ch === '"') return true;
|
|
211
|
+
i++;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getTransformTextValue(node: { [key: string]: unknown }): unknown {
|
|
217
|
+
const data = node['data'];
|
|
218
|
+
if (data === undefined || data === null) return undefined;
|
|
219
|
+
if (typeof data !== 'object') return undefined;
|
|
220
|
+
return (data as { [key: string]: unknown })['value'];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getNodeIdLabel(node: { [key: string]: unknown }): string {
|
|
224
|
+
return typeof node['id'] === 'string' ? node['id'] : 'unknown';
|
|
225
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
USER_SELECTION_DEFAULT_MAX,
|
|
3
|
+
USER_SELECTION_MAX_CANDIDATES,
|
|
4
|
+
USER_SELECTION_MIN_SELECT,
|
|
5
|
+
} from './user_selection_limits';
|
|
6
|
+
|
|
1
7
|
const VALID_NODE_TYPES = new Set([
|
|
2
8
|
'custom-model',
|
|
3
9
|
'for-each',
|
|
@@ -12,8 +18,6 @@ const VALID_NODE_TYPES = new Set([
|
|
|
12
18
|
'workflow',
|
|
13
19
|
]);
|
|
14
20
|
|
|
15
|
-
const USER_SELECTION_MIN_SELECT = 1;
|
|
16
|
-
const USER_SELECTION_MAX_SELECT = 100;
|
|
17
21
|
const USER_SELECTION_INPUT_TYPES = new Set(['file_array', 'string_array']);
|
|
18
22
|
|
|
19
23
|
type RawRef = {
|
|
@@ -58,7 +62,7 @@ type RawNode = {
|
|
|
58
62
|
* - logic → logicType
|
|
59
63
|
* - for-each → loopBodyNodeIds (non-empty) OR count (>0)
|
|
60
64
|
* - user-selection → exactly one file_array/string_array input (optional connection);
|
|
61
|
-
* input.min ≤ input.max
|
|
65
|
+
* input.min in 1..50, input.max in 1..100, input.min ≤ input.max
|
|
62
66
|
* 5. Cross-references all point to node IDs that exist in the flow:
|
|
63
67
|
* - dependsOn entries
|
|
64
68
|
* - input ref.node (excluding "workflow" which is a reserved keyword)
|
|
@@ -321,26 +325,26 @@ function validateUserSelectionNode(label: string, node: RawNode): void {
|
|
|
321
325
|
const input = selectionInputs[0]!;
|
|
322
326
|
|
|
323
327
|
const min = input.min === undefined ? USER_SELECTION_MIN_SELECT : input.min;
|
|
324
|
-
const max = input.max === undefined ?
|
|
328
|
+
const max = input.max === undefined ? USER_SELECTION_DEFAULT_MAX : input.max;
|
|
325
329
|
|
|
326
330
|
if (
|
|
327
331
|
typeof min !== 'number' ||
|
|
328
332
|
!Number.isInteger(min) ||
|
|
329
333
|
min < USER_SELECTION_MIN_SELECT ||
|
|
330
|
-
min >
|
|
334
|
+
min > USER_SELECTION_MAX_CANDIDATES
|
|
331
335
|
) {
|
|
332
336
|
throw new Error(
|
|
333
|
-
`Node ${label} (user-selection) input.min must be an integer between ${USER_SELECTION_MIN_SELECT} and ${
|
|
337
|
+
`Node ${label} (user-selection) input.min must be an integer between ${USER_SELECTION_MIN_SELECT} and ${USER_SELECTION_MAX_CANDIDATES}`,
|
|
334
338
|
);
|
|
335
339
|
}
|
|
336
340
|
if (
|
|
337
341
|
typeof max !== 'number' ||
|
|
338
342
|
!Number.isInteger(max) ||
|
|
339
343
|
max < USER_SELECTION_MIN_SELECT ||
|
|
340
|
-
max >
|
|
344
|
+
max > USER_SELECTION_DEFAULT_MAX
|
|
341
345
|
) {
|
|
342
346
|
throw new Error(
|
|
343
|
-
`Node ${label} (user-selection) input.max must be an integer between ${USER_SELECTION_MIN_SELECT} and ${
|
|
347
|
+
`Node ${label} (user-selection) input.max must be an integer between ${USER_SELECTION_MIN_SELECT} and ${USER_SELECTION_DEFAULT_MAX}`,
|
|
344
348
|
);
|
|
345
349
|
}
|
|
346
350
|
if (min > max) {
|