@uipath/packager-tool-flow 1.201.0-preview.131 → 1.201.0-preview.133
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/dist/agents.d.ts +6 -1
- package/dist/flow-tool.d.ts +11 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +157 -49
- package/dist/inline-agent-utils.d.ts +12 -0
- package/dist/operate-runtime-options.d.ts +10 -0
- package/dist/voice-agent-definitions.d.ts +56 -0
- package/package.json +5 -5
- package/src/agents.ts +10 -1
- package/src/flow-tool.ts +120 -57
- package/src/index.ts +11 -1
- package/src/inline-agent-utils.ts +31 -5
- package/src/operate-runtime-options.ts +15 -0
- package/src/voice-agent-definitions.ts +155 -0
package/dist/agents.d.ts
CHANGED
|
@@ -16,4 +16,9 @@ export declare function buildInlineAgentContract(config: {
|
|
|
16
16
|
projectPath: string;
|
|
17
17
|
outputPath: string;
|
|
18
18
|
}): Promise<InlineAgentContract>;
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Build every root-level agent directory into the content folder and merge
|
|
21
|
+
* their binding resources. Returns each built agent.json keyed by its
|
|
22
|
+
* directory name (the source UUID).
|
|
23
|
+
*/
|
|
24
|
+
export declare const processAgents: (fileSystem: IFileSystem, logger: IToolLogger, projectPath: string, contentFolder: string) => Promise<Map<string, Record<string, unknown>>>;
|
package/dist/flow-tool.d.ts
CHANGED
|
@@ -28,6 +28,17 @@ export declare class FlowTool extends ProjectTool {
|
|
|
28
28
|
* of any stale artifacts that may exist on disk.
|
|
29
29
|
*/
|
|
30
30
|
private generateFlowPackagingArtifacts;
|
|
31
|
+
/**
|
|
32
|
+
* `builtAgents` plus, for each voice node whose agent was not built, the
|
|
33
|
+
* agent.json copied into the content folder. Voice nodes embed the
|
|
34
|
+
* definition into the BPMN, so — unlike text agents — a source missing
|
|
35
|
+
* from the map fails the whole build, not just an entry point.
|
|
36
|
+
* A source with no copied agent.json stays absent;
|
|
37
|
+
* `buildVoiceAgentDefinitions` reports it.
|
|
38
|
+
*/
|
|
39
|
+
private resolveVoiceAgents;
|
|
40
|
+
/** Read an `agent.json` copied in without going through `buildAgent`. */
|
|
41
|
+
private readCopiedAgentJson;
|
|
31
42
|
/**
|
|
32
43
|
* Copy project files into the correct nupkg structure.
|
|
33
44
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,6 @@ export { replaceExporterVersion } from "./exporter-version.js";
|
|
|
3
3
|
export { createFlowRefResolver, type FlowIoLogger, migrateRawWorkflow, readFlowWorkflow, resolveAndMigrateWorkflow, resolveWorkflowRefs, } from "./flow-io.js";
|
|
4
4
|
export { FlowTool } from "./flow-tool.js";
|
|
5
5
|
export { FlowToolFactory } from "./flow-tool-factory.js";
|
|
6
|
-
export { setPublishIntentOnInlineAgents } from "./inline-agent-utils.js";
|
|
6
|
+
export { isInlineAgentNodeType, readInlineAgentSource, setPublishIntentOnInlineAgents, } from "./inline-agent-utils.js";
|
|
7
|
+
export { operateRuntimeOptions } from "./operate-runtime-options.js";
|
|
8
|
+
export { assertVoiceAgentDefinitionsEmbedded, type VoiceAgentFlowNode, voiceConvertOptions, } from "./voice-agent-definitions.js";
|
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
// package.json
|
|
33
33
|
var package_default = {
|
|
34
34
|
name: "@uipath/packager-tool-flow",
|
|
35
|
-
version: "1.201.0-preview.
|
|
35
|
+
version: "1.201.0-preview.133",
|
|
36
36
|
description: "UiPath Flow tool implementation",
|
|
37
37
|
type: "module",
|
|
38
38
|
exports: {
|
|
@@ -75,17 +75,17 @@ var package_default = {
|
|
|
75
75
|
license: "ISC",
|
|
76
76
|
peerDependencies: {
|
|
77
77
|
"@uipath/filesystem": "workspace:*",
|
|
78
|
-
"@uipath/flow-converter": "^0.
|
|
79
|
-
"@uipath/flow-core": "^0.
|
|
78
|
+
"@uipath/flow-converter": "^0.53.6",
|
|
79
|
+
"@uipath/flow-core": "^0.92.5",
|
|
80
80
|
"@uipath/flow-migrations": "^0.33.0",
|
|
81
|
-
"@uipath/flow-schema": "^0.
|
|
81
|
+
"@uipath/flow-schema": "^0.57.6",
|
|
82
82
|
"@uipath/solutionpackager-tool-core": "workspace:*",
|
|
83
83
|
"@uipath/tool-agent": "^2.0.0"
|
|
84
84
|
},
|
|
85
85
|
devDependencies: {
|
|
86
86
|
"@types/node": "^25.5.2",
|
|
87
87
|
"@uipath/filesystem": "workspace:*",
|
|
88
|
-
"@uipath/flow-core": "^0.
|
|
88
|
+
"@uipath/flow-core": "^0.92.5",
|
|
89
89
|
"@uipath/solutionpackager-tool-core": "workspace:*",
|
|
90
90
|
"@uipath/tool-agent": "^2.0.0",
|
|
91
91
|
"@vitest/browser": "^4.1.6",
|
|
@@ -148,6 +148,7 @@ async function buildInlineAgentContract(config) {
|
|
|
148
148
|
}
|
|
149
149
|
var processAgents = async (fileSystem, logger, projectPath, contentFolder) => {
|
|
150
150
|
const agentResults = [];
|
|
151
|
+
const builtAgents = new Map;
|
|
151
152
|
const entries = await fileSystem.readdir(projectPath);
|
|
152
153
|
for (const entry of entries) {
|
|
153
154
|
const sourceDir = Path2.join(projectPath, entry);
|
|
@@ -168,11 +169,13 @@ var processAgents = async (fileSystem, logger, projectPath, contentFolder) => {
|
|
|
168
169
|
logger
|
|
169
170
|
});
|
|
170
171
|
agentResults.push(result);
|
|
172
|
+
builtAgents.set(entry, result.agent);
|
|
171
173
|
}
|
|
172
174
|
const agentBindingResources = agentResults.flatMap(toBindingsResources);
|
|
173
175
|
if (agentBindingResources.length > 0) {
|
|
174
176
|
await mergeAgentBindings(fileSystem, contentFolder, agentBindingResources);
|
|
175
177
|
}
|
|
178
|
+
return builtAgents;
|
|
176
179
|
};
|
|
177
180
|
|
|
178
181
|
// src/ensure-process-bindings.ts
|
|
@@ -431,15 +434,29 @@ async function readUtf8OrNull(fs, filePath) {
|
|
|
431
434
|
}
|
|
432
435
|
|
|
433
436
|
// src/inline-agent-utils.ts
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
437
|
+
import { isAgentNodeType } from "@uipath/flow-schema";
|
|
438
|
+
function readInlineAgentSource(node) {
|
|
439
|
+
const candidates = [
|
|
440
|
+
node.inputs?.source,
|
|
441
|
+
node.model?.source,
|
|
442
|
+
node.inputs?.agentProjectId,
|
|
443
|
+
node.model?.agentProjectId
|
|
444
|
+
];
|
|
445
|
+
for (const candidate of candidates) {
|
|
446
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
447
|
+
return candidate;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
function isInlineAgentNodeType(type) {
|
|
453
|
+
return isAgentNodeType(type);
|
|
454
|
+
}
|
|
438
455
|
function setPublishIntentOnInlineAgents(fileFormat) {
|
|
439
456
|
const ff = fileFormat;
|
|
440
457
|
for (const def of ff.definitions ?? []) {
|
|
441
458
|
const d = def;
|
|
442
|
-
if (
|
|
459
|
+
if (isInlineAgentNodeType(d.nodeType)) {
|
|
443
460
|
const model = d.model ?? {};
|
|
444
461
|
model.__packageIntent = "Publish";
|
|
445
462
|
d.model = model;
|
|
@@ -447,6 +464,80 @@ function setPublishIntentOnInlineAgents(fileFormat) {
|
|
|
447
464
|
}
|
|
448
465
|
}
|
|
449
466
|
|
|
467
|
+
// src/operate-runtime-options.ts
|
|
468
|
+
var CONVERSATION_TRIGGER_NODE_TYPE = "core.trigger.conversation";
|
|
469
|
+
function operateRuntimeOptions(nodes) {
|
|
470
|
+
return nodes.some((node) => node.type === CONVERSATION_TRIGGER_NODE_TYPE) ? { isConversational: true } : undefined;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/voice-agent-definitions.ts
|
|
474
|
+
import { isVoiceAgentNodeType } from "@uipath/flow-schema";
|
|
475
|
+
function nodeLabel(node) {
|
|
476
|
+
const label = node.display?.label;
|
|
477
|
+
return typeof label === "string" && label.length > 0 ? label : node.id;
|
|
478
|
+
}
|
|
479
|
+
function collectVoiceAgentNodes(nodes) {
|
|
480
|
+
const result = [];
|
|
481
|
+
for (const node of nodes) {
|
|
482
|
+
if (!isVoiceAgentNodeType(node.type))
|
|
483
|
+
continue;
|
|
484
|
+
const source = readInlineAgentSource(node);
|
|
485
|
+
if (!source) {
|
|
486
|
+
throw new Error(`Voice agent node "${nodeLabel(node)}" has no inputs.source. ` + "Set it to the UUID of the agent directory inside the flow project.");
|
|
487
|
+
}
|
|
488
|
+
result.push({ node, source });
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
function buildVoiceAgentDefinitions(nodes, builtAgents) {
|
|
493
|
+
const definitions = {};
|
|
494
|
+
for (const { node, source } of collectVoiceAgentNodes(nodes)) {
|
|
495
|
+
if (definitions[source])
|
|
496
|
+
continue;
|
|
497
|
+
const agent = builtAgents.get(source);
|
|
498
|
+
if (!agent) {
|
|
499
|
+
throw new Error(`Missing agent definition for voice agent node "${nodeLabel(node)}" (${source}). ` + `Make sure ${source}/agent.json exists in the flow project.`);
|
|
500
|
+
}
|
|
501
|
+
definitions[source] = JSON.stringify({
|
|
502
|
+
...agent,
|
|
503
|
+
resources: agent.resources ?? [],
|
|
504
|
+
features: agent.features ?? []
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return definitions;
|
|
508
|
+
}
|
|
509
|
+
function assertNoSubflowVoiceAgents(flowJson) {
|
|
510
|
+
const workflow = JSON.parse(flowJson);
|
|
511
|
+
for (const [subflowId, entry] of Object.entries(workflow.subflows ?? {})) {
|
|
512
|
+
for (const node of entry.nodes ?? []) {
|
|
513
|
+
if (!isVoiceAgentNodeType(node.type))
|
|
514
|
+
continue;
|
|
515
|
+
throw new Error(`Voice agent node "${nodeLabel(node)}" is inside subflow "${subflowId}". ` + "Voice agent nodes are not supported inside subflows — move it to the top-level flow.");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function voiceConvertOptions(config) {
|
|
520
|
+
assertNoSubflowVoiceAgents(config.flowJson);
|
|
521
|
+
const voiceAgentDefinitions = buildVoiceAgentDefinitions(config.nodes, config.builtAgents);
|
|
522
|
+
if (Object.keys(voiceAgentDefinitions).length === 0) {
|
|
523
|
+
return config.convertOptions;
|
|
524
|
+
}
|
|
525
|
+
const options = {
|
|
526
|
+
...config.convertOptions,
|
|
527
|
+
voiceAgentDefinitions
|
|
528
|
+
};
|
|
529
|
+
return options;
|
|
530
|
+
}
|
|
531
|
+
function assertVoiceAgentDefinitionsEmbedded(bpmn, nodes) {
|
|
532
|
+
const voiceNodes = collectVoiceAgentNodes(nodes);
|
|
533
|
+
if (voiceNodes.length === 0)
|
|
534
|
+
return;
|
|
535
|
+
if (bpmn.includes('name="agentDefinition"'))
|
|
536
|
+
return;
|
|
537
|
+
const labels = voiceNodes.map(({ node }) => `"${nodeLabel(node)}"`).join(", ");
|
|
538
|
+
throw new Error(`Converted BPMN carries no agentDefinition for voice agent node(s) ${labels}. ` + "The installed @uipath/flow-converter does not forward voiceAgentDefinitions " + "to the BPMN serializer — upgrade it. Packaging a voice flow without the " + "embedded agent definition produces a package that deploys but drops every call.");
|
|
539
|
+
}
|
|
540
|
+
|
|
450
541
|
// src/flow-tool.ts
|
|
451
542
|
var FlowConstants = {
|
|
452
543
|
EntryPointsFileName: "entry-points.json",
|
|
@@ -475,12 +566,12 @@ class FlowTool extends ProjectTool {
|
|
|
475
566
|
this.logger.progress("Copying files...");
|
|
476
567
|
await this.copyProjectFiles(options.projectPath, contentFolder);
|
|
477
568
|
this.logger.progress("Processing agents...");
|
|
478
|
-
await processAgents(this.fileSystem, this.logger, options.projectPath, contentFolder);
|
|
569
|
+
const builtAgents = await processAgents(this.fileSystem, this.logger, options.projectPath, contentFolder);
|
|
479
570
|
this.logger.progress("Resolving process bindings...");
|
|
480
571
|
await this.resolveProcessBindings(contentFolder);
|
|
481
572
|
const projectId = options.projectStorageId ?? await ensureProjectId(options.projectPath, this.fileSystem);
|
|
482
573
|
this.logger.progress("Generating packaging artifacts from .flow...");
|
|
483
|
-
await this.generateFlowPackagingArtifacts(contentFolder, projectId);
|
|
574
|
+
await this.generateFlowPackagingArtifacts(contentFolder, projectId, builtAgents);
|
|
484
575
|
this.logger.progress("Creating operate.json file...");
|
|
485
576
|
await this.createOperateFile(contentFolder, projectId);
|
|
486
577
|
this.logger.progress("Creating package-descriptor.json file...");
|
|
@@ -543,7 +634,7 @@ class FlowTool extends ProjectTool {
|
|
|
543
634
|
await this.fileSystem.writeFile(flowPath, JSON.stringify(workflow, null, 4));
|
|
544
635
|
}
|
|
545
636
|
}
|
|
546
|
-
async generateFlowPackagingArtifacts(contentFolder, projectId) {
|
|
637
|
+
async generateFlowPackagingArtifacts(contentFolder, projectId, builtAgents) {
|
|
547
638
|
const entries = await this.fileSystem.readdir(contentFolder);
|
|
548
639
|
const flowFile = entries.find((e) => e.endsWith(".flow"));
|
|
549
640
|
if (!flowFile) {
|
|
@@ -567,8 +658,15 @@ class FlowTool extends ProjectTool {
|
|
|
567
658
|
const fileFormat = inMemoryWorkflowToFileFormat(workflow);
|
|
568
659
|
setPublishIntentOnInlineAgents(fileFormat);
|
|
569
660
|
const resolvedFlowJson = JSON.stringify(fileFormat);
|
|
570
|
-
const
|
|
661
|
+
const flowNodes = workflow.nodes ?? [];
|
|
662
|
+
const voiceAgents = await this.resolveVoiceAgents(flowNodes, builtAgents, contentFolder);
|
|
663
|
+
const { bpmn: rawBpmn } = await convertFlowToBpmn(resolvedFlowJson, voiceConvertOptions({
|
|
664
|
+
flowJson: resolvedFlowJson,
|
|
665
|
+
nodes: flowNodes,
|
|
666
|
+
builtAgents: voiceAgents
|
|
667
|
+
}));
|
|
571
668
|
const bpmn = replaceExporterVersion(rawBpmn, package_default.version);
|
|
669
|
+
assertVoiceAgentDefinitionsEmbedded(bpmn, flowNodes);
|
|
572
670
|
await this.fileSystem.writeFile(bpmnPath, bpmn);
|
|
573
671
|
const packagingNodes = (workflow.nodes ?? []).map((node) => ({
|
|
574
672
|
id: node.id,
|
|
@@ -583,49 +681,24 @@ class FlowTool extends ProjectTool {
|
|
|
583
681
|
const definitions = workflow.definitions ?? [];
|
|
584
682
|
const entryPointsPath = Path3.join(contentFolder, FlowConstants.EntryPointsFileName);
|
|
585
683
|
const entryPoints = getEntryPoints(bpmnFileName, packagingNodes, variables, definitions, ProjectType.Flow);
|
|
586
|
-
const INLINE_AGENT_TYPES = [
|
|
587
|
-
"uipath.agent.autonomous",
|
|
588
|
-
"uipath.agent.conversational"
|
|
589
|
-
];
|
|
590
|
-
const readSource = (n) => {
|
|
591
|
-
const inputs = n.inputs;
|
|
592
|
-
const model = n.model;
|
|
593
|
-
const candidates = [
|
|
594
|
-
inputs?.source,
|
|
595
|
-
model?.source,
|
|
596
|
-
inputs?.agentProjectId,
|
|
597
|
-
model?.agentProjectId
|
|
598
|
-
];
|
|
599
|
-
for (const c of candidates) {
|
|
600
|
-
if (typeof c === "string" && c.length > 0)
|
|
601
|
-
return c;
|
|
602
|
-
}
|
|
603
|
-
return;
|
|
604
|
-
};
|
|
605
684
|
const agentEntryPoints = [];
|
|
606
685
|
const seenSources = new Set;
|
|
607
686
|
for (const node of workflow.nodes ?? []) {
|
|
608
687
|
const n = node;
|
|
609
|
-
if (!
|
|
688
|
+
if (!isInlineAgentNodeType(n.type))
|
|
610
689
|
continue;
|
|
611
|
-
const source =
|
|
690
|
+
const source = readInlineAgentSource(n);
|
|
612
691
|
if (!source)
|
|
613
692
|
continue;
|
|
614
693
|
if (seenSources.has(source))
|
|
615
694
|
continue;
|
|
616
695
|
seenSources.add(source);
|
|
617
|
-
|
|
618
|
-
if (!
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (raw) {
|
|
624
|
-
const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
625
|
-
agent = JSON.parse(text);
|
|
626
|
-
}
|
|
627
|
-
} catch {
|
|
628
|
-
continue;
|
|
696
|
+
let agent = voiceAgents.get(source);
|
|
697
|
+
if (!agent) {
|
|
698
|
+
agent = await this.readCopiedAgentJson(contentFolder, source);
|
|
699
|
+
if (!agent)
|
|
700
|
+
continue;
|
|
701
|
+
this.logger.warn(`Inline agent "${source}" has no root-level agent directory, so it ` + "was not built. Its entry point comes from the agent.json copied " + "into the package. Move the agent to the project root to get the " + "full build (migration, validation, binding resources).");
|
|
629
702
|
}
|
|
630
703
|
const inputSchema = agent.inputSchema ?? {
|
|
631
704
|
type: "object",
|
|
@@ -655,9 +728,39 @@ class FlowTool extends ProjectTool {
|
|
|
655
728
|
const startEventMatch = bpmn.match(/<bpmn:startEvent\s+id="([^"]+)"/);
|
|
656
729
|
const startEventId = startEventMatch?.[1] ?? "start";
|
|
657
730
|
const mainEntryPoint = `/${bpmnFileName}#${startEventId}`;
|
|
658
|
-
|
|
731
|
+
const operateJson = generateOperateJson(projectId || workflow.id || "", mainEntryPoint, operateRuntimeOptions(flowNodes));
|
|
732
|
+
await this.fileSystem.writeFile(operatePath, `${JSON.stringify(operateJson, null, 2)}
|
|
659
733
|
`);
|
|
660
734
|
}
|
|
735
|
+
async resolveVoiceAgents(flowNodes, builtAgents, contentFolder) {
|
|
736
|
+
const voiceAgents = new Map(builtAgents);
|
|
737
|
+
for (const { source } of collectVoiceAgentNodes(flowNodes)) {
|
|
738
|
+
if (voiceAgents.has(source))
|
|
739
|
+
continue;
|
|
740
|
+
const copied = await this.readCopiedAgentJson(contentFolder, source);
|
|
741
|
+
if (!copied)
|
|
742
|
+
continue;
|
|
743
|
+
voiceAgents.set(source, copied);
|
|
744
|
+
this.logger.warn(`Voice agent "${source}" has no root-level agent directory, so it ` + "was not built. Its embedded definition and entry point come from " + "the agent.json copied into the package. Move the agent to the " + "project root to get the full build (migration, validation, " + "binding resources).");
|
|
745
|
+
}
|
|
746
|
+
return voiceAgents;
|
|
747
|
+
}
|
|
748
|
+
async readCopiedAgentJson(contentFolder, source) {
|
|
749
|
+
const agentJsonPath = Path3.join(contentFolder, source, "agent.json");
|
|
750
|
+
if (!await this.fileSystem.exists(agentJsonPath)) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
const raw = await this.fileSystem.readFile(agentJsonPath);
|
|
755
|
+
if (!raw) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
759
|
+
return JSON.parse(text);
|
|
760
|
+
} catch {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
661
764
|
async copyProjectFiles(projectPath, contentFolder) {
|
|
662
765
|
await this.fileSystem.mkdir(contentFolder);
|
|
663
766
|
const entries = await this.fileSystem.readdir(projectPath);
|
|
@@ -735,16 +838,21 @@ class FlowToolFactory {
|
|
|
735
838
|
// src/index.ts
|
|
736
839
|
toolsFactoryRepository.registerProjectToolFactory(new FlowToolFactory);
|
|
737
840
|
export {
|
|
841
|
+
voiceConvertOptions,
|
|
738
842
|
setPublishIntentOnInlineAgents,
|
|
739
843
|
resolveWorkflowRefs,
|
|
740
844
|
resolveAndMigrateWorkflow,
|
|
741
845
|
replaceExporterVersion,
|
|
846
|
+
readInlineAgentSource,
|
|
742
847
|
readFlowWorkflow,
|
|
848
|
+
operateRuntimeOptions,
|
|
743
849
|
migrateRawWorkflow,
|
|
850
|
+
isInlineAgentNodeType,
|
|
744
851
|
createFlowRefResolver,
|
|
745
852
|
buildInlineAgentContract,
|
|
853
|
+
assertVoiceAgentDefinitionsEmbedded,
|
|
746
854
|
FlowToolFactory,
|
|
747
855
|
FlowTool
|
|
748
856
|
};
|
|
749
857
|
|
|
750
|
-
//# debugId=
|
|
858
|
+
//# debugId=7E6DF946A11A786164756E2164756E21
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the inline-agent's projectId UUID from a flow node. Falls back to
|
|
3
|
+
* the legacy `model.source` / `agentProjectId` aliases so packaging tolerates
|
|
4
|
+
* already-deployed `.flow` files; `model-source-validator.ts` flags those at
|
|
5
|
+
* validate time.
|
|
6
|
+
*/
|
|
7
|
+
export declare function readInlineAgentSource(node: {
|
|
8
|
+
inputs?: Record<string, unknown>;
|
|
9
|
+
model?: Record<string, unknown>;
|
|
10
|
+
}): string | undefined;
|
|
11
|
+
/** True for a known inline-agent node type. */
|
|
12
|
+
export declare function isInlineAgentNodeType(type: string | undefined): boolean;
|
|
1
13
|
/**
|
|
2
14
|
* Set `__packageIntent = "Publish"` on inline agent definitions in a
|
|
3
15
|
* file-format workflow so the BPMN converter emits "content/" prefixed
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** `runtimeOptions` for a flow's `operate.json`, derived from its nodes. */
|
|
2
|
+
/**
|
|
3
|
+
* A conversation trigger means the flow can be chatted with, so it packs with
|
|
4
|
+
* `isConversational: true`. Nothing else sets it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function operateRuntimeOptions(nodes: ReadonlyArray<{
|
|
7
|
+
type?: string;
|
|
8
|
+
}>): {
|
|
9
|
+
isConversational: true;
|
|
10
|
+
} | undefined;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice inline agents ship their built agent definition inside the BPMN
|
|
3
|
+
* (`agentDefinition` on the StartInlineAgentJob activity). The BPMN serializer
|
|
4
|
+
* writes it from the `voiceAgentDefinitions` convert option; a package without
|
|
5
|
+
* it deploys and then drops every call, so
|
|
6
|
+
* {@link assertVoiceAgentDefinitionsEmbedded} fails the build.
|
|
7
|
+
*
|
|
8
|
+
* Callers convert the flow themselves — this module only builds the convert
|
|
9
|
+
* option and checks the result.
|
|
10
|
+
*/
|
|
11
|
+
import type { ConvertOptions } from "@uipath/flow-converter";
|
|
12
|
+
/** `ConvertOptions` plus the `voiceAgentDefinitions` map the serializer reads. */
|
|
13
|
+
export interface VoiceAwareConvertOptions extends ConvertOptions {
|
|
14
|
+
voiceAgentDefinitions?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
export interface VoiceAgentFlowNode {
|
|
17
|
+
id: string;
|
|
18
|
+
type: string;
|
|
19
|
+
display?: Record<string, unknown>;
|
|
20
|
+
inputs?: Record<string, unknown>;
|
|
21
|
+
model?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
/** Voice agent nodes with their resolved source UUIDs. */
|
|
24
|
+
export declare function collectVoiceAgentNodes(nodes: VoiceAgentFlowNode[]): Array<{
|
|
25
|
+
node: VoiceAgentFlowNode;
|
|
26
|
+
source: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Build the `voiceAgentDefinitions` map. `builtAgents` holds each source
|
|
30
|
+
* UUID's built agent.json (post `buildAgent`).
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildVoiceAgentDefinitions(nodes: VoiceAgentFlowNode[], builtAgents: ReadonlyMap<string, Record<string, unknown>>): Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* The convert options a flow's voice nodes require: the built definitions the
|
|
35
|
+
* serializer embeds, merged onto the caller's own options. Returns
|
|
36
|
+
* `convertOptions` unchanged when the flow has no voice nodes.
|
|
37
|
+
*
|
|
38
|
+
* Throws when a voice node sits in a subflow or its agent is missing — before
|
|
39
|
+
* a BPMN is written, not after.
|
|
40
|
+
*/
|
|
41
|
+
export declare function voiceConvertOptions(config: {
|
|
42
|
+
/** The resolved file-format workflow, already stringified. */
|
|
43
|
+
flowJson: string;
|
|
44
|
+
/** The workflow's nodes — scanned for voice agent nodes. */
|
|
45
|
+
nodes: VoiceAgentFlowNode[];
|
|
46
|
+
/** Source UUID → built agent.json for every packaged inline agent. */
|
|
47
|
+
builtAgents: ReadonlyMap<string, Record<string, unknown>>;
|
|
48
|
+
/** The caller's own converter options (e.g. `detached` on debug). */
|
|
49
|
+
convertOptions?: ConvertOptions;
|
|
50
|
+
}): ConvertOptions | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Whole-document check: one `agentDefinition` anywhere satisfies it. None at
|
|
53
|
+
* all means the installed converter dropped the option, which drops every
|
|
54
|
+
* node at once. Per-node gaps are the serializer's job — it raises on those.
|
|
55
|
+
*/
|
|
56
|
+
export declare function assertVoiceAgentDefinitionsEmbedded(bpmn: string, nodes: VoiceAgentFlowNode[]): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/packager-tool-flow",
|
|
3
|
-
"version": "1.201.0-preview.
|
|
3
|
+
"version": "1.201.0-preview.133",
|
|
4
4
|
"description": "UiPath Flow tool implementation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"license": "ISC",
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"@uipath/filesystem": "1.201.0",
|
|
29
|
-
"@uipath/flow-converter": "^0.
|
|
30
|
-
"@uipath/flow-core": "^0.
|
|
29
|
+
"@uipath/flow-converter": "^0.53.6",
|
|
30
|
+
"@uipath/flow-core": "^0.92.5",
|
|
31
31
|
"@uipath/flow-migrations": "^0.33.0",
|
|
32
|
-
"@uipath/flow-schema": "^0.
|
|
32
|
+
"@uipath/flow-schema": "^0.57.6",
|
|
33
33
|
"@uipath/solutionpackager-tool-core": "1.201.0",
|
|
34
34
|
"@uipath/tool-agent": "^2.0.0"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "7933378ad5276ac369900293a1447474dcec6826"
|
|
37
37
|
}
|
package/src/agents.ts
CHANGED
|
@@ -41,13 +41,19 @@ export async function buildInlineAgentContract(config: {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Build every root-level agent directory into the content folder and merge
|
|
46
|
+
* their binding resources. Returns each built agent.json keyed by its
|
|
47
|
+
* directory name (the source UUID).
|
|
48
|
+
*/
|
|
44
49
|
export const processAgents = async (
|
|
45
50
|
fileSystem: IFileSystem,
|
|
46
51
|
logger: IToolLogger,
|
|
47
52
|
projectPath: string,
|
|
48
53
|
contentFolder: string,
|
|
49
|
-
): Promise<
|
|
54
|
+
): Promise<Map<string, Record<string, unknown>>> => {
|
|
50
55
|
const agentResults: AgentBuildResult[] = [];
|
|
56
|
+
const builtAgents = new Map<string, Record<string, unknown>>();
|
|
51
57
|
|
|
52
58
|
const entries = await fileSystem.readdir(projectPath);
|
|
53
59
|
for (const entry of entries) {
|
|
@@ -72,6 +78,7 @@ export const processAgents = async (
|
|
|
72
78
|
logger,
|
|
73
79
|
});
|
|
74
80
|
agentResults.push(result);
|
|
81
|
+
builtAgents.set(entry, result.agent as Record<string, unknown>);
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
const agentBindingResources = agentResults.flatMap(toBindingsResources);
|
|
@@ -82,4 +89,6 @@ export const processAgents = async (
|
|
|
82
89
|
agentBindingResources,
|
|
83
90
|
);
|
|
84
91
|
}
|
|
92
|
+
|
|
93
|
+
return builtAgents;
|
|
85
94
|
};
|
package/src/flow-tool.ts
CHANGED
|
@@ -39,7 +39,18 @@ import { processAgents } from "./agents.js";
|
|
|
39
39
|
import { ensureProcessBindings } from "./ensure-process-bindings.js";
|
|
40
40
|
import { replaceExporterVersion } from "./exporter-version.js";
|
|
41
41
|
import { readFlowWorkflow } from "./flow-io.js";
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
isInlineAgentNodeType,
|
|
44
|
+
readInlineAgentSource,
|
|
45
|
+
setPublishIntentOnInlineAgents,
|
|
46
|
+
} from "./inline-agent-utils.js";
|
|
47
|
+
import { operateRuntimeOptions } from "./operate-runtime-options.js";
|
|
48
|
+
import {
|
|
49
|
+
assertVoiceAgentDefinitionsEmbedded,
|
|
50
|
+
collectVoiceAgentNodes,
|
|
51
|
+
type VoiceAgentFlowNode,
|
|
52
|
+
voiceConvertOptions,
|
|
53
|
+
} from "./voice-agent-definitions.js";
|
|
43
54
|
|
|
44
55
|
const FlowConstants = {
|
|
45
56
|
EntryPointsFileName: "entry-points.json",
|
|
@@ -94,7 +105,7 @@ export class FlowTool extends ProjectTool {
|
|
|
94
105
|
await this.copyProjectFiles(options.projectPath, contentFolder);
|
|
95
106
|
|
|
96
107
|
this.logger.progress("Processing agents...");
|
|
97
|
-
await processAgents(
|
|
108
|
+
const builtAgents = await processAgents(
|
|
98
109
|
this.fileSystem,
|
|
99
110
|
this.logger,
|
|
100
111
|
options.projectPath,
|
|
@@ -111,7 +122,11 @@ export class FlowTool extends ProjectTool {
|
|
|
111
122
|
this.logger.progress(
|
|
112
123
|
"Generating packaging artifacts from .flow...",
|
|
113
124
|
);
|
|
114
|
-
await this.generateFlowPackagingArtifacts(
|
|
125
|
+
await this.generateFlowPackagingArtifacts(
|
|
126
|
+
contentFolder,
|
|
127
|
+
projectId,
|
|
128
|
+
builtAgents,
|
|
129
|
+
);
|
|
115
130
|
|
|
116
131
|
this.logger.progress("Creating operate.json file...");
|
|
117
132
|
await this.createOperateFile(contentFolder, projectId);
|
|
@@ -231,6 +246,7 @@ export class FlowTool extends ProjectTool {
|
|
|
231
246
|
private async generateFlowPackagingArtifacts(
|
|
232
247
|
contentFolder: string,
|
|
233
248
|
projectId: string,
|
|
249
|
+
builtAgents: ReadonlyMap<string, Record<string, unknown>>,
|
|
234
250
|
): Promise<void> {
|
|
235
251
|
// Find the .flow file in content folder
|
|
236
252
|
const entries = await this.fileSystem.readdir(contentFolder);
|
|
@@ -266,8 +282,28 @@ export class FlowTool extends ProjectTool {
|
|
|
266
282
|
const fileFormat = inMemoryWorkflowToFileFormat(workflow);
|
|
267
283
|
setPublishIntentOnInlineAgents(fileFormat);
|
|
268
284
|
const resolvedFlowJson = JSON.stringify(fileFormat);
|
|
269
|
-
|
|
285
|
+
|
|
286
|
+
// `builtAgents` holds only what processAgents built (root-level agent
|
|
287
|
+
// dirs). A voice agent that reached the content folder some other way
|
|
288
|
+
// (a Studio Web download's `content/<uuid>/` layout) still has to
|
|
289
|
+
// embed a definition — a BPMN without one deploys and then drops
|
|
290
|
+
// every call — so fall back to its copied agent.json.
|
|
291
|
+
const flowNodes = (workflow.nodes ?? []) as VoiceAgentFlowNode[];
|
|
292
|
+
const voiceAgents = await this.resolveVoiceAgents(
|
|
293
|
+
flowNodes,
|
|
294
|
+
builtAgents,
|
|
295
|
+
contentFolder,
|
|
296
|
+
);
|
|
297
|
+
const { bpmn: rawBpmn } = await convertFlowToBpmn(
|
|
298
|
+
resolvedFlowJson,
|
|
299
|
+
voiceConvertOptions({
|
|
300
|
+
flowJson: resolvedFlowJson,
|
|
301
|
+
nodes: flowNodes,
|
|
302
|
+
builtAgents: voiceAgents,
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
270
305
|
const bpmn = replaceExporterVersion(rawBpmn, pkg.version);
|
|
306
|
+
assertVoiceAgentDefinitionsEmbedded(bpmn, flowNodes);
|
|
271
307
|
await this.fileSystem.writeFile(bpmnPath, bpmn);
|
|
272
308
|
|
|
273
309
|
// Map file-format nodes to flat PackagingNode shape so
|
|
@@ -303,66 +339,33 @@ export class FlowTool extends ProjectTool {
|
|
|
303
339
|
ProjectType.Flow,
|
|
304
340
|
);
|
|
305
341
|
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
// accepts both, so packaging must too — otherwise legacy flows
|
|
310
|
-
// produce a .nupkg whose entry-points.json silently omits the
|
|
311
|
-
// agent, and runtime returns 404 for `StartInlineAgentJob`.
|
|
312
|
-
//
|
|
313
|
-
// Canonical copy of this resolution rule lives at
|
|
314
|
-
// `flow-tool/src/services/packaging-utils.ts:readInlineAgentSource`.
|
|
315
|
-
// It's duplicated here (rather than imported) because `flow-tool`
|
|
316
|
-
// already depends on this package, so the reverse import would
|
|
317
|
-
// be a cycle. Keep both in sync; longer-term the helper should
|
|
318
|
-
// move to `@uipath/flow-schema` (an external dep both can share).
|
|
319
|
-
const INLINE_AGENT_TYPES = [
|
|
320
|
-
"uipath.agent.autonomous",
|
|
321
|
-
"uipath.agent.conversational",
|
|
322
|
-
];
|
|
323
|
-
const readSource = (n: Record<string, unknown>): string | undefined => {
|
|
324
|
-
const inputs = n.inputs as Record<string, unknown> | undefined;
|
|
325
|
-
const model = n.model as Record<string, unknown> | undefined;
|
|
326
|
-
const candidates: unknown[] = [
|
|
327
|
-
inputs?.source,
|
|
328
|
-
model?.source,
|
|
329
|
-
inputs?.agentProjectId,
|
|
330
|
-
model?.agentProjectId,
|
|
331
|
-
];
|
|
332
|
-
for (const c of candidates) {
|
|
333
|
-
if (typeof c === "string" && c.length > 0) return c;
|
|
334
|
-
}
|
|
335
|
-
return undefined;
|
|
336
|
-
};
|
|
342
|
+
// `readInlineAgentSource` accepts the legacy `model.source` too, the
|
|
343
|
+
// same as validation; otherwise entry-points.json omits the agent and
|
|
344
|
+
// runtime 404s on `StartInlineAgentJob`.
|
|
337
345
|
const agentEntryPoints: EntryPoint[] = [];
|
|
338
346
|
const seenSources = new Set<string>();
|
|
339
347
|
for (const node of workflow.nodes ?? []) {
|
|
340
348
|
const n = node as Record<string, unknown>;
|
|
341
|
-
if (!
|
|
342
|
-
const source =
|
|
349
|
+
if (!isInlineAgentNodeType(n.type as string)) continue;
|
|
350
|
+
const source = readInlineAgentSource(n);
|
|
343
351
|
if (!source) continue;
|
|
344
352
|
if (seenSources.has(source)) continue;
|
|
345
353
|
seenSources.add(source);
|
|
346
354
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
);
|
|
352
|
-
if (!
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
: new TextDecoder().decode(raw);
|
|
362
|
-
agent = JSON.parse(text) as Record<string, unknown>;
|
|
363
|
-
}
|
|
364
|
-
} catch {
|
|
365
|
-
continue;
|
|
355
|
+
// `processAgents` only scans root-level agent dirs, so a
|
|
356
|
+
// `content/<uuid>/` layout is copied but never built.
|
|
357
|
+
// `voiceAgents` already carries the copied fallback for voice
|
|
358
|
+
// sources (and their warn), so those don't warn twice here.
|
|
359
|
+
let agent = voiceAgents.get(source);
|
|
360
|
+
if (!agent) {
|
|
361
|
+
agent = await this.readCopiedAgentJson(contentFolder, source);
|
|
362
|
+
if (!agent) continue;
|
|
363
|
+
this.logger.warn(
|
|
364
|
+
`Inline agent "${source}" has no root-level agent directory, so it ` +
|
|
365
|
+
"was not built. Its entry point comes from the agent.json copied " +
|
|
366
|
+
"into the package. Move the agent to the project root to get the " +
|
|
367
|
+
"full build (migration, validation, binding resources).",
|
|
368
|
+
);
|
|
366
369
|
}
|
|
367
370
|
|
|
368
371
|
const inputSchema = (agent.inputSchema ?? {
|
|
@@ -412,12 +415,72 @@ export class FlowTool extends ProjectTool {
|
|
|
412
415
|
const startEventMatch = bpmn.match(/<bpmn:startEvent\s+id="([^"]+)"/);
|
|
413
416
|
const startEventId = startEventMatch?.[1] ?? "start";
|
|
414
417
|
const mainEntryPoint = `/${bpmnFileName}#${startEventId}`;
|
|
418
|
+
const operateJson = generateOperateJson(
|
|
419
|
+
projectId || workflow.id || "",
|
|
420
|
+
mainEntryPoint,
|
|
421
|
+
operateRuntimeOptions(flowNodes),
|
|
422
|
+
);
|
|
415
423
|
await this.fileSystem.writeFile(
|
|
416
424
|
operatePath,
|
|
417
|
-
`${JSON.stringify(
|
|
425
|
+
`${JSON.stringify(operateJson, null, 2)}\n`,
|
|
418
426
|
);
|
|
419
427
|
}
|
|
420
428
|
|
|
429
|
+
/**
|
|
430
|
+
* `builtAgents` plus, for each voice node whose agent was not built, the
|
|
431
|
+
* agent.json copied into the content folder. Voice nodes embed the
|
|
432
|
+
* definition into the BPMN, so — unlike text agents — a source missing
|
|
433
|
+
* from the map fails the whole build, not just an entry point.
|
|
434
|
+
* A source with no copied agent.json stays absent;
|
|
435
|
+
* `buildVoiceAgentDefinitions` reports it.
|
|
436
|
+
*/
|
|
437
|
+
private async resolveVoiceAgents(
|
|
438
|
+
flowNodes: VoiceAgentFlowNode[],
|
|
439
|
+
builtAgents: ReadonlyMap<string, Record<string, unknown>>,
|
|
440
|
+
contentFolder: string,
|
|
441
|
+
): Promise<Map<string, Record<string, unknown>>> {
|
|
442
|
+
const voiceAgents = new Map(builtAgents);
|
|
443
|
+
for (const { source } of collectVoiceAgentNodes(flowNodes)) {
|
|
444
|
+
if (voiceAgents.has(source)) continue;
|
|
445
|
+
const copied = await this.readCopiedAgentJson(
|
|
446
|
+
contentFolder,
|
|
447
|
+
source,
|
|
448
|
+
);
|
|
449
|
+
if (!copied) continue;
|
|
450
|
+
voiceAgents.set(source, copied);
|
|
451
|
+
this.logger.warn(
|
|
452
|
+
`Voice agent "${source}" has no root-level agent directory, so it ` +
|
|
453
|
+
"was not built. Its embedded definition and entry point come from " +
|
|
454
|
+
"the agent.json copied into the package. Move the agent to the " +
|
|
455
|
+
"project root to get the full build (migration, validation, " +
|
|
456
|
+
"binding resources).",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return voiceAgents;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Read an `agent.json` copied in without going through `buildAgent`. */
|
|
463
|
+
private async readCopiedAgentJson(
|
|
464
|
+
contentFolder: string,
|
|
465
|
+
source: string,
|
|
466
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
467
|
+
const agentJsonPath = Path.join(contentFolder, source, "agent.json");
|
|
468
|
+
if (!(await this.fileSystem.exists(agentJsonPath))) {
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const raw = await this.fileSystem.readFile(agentJsonPath);
|
|
473
|
+
if (!raw) {
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
const text =
|
|
477
|
+
typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
478
|
+
return JSON.parse(text) as Record<string, unknown>;
|
|
479
|
+
} catch {
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
421
484
|
/**
|
|
422
485
|
* Copy project files into the correct nupkg structure.
|
|
423
486
|
*
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,16 @@ export {
|
|
|
23
23
|
} from "./flow-io.js";
|
|
24
24
|
export { FlowTool } from "./flow-tool.js";
|
|
25
25
|
export { FlowToolFactory } from "./flow-tool-factory.js";
|
|
26
|
-
export {
|
|
26
|
+
export {
|
|
27
|
+
isInlineAgentNodeType,
|
|
28
|
+
readInlineAgentSource,
|
|
29
|
+
setPublishIntentOnInlineAgents,
|
|
30
|
+
} from "./inline-agent-utils.js";
|
|
31
|
+
export { operateRuntimeOptions } from "./operate-runtime-options.js";
|
|
32
|
+
export {
|
|
33
|
+
assertVoiceAgentDefinitionsEmbedded,
|
|
34
|
+
type VoiceAgentFlowNode,
|
|
35
|
+
voiceConvertOptions,
|
|
36
|
+
} from "./voice-agent-definitions.js";
|
|
27
37
|
|
|
28
38
|
toolsFactoryRepository.registerProjectToolFactory(new FlowToolFactory());
|
|
@@ -1,7 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { isAgentNodeType } from "@uipath/flow-schema";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the inline-agent's projectId UUID from a flow node. Falls back to
|
|
5
|
+
* the legacy `model.source` / `agentProjectId` aliases so packaging tolerates
|
|
6
|
+
* already-deployed `.flow` files; `model-source-validator.ts` flags those at
|
|
7
|
+
* validate time.
|
|
8
|
+
*/
|
|
9
|
+
export function readInlineAgentSource(node: {
|
|
10
|
+
inputs?: Record<string, unknown>;
|
|
11
|
+
model?: Record<string, unknown>;
|
|
12
|
+
}): string | undefined {
|
|
13
|
+
const candidates: unknown[] = [
|
|
14
|
+
node.inputs?.source,
|
|
15
|
+
node.model?.source,
|
|
16
|
+
node.inputs?.agentProjectId,
|
|
17
|
+
node.model?.agentProjectId,
|
|
18
|
+
];
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** True for a known inline-agent node type. */
|
|
28
|
+
export function isInlineAgentNodeType(type: string | undefined): boolean {
|
|
29
|
+
return isAgentNodeType(type);
|
|
30
|
+
}
|
|
5
31
|
|
|
6
32
|
/**
|
|
7
33
|
* Set `__packageIntent = "Publish"` on inline agent definitions in a
|
|
@@ -14,7 +40,7 @@ export function setPublishIntentOnInlineAgents(fileFormat: unknown): void {
|
|
|
14
40
|
const ff = fileFormat as Record<string, unknown[]>;
|
|
15
41
|
for (const def of ff.definitions ?? []) {
|
|
16
42
|
const d = def as Record<string, unknown>;
|
|
17
|
-
if (
|
|
43
|
+
if (isInlineAgentNodeType(d.nodeType as string)) {
|
|
18
44
|
const model = (d.model ?? {}) as Record<string, unknown>;
|
|
19
45
|
model.__packageIntent = "Publish";
|
|
20
46
|
d.model = model;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** `runtimeOptions` for a flow's `operate.json`, derived from its nodes. */
|
|
2
|
+
|
|
3
|
+
const CONVERSATION_TRIGGER_NODE_TYPE = "core.trigger.conversation";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A conversation trigger means the flow can be chatted with, so it packs with
|
|
7
|
+
* `isConversational: true`. Nothing else sets it.
|
|
8
|
+
*/
|
|
9
|
+
export function operateRuntimeOptions(
|
|
10
|
+
nodes: ReadonlyArray<{ type?: string }>,
|
|
11
|
+
): { isConversational: true } | undefined {
|
|
12
|
+
return nodes.some((node) => node.type === CONVERSATION_TRIGGER_NODE_TYPE)
|
|
13
|
+
? { isConversational: true }
|
|
14
|
+
: undefined;
|
|
15
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice inline agents ship their built agent definition inside the BPMN
|
|
3
|
+
* (`agentDefinition` on the StartInlineAgentJob activity). The BPMN serializer
|
|
4
|
+
* writes it from the `voiceAgentDefinitions` convert option; a package without
|
|
5
|
+
* it deploys and then drops every call, so
|
|
6
|
+
* {@link assertVoiceAgentDefinitionsEmbedded} fails the build.
|
|
7
|
+
*
|
|
8
|
+
* Callers convert the flow themselves — this module only builds the convert
|
|
9
|
+
* option and checks the result.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ConvertOptions } from "@uipath/flow-converter";
|
|
13
|
+
import { isVoiceAgentNodeType } from "@uipath/flow-schema";
|
|
14
|
+
|
|
15
|
+
import { readInlineAgentSource } from "./inline-agent-utils.js";
|
|
16
|
+
|
|
17
|
+
/** `ConvertOptions` plus the `voiceAgentDefinitions` map the serializer reads. */
|
|
18
|
+
export interface VoiceAwareConvertOptions extends ConvertOptions {
|
|
19
|
+
voiceAgentDefinitions?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VoiceAgentFlowNode {
|
|
23
|
+
id: string;
|
|
24
|
+
type: string;
|
|
25
|
+
display?: Record<string, unknown>;
|
|
26
|
+
inputs?: Record<string, unknown>;
|
|
27
|
+
model?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function nodeLabel(node: VoiceAgentFlowNode): string {
|
|
31
|
+
const label = node.display?.label;
|
|
32
|
+
return typeof label === "string" && label.length > 0 ? label : node.id;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Voice agent nodes with their resolved source UUIDs. */
|
|
36
|
+
export function collectVoiceAgentNodes(
|
|
37
|
+
nodes: VoiceAgentFlowNode[],
|
|
38
|
+
): Array<{ node: VoiceAgentFlowNode; source: string }> {
|
|
39
|
+
const result: Array<{ node: VoiceAgentFlowNode; source: string }> = [];
|
|
40
|
+
for (const node of nodes) {
|
|
41
|
+
if (!isVoiceAgentNodeType(node.type)) continue;
|
|
42
|
+
const source = readInlineAgentSource(node);
|
|
43
|
+
if (!source) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Voice agent node "${nodeLabel(node)}" has no inputs.source. ` +
|
|
46
|
+
"Set it to the UUID of the agent directory inside the flow project.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
result.push({ node, source });
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the `voiceAgentDefinitions` map. `builtAgents` holds each source
|
|
56
|
+
* UUID's built agent.json (post `buildAgent`).
|
|
57
|
+
*/
|
|
58
|
+
export function buildVoiceAgentDefinitions(
|
|
59
|
+
nodes: VoiceAgentFlowNode[],
|
|
60
|
+
builtAgents: ReadonlyMap<string, Record<string, unknown>>,
|
|
61
|
+
): Record<string, string> {
|
|
62
|
+
const definitions: Record<string, string> = {};
|
|
63
|
+
for (const { node, source } of collectVoiceAgentNodes(nodes)) {
|
|
64
|
+
if (definitions[source]) continue;
|
|
65
|
+
const agent = builtAgents.get(source);
|
|
66
|
+
if (!agent) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Missing agent definition for voice agent node "${nodeLabel(node)}" (${source}). ` +
|
|
69
|
+
`Make sure ${source}/agent.json exists in the flow project.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
definitions[source] = JSON.stringify({
|
|
73
|
+
...agent,
|
|
74
|
+
resources: (agent.resources as unknown[]) ?? [],
|
|
75
|
+
features: (agent.features as unknown[]) ?? [],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return definitions;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Only top-level voice nodes get definitions, so a subflow one would ship a
|
|
83
|
+
* serviceTask with no `agentDefinition`. The CLI validate rule rejects the
|
|
84
|
+
* same flow; both go away when the platform supports it.
|
|
85
|
+
*/
|
|
86
|
+
function assertNoSubflowVoiceAgents(flowJson: string): void {
|
|
87
|
+
const workflow = JSON.parse(flowJson) as {
|
|
88
|
+
subflows?: Record<string, { nodes?: VoiceAgentFlowNode[] }>;
|
|
89
|
+
};
|
|
90
|
+
for (const [subflowId, entry] of Object.entries(workflow.subflows ?? {})) {
|
|
91
|
+
for (const node of entry.nodes ?? []) {
|
|
92
|
+
if (!isVoiceAgentNodeType(node.type)) continue;
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Voice agent node "${nodeLabel(node)}" is inside subflow "${subflowId}". ` +
|
|
95
|
+
"Voice agent nodes are not supported inside subflows — move it to the top-level flow.",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The convert options a flow's voice nodes require: the built definitions the
|
|
103
|
+
* serializer embeds, merged onto the caller's own options. Returns
|
|
104
|
+
* `convertOptions` unchanged when the flow has no voice nodes.
|
|
105
|
+
*
|
|
106
|
+
* Throws when a voice node sits in a subflow or its agent is missing — before
|
|
107
|
+
* a BPMN is written, not after.
|
|
108
|
+
*/
|
|
109
|
+
export function voiceConvertOptions(config: {
|
|
110
|
+
/** The resolved file-format workflow, already stringified. */
|
|
111
|
+
flowJson: string;
|
|
112
|
+
/** The workflow's nodes — scanned for voice agent nodes. */
|
|
113
|
+
nodes: VoiceAgentFlowNode[];
|
|
114
|
+
/** Source UUID → built agent.json for every packaged inline agent. */
|
|
115
|
+
builtAgents: ReadonlyMap<string, Record<string, unknown>>;
|
|
116
|
+
/** The caller's own converter options (e.g. `detached` on debug). */
|
|
117
|
+
convertOptions?: ConvertOptions;
|
|
118
|
+
}): ConvertOptions | undefined {
|
|
119
|
+
assertNoSubflowVoiceAgents(config.flowJson);
|
|
120
|
+
const voiceAgentDefinitions = buildVoiceAgentDefinitions(
|
|
121
|
+
config.nodes,
|
|
122
|
+
config.builtAgents,
|
|
123
|
+
);
|
|
124
|
+
if (Object.keys(voiceAgentDefinitions).length === 0) {
|
|
125
|
+
return config.convertOptions;
|
|
126
|
+
}
|
|
127
|
+
const options: VoiceAwareConvertOptions = {
|
|
128
|
+
...config.convertOptions,
|
|
129
|
+
voiceAgentDefinitions,
|
|
130
|
+
};
|
|
131
|
+
return options;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whole-document check: one `agentDefinition` anywhere satisfies it. None at
|
|
136
|
+
* all means the installed converter dropped the option, which drops every
|
|
137
|
+
* node at once. Per-node gaps are the serializer's job — it raises on those.
|
|
138
|
+
*/
|
|
139
|
+
export function assertVoiceAgentDefinitionsEmbedded(
|
|
140
|
+
bpmn: string,
|
|
141
|
+
nodes: VoiceAgentFlowNode[],
|
|
142
|
+
): void {
|
|
143
|
+
const voiceNodes = collectVoiceAgentNodes(nodes);
|
|
144
|
+
if (voiceNodes.length === 0) return;
|
|
145
|
+
if (bpmn.includes('name="agentDefinition"')) return;
|
|
146
|
+
const labels = voiceNodes
|
|
147
|
+
.map(({ node }) => `"${nodeLabel(node)}"`)
|
|
148
|
+
.join(", ");
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Converted BPMN carries no agentDefinition for voice agent node(s) ${labels}. ` +
|
|
151
|
+
"The installed @uipath/flow-converter does not forward voiceAgentDefinitions " +
|
|
152
|
+
"to the BPMN serializer — upgrade it. Packaging a voice flow without the " +
|
|
153
|
+
"embedded agent definition produces a package that deploys but drops every call.",
|
|
154
|
+
);
|
|
155
|
+
}
|