@zerowidth/workbench-sdk 2.1.3 → 2.2.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.
@@ -8,8 +8,8 @@
8
8
  {
9
9
  "name": "json",
10
10
  "display_name": "JSON",
11
- "type": "object",
12
- "description": "The JSON object to convert to XML",
11
+ "type": "object or array of objects",
12
+ "description": "The JSON object, or array of objects, to convert to XML",
13
13
  "required": true
14
14
  },
15
15
  {
@@ -8,8 +8,8 @@
8
8
  {
9
9
  "name": "json",
10
10
  "display_name": "JSON",
11
- "type": "object",
12
- "description": "The JSON object to convert to YAML",
11
+ "type": "object or array of objects",
12
+ "description": "The JSON object, or array of objects, to convert to YAML",
13
13
  "required": true
14
14
  },
15
15
  {
@@ -97,5 +97,14 @@
97
97
  "expected": {
98
98
  "yaml": "null"
99
99
  }
100
+ },
101
+ {
102
+ "description": "Top-level array of objects",
103
+ "inputs": {
104
+ "json": [{"name": "Alice"}, {"name": "Bob"}]
105
+ },
106
+ "expected": {
107
+ "yaml": "- name: Alice\n- name: Bob"
108
+ }
100
109
  }
101
110
  ]
@@ -23,7 +23,7 @@
23
23
  {
24
24
  "name": "variables",
25
25
  "display_name": "Variables",
26
- "type": "object",
26
+ "type": "object or array of objects",
27
27
  "allow_multiple": true,
28
28
  "description": "Connect key-value pairs to inject into any matched {{key}} in the text content."
29
29
  }
@@ -2,6 +2,23 @@
2
2
  * Process function for the Message node.
3
3
  * Outputs a message object, either from the input or from the settings.
4
4
  */
5
+ /**
6
+ * Render a variable value for injection into message text.
7
+ * Strings pass through untouched; everything else is JSON encoded so that
8
+ * nested objects and arrays read as data instead of "[object Object]".
9
+ */
10
+ const renderVariable = (value) => {
11
+ if (value === null || value === undefined) return "";
12
+ if (typeof value === "string") return value;
13
+ try {
14
+ return JSON.stringify(value, null, 2);
15
+ } catch {
16
+ // Circular structures (or anything else JSON can't encode) fall back to
17
+ // the default coercion rather than failing the whole message.
18
+ return String(value);
19
+ }
20
+ };
21
+
5
22
  export default async ({inputs, settings, config}) => {
6
23
 
7
24
  // If an input value is provided, use it; otherwise use the value from settings
@@ -17,6 +34,12 @@ export default async ({inputs, settings, config}) => {
17
34
  if(!inputs.variables) {
18
35
  inputs.variables = [];
19
36
  }
37
+
38
+ // A single connection can deliver an array of key-value objects, so flatten
39
+ // one level before looking keys up.
40
+ const variables = (Array.isArray(inputs.variables) ? inputs.variables : [inputs.variables])
41
+ .flatMap(entry => Array.isArray(entry) ? entry : [entry])
42
+ .filter(entry => entry !== null && typeof entry === "object");
20
43
 
21
44
  // if we have variables and text content, we need to replace the text content with the variables
22
45
  // do we have a text content item and what index is it
@@ -25,9 +48,9 @@ export default async ({inputs, settings, config}) => {
25
48
  message.content[textContentIndex].text = message.content[textContentIndex].text.replace(/\{\{(.*?)\}\}/g, (match, p1) => {
26
49
 
27
50
  // look for a variable with the key p1
28
- let variable = inputs.variables.find(variable => Object.keys(variable).find(key => key === p1));
51
+ let variable = variables.find(variable => Object.keys(variable).find(key => key === p1));
29
52
  if(variable) {
30
- return variable[p1];
53
+ return renderVariable(variable[p1]);
31
54
  }
32
55
  return match;
33
56
  });
@@ -0,0 +1,85 @@
1
+ [
2
+ {
3
+ "description": "String variable is injected as-is",
4
+ "inputs": {
5
+ "variables": [
6
+ {
7
+ "name": "Ada"
8
+ }
9
+ ]
10
+ },
11
+ "settings": {
12
+ "role": "user",
13
+ "content": "Hello {{name}}!"
14
+ },
15
+ "expected": {
16
+ "message": {
17
+ "role": "user",
18
+ "content": [
19
+ {
20
+ "type": "text",
21
+ "text": "Hello Ada!"
22
+ }
23
+ ]
24
+ }
25
+ }
26
+ },
27
+ {
28
+ "description": "Nested object variable is JSON stringified, not [object Object]",
29
+ "inputs": {
30
+ "variables": [
31
+ {
32
+ "order": {
33
+ "id": 7,
34
+ "paid": true
35
+ }
36
+ }
37
+ ]
38
+ },
39
+ "settings": {
40
+ "role": "user",
41
+ "content": "Order: {{order}}"
42
+ },
43
+ "expected": {
44
+ "message": {
45
+ "role": "user",
46
+ "content": [
47
+ {
48
+ "type": "text",
49
+ "text": "Order: {\n \"id\": 7,\n \"paid\": true\n}"
50
+ }
51
+ ]
52
+ }
53
+ }
54
+ },
55
+ {
56
+ "description": "Array of objects delivered as a single connection is flattened",
57
+ "inputs": {
58
+ "variables": [
59
+ [
60
+ {
61
+ "first": "Ada"
62
+ },
63
+ {
64
+ "last": "Lovelace"
65
+ }
66
+ ]
67
+ ]
68
+ },
69
+ "settings": {
70
+ "role": "user",
71
+ "content": "{{first}} {{last}}"
72
+ },
73
+ "expected": {
74
+ "message": {
75
+ "role": "user",
76
+ "content": [
77
+ {
78
+ "type": "text",
79
+ "text": "Ada Lovelace"
80
+ }
81
+ ]
82
+ }
83
+ }
84
+ }
85
+ ]
@@ -8,8 +8,8 @@
8
8
  {
9
9
  "name": "objects",
10
10
  "display_name": "Objects",
11
- "type": "object",
12
- "description": "Objects to merge (later objects override earlier ones)",
11
+ "type": "object or array of objects",
12
+ "description": "Objects to merge, individually or as arrays of objects (later objects override earlier ones)",
13
13
  "required": true,
14
14
  "allow_multiple": true
15
15
  },
@@ -2,8 +2,12 @@ export default async ({ inputs, settings, config }) => {
2
2
  const objects = inputs.objects;
3
3
  const arrayMode = inputs.array_mode ?? "replace";
4
4
 
5
- // Normalize to array
6
- const objectList = Array.isArray(objects) ? objects : [objects];
5
+ // Normalize to a flat array. A single connection can deliver an array of
6
+ // objects, and a multi-connection input can deliver arrays alongside plain
7
+ // objects, so flatten one level before filtering.
8
+ const objectList = (Array.isArray(objects) ? objects : [objects]).flatMap(
9
+ (entry) => (Array.isArray(entry) ? entry : [entry])
10
+ );
7
11
 
8
12
  // Filter out non-objects
9
13
  const validObjects = objectList.filter(
@@ -142,5 +142,17 @@
142
142
  "expected": {
143
143
  "merged": {"level1": {"level2": {"level3": {"a": 1, "b": 2, "c": 3}}}}
144
144
  }
145
+ },
146
+ {
147
+ "description": "Array of objects delivered as a single connection is flattened",
148
+ "inputs": {
149
+ "objects": [
150
+ [{"a": 1}, {"b": 2}],
151
+ {"c": 3}
152
+ ]
153
+ },
154
+ "expected": {
155
+ "merged": {"a": 1, "b": 2, "c": 3}
156
+ }
145
157
  }
146
158
  ]
@@ -14,8 +14,8 @@
14
14
  {
15
15
  "name": "variables",
16
16
  "display_name": "Variables",
17
- "description": "Object containing variable values",
18
- "type": "object",
17
+ "description": "Object containing variable values, or an array of such objects to merge",
18
+ "type": "object or array of objects",
19
19
  "required": true
20
20
  }
21
21
  ],
@@ -1,15 +1,43 @@
1
+ /**
2
+ * Render a variable value for injection into the template.
3
+ * Strings pass through untouched; everything else is JSON encoded so that
4
+ * nested objects and arrays read as data instead of "[object Object]".
5
+ */
6
+ const renderVariable = (value) => {
7
+ if (value === null || value === undefined) return "";
8
+ if (typeof value === "string") return value;
9
+ try {
10
+ return JSON.stringify(value, null, 2);
11
+ } catch {
12
+ // Circular structures (or anything else JSON can't encode) fall back to
13
+ // the default coercion rather than failing the whole template.
14
+ return String(value);
15
+ }
16
+ };
17
+
1
18
  export default async ({inputs, settings, config}) => {
2
19
 
3
20
  const template = String(inputs.template || "");
4
- const variables = inputs.variables || {};
21
+
22
+ // The input may arrive as a single object, or as an array of objects when
23
+ // it's fed from an "array of objects" output. Merge arrays into one lookup.
24
+ const raw = inputs.variables || {};
25
+ const variables = Array.isArray(raw)
26
+ ? raw.reduce((acc, entry) => {
27
+ if (Array.isArray(entry)) return Object.assign(acc, ...entry.filter(e => e && typeof e === "object"));
28
+ if (entry && typeof entry === "object") return Object.assign(acc, entry);
29
+ return acc;
30
+ }, {})
31
+ : raw;
32
+
5
33
  const keepMissing = settings.keep_missing || false;
6
-
34
+
7
35
  const text = template.replace(/\{([^}]+)\}/g, (match, key) => {
8
36
  if (key in variables) {
9
- return String(variables[key]);
37
+ return renderVariable(variables[key]);
10
38
  }
11
39
  return keepMissing ? match : "";
12
40
  });
13
-
41
+
14
42
  return { text };
15
- };
43
+ };
@@ -59,5 +59,57 @@
59
59
  "expected": {
60
60
  "text": "Hello World! "
61
61
  }
62
+ },
63
+ {
64
+ "description": "Object variable is JSON stringified, not [object Object]",
65
+ "inputs": {
66
+ "template": "Config: {config}",
67
+ "variables": {"config": {"host": "localhost", "port": 8080}}
68
+ },
69
+ "settings": {
70
+ "keep_missing": false
71
+ },
72
+ "expected": {
73
+ "text": "Config: {\n \"host\": \"localhost\",\n \"port\": 8080\n}"
74
+ }
75
+ },
76
+ {
77
+ "description": "Array variable is JSON stringified",
78
+ "inputs": {
79
+ "template": "Items: {items}",
80
+ "variables": {"items": [1, 2]}
81
+ },
82
+ "settings": {
83
+ "keep_missing": false
84
+ },
85
+ "expected": {
86
+ "text": "Items: [\n 1,\n 2\n]"
87
+ }
88
+ },
89
+ {
90
+ "description": "Array of objects input is merged into one lookup",
91
+ "inputs": {
92
+ "template": "{greeting} {name}!",
93
+ "variables": [{"greeting": "Hello"}, {"name": "World"}]
94
+ },
95
+ "settings": {
96
+ "keep_missing": false
97
+ },
98
+ "expected": {
99
+ "text": "Hello World!"
100
+ }
101
+ },
102
+ {
103
+ "description": "Null variable renders as empty string",
104
+ "inputs": {
105
+ "template": "Value: {value}.",
106
+ "variables": {"value": null}
107
+ },
108
+ "settings": {
109
+ "keep_missing": false
110
+ },
111
+ "expected": {
112
+ "text": "Value: ."
113
+ }
62
114
  }
63
115
  ]
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "name": "variables",
15
15
  "display_name": "Variables",
16
- "type": "object",
16
+ "type": "object or array of objects",
17
17
  "allow_multiple": true,
18
18
  "description": "Connect key-value pairs to inject into any matched {{key}} in the text content."
19
19
  },
@@ -2,12 +2,35 @@
2
2
  * Process function for the System Prompt node.
3
3
  * Outputs a message object, containing the prompt text and system role and text
4
4
  */
5
+ /**
6
+ * Render a variable value for injection into prompt text.
7
+ * Strings pass through untouched; everything else is JSON encoded so that
8
+ * nested objects and arrays read as data instead of "[object Object]".
9
+ */
10
+ const renderVariable = (value) => {
11
+ if (value === null || value === undefined) return "";
12
+ if (typeof value === "string") return value;
13
+ try {
14
+ return JSON.stringify(value, null, 2);
15
+ } catch {
16
+ // Circular structures (or anything else JSON can't encode) fall back to
17
+ // the default coercion rather than failing the whole prompt.
18
+ return String(value);
19
+ }
20
+ };
21
+
5
22
  export default async ({inputs, settings, config}) => {
6
23
  // Initialize variables array if not provided
7
24
  if(!inputs.variables) {
8
25
  inputs.variables = [];
9
26
  }
10
27
 
28
+ // A single connection can deliver an array of key-value objects, so flatten
29
+ // one level before looking keys up.
30
+ const variables = (Array.isArray(inputs.variables) ? inputs.variables : [inputs.variables])
31
+ .flatMap(entry => Array.isArray(entry) ? entry : [entry])
32
+ .filter(entry => entry !== null && typeof entry === "object");
33
+
11
34
  // Get the base content from settings
12
35
  let baseContent = settings.content || "";
13
36
 
@@ -47,9 +70,9 @@ export default async ({inputs, settings, config}) => {
47
70
  // Process variables
48
71
  message.content[0].text = message.content[0].text.replace(/\{\{(.*?)\}\}/g, (match, p1) => {
49
72
  // look for a variable with the key p1
50
- let variable = inputs.variables.find(variable => Object.keys(variable).find(key => key === p1));
73
+ let variable = variables.find(variable => Object.keys(variable).find(key => key === p1));
51
74
  if(variable) {
52
- return variable[p1];
75
+ return renderVariable(variable[p1]);
53
76
  }
54
77
  return match;
55
78
  });
@@ -0,0 +1,113 @@
1
+ [
2
+ {
3
+ "description": "String variable is injected as-is",
4
+ "inputs": {
5
+ "variables": [
6
+ {
7
+ "name": "Ada"
8
+ }
9
+ ]
10
+ },
11
+ "settings": {
12
+ "content": "Hello {{name}}!"
13
+ },
14
+ "expected": {
15
+ "message": {
16
+ "role": "system",
17
+ "content": [
18
+ {
19
+ "type": "text",
20
+ "text": "Hello Ada!"
21
+ }
22
+ ]
23
+ },
24
+ "prompt": "Hello Ada!"
25
+ }
26
+ },
27
+ {
28
+ "description": "Nested object variable is JSON stringified, not [object Object]",
29
+ "inputs": {
30
+ "variables": [
31
+ {
32
+ "profile": {
33
+ "name": "Ada",
34
+ "langs": [
35
+ "js",
36
+ "py"
37
+ ]
38
+ }
39
+ }
40
+ ]
41
+ },
42
+ "settings": {
43
+ "content": "Profile: {{profile}}"
44
+ },
45
+ "expected": {
46
+ "message": {
47
+ "role": "system",
48
+ "content": [
49
+ {
50
+ "type": "text",
51
+ "text": "Profile: {\n \"name\": \"Ada\",\n \"langs\": [\n \"js\",\n \"py\"\n ]\n}"
52
+ }
53
+ ]
54
+ },
55
+ "prompt": "Profile: {\n \"name\": \"Ada\",\n \"langs\": [\n \"js\",\n \"py\"\n ]\n}"
56
+ }
57
+ },
58
+ {
59
+ "description": "Array of objects delivered as a single connection is flattened",
60
+ "inputs": {
61
+ "variables": [
62
+ [
63
+ {
64
+ "first": "Ada"
65
+ },
66
+ {
67
+ "last": "Lovelace"
68
+ }
69
+ ]
70
+ ]
71
+ },
72
+ "settings": {
73
+ "content": "{{first}} {{last}}"
74
+ },
75
+ "expected": {
76
+ "message": {
77
+ "role": "system",
78
+ "content": [
79
+ {
80
+ "type": "text",
81
+ "text": "Ada Lovelace"
82
+ }
83
+ ]
84
+ },
85
+ "prompt": "Ada Lovelace"
86
+ }
87
+ },
88
+ {
89
+ "description": "Unmatched placeholder is left intact",
90
+ "inputs": {
91
+ "variables": [
92
+ {
93
+ "name": "Ada"
94
+ }
95
+ ]
96
+ },
97
+ "settings": {
98
+ "content": "Hello {{missing}}!"
99
+ },
100
+ "expected": {
101
+ "message": {
102
+ "role": "system",
103
+ "content": [
104
+ {
105
+ "type": "text",
106
+ "text": "Hello {{missing}}!"
107
+ }
108
+ ]
109
+ },
110
+ "prompt": "Hello {{missing}}!"
111
+ }
112
+ }
113
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerowidth/workbench-sdk",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "dependencies": {
5
5
  "adm-zip": "^0.5.16",
6
6
  "ajv": "^8.17.1",