@scenario-labs/sdk 3.3.0 → 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 +34 -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/asset.d.mts +1 -1
- package/lib/asset.d.ts +1 -1
- package/lib/asset.js +1 -1
- package/lib/asset.mjs +1 -1
- package/lib/upload.d.mts +10 -5
- package/lib/upload.d.mts.map +1 -1
- package/lib/upload.d.ts +10 -5
- package/lib/upload.d.ts.map +1 -1
- package/lib/upload.js.map +1 -1
- package/lib/upload.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.d.mts +4 -2
- package/lib/workflow.d.mts.map +1 -1
- package/lib/workflow.d.ts +4 -2
- package/lib/workflow.d.ts.map +1 -1
- package/lib/workflow.js +5 -1
- package/lib/workflow.js.map +1 -1
- package/lib/workflow.mjs +5 -1
- package/lib/workflow.mjs.map +1 -1
- 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 +155 -53
- package/lib/workflow_converter.js.map +1 -1
- package/lib/workflow_converter.mjs +155 -53
- 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/asset.ts +1 -1
- package/src/lib/upload.ts +10 -5
- package/src/lib/user_selection_limits.ts +14 -0
- package/src/lib/workflow.ts +12 -1
- package/src/lib/workflow_converter.ts +176 -55
- 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
package/src/lib/upload.ts
CHANGED
|
@@ -11,21 +11,26 @@ import {
|
|
|
11
11
|
type UploadTriggerActionResponse,
|
|
12
12
|
} from '../resources/uploads';
|
|
13
13
|
import type { AssetRetrieveResponse } from '../resources/assets/assets';
|
|
14
|
-
import type {
|
|
14
|
+
import type { WithAsset } from './asset';
|
|
15
|
+
import type { EnhancedModelRetrieveResponse } from './models';
|
|
15
16
|
import { type Scope, clientProjectId, sentScope, withScope } from './scope';
|
|
16
17
|
|
|
17
18
|
type UploadKind = NonNullable<UploadCreateParams['kind']>;
|
|
18
19
|
|
|
19
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The resolved entity returned after a successful upload, narrowed by `kind`.
|
|
22
|
+
* It comes from the enhanced `retrieve()`, so a model has `.run()` and an asset
|
|
23
|
+
* has `.download()`.
|
|
24
|
+
*/
|
|
20
25
|
export type UploadResult<K extends UploadKind> =
|
|
21
|
-
K extends 'model' ?
|
|
26
|
+
K extends 'model' ? EnhancedModelRetrieveResponse : WithAsset<AssetRetrieveResponse>;
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* Accepted upload inputs.
|
|
25
30
|
* - `string` — a local file path, read from disk (Node only; throws in browsers).
|
|
26
31
|
* - `Uint8Array` — raw bytes. `Buffer` is a `Uint8Array` and works too.
|
|
27
|
-
* - `Blob` / `File` — browser-native, or Node
|
|
28
|
-
* constructors.
|
|
32
|
+
* - `Blob` / `File` — browser-native, or in Node.js via `fs.openAsBlob()` or
|
|
33
|
+
* the `Blob` / `File` constructors.
|
|
29
34
|
*/
|
|
30
35
|
export type UploadFileInput = string | Uint8Array | Blob;
|
|
31
36
|
|
|
@@ -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;
|
package/src/lib/workflow.ts
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
Workflows,
|
|
5
5
|
WorkflowUserApprovalResponse,
|
|
6
6
|
type WorkflowApproveUserParams,
|
|
7
|
+
type WorkflowUserSelectionParams,
|
|
8
|
+
type WorkflowUserSelectionResponse,
|
|
7
9
|
type WorkflowRetrieveParams,
|
|
8
10
|
type WorkflowRetrieveResponse,
|
|
9
11
|
type WorkflowRunParams,
|
|
@@ -99,7 +101,7 @@ export type EnhancedWorkflowRetrieveResponse = Omit<WorkflowRetrieveResponse, 'w
|
|
|
99
101
|
/**
|
|
100
102
|
* Enhanced Workflows resource.
|
|
101
103
|
* Extends the generated {@link Workflows} — all original methods are inherited.
|
|
102
|
-
* Overrides `retrieve()`, `run()`, and `
|
|
104
|
+
* Overrides `retrieve()`, `run()`, `userApproval()` and `userSelection()` to return enriched response objects.
|
|
103
105
|
*/
|
|
104
106
|
export class EnhancedWorkflows extends Workflows {
|
|
105
107
|
/**
|
|
@@ -135,4 +137,13 @@ export class EnhancedWorkflows extends Workflows {
|
|
|
135
137
|
): APIPromise<WithJob<WorkflowUserApprovalResponse>> {
|
|
136
138
|
return enhanceJob(this._client, super.userApproval(workflowID, body, options));
|
|
137
139
|
}
|
|
140
|
+
|
|
141
|
+
/** Answer a user selection node. Returns an enhanced response where `job` has `.wait()`. */
|
|
142
|
+
override userSelection(
|
|
143
|
+
workflowID: string,
|
|
144
|
+
params: WorkflowUserSelectionParams,
|
|
145
|
+
options?: RequestOptions,
|
|
146
|
+
): APIPromise<WithJob<WorkflowUserSelectionResponse>> {
|
|
147
|
+
return enhanceJob(this._client, super.userSelection(workflowID, params, options));
|
|
148
|
+
}
|
|
138
149
|
}
|
|
@@ -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,34 +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
|
|
959
|
-
|
|
1045
|
+
const data = graph.nodeById.get(id)?.data as
|
|
1046
|
+
| { mode?: SliceAssetsMode; isRandomize?: boolean; isSeedLocked?: boolean }
|
|
1047
|
+
| undefined;
|
|
1048
|
+
return !!data && getSliceAssetsMode(data) === 'randomize' && data.isSeedLocked !== true;
|
|
960
1049
|
};
|
|
961
|
-
// edge convention: `{ source: consumer, target: provider }` — an input handle
|
|
962
|
-
// on `source` reads an output handle on `target`.
|
|
963
|
-
const providersOf = (id: string): string[] => edges.filter((e) => e.source === id).map((e) => e.target);
|
|
964
|
-
const consumersOf = (id: string): string[] => edges.filter((e) => e.target === id).map((e) => e.source);
|
|
965
1050
|
|
|
966
|
-
//
|
|
1051
|
+
// The nodes that feed the body without going through a loop node.
|
|
967
1052
|
const upstream = new Set<string>();
|
|
968
1053
|
const upstreamQueue = [...bodyNodeIds];
|
|
969
1054
|
while (upstreamQueue.length > 0) {
|
|
970
|
-
for (const provider of
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
}
|
|
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);
|
|
975
1059
|
}
|
|
976
1060
|
}
|
|
977
1061
|
|
|
@@ -983,16 +1067,16 @@ function addRandomizeClosureToBody(
|
|
|
983
1067
|
let escapesLoop = false;
|
|
984
1068
|
while (chainQueue.length > 0) {
|
|
985
1069
|
const id = chainQueue.pop()!;
|
|
986
|
-
|
|
987
|
-
chain.
|
|
988
|
-
const consumers =
|
|
989
|
-
|
|
990
|
-
|
|
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;
|
|
991
1076
|
break;
|
|
992
1077
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
}
|
|
1078
|
+
chain.add(id);
|
|
1079
|
+
chainQueue.push(...consumers);
|
|
996
1080
|
}
|
|
997
1081
|
if (!escapesLoop) for (const id of chain) bodyNodeIds.add(id);
|
|
998
1082
|
}
|
|
@@ -1273,13 +1357,14 @@ export function convertWorkflowEditorToFlow(
|
|
|
1273
1357
|
const forEachLoopFlowIds = new Map<string, string[]>();
|
|
1274
1358
|
const forEachFlowItemRefs = new Map<string, WorkflowEditorFlowItem>();
|
|
1275
1359
|
|
|
1360
|
+
const loopGraph = buildLoopGraph(nodes, edges);
|
|
1276
1361
|
for (const node of sortedNodes) {
|
|
1277
1362
|
if (node.type !== 'forEach') continue;
|
|
1278
1363
|
const endNode = nodes.find(
|
|
1279
1364
|
(n) => n.type === 'forEachEnd' && (n.data as { parentNodeId?: string }).parentNodeId === node.id,
|
|
1280
1365
|
);
|
|
1281
1366
|
if (!endNode) continue;
|
|
1282
|
-
forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges,
|
|
1367
|
+
forEachBodyEditorNodeIds.set(node.id, getForEachBodyEditorNodeIds(node.id, endNode.id, edges, loopGraph));
|
|
1283
1368
|
forEachLoopFlowIds.set(node.id, []);
|
|
1284
1369
|
}
|
|
1285
1370
|
|
|
@@ -1658,9 +1743,10 @@ export function convertWorkflowEditorToFlow(
|
|
|
1658
1743
|
const outHandle = targetNode.data.outputHandles?.find((h) => h.id === edge.targetHandle);
|
|
1659
1744
|
const outHandleName = outHandle?.name ?? 'output';
|
|
1660
1745
|
const targetFlowId = nodeIdToFlowIdMap.get(targetNode.id) ?? targetNode.id;
|
|
1746
|
+
const varName = `${targetFlowId}_${outHandleName}`;
|
|
1661
1747
|
flowInputs.push({
|
|
1662
|
-
name:
|
|
1663
|
-
type: getPromptBuilderInputType(targetNode, outHandle, outHandleName),
|
|
1748
|
+
name: varName,
|
|
1749
|
+
type: getPromptBuilderInputType(targetNode, outHandle, outHandleName, value, varName),
|
|
1664
1750
|
ref,
|
|
1665
1751
|
});
|
|
1666
1752
|
}
|
|
@@ -1702,6 +1788,35 @@ export function convertWorkflowEditorToFlow(
|
|
|
1702
1788
|
inputs: flowInputs.length > 0 ? flowInputs : undefined,
|
|
1703
1789
|
});
|
|
1704
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
|
+
|
|
1705
1820
|
// ------------------------------------
|
|
1706
1821
|
// GroupItems -> array concatenation
|
|
1707
1822
|
// ------------------------------------
|
|
@@ -1765,7 +1880,10 @@ export function convertWorkflowEditorToFlow(
|
|
|
1765
1880
|
getOutputHandleVarName,
|
|
1766
1881
|
);
|
|
1767
1882
|
const count = clampUserSelectionCount(parseInt(String(sliceData.count ?? ''), 10) || 1);
|
|
1768
|
-
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;
|
|
1769
1887
|
const arrayType = getUserSelectionArrayType(flowInputs);
|
|
1770
1888
|
|
|
1771
1889
|
// Single array-typed source can feed user-selection directly; scalars must be wrapped.
|
|
@@ -1901,6 +2019,9 @@ export function convertWorkflowEditorToFlow(
|
|
|
1901
2019
|
}
|
|
1902
2020
|
}
|
|
1903
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>();
|
|
1904
2025
|
const edgesIntoIfElse = edges.filter((e) => e.target === node.id);
|
|
1905
2026
|
for (const fieldKey of fieldsUsed) {
|
|
1906
2027
|
const resolved = resolveIfElseConditionField(fieldKey, nodes);
|
|
@@ -1915,7 +2036,7 @@ export function convertWorkflowEditorToFlow(
|
|
|
1915
2036
|
: 'output';
|
|
1916
2037
|
|
|
1917
2038
|
const celVarName = getOutputHandleVarName(fieldNodeId, outputHandleName);
|
|
1918
|
-
|
|
2039
|
+
celFieldNameMap.set(fieldKey, celVarName);
|
|
1919
2040
|
|
|
1920
2041
|
const edgeToIfElse =
|
|
1921
2042
|
edgesIntoIfElse
|
|
@@ -1951,7 +2072,7 @@ export function convertWorkflowEditorToFlow(
|
|
|
1951
2072
|
|
|
1952
2073
|
const cases: { condition: string; value: string }[] = [];
|
|
1953
2074
|
for (let i = 0; i < conditionBlocks.length; i++) {
|
|
1954
|
-
const condition = conditionBlockToCEL(conditionBlocks[i]!,
|
|
2075
|
+
const condition = conditionBlockToCEL(conditionBlocks[i]!, celFieldNameMap);
|
|
1955
2076
|
if (condition) cases.push({ condition, value: String(i + 2) });
|
|
1956
2077
|
}
|
|
1957
2078
|
|
|
@@ -2098,7 +2219,7 @@ function getSliceAssetsMode(data: { mode?: SliceAssetsMode; isRandomize?: boolea
|
|
|
2098
2219
|
}
|
|
2099
2220
|
|
|
2100
2221
|
function clampUserSelectionCount(count: number): number {
|
|
2101
|
-
return Math.min(
|
|
2222
|
+
return Math.min(USER_SELECTION_MAX_CANDIDATES, Math.max(USER_SELECTION_MIN_SELECT, count));
|
|
2102
2223
|
}
|
|
2103
2224
|
|
|
2104
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
|
+
}
|