@nyaruka/temba-components 0.129.8 → 0.129.9
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 +27 -3
- package/demo/data/flows/sample-flow.json +186 -96
- package/dist/temba-components.js +414 -351
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/excellent/helpers.js +2 -2
- package/out-tsc/src/excellent/helpers.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +25 -7
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +11 -1
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +133 -290
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
- package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
- package/out-tsc/src/flow/actions/call_llm.js +56 -3
- package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
- package/out-tsc/src/flow/actions/call_webhook.js +1 -1
- package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
- package/out-tsc/src/flow/actions/open_ticket.js +65 -3
- package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
- package/out-tsc/src/flow/actions/set_run_result.js +75 -0
- package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
- package/out-tsc/src/flow/config.js +4 -0
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
- package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/flow/types.js +0 -65
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +18 -61
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +305 -0
- package/out-tsc/src/form/FieldRenderer.js.map +1 -0
- package/out-tsc/src/form/FormField.js +3 -3
- package/out-tsc/src/form/FormField.js.map +1 -1
- package/out-tsc/src/form/TextInput.js +1 -1
- package/out-tsc/src/form/TextInput.js.map +1 -1
- package/out-tsc/src/form/select/Select.js +48 -20
- package/out-tsc/src/form/select/Select.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +39 -13
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/markdown.js +13 -11
- package/out-tsc/src/markdown.js.map +1 -1
- package/out-tsc/test/ActionHelper.js +2 -0
- package/out-tsc/test/ActionHelper.js.map +1 -1
- package/out-tsc/test/NodeHelper.js +148 -0
- package/out-tsc/test/NodeHelper.js.map +1 -0
- package/out-tsc/test/actions/call_llm.test.js +103 -0
- package/out-tsc/test/actions/call_llm.test.js.map +1 -0
- package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
- package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
- package/out-tsc/test/nodes/split_by_random.test.js +150 -0
- package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
- package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
- package/out-tsc/test/temba-add-input-labels.test.js +70 -0
- package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
- package/out-tsc/test/temba-field-renderer.test.js +296 -0
- package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
- package/out-tsc/test/temba-markdown.test.js +1 -1
- package/out-tsc/test/temba-markdown.test.js.map +1 -1
- package/out-tsc/test/temba-node-editor.test.js +400 -0
- package/out-tsc/test/temba-node-editor.test.js.map +1 -1
- package/out-tsc/test/temba-select.test.js +6 -3
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/temba-webchat.test.js +1 -1
- package/out-tsc/test/temba-webchat.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
- package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
- package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
- package/screenshots/truth/editor/router.png +0 -0
- package/screenshots/truth/editor/send_msg.png +0 -0
- package/screenshots/truth/editor/set_contact_language.png +0 -0
- package/screenshots/truth/editor/set_contact_name.png +0 -0
- package/screenshots/truth/editor/set_run_result.png +0 -0
- package/screenshots/truth/editor/wait.png +0 -0
- package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
- package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
- package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
- package/screenshots/truth/field-renderer/context-comparison.png +0 -0
- package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
- package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
- package/screenshots/truth/field-renderer/select-multi.png +0 -0
- package/screenshots/truth/field-renderer/select-no-label.png +0 -0
- package/screenshots/truth/field-renderer/select-with-label.png +0 -0
- package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
- package/screenshots/truth/field-renderer/text-no-label.png +0 -0
- package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
- package/screenshots/truth/field-renderer/text-with-label.png +0 -0
- package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
- package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/screenshots/truth/omnibox/selected.png +0 -0
- package/screenshots/truth/select/functions.png +0 -0
- package/screenshots/truth/select/multi-with-endpoint.png +0 -0
- package/screenshots/truth/select/search-enabled.png +0 -0
- package/src/events.ts +8 -1
- package/src/excellent/helpers.ts +2 -2
- package/src/flow/CanvasNode.ts +22 -1
- package/src/flow/Editor.ts +12 -1
- package/src/flow/NodeEditor.ts +186 -374
- package/src/flow/actions/add_input_labels.ts +45 -0
- package/src/flow/actions/call_llm.ts +57 -3
- package/src/flow/actions/call_webhook.ts +1 -1
- package/src/flow/actions/open_ticket.ts +74 -3
- package/src/flow/actions/set_run_result.ts +83 -0
- package/src/flow/config.ts +4 -0
- package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
- package/src/flow/nodes/split_by_ticket.ts +19 -0
- package/src/flow/nodes/wait_for_response.ts +28 -1
- package/src/flow/types.ts +26 -127
- package/src/form/ArrayEditor.ts +34 -82
- package/src/form/FieldRenderer.ts +465 -0
- package/src/form/FormField.ts +3 -3
- package/src/form/TextInput.ts +1 -1
- package/src/form/select/Select.ts +51 -20
- package/src/live/ContactChat.ts +39 -15
- package/src/markdown.ts +19 -11
- package/src/store/flow-definition.d.ts +5 -2
- package/static/api/labels.json +31 -0
- package/static/api/topics.json +24 -9
- package/static/api/users.json +35 -16
- package/static/css/temba-components.css +3 -3
- package/stress-test.js +18 -13
- package/test/ActionHelper.ts +2 -0
- package/test/NodeHelper.ts +184 -0
- package/test/actions/call_llm.test.ts +137 -0
- package/test/nodes/README.md +78 -0
- package/test/nodes/split_by_llm_categorize.test.ts +698 -0
- package/test/nodes/split_by_random.test.ts +177 -0
- package/test/nodes/wait_for_digits.test.ts +176 -0
- package/test/nodes/wait_for_response.test.ts +206 -0
- package/test/temba-add-input-labels.test.ts +87 -0
- package/test/temba-field-renderer.test.ts +482 -0
- package/test/temba-markdown.test.ts +1 -1
- package/test/temba-node-editor.test.ts +496 -0
- package/test/temba-select.test.ts +6 -6
- package/test/temba-webchat.test.ts +1 -1
- package/test-assets/select/llms.json +18 -0
- package/web-dev-mock.mjs +96 -6
- package/web-dev-server.config.mjs +29 -7
- package/test/temba-flow-editor.test.ts.backup +0 -563
- package/test/temba-utils-index.test.ts.backup +0 -1737
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { call_llm } from '../../src/flow/actions/call_llm';
|
|
3
|
+
import { ActionTest } from '../ActionHelper';
|
|
4
|
+
/**
|
|
5
|
+
* Test suite for the call_llm action configuration.
|
|
6
|
+
*/
|
|
7
|
+
describe('call_llm action config', () => {
|
|
8
|
+
const helper = new ActionTest(call_llm, 'call_llm');
|
|
9
|
+
describe('basic properties', () => {
|
|
10
|
+
helper.testBasicProperties();
|
|
11
|
+
it('has correct name', () => {
|
|
12
|
+
expect(call_llm.name).to.equal('Call AI');
|
|
13
|
+
});
|
|
14
|
+
it('has form configuration', () => {
|
|
15
|
+
expect(call_llm.form).to.exist;
|
|
16
|
+
expect(call_llm.form.llm).to.exist;
|
|
17
|
+
expect(call_llm.form.instructions).to.exist;
|
|
18
|
+
expect(call_llm.form.input).to.exist;
|
|
19
|
+
});
|
|
20
|
+
it('has layout configuration', () => {
|
|
21
|
+
expect(call_llm.layout).to.exist;
|
|
22
|
+
expect(call_llm.layout).to.deep.equal(['llm', 'input', 'instructions']);
|
|
23
|
+
});
|
|
24
|
+
it('has data transformation functions', () => {
|
|
25
|
+
expect(call_llm.toFormData).to.be.a('function');
|
|
26
|
+
expect(call_llm.fromFormData).to.be.a('function');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('data transformations', () => {
|
|
30
|
+
it('converts action to form data correctly', () => {
|
|
31
|
+
const action = {
|
|
32
|
+
uuid: 'test-llm-1',
|
|
33
|
+
type: 'call_llm',
|
|
34
|
+
input: '@input',
|
|
35
|
+
llm: { uuid: 'gpt-4', name: 'GPT 4.1' },
|
|
36
|
+
instructions: 'Translate to French',
|
|
37
|
+
result_name: 'translated_text'
|
|
38
|
+
};
|
|
39
|
+
const formData = call_llm.toFormData(action);
|
|
40
|
+
expect(formData.uuid).to.equal('test-llm-1');
|
|
41
|
+
expect(formData.llm).to.deep.equal([{ value: 'gpt-4', name: 'GPT 4.1' }]);
|
|
42
|
+
expect(formData.instructions).to.equal('Translate to French');
|
|
43
|
+
expect(formData.input).to.equal('@input');
|
|
44
|
+
});
|
|
45
|
+
it('converts form data to action correctly', () => {
|
|
46
|
+
const formData = {
|
|
47
|
+
uuid: 'test-llm-2',
|
|
48
|
+
llm: [{ value: 'gpt-5', name: 'GPT 5' }],
|
|
49
|
+
instructions: 'Summarize the following text',
|
|
50
|
+
input: '@input'
|
|
51
|
+
};
|
|
52
|
+
const action = call_llm.fromFormData(formData);
|
|
53
|
+
expect(action.uuid).to.equal('test-llm-2');
|
|
54
|
+
expect(action.type).to.equal('call_llm');
|
|
55
|
+
expect(action.llm).to.deep.equal({ uuid: 'gpt-5', name: 'GPT 5' });
|
|
56
|
+
expect(action.instructions).to.equal('Summarize the following text');
|
|
57
|
+
expect(action.input).to.equal('@input');
|
|
58
|
+
});
|
|
59
|
+
it('handles empty form data', () => {
|
|
60
|
+
const formData = {
|
|
61
|
+
uuid: 'test-llm-3',
|
|
62
|
+
llm: [],
|
|
63
|
+
instructions: '',
|
|
64
|
+
input: ''
|
|
65
|
+
};
|
|
66
|
+
const action = call_llm.fromFormData(formData);
|
|
67
|
+
expect(action.llm).to.deep.equal({ uuid: '', name: '' });
|
|
68
|
+
expect(action.instructions).to.equal('');
|
|
69
|
+
expect(action.input).to.equal('@input');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('action scenarios', () => {
|
|
73
|
+
helper.testAction({
|
|
74
|
+
uuid: 'test-action-1',
|
|
75
|
+
type: 'call_llm',
|
|
76
|
+
llm: { uuid: 'gpt-4', name: 'GPT 4.1' },
|
|
77
|
+
instructions: 'Translate to French',
|
|
78
|
+
input: '@input'
|
|
79
|
+
}, 'translation-task');
|
|
80
|
+
helper.testAction({
|
|
81
|
+
uuid: 'test-action-2',
|
|
82
|
+
type: 'call_llm',
|
|
83
|
+
llm: { uuid: 'gpt-5', name: 'GPT 5' },
|
|
84
|
+
instructions: 'Analyze the sentiment of the following message and classify it as positive, negative, or neutral. Provide a brief explanation for your classification.',
|
|
85
|
+
input: '@input'
|
|
86
|
+
}, 'sentiment-analysis');
|
|
87
|
+
helper.testAction({
|
|
88
|
+
uuid: 'test-action-3',
|
|
89
|
+
type: 'call_llm',
|
|
90
|
+
llm: { uuid: 'gpt-4', name: 'GPT 4.1' },
|
|
91
|
+
instructions: 'Summarize the key points from the conversation above in bullet format.',
|
|
92
|
+
input: '@input'
|
|
93
|
+
}, 'summarization');
|
|
94
|
+
helper.testAction({
|
|
95
|
+
uuid: 'test-action-4',
|
|
96
|
+
type: 'call_llm',
|
|
97
|
+
llm: { uuid: 'gpt-5', name: 'GPT 5' },
|
|
98
|
+
instructions: 'Extract any contact information (phone numbers, email addresses) from the text and format them as a JSON object.',
|
|
99
|
+
input: '@input'
|
|
100
|
+
}, 'information-extraction');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
//# sourceMappingURL=call_llm.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"call_llm.test.js","sourceRoot":"","sources":["../../../test/actions/call_llm.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAE3D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C;;GAEG;AACH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAEpD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAE7B,EAAE,CAAC,kBAAkB,EAAE,GAAG,EAAE;YAC1B,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;YAChC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC;YAC/B,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC;YACnC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC;YAC5C,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;YAClC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC;YACjC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;YAC3C,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;YAChD,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;QACpC,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,MAAM,GAAY;gBACtB,IAAI,EAAE,YAAY;gBAClB,IAAI,EAAE,UAAU;gBAChB,KAAK,EAAE,QAAQ;gBACf,GAAG,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;gBACvC,YAAY,EAAE,qBAAqB;gBACnC,WAAW,EAAE,iBAAiB;aAC/B,CAAC;YAEF,MAAM,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAE7C,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAC7C,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;YAC1E,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;YAC9D,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,QAAQ,GAAG;gBACf,IAAI,EAAE,YAAY;gBAClB,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;gBACxC,YAAY,EAAE,8BAA8B;gBAC5C,KAAK,EAAE,QAAQ;aAChB,CAAC;YAEF,MAAM,MAAM,GAAG,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAY,CAAC;YAE1D,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAC3C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACzC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YACnE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YACrE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;YACjC,MAAM,QAAQ,GAAG;gBACf,IAAI,EAAE,YAAY;gBAClB,GAAG,EAAE,EAAE;gBACP,YAAY,EAAE,EAAE;gBAChB,KAAK,EAAE,EAAE;aACV,CAAC;YAEF,MAAM,MAAM,GAAG,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAY,CAAC;YAE1D,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;YACzD,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACzC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,UAAU,CACf;YACE,IAAI,EAAE,eAAe;YACrB,IAAI,EAAE,UAAU;YAChB,GAAG,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;YACvC,YAAY,EAAE,qBAAqB;YACnC,KAAK,EAAE,QAAQ;SACL,EACZ,kBAAkB,CACnB,CAAC;QAEF,MAAM,CAAC,UAAU,CACf;YACE,IAAI,EAAE,eAAe;YACrB,IAAI,EAAE,UAAU;YAChB,GAAG,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;YACrC,YAAY,EACV,wJAAwJ;YAC1J,KAAK,EAAE,QAAQ;SACL,EACZ,oBAAoB,CACrB,CAAC;QAEF,MAAM,CAAC,UAAU,CACf;YACE,IAAI,EAAE,eAAe;YACrB,IAAI,EAAE,UAAU;YAChB,GAAG,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;YACvC,YAAY,EACV,wEAAwE;YAC1E,KAAK,EAAE,QAAQ;SACL,EACZ,eAAe,CAChB,CAAC;QAEF,MAAM,CAAC,UAAU,CACf;YACE,IAAI,EAAE,eAAe;YACrB,IAAI,EAAE,UAAU;YAChB,GAAG,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;YACrC,YAAY,EACV,kHAAkH;YACpH,KAAK,EAAE,QAAQ;SACL,EACZ,wBAAwB,CACzB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { expect } from '@open-wc/testing';\nimport { call_llm } from '../../src/flow/actions/call_llm';\nimport { CallLLM } from '../../src/store/flow-definition';\nimport { ActionTest } from '../ActionHelper';\n\n/**\n * Test suite for the call_llm action configuration.\n */\ndescribe('call_llm action config', () => {\n const helper = new ActionTest(call_llm, 'call_llm');\n\n describe('basic properties', () => {\n helper.testBasicProperties();\n\n it('has correct name', () => {\n expect(call_llm.name).to.equal('Call AI');\n });\n\n it('has form configuration', () => {\n expect(call_llm.form).to.exist;\n expect(call_llm.form.llm).to.exist;\n expect(call_llm.form.instructions).to.exist;\n expect(call_llm.form.input).to.exist;\n });\n\n it('has layout configuration', () => {\n expect(call_llm.layout).to.exist;\n expect(call_llm.layout).to.deep.equal(['llm', 'input', 'instructions']);\n });\n\n it('has data transformation functions', () => {\n expect(call_llm.toFormData).to.be.a('function');\n expect(call_llm.fromFormData).to.be.a('function');\n });\n });\n\n describe('data transformations', () => {\n it('converts action to form data correctly', () => {\n const action: CallLLM = {\n uuid: 'test-llm-1',\n type: 'call_llm',\n input: '@input',\n llm: { uuid: 'gpt-4', name: 'GPT 4.1' },\n instructions: 'Translate to French',\n result_name: 'translated_text'\n };\n\n const formData = call_llm.toFormData(action);\n\n expect(formData.uuid).to.equal('test-llm-1');\n expect(formData.llm).to.deep.equal([{ value: 'gpt-4', name: 'GPT 4.1' }]);\n expect(formData.instructions).to.equal('Translate to French');\n expect(formData.input).to.equal('@input');\n });\n\n it('converts form data to action correctly', () => {\n const formData = {\n uuid: 'test-llm-2',\n llm: [{ value: 'gpt-5', name: 'GPT 5' }],\n instructions: 'Summarize the following text',\n input: '@input'\n };\n\n const action = call_llm.fromFormData(formData) as CallLLM;\n\n expect(action.uuid).to.equal('test-llm-2');\n expect(action.type).to.equal('call_llm');\n expect(action.llm).to.deep.equal({ uuid: 'gpt-5', name: 'GPT 5' });\n expect(action.instructions).to.equal('Summarize the following text');\n expect(action.input).to.equal('@input');\n });\n\n it('handles empty form data', () => {\n const formData = {\n uuid: 'test-llm-3',\n llm: [],\n instructions: '',\n input: ''\n };\n\n const action = call_llm.fromFormData(formData) as CallLLM;\n\n expect(action.llm).to.deep.equal({ uuid: '', name: '' });\n expect(action.instructions).to.equal('');\n expect(action.input).to.equal('@input');\n });\n });\n\n describe('action scenarios', () => {\n helper.testAction(\n {\n uuid: 'test-action-1',\n type: 'call_llm',\n llm: { uuid: 'gpt-4', name: 'GPT 4.1' },\n instructions: 'Translate to French',\n input: '@input'\n } as CallLLM,\n 'translation-task'\n );\n\n helper.testAction(\n {\n uuid: 'test-action-2',\n type: 'call_llm',\n llm: { uuid: 'gpt-5', name: 'GPT 5' },\n instructions:\n 'Analyze the sentiment of the following message and classify it as positive, negative, or neutral. Provide a brief explanation for your classification.',\n input: '@input'\n } as CallLLM,\n 'sentiment-analysis'\n );\n\n helper.testAction(\n {\n uuid: 'test-action-3',\n type: 'call_llm',\n llm: { uuid: 'gpt-4', name: 'GPT 4.1' },\n instructions:\n 'Summarize the key points from the conversation above in bullet format.',\n input: '@input'\n } as CallLLM,\n 'summarization'\n );\n\n helper.testAction(\n {\n uuid: 'test-action-4',\n type: 'call_llm',\n llm: { uuid: 'gpt-5', name: 'GPT 5' },\n instructions:\n 'Extract any contact information (phone numbers, email addresses) from the text and format them as a JSON object.',\n input: '@input'\n } as CallLLM,\n 'information-extraction'\n );\n });\n});\n"]}
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { split_by_llm_categorize } from '../../src/flow/nodes/split_by_llm_categorize';
|
|
3
|
+
import { NodeTest } from '../NodeHelper';
|
|
4
|
+
// Helper function to create routers with proper cases and exits
|
|
5
|
+
function createSplitRouter(categoryNames) {
|
|
6
|
+
const categories = [];
|
|
7
|
+
const exits = [];
|
|
8
|
+
const cases = [];
|
|
9
|
+
// Add user categories
|
|
10
|
+
categoryNames.forEach((categoryName) => {
|
|
11
|
+
const categoryUuid = `category-${categoryName
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/\s+/g, '-')}`;
|
|
14
|
+
const exitUuid = `exit-${categoryName.toLowerCase().replace(/\s+/g, '-')}`;
|
|
15
|
+
const caseUuid = `case-${categoryName.toLowerCase().replace(/\s+/g, '-')}`;
|
|
16
|
+
categories.push({
|
|
17
|
+
uuid: categoryUuid,
|
|
18
|
+
name: categoryName,
|
|
19
|
+
exit_uuid: exitUuid
|
|
20
|
+
});
|
|
21
|
+
exits.push({
|
|
22
|
+
uuid: exitUuid,
|
|
23
|
+
destination_uuid: null
|
|
24
|
+
});
|
|
25
|
+
cases.push({
|
|
26
|
+
uuid: caseUuid,
|
|
27
|
+
type: 'has_only_text',
|
|
28
|
+
arguments: [categoryName],
|
|
29
|
+
category_uuid: categoryUuid
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
// Add "Other" category (default)
|
|
33
|
+
const otherCategoryUuid = 'category-other';
|
|
34
|
+
const otherExitUuid = 'exit-other';
|
|
35
|
+
categories.push({
|
|
36
|
+
uuid: otherCategoryUuid,
|
|
37
|
+
name: 'Other',
|
|
38
|
+
exit_uuid: otherExitUuid
|
|
39
|
+
});
|
|
40
|
+
exits.push({
|
|
41
|
+
uuid: otherExitUuid,
|
|
42
|
+
destination_uuid: null
|
|
43
|
+
});
|
|
44
|
+
// Add "Failure" category
|
|
45
|
+
const failureCategoryUuid = 'category-failure';
|
|
46
|
+
const failureExitUuid = 'exit-failure';
|
|
47
|
+
const failureCaseUuid = 'case-failure';
|
|
48
|
+
categories.push({
|
|
49
|
+
uuid: failureCategoryUuid,
|
|
50
|
+
name: 'Failure',
|
|
51
|
+
exit_uuid: failureExitUuid
|
|
52
|
+
});
|
|
53
|
+
exits.push({
|
|
54
|
+
uuid: failureExitUuid,
|
|
55
|
+
destination_uuid: null
|
|
56
|
+
});
|
|
57
|
+
// Add failure case for <ERROR>
|
|
58
|
+
cases.push({
|
|
59
|
+
uuid: failureCaseUuid,
|
|
60
|
+
type: 'has_only_text',
|
|
61
|
+
arguments: ['<ERROR>'],
|
|
62
|
+
category_uuid: failureCategoryUuid
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
router: {
|
|
66
|
+
type: 'switch',
|
|
67
|
+
categories: categories,
|
|
68
|
+
default_category_uuid: otherCategoryUuid,
|
|
69
|
+
operand: '@locals._llm_output',
|
|
70
|
+
cases: cases
|
|
71
|
+
},
|
|
72
|
+
exits: exits
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Test suite for the split_by_llm_categorize node configuration.
|
|
77
|
+
*/
|
|
78
|
+
describe('split_by_llm_categorize node config', () => {
|
|
79
|
+
const helper = new NodeTest(split_by_llm_categorize, 'split_by_llm_categorize');
|
|
80
|
+
describe('basic properties', () => {
|
|
81
|
+
helper.testBasicProperties();
|
|
82
|
+
it('has correct name', () => {
|
|
83
|
+
expect(split_by_llm_categorize.name).to.equal('Split by AI');
|
|
84
|
+
});
|
|
85
|
+
it('has correct type', () => {
|
|
86
|
+
expect(split_by_llm_categorize.type).to.equal('split_by_llm_categorize');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('node scenarios', () => {
|
|
90
|
+
const basicRouter = createSplitRouter(['Greeting', 'Question']);
|
|
91
|
+
helper.testNode({
|
|
92
|
+
uuid: 'test-node-1',
|
|
93
|
+
actions: [
|
|
94
|
+
{
|
|
95
|
+
uuid: 'call-llm-uuid',
|
|
96
|
+
type: 'call_llm',
|
|
97
|
+
llm: { uuid: 'llm-123', name: 'Claude' },
|
|
98
|
+
input: '@input',
|
|
99
|
+
instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
|
|
100
|
+
output_local: '_llm_output'
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
router: basicRouter.router,
|
|
104
|
+
exits: basicRouter.exits
|
|
105
|
+
}, { type: 'split_by_llm_categorize' }, 'basic-categorization');
|
|
106
|
+
const premiumRouter = createSplitRouter(['Premium', 'Regular', 'VIP']);
|
|
107
|
+
helper.testNode({
|
|
108
|
+
uuid: 'test-node-2',
|
|
109
|
+
actions: [
|
|
110
|
+
{
|
|
111
|
+
uuid: 'call-llm-uuid-2',
|
|
112
|
+
type: 'call_llm',
|
|
113
|
+
llm: { uuid: 'llm-456', name: 'GPT-4' },
|
|
114
|
+
input: '@contact.name',
|
|
115
|
+
instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
|
|
116
|
+
output_local: '_llm_output'
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
router: premiumRouter.router,
|
|
120
|
+
exits: premiumRouter.exits
|
|
121
|
+
}, { type: 'split_by_llm_categorize' }, 'custom-input-and-result-name');
|
|
122
|
+
const priorityRouter = createSplitRouter([
|
|
123
|
+
'High',
|
|
124
|
+
'Medium',
|
|
125
|
+
'Low',
|
|
126
|
+
'Critical',
|
|
127
|
+
'Urgent'
|
|
128
|
+
]);
|
|
129
|
+
helper.testNode({
|
|
130
|
+
uuid: 'test-node-3',
|
|
131
|
+
actions: [
|
|
132
|
+
{
|
|
133
|
+
uuid: 'call-llm-uuid-3',
|
|
134
|
+
type: 'call_llm',
|
|
135
|
+
llm: { uuid: 'llm-789', name: 'Gemini' },
|
|
136
|
+
input: '@fields.priority',
|
|
137
|
+
instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
|
|
138
|
+
output_local: '_llm_output'
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
router: priorityRouter.router,
|
|
142
|
+
exits: priorityRouter.exits
|
|
143
|
+
}, { type: 'split_by_llm_categorize' }, 'many-categories');
|
|
144
|
+
const minimalRouter = createSplitRouter(['Yes']);
|
|
145
|
+
helper.testNode({
|
|
146
|
+
uuid: 'test-node-4',
|
|
147
|
+
actions: [
|
|
148
|
+
{
|
|
149
|
+
uuid: 'call-llm-uuid-4',
|
|
150
|
+
type: 'call_llm',
|
|
151
|
+
llm: { uuid: 'llm-minimal', name: 'Basic LLM' },
|
|
152
|
+
input: '@input',
|
|
153
|
+
instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
|
|
154
|
+
output_local: '_llm_output'
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
router: minimalRouter.router,
|
|
158
|
+
exits: minimalRouter.exits
|
|
159
|
+
}, { type: 'split_by_llm_categorize' }, 'minimal-categories');
|
|
160
|
+
const feedbackRouter = createSplitRouter([
|
|
161
|
+
'Bug Report',
|
|
162
|
+
'Feature Request',
|
|
163
|
+
'General Feedback',
|
|
164
|
+
'Support Request'
|
|
165
|
+
]);
|
|
166
|
+
helper.testNode({
|
|
167
|
+
uuid: 'test-node-5',
|
|
168
|
+
actions: [
|
|
169
|
+
{
|
|
170
|
+
uuid: 'call-llm-uuid-5',
|
|
171
|
+
type: 'call_llm',
|
|
172
|
+
llm: { uuid: 'llm-special', name: 'Special Characters LLM' },
|
|
173
|
+
input: '@contact.fields.feedback',
|
|
174
|
+
instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
|
|
175
|
+
output_local: '_llm_output'
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
router: feedbackRouter.router,
|
|
179
|
+
exits: feedbackRouter.exits
|
|
180
|
+
}, { type: 'split_by_llm_categorize' }, 'feedback-categorization');
|
|
181
|
+
});
|
|
182
|
+
describe('round-trip conversion validation', () => {
|
|
183
|
+
it('converts to form data correctly', () => {
|
|
184
|
+
const testRouter = createSplitRouter(['Greeting', 'Question']);
|
|
185
|
+
const node = {
|
|
186
|
+
uuid: 'test-node',
|
|
187
|
+
actions: [
|
|
188
|
+
{
|
|
189
|
+
uuid: 'call-llm-uuid',
|
|
190
|
+
type: 'call_llm',
|
|
191
|
+
llm: { uuid: 'llm-123', name: 'Test LLM' },
|
|
192
|
+
input: '@input',
|
|
193
|
+
instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
|
|
194
|
+
output_local: '_llm_output'
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
router: testRouter.router,
|
|
198
|
+
exits: testRouter.exits
|
|
199
|
+
};
|
|
200
|
+
const formData = split_by_llm_categorize.toFormData(node);
|
|
201
|
+
expect(formData.uuid).to.equal('test-node');
|
|
202
|
+
expect(formData.llm).to.deep.equal([
|
|
203
|
+
{ value: 'llm-123', name: 'Test LLM' }
|
|
204
|
+
]);
|
|
205
|
+
expect(formData.input).to.equal('@input');
|
|
206
|
+
expect(formData.categories).to.deep.equal([
|
|
207
|
+
{ name: 'Greeting' },
|
|
208
|
+
{ name: 'Question' }
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
it('converts from form data correctly', () => {
|
|
212
|
+
const formData = {
|
|
213
|
+
uuid: 'test-node',
|
|
214
|
+
llm: [{ value: 'llm-456', name: 'GPT-4' }],
|
|
215
|
+
input: '@contact.name',
|
|
216
|
+
categories: [{ name: 'Premium' }, { name: 'Regular' }]
|
|
217
|
+
};
|
|
218
|
+
const originalNode = {
|
|
219
|
+
uuid: 'test-node',
|
|
220
|
+
actions: [],
|
|
221
|
+
exits: []
|
|
222
|
+
};
|
|
223
|
+
const result = split_by_llm_categorize.fromFormData(formData, originalNode);
|
|
224
|
+
expect(result.uuid).to.equal('test-node');
|
|
225
|
+
expect(result.actions).to.have.length(1);
|
|
226
|
+
expect(result.actions[0].type).to.equal('call_llm');
|
|
227
|
+
expect(result.actions[0].llm.uuid).to.equal('llm-456');
|
|
228
|
+
expect(result.actions[0].llm.name).to.equal('GPT-4');
|
|
229
|
+
expect(result.actions[0].input).to.equal('@contact.name');
|
|
230
|
+
// Should have user categories plus Other and Failure
|
|
231
|
+
expect(result.router.categories).to.have.length(4);
|
|
232
|
+
const categoryNames = result.router.categories.map((cat) => cat.name);
|
|
233
|
+
expect(categoryNames).to.include.members([
|
|
234
|
+
'Premium',
|
|
235
|
+
'Regular',
|
|
236
|
+
'Other',
|
|
237
|
+
'Failure'
|
|
238
|
+
]);
|
|
239
|
+
// Should have corresponding exits
|
|
240
|
+
expect(result.exits).to.have.length(4);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe('edge cases and validation', () => {
|
|
244
|
+
it('handles categories with empty names correctly', () => {
|
|
245
|
+
const formData = {
|
|
246
|
+
uuid: 'test-node-uuid',
|
|
247
|
+
llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
|
|
248
|
+
input: '@input',
|
|
249
|
+
categories: [
|
|
250
|
+
{ name: 'Valid Category' },
|
|
251
|
+
{ name: '' }, // empty name
|
|
252
|
+
{ name: ' ' }, // only whitespace
|
|
253
|
+
{ name: 'Another Valid' }
|
|
254
|
+
],
|
|
255
|
+
result_name: 'Intent'
|
|
256
|
+
};
|
|
257
|
+
const originalNode = {
|
|
258
|
+
uuid: 'test-node-uuid',
|
|
259
|
+
actions: [],
|
|
260
|
+
exits: []
|
|
261
|
+
};
|
|
262
|
+
const result = split_by_llm_categorize.fromFormData(formData, originalNode);
|
|
263
|
+
// Should only include non-empty categories
|
|
264
|
+
const userCategories = result.router.categories.filter((cat) => cat.name !== 'Other' && cat.name !== 'Failure');
|
|
265
|
+
expect(userCategories).to.have.length(2);
|
|
266
|
+
expect(userCategories.map((cat) => cat.name)).to.deep.equal([
|
|
267
|
+
'Valid Category',
|
|
268
|
+
'Another Valid'
|
|
269
|
+
]);
|
|
270
|
+
});
|
|
271
|
+
it('handles categories with special characters', () => {
|
|
272
|
+
const formData = {
|
|
273
|
+
uuid: 'test-node-uuid',
|
|
274
|
+
llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
|
|
275
|
+
input: '@input',
|
|
276
|
+
categories: [
|
|
277
|
+
{ name: 'Category-1' },
|
|
278
|
+
{ name: 'Category_2' },
|
|
279
|
+
{ name: 'Category@3' },
|
|
280
|
+
{ name: 'Category with spaces' }
|
|
281
|
+
],
|
|
282
|
+
result_name: 'Intent'
|
|
283
|
+
};
|
|
284
|
+
const originalNode = {
|
|
285
|
+
uuid: 'test-node-uuid',
|
|
286
|
+
actions: [],
|
|
287
|
+
exits: []
|
|
288
|
+
};
|
|
289
|
+
const result = split_by_llm_categorize.fromFormData(formData, originalNode);
|
|
290
|
+
// Should preserve all special characters in category names
|
|
291
|
+
const userCategories = result.router.categories.filter((cat) => cat.name !== 'Other' && cat.name !== 'Failure');
|
|
292
|
+
expect(userCategories).to.have.length(4);
|
|
293
|
+
expect(userCategories.map((cat) => cat.name)).to.include.members([
|
|
294
|
+
'Category-1',
|
|
295
|
+
'Category_2',
|
|
296
|
+
'Category@3',
|
|
297
|
+
'Category with spaces'
|
|
298
|
+
]);
|
|
299
|
+
// Verify cases also have correct names
|
|
300
|
+
const caseNames = result
|
|
301
|
+
.router.cases.filter((c) => c.arguments[0] !== '<ERROR>')
|
|
302
|
+
.map((c) => c.arguments[0]);
|
|
303
|
+
expect(caseNames).to.include.members([
|
|
304
|
+
'Category-1',
|
|
305
|
+
'Category_2',
|
|
306
|
+
'Category@3',
|
|
307
|
+
'Category with spaces'
|
|
308
|
+
]);
|
|
309
|
+
});
|
|
310
|
+
it('maintains UUID consistency between categories, cases, and exits', () => {
|
|
311
|
+
const formData = {
|
|
312
|
+
uuid: 'test-node-uuid',
|
|
313
|
+
llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
|
|
314
|
+
input: '@input',
|
|
315
|
+
categories: [{ name: 'Test Category' }],
|
|
316
|
+
result_name: 'Intent'
|
|
317
|
+
};
|
|
318
|
+
const originalNode = {
|
|
319
|
+
uuid: 'test-node-uuid',
|
|
320
|
+
actions: [],
|
|
321
|
+
exits: []
|
|
322
|
+
};
|
|
323
|
+
const result = split_by_llm_categorize.fromFormData(formData, originalNode);
|
|
324
|
+
// Find the test category
|
|
325
|
+
const testCategory = result.router.categories.find((cat) => cat.name === 'Test Category');
|
|
326
|
+
const testCase = result.router.cases.find((c) => c.arguments[0] === 'Test Category');
|
|
327
|
+
const testExit = result.exits.find((exit) => exit.uuid === testCategory.exit_uuid);
|
|
328
|
+
// Verify UUID consistency
|
|
329
|
+
expect(testCase.category_uuid).to.equal(testCategory.uuid);
|
|
330
|
+
expect(testExit.uuid).to.equal(testCategory.exit_uuid);
|
|
331
|
+
});
|
|
332
|
+
it('generates unique UUIDs for each run', () => {
|
|
333
|
+
const formData = {
|
|
334
|
+
uuid: 'test-node-uuid',
|
|
335
|
+
llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
|
|
336
|
+
input: '@input',
|
|
337
|
+
categories: [{ name: 'Test' }],
|
|
338
|
+
result_name: 'Intent'
|
|
339
|
+
};
|
|
340
|
+
const originalNode = {
|
|
341
|
+
uuid: 'test-node-uuid',
|
|
342
|
+
actions: [],
|
|
343
|
+
exits: []
|
|
344
|
+
};
|
|
345
|
+
const result1 = split_by_llm_categorize.fromFormData(formData, originalNode);
|
|
346
|
+
const result2 = split_by_llm_categorize.fromFormData(formData, originalNode);
|
|
347
|
+
// UUIDs should be different for each generation
|
|
348
|
+
expect(result1.actions[0].uuid).to.not.equal(result2.actions[0].uuid);
|
|
349
|
+
expect(result1.router.categories[0].uuid).to.not.equal(result2.router.categories[0].uuid);
|
|
350
|
+
expect(result1.exits[0].uuid).to.not.equal(result2.exits[0].uuid);
|
|
351
|
+
});
|
|
352
|
+
it('roundtrip conversion (fromFormData -> toFormData) works correctly', () => {
|
|
353
|
+
const originalFormData = {
|
|
354
|
+
uuid: 'test-node-uuid',
|
|
355
|
+
llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
|
|
356
|
+
input: '@custom.input',
|
|
357
|
+
categories: [{ name: 'Category1' }, { name: 'Category2' }],
|
|
358
|
+
result_name: 'CustomResult'
|
|
359
|
+
};
|
|
360
|
+
const originalNode = {
|
|
361
|
+
uuid: 'test-node-uuid',
|
|
362
|
+
actions: [],
|
|
363
|
+
exits: []
|
|
364
|
+
};
|
|
365
|
+
// Convert form data to node
|
|
366
|
+
const node = split_by_llm_categorize.fromFormData(originalFormData, originalNode);
|
|
367
|
+
// Convert back to form data
|
|
368
|
+
const recoveredFormData = split_by_llm_categorize.toFormData(node);
|
|
369
|
+
// Should match original data
|
|
370
|
+
expect(recoveredFormData.uuid).to.equal(originalFormData.uuid);
|
|
371
|
+
expect(recoveredFormData.llm).to.deep.equal(originalFormData.llm);
|
|
372
|
+
expect(recoveredFormData.input).to.equal(originalFormData.input);
|
|
373
|
+
expect(recoveredFormData.categories).to.deep.equal(originalFormData.categories);
|
|
374
|
+
});
|
|
375
|
+
it('handles max 10 categories requirement', () => {
|
|
376
|
+
// Create 12 categories to test the limit
|
|
377
|
+
const categories = Array.from({ length: 12 }, (_, i) => ({
|
|
378
|
+
name: `Category${i + 1}`
|
|
379
|
+
}));
|
|
380
|
+
const formData = {
|
|
381
|
+
uuid: 'test-node-uuid',
|
|
382
|
+
llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
|
|
383
|
+
input: '@input',
|
|
384
|
+
categories: categories,
|
|
385
|
+
result_name: 'Intent'
|
|
386
|
+
};
|
|
387
|
+
const originalNode = {
|
|
388
|
+
uuid: 'test-node-uuid',
|
|
389
|
+
actions: [],
|
|
390
|
+
exits: []
|
|
391
|
+
};
|
|
392
|
+
const result = split_by_llm_categorize.fromFormData(formData, originalNode);
|
|
393
|
+
// Should process all categories provided (fromFormData doesn't enforce the limit, validation should)
|
|
394
|
+
const userCategories = result.router.categories.filter((cat) => cat.name !== 'Other' && cat.name !== 'Failure');
|
|
395
|
+
expect(userCategories).to.have.length(12);
|
|
396
|
+
// Note: The actual 10-category limit should be enforced by the UI validation
|
|
397
|
+
// which uses the maxItems: 10 property in the form configuration
|
|
398
|
+
});
|
|
399
|
+
it('preserves original node UUID', () => {
|
|
400
|
+
const formData = {
|
|
401
|
+
uuid: 'should-be-ignored',
|
|
402
|
+
llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
|
|
403
|
+
input: '@input',
|
|
404
|
+
categories: [{ name: 'Test' }],
|
|
405
|
+
result_name: 'Intent'
|
|
406
|
+
};
|
|
407
|
+
const originalNode = {
|
|
408
|
+
uuid: 'original-node-uuid',
|
|
409
|
+
actions: [],
|
|
410
|
+
exits: []
|
|
411
|
+
};
|
|
412
|
+
const result = split_by_llm_categorize.fromFormData(formData, originalNode);
|
|
413
|
+
// Should use original node UUID, not the one from form data
|
|
414
|
+
expect(result.uuid).to.equal('original-node-uuid');
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
describe('validation', () => {
|
|
418
|
+
it('should validate duplicate category names', () => {
|
|
419
|
+
const formData = {
|
|
420
|
+
uuid: 'test-node-uuid',
|
|
421
|
+
llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
|
|
422
|
+
input: '@input',
|
|
423
|
+
categories: [
|
|
424
|
+
{ name: 'Category1' },
|
|
425
|
+
{ name: 'Category2' },
|
|
426
|
+
{ name: 'Category1' }, // duplicate
|
|
427
|
+
{ name: 'category2' }, // case insensitive duplicate
|
|
428
|
+
{ name: 'Category3' }
|
|
429
|
+
]
|
|
430
|
+
};
|
|
431
|
+
const validationResult = split_by_llm_categorize.validate(formData);
|
|
432
|
+
expect(validationResult.valid).to.be.false;
|
|
433
|
+
expect(validationResult.errors.categories).to.include('Duplicate category names found');
|
|
434
|
+
expect(validationResult.errors.categories).to.include('Category1');
|
|
435
|
+
expect(validationResult.errors.categories).to.include('Category2');
|
|
436
|
+
expect(validationResult.errors.categories).to.include('category2');
|
|
437
|
+
});
|
|
438
|
+
it('should pass validation with unique category names', () => {
|
|
439
|
+
const formData = {
|
|
440
|
+
uuid: 'test-node-uuid',
|
|
441
|
+
llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
|
|
442
|
+
input: '@input',
|
|
443
|
+
categories: [
|
|
444
|
+
{ name: 'Category1' },
|
|
445
|
+
{ name: 'Category2' },
|
|
446
|
+
{ name: 'Category3' }
|
|
447
|
+
]
|
|
448
|
+
};
|
|
449
|
+
const validationResult = split_by_llm_categorize.validate(formData);
|
|
450
|
+
expect(validationResult.valid).to.be.true;
|
|
451
|
+
expect(Object.keys(validationResult.errors)).to.have.length(0);
|
|
452
|
+
});
|
|
453
|
+
it('should ignore empty categories in validation', () => {
|
|
454
|
+
const formData = {
|
|
455
|
+
uuid: 'test-node-uuid',
|
|
456
|
+
llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
|
|
457
|
+
input: '@input',
|
|
458
|
+
categories: [
|
|
459
|
+
{ name: 'Category1' },
|
|
460
|
+
{ name: '' }, // empty
|
|
461
|
+
{ name: ' ' }, // whitespace only
|
|
462
|
+
{ name: 'Category2' }
|
|
463
|
+
]
|
|
464
|
+
};
|
|
465
|
+
const validationResult = split_by_llm_categorize.validate(formData);
|
|
466
|
+
expect(validationResult.valid).to.be.true;
|
|
467
|
+
expect(Object.keys(validationResult.errors)).to.have.length(0);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
describe('JSON output verification', () => {
|
|
471
|
+
it('generates JSON matching the exact format from the issue', () => {
|
|
472
|
+
const formData = {
|
|
473
|
+
uuid: '145eb3d3-b841-4e66-abac-297ae525c7ad',
|
|
474
|
+
llm: [
|
|
475
|
+
{ value: '1c06c884-39dd-4ce4-ad9f-9a01cbe6c000', name: 'Claude' }
|
|
476
|
+
],
|
|
477
|
+
input: '@input',
|
|
478
|
+
categories: [{ name: 'Flights' }, { name: 'Hotels' }],
|
|
479
|
+
result_name: 'Intent'
|
|
480
|
+
};
|
|
481
|
+
const originalNode = {
|
|
482
|
+
uuid: '145eb3d3-b841-4e66-abac-297ae525c7ad',
|
|
483
|
+
actions: [],
|
|
484
|
+
exits: []
|
|
485
|
+
};
|
|
486
|
+
const result = split_by_llm_categorize.fromFormData(formData, originalNode);
|
|
487
|
+
// Verify the call_llm action
|
|
488
|
+
const callLlmAction = result.actions[0];
|
|
489
|
+
expect(callLlmAction.type).to.equal('call_llm');
|
|
490
|
+
expect(callLlmAction.llm.uuid).to.equal('1c06c884-39dd-4ce4-ad9f-9a01cbe6c000');
|
|
491
|
+
expect(callLlmAction.llm.name).to.equal('Claude');
|
|
492
|
+
expect(callLlmAction.instructions).to.equal('@(prompt("categorize", slice(node.categories, 0, -2)))');
|
|
493
|
+
expect(callLlmAction.input).to.equal('@input');
|
|
494
|
+
expect(callLlmAction.output_local).to.equal('_llm_output');
|
|
495
|
+
// Verify the router structure
|
|
496
|
+
const router = result.router;
|
|
497
|
+
expect(router.type).to.equal('switch');
|
|
498
|
+
expect(router.operand).to.equal('@locals._llm_output');
|
|
499
|
+
// Verify categories structure
|
|
500
|
+
expect(router.categories).to.have.length(4);
|
|
501
|
+
const categoryNames = router.categories.map((cat) => cat.name);
|
|
502
|
+
expect(categoryNames).to.include.members([
|
|
503
|
+
'Flights',
|
|
504
|
+
'Hotels',
|
|
505
|
+
'Other',
|
|
506
|
+
'Failure'
|
|
507
|
+
]);
|
|
508
|
+
// Verify cases structure
|
|
509
|
+
expect(router.cases).to.have.length(3);
|
|
510
|
+
const caseArguments = router.cases.map((c) => c.arguments[0]);
|
|
511
|
+
expect(caseArguments).to.include.members([
|
|
512
|
+
'Flights',
|
|
513
|
+
'Hotels',
|
|
514
|
+
'<ERROR>'
|
|
515
|
+
]);
|
|
516
|
+
// Verify all cases use has_only_text
|
|
517
|
+
router.cases.forEach((caseItem) => {
|
|
518
|
+
expect(caseItem.type).to.equal('has_only_text');
|
|
519
|
+
});
|
|
520
|
+
// Verify exits match categories
|
|
521
|
+
expect(result.exits).to.have.length(4);
|
|
522
|
+
router.categories.forEach((category) => {
|
|
523
|
+
const matchingExit = result.exits.find((exit) => exit.uuid === category.exit_uuid);
|
|
524
|
+
expect(matchingExit).to.exist;
|
|
525
|
+
});
|
|
526
|
+
// Verify default category is "Other"
|
|
527
|
+
const otherCategory = router.categories.find((cat) => cat.name === 'Other');
|
|
528
|
+
expect(router.default_category_uuid).to.equal(otherCategory.uuid);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
//# sourceMappingURL=split_by_llm_categorize.test.js.map
|