@cognigy/plugin-engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +245 -0
- package/dist/api/client.d.ts +21 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +123 -0
- package/dist/api/client.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +141 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +2 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +36 -0
- package/dist/instructions.js.map +1 -0
- package/dist/schemas/tools.d.ts +1846 -0
- package/dist/schemas/tools.d.ts.map +1 -0
- package/dist/schemas/tools.js +581 -0
- package/dist/schemas/tools.js.map +1 -0
- package/dist/tools/definitions.d.ts +12 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +1522 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/filters.d.ts +13 -0
- package/dist/tools/filters.d.ts.map +1 -0
- package/dist/tools/filters.js +107 -0
- package/dist/tools/filters.js.map +1 -0
- package/dist/tools/handlers.d.ts +71 -0
- package/dist/tools/handlers.d.ts.map +1 -0
- package/dist/tools/handlers.js +3603 -0
- package/dist/tools/handlers.js.map +1 -0
- package/dist/tools/nodeRegistry.d.ts +37 -0
- package/dist/tools/nodeRegistry.d.ts.map +1 -0
- package/dist/tools/nodeRegistry.js +175 -0
- package/dist/tools/nodeRegistry.js.map +1 -0
- package/dist/tools/packageManagement.d.ts +140 -0
- package/dist/tools/packageManagement.d.ts.map +1 -0
- package/dist/tools/packageManagement.js +455 -0
- package/dist/tools/packageManagement.js.map +1 -0
- package/dist/tools/voiceChecklist.d.ts +80 -0
- package/dist/tools/voiceChecklist.d.ts.map +1 -0
- package/dist/tools/voiceChecklist.js +635 -0
- package/dist/tools/voiceChecklist.js.map +1 -0
- package/dist/tools/webchatSettings.d.ts +14 -0
- package/dist/tools/webchatSettings.d.ts.map +1 -0
- package/dist/tools/webchatSettings.js +462 -0
- package/dist/tools/webchatSettings.js.map +1 -0
- package/dist/utils/logger.d.ts +19 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +52 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/rateLimiter.d.ts +33 -0
- package/dist/utils/rateLimiter.d.ts.map +1 -0
- package/dist/utils/rateLimiter.js +68 -0
- package/dist/utils/rateLimiter.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,3603 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, statSync, } from "fs";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { pipeline } from "stream/promises";
|
|
4
|
+
import { basename, dirname, isAbsolute, join } from "path";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { pathToFileURL } from "url";
|
|
7
|
+
import axios from "axios";
|
|
8
|
+
import { logger } from "../utils/logger.js";
|
|
9
|
+
import { filterResponse, filterList, withHints } from "./filters.js";
|
|
10
|
+
import { buildWebchatSettings, deepMerge } from "./webchatSettings.js";
|
|
11
|
+
import { getNodeEntry, supportedNodeTypes } from "./nodeRegistry.js";
|
|
12
|
+
import { evaluateChecks, summarize, nodeId as voiceNodeId, } from "./voiceChecklist.js";
|
|
13
|
+
import * as schemas from "../schemas/tools.js";
|
|
14
|
+
import { buildPackageExportablePreview, buildPackageExportPlan, buildPackageImportPreview, normalizeTask, } from "./packageManagement.js";
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Constants
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const DEFAULT_AGENT_IMAGE = "default-avatar:1";
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
async function retryGetEntryNode(apiClient, flowId, maxRetries = 3, delayMs = 500) {
|
|
23
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
24
|
+
const nodes = await apiClient.get(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
25
|
+
params: { limit: 10 },
|
|
26
|
+
});
|
|
27
|
+
const items = nodes.items ?? nodes;
|
|
28
|
+
const entry = (Array.isArray(items) ? items : []).find((n) => n.isEntryPoint) ??
|
|
29
|
+
(Array.isArray(items) ? items[0] : undefined);
|
|
30
|
+
if (entry)
|
|
31
|
+
return entry;
|
|
32
|
+
if (i < maxRetries - 1)
|
|
33
|
+
await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
|
|
34
|
+
}
|
|
35
|
+
throw new Error("Could not find entry node in flow");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Transform user-friendly config into the exact format the Cognigy API descriptor
|
|
39
|
+
* validator expects. Config keys must match descriptor field keys exactly — extra
|
|
40
|
+
* or unknown keys cause "Node config validation failed".
|
|
41
|
+
*
|
|
42
|
+
* Descriptor field schemas (from shared/charts/descriptors/):
|
|
43
|
+
* say: { say: { text: string[], type: "text", data: "", linear: false, loop: false, _cognigy: {} } }
|
|
44
|
+
* question: { say: { text: string[], ... }, type: "text"|"yesNo"|"email"|... }
|
|
45
|
+
* if: { condition: { type: "rule", rule: { left, operand, right } } }
|
|
46
|
+
* switch: { switch: { type: "intent"|"state"|"cognigyScript", operator: string } }
|
|
47
|
+
* addToContext: { key: string, value: string, mode?: "simple"|"array" }
|
|
48
|
+
* sleep: { milliseconds: number }
|
|
49
|
+
* code: { code: string }
|
|
50
|
+
* httpRequest: { url, type, headers, ... } — keys match descriptor directly
|
|
51
|
+
* goTo: { flowNode: { flow, node }, ... }
|
|
52
|
+
*/
|
|
53
|
+
const SAY_DEFAULTS = {
|
|
54
|
+
data: "",
|
|
55
|
+
linear: false,
|
|
56
|
+
loop: false,
|
|
57
|
+
type: "text",
|
|
58
|
+
_cognigy: {},
|
|
59
|
+
};
|
|
60
|
+
function buildSayObject(text) {
|
|
61
|
+
const textArr = Array.isArray(text)
|
|
62
|
+
? text
|
|
63
|
+
: text != null
|
|
64
|
+
? [String(text)]
|
|
65
|
+
: [];
|
|
66
|
+
return { ...SAY_DEFAULTS, text: textArr };
|
|
67
|
+
}
|
|
68
|
+
function buildRichSayObject(text, outputType, richData) {
|
|
69
|
+
const typeKeyMap = {
|
|
70
|
+
quickReplies: "_quickReplies",
|
|
71
|
+
buttons: "_buttons",
|
|
72
|
+
gallery: "_gallery",
|
|
73
|
+
list: "_list",
|
|
74
|
+
image: "_image",
|
|
75
|
+
video: "_video",
|
|
76
|
+
audio: "_audio",
|
|
77
|
+
adaptiveCard: "_adaptiveCard",
|
|
78
|
+
};
|
|
79
|
+
const dataKey = typeKeyMap[outputType];
|
|
80
|
+
if (!dataKey)
|
|
81
|
+
return buildSayObject(text);
|
|
82
|
+
const textArr = Array.isArray(text)
|
|
83
|
+
? text
|
|
84
|
+
: text != null
|
|
85
|
+
? [String(text)]
|
|
86
|
+
: [];
|
|
87
|
+
const textStr = textArr.length > 0 ? textArr[0] : "";
|
|
88
|
+
let richPayload;
|
|
89
|
+
if (outputType === "quickReplies") {
|
|
90
|
+
const qrs = (Array.isArray(richData) ? richData : []).map((qr, i) => ({
|
|
91
|
+
id: qr.id ?? i + 1,
|
|
92
|
+
title: qr.title ?? "",
|
|
93
|
+
payload: qr.payload ?? "",
|
|
94
|
+
contentType: qr.contentType ?? "postback",
|
|
95
|
+
imageUrl: qr.imageUrl ?? "",
|
|
96
|
+
imageAltText: qr.imageAltText ?? "",
|
|
97
|
+
condition: qr.condition ?? "",
|
|
98
|
+
}));
|
|
99
|
+
richPayload = { type: "quick_replies", text: textStr, quickReplies: qrs };
|
|
100
|
+
}
|
|
101
|
+
else if (outputType === "buttons") {
|
|
102
|
+
const btns = (Array.isArray(richData) ? richData : []).map((btn, i) => ({
|
|
103
|
+
id: btn.id ?? i + 1,
|
|
104
|
+
type: btn.type ?? "postback",
|
|
105
|
+
title: btn.title ?? "",
|
|
106
|
+
payload: btn.payload ?? "",
|
|
107
|
+
url: btn.url ?? "",
|
|
108
|
+
...(btn.condition ? { condition: btn.condition } : {}),
|
|
109
|
+
}));
|
|
110
|
+
richPayload = { type: "buttons", text: textStr, buttons: btns };
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
richPayload =
|
|
114
|
+
typeof richData === "object"
|
|
115
|
+
? { ...richData, text: textStr }
|
|
116
|
+
: { text: textStr };
|
|
117
|
+
}
|
|
118
|
+
const channelData = { [dataKey]: richPayload };
|
|
119
|
+
return {
|
|
120
|
+
...SAY_DEFAULTS,
|
|
121
|
+
type: outputType,
|
|
122
|
+
text: textArr,
|
|
123
|
+
_cognigy: { _default: channelData },
|
|
124
|
+
_data: { _cognigy: { _default: channelData } },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function transformConfigForApi(nodeType, config) {
|
|
128
|
+
if (!config || Object.keys(config).length === 0)
|
|
129
|
+
return config;
|
|
130
|
+
switch (nodeType) {
|
|
131
|
+
case "say": {
|
|
132
|
+
if (config.say && typeof config.say === "object")
|
|
133
|
+
return config;
|
|
134
|
+
const { text, quickReplies, buttons, gallery, list, image, video, audio, adaptiveCard, ...rest } = config;
|
|
135
|
+
const richTypeMap = [
|
|
136
|
+
["quickReplies", quickReplies],
|
|
137
|
+
["buttons", buttons],
|
|
138
|
+
["gallery", gallery],
|
|
139
|
+
["list", list],
|
|
140
|
+
["image", image],
|
|
141
|
+
["video", video],
|
|
142
|
+
["audio", audio],
|
|
143
|
+
["adaptiveCard", adaptiveCard],
|
|
144
|
+
];
|
|
145
|
+
const activeRich = richTypeMap.find(([, val]) => val !== undefined);
|
|
146
|
+
if (activeRich) {
|
|
147
|
+
return {
|
|
148
|
+
say: buildRichSayObject(text, activeRich[0], activeRich[1]),
|
|
149
|
+
...rest,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return { say: buildSayObject(text), ...rest };
|
|
153
|
+
}
|
|
154
|
+
case "question": {
|
|
155
|
+
if (config.say && typeof config.say === "object")
|
|
156
|
+
return config;
|
|
157
|
+
const { text, quickReplies, buttons, ...rest } = config;
|
|
158
|
+
const out = { ...rest };
|
|
159
|
+
if (text !== undefined) {
|
|
160
|
+
if (quickReplies) {
|
|
161
|
+
out.say = buildRichSayObject(text, "quickReplies", quickReplies);
|
|
162
|
+
}
|
|
163
|
+
else if (buttons) {
|
|
164
|
+
out.say = buildRichSayObject(text, "buttons", buttons);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
out.say = buildSayObject(text);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
case "if": {
|
|
173
|
+
const cond = config.condition;
|
|
174
|
+
if (typeof cond === "string") {
|
|
175
|
+
return {
|
|
176
|
+
condition: {
|
|
177
|
+
condition: cond,
|
|
178
|
+
type: "condition",
|
|
179
|
+
rule: { left: "1", operand: "eq", right: "1" },
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (typeof cond === "object" && cond !== null) {
|
|
184
|
+
if (!cond.type)
|
|
185
|
+
cond.type = "condition";
|
|
186
|
+
if (!cond.rule) {
|
|
187
|
+
cond.rule = { left: "1", operand: "eq", right: "1" };
|
|
188
|
+
}
|
|
189
|
+
return { condition: cond };
|
|
190
|
+
}
|
|
191
|
+
return config;
|
|
192
|
+
}
|
|
193
|
+
case "switch": {
|
|
194
|
+
if (config.switch && typeof config.switch === "object")
|
|
195
|
+
return config;
|
|
196
|
+
const lookupType = config.type ?? "intent";
|
|
197
|
+
const operatorMap = {
|
|
198
|
+
intent: "ci.intent",
|
|
199
|
+
state: "ci.state",
|
|
200
|
+
type: "ci.type",
|
|
201
|
+
cognigyScript: config.condition ?? "",
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
switch: {
|
|
205
|
+
type: lookupType,
|
|
206
|
+
operator: operatorMap[lookupType] ?? lookupType,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
case "sleep": {
|
|
211
|
+
if (config.milliseconds !== undefined)
|
|
212
|
+
return config;
|
|
213
|
+
if (config.delay !== undefined)
|
|
214
|
+
return { milliseconds: config.delay };
|
|
215
|
+
return config;
|
|
216
|
+
}
|
|
217
|
+
case "addToContext": {
|
|
218
|
+
if (config.key !== undefined)
|
|
219
|
+
return config;
|
|
220
|
+
if (Array.isArray(config.contextEntries) &&
|
|
221
|
+
config.contextEntries.length > 0) {
|
|
222
|
+
return {
|
|
223
|
+
key: config.contextEntries[0].key,
|
|
224
|
+
value: config.contextEntries[0].value,
|
|
225
|
+
mode: "simple",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return config;
|
|
229
|
+
}
|
|
230
|
+
case "goTo": {
|
|
231
|
+
if (config.flowNode)
|
|
232
|
+
return config;
|
|
233
|
+
const { flowId: targetFlowId, nodeId: targetNodeId, mode: goToMode, ...rest } = config;
|
|
234
|
+
if (targetFlowId || targetNodeId) {
|
|
235
|
+
const baseConfig = {
|
|
236
|
+
flowNode: { flow: targetFlowId ?? "", node: targetNodeId ?? "" },
|
|
237
|
+
...rest,
|
|
238
|
+
};
|
|
239
|
+
if (goToMode !== undefined) {
|
|
240
|
+
return { ...baseConfig, executionMode: goToMode };
|
|
241
|
+
}
|
|
242
|
+
return baseConfig;
|
|
243
|
+
}
|
|
244
|
+
return config;
|
|
245
|
+
}
|
|
246
|
+
case "httpRequest": {
|
|
247
|
+
const out = { ...config };
|
|
248
|
+
if (out.headers &&
|
|
249
|
+
typeof out.headers === "object" &&
|
|
250
|
+
!Array.isArray(out.headers)) {
|
|
251
|
+
out.headers = JSON.stringify(out.headers);
|
|
252
|
+
}
|
|
253
|
+
if (out.contextStore !== undefined) {
|
|
254
|
+
out.storeLocation = "context";
|
|
255
|
+
out.contextKey = out.contextStore;
|
|
256
|
+
delete out.contextStore;
|
|
257
|
+
}
|
|
258
|
+
if (out.inputStore !== undefined) {
|
|
259
|
+
if (!out.storeLocation)
|
|
260
|
+
out.storeLocation = "input";
|
|
261
|
+
out.inputKey = out.inputStore;
|
|
262
|
+
delete out.inputStore;
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
case "case": {
|
|
267
|
+
if (config.case && typeof config.case === "object")
|
|
268
|
+
return config;
|
|
269
|
+
const val = config.value;
|
|
270
|
+
if (val !== undefined) {
|
|
271
|
+
return { case: { value: val } };
|
|
272
|
+
}
|
|
273
|
+
return config;
|
|
274
|
+
}
|
|
275
|
+
default:
|
|
276
|
+
return config;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function identifyFailedStep(agentId, flowId, endpointId) {
|
|
280
|
+
if (!agentId)
|
|
281
|
+
return "agent";
|
|
282
|
+
if (!flowId)
|
|
283
|
+
return "flow";
|
|
284
|
+
if (!endpointId)
|
|
285
|
+
return "endpoint";
|
|
286
|
+
return "node";
|
|
287
|
+
}
|
|
288
|
+
const TOOL_TYPE_MAP = {
|
|
289
|
+
tool: { type: "aiAgentJobTool", extension: "@cognigy/basic-nodes" },
|
|
290
|
+
knowledge: { type: "knowledgeTool", extension: "@cognigy/basic-nodes" },
|
|
291
|
+
send_email: { type: "sendEmailTool", extension: "@cognigy/basic-nodes" },
|
|
292
|
+
mcp: { type: "aiAgentJobMCPTool", extension: "@cognigy/basic-nodes" },
|
|
293
|
+
http: { type: "aiAgentJobTool", extension: "@cognigy/basic-nodes" },
|
|
294
|
+
};
|
|
295
|
+
const RESOLVE_NODE_MAP = {
|
|
296
|
+
tool: { type: "aiAgentToolAnswer", label: "Resolve Tool Action" },
|
|
297
|
+
mcp: { type: "aiAgentJobCallMCPTool", label: "Call MCP Tool" },
|
|
298
|
+
knowledge: null,
|
|
299
|
+
send_email: null,
|
|
300
|
+
http: null, // HTTP handles its own resolve node creation
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Translate user-friendly HTTP fields (method, body, headers-as-object) into
|
|
304
|
+
* the Cognigy httpRequest node descriptor field names (type, payloadType/
|
|
305
|
+
* payloadJSON/payloadText, headers-as-JSON-string).
|
|
306
|
+
*/
|
|
307
|
+
function buildHttpNodeConfig(http) {
|
|
308
|
+
const cfg = {};
|
|
309
|
+
if (http.url)
|
|
310
|
+
cfg.url = http.url;
|
|
311
|
+
if (http.method)
|
|
312
|
+
cfg.type = http.method;
|
|
313
|
+
else if (http.url)
|
|
314
|
+
cfg.type = "GET";
|
|
315
|
+
if (http.headers)
|
|
316
|
+
cfg.headers = JSON.stringify(http.headers);
|
|
317
|
+
if (http.body) {
|
|
318
|
+
try {
|
|
319
|
+
cfg.payloadType = "json";
|
|
320
|
+
cfg.payloadJSON = JSON.parse(http.body);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
cfg.payloadType = "text";
|
|
324
|
+
cfg.payloadText = http.body;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return cfg;
|
|
328
|
+
}
|
|
329
|
+
const AI_AGENT_TOOL_TYPES = new Set([
|
|
330
|
+
"aiAgentJobDefault",
|
|
331
|
+
"aiAgentJobTool",
|
|
332
|
+
"aiAgentJobMCPTool",
|
|
333
|
+
"knowledgeTool",
|
|
334
|
+
"handoverToAiAgentTool",
|
|
335
|
+
"handoverToHumanAgentTool",
|
|
336
|
+
"sendEmailTool",
|
|
337
|
+
"executeWorkflowTool",
|
|
338
|
+
]);
|
|
339
|
+
const MCP_MANAGED_TOOL_TYPES = new Set([
|
|
340
|
+
"aiAgentJobTool",
|
|
341
|
+
"aiAgentJobMCPTool",
|
|
342
|
+
"knowledgeTool",
|
|
343
|
+
"sendEmailTool",
|
|
344
|
+
]);
|
|
345
|
+
const PROVIDER_CONNECTION_TYPE = {
|
|
346
|
+
openAI: "OpenAIProvider",
|
|
347
|
+
azureOpenAI: "AzureOpenAIProviderV2",
|
|
348
|
+
anthropic: "AnthropicProvider",
|
|
349
|
+
google: "GoogleVertexAIProvider",
|
|
350
|
+
mistral: "MistralProvider",
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* Resolve the flow ID for an AI Agent. The Cognigy agent record doesn't store
|
|
354
|
+
* a direct flowId reference, so we try multiple strategies:
|
|
355
|
+
* 1. Direct field on the agent object (future-proofing)
|
|
356
|
+
* 2. GET /v2.0/aiagents/{id}/jobs — returns Job nodes that reference this agent
|
|
357
|
+
* 3. Search project flows for one whose name matches "{agentName} Flow"
|
|
358
|
+
*/
|
|
359
|
+
async function resolveFlowForAgent(apiClient, agentId) {
|
|
360
|
+
const agent = await apiClient.get(`/v2.0/aiagents/${agentId}`);
|
|
361
|
+
// Strategy 1: direct field
|
|
362
|
+
const directId = agent.flowId || agent.flow?._id || agent.flow?.id;
|
|
363
|
+
if (directId)
|
|
364
|
+
return { flowId: directId, agent };
|
|
365
|
+
// Strategy 2: /jobs endpoint — returns nodes referencing this agent
|
|
366
|
+
try {
|
|
367
|
+
const jobs = await apiClient.get(`/v2.0/aiagents/${agentId}/jobs`);
|
|
368
|
+
const items = jobs.items ?? jobs;
|
|
369
|
+
if (Array.isArray(items) && items.length > 0) {
|
|
370
|
+
const flowId = items[0].flowId || items[0].flow?._id || items[0].parentId;
|
|
371
|
+
if (flowId)
|
|
372
|
+
return { flowId, agent };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// endpoint may not exist on all versions — fall through
|
|
377
|
+
}
|
|
378
|
+
// Strategy 3: search project flows by naming convention
|
|
379
|
+
const projectId = agent.projectReference ||
|
|
380
|
+
agent.projectId ||
|
|
381
|
+
agent.project?._id ||
|
|
382
|
+
agent.project?.id;
|
|
383
|
+
if (projectId) {
|
|
384
|
+
try {
|
|
385
|
+
const flows = await apiClient.get("/v2.0/flows", {
|
|
386
|
+
params: { projectId, limit: 100 },
|
|
387
|
+
});
|
|
388
|
+
const flowItems = flows.items ?? flows;
|
|
389
|
+
if (Array.isArray(flowItems)) {
|
|
390
|
+
const match = flowItems.find((f) => f.name === `${agent.name} Flow`);
|
|
391
|
+
if (match)
|
|
392
|
+
return { flowId: match._id || match.id, agent };
|
|
393
|
+
// Last resort: scan all flows for an aiAgentJob node referencing this agent
|
|
394
|
+
for (const f of flowItems) {
|
|
395
|
+
const fid = f._id || f.id;
|
|
396
|
+
try {
|
|
397
|
+
const nodes = await apiClient.get(`/v2.0/flows/${fid}/chart/nodes`, {
|
|
398
|
+
params: { limit: 50 },
|
|
399
|
+
});
|
|
400
|
+
const nodeItems = nodes.items ?? nodes;
|
|
401
|
+
const jobNode = (Array.isArray(nodeItems) ? nodeItems : []).find((n) => n.type === "aiAgentJob" &&
|
|
402
|
+
n.config?.aiAgent === agent.referenceId);
|
|
403
|
+
if (jobNode)
|
|
404
|
+
return { flowId: fid, agent };
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// skip flows we can't read
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// fall through
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// ToolHandlers
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
export class ToolHandlers {
|
|
422
|
+
apiClient;
|
|
423
|
+
endpointBaseUrl;
|
|
424
|
+
webchatBaseUrl;
|
|
425
|
+
staticFilesBaseUrl;
|
|
426
|
+
static SENSITIVE_KEYS = new Set([
|
|
427
|
+
"apiKey",
|
|
428
|
+
"headers",
|
|
429
|
+
"body",
|
|
430
|
+
"preProcessCode",
|
|
431
|
+
"postProcessCode",
|
|
432
|
+
]);
|
|
433
|
+
static DEFAULT_PACKAGE_TIMEOUT_MS = 600000;
|
|
434
|
+
static TASK_POLL_INTERVAL_MS = 3000;
|
|
435
|
+
constructor(apiClient, endpointBaseUrl, webchatBaseUrl = "", staticFilesBaseUrl = "") {
|
|
436
|
+
this.apiClient = apiClient;
|
|
437
|
+
this.endpointBaseUrl = endpointBaseUrl;
|
|
438
|
+
this.webchatBaseUrl = webchatBaseUrl;
|
|
439
|
+
this.staticFilesBaseUrl = staticFilesBaseUrl;
|
|
440
|
+
}
|
|
441
|
+
sanitizeArgs(args) {
|
|
442
|
+
const result = {};
|
|
443
|
+
for (const [key, value] of Object.entries(args)) {
|
|
444
|
+
result[key] = ToolHandlers.SENSITIVE_KEYS.has(key) ? "[REDACTED]" : value;
|
|
445
|
+
}
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
async readTask(taskId, projectId) {
|
|
449
|
+
return this.apiClient.get(`/new/v2.0/tasks/${taskId}`, {
|
|
450
|
+
...(projectId ? { params: { projectId } } : {}),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
async waitForTask(taskId, projectId, timeoutMs = ToolHandlers.DEFAULT_PACKAGE_TIMEOUT_MS) {
|
|
454
|
+
const startedAt = Date.now();
|
|
455
|
+
let task = await this.readTask(taskId, projectId);
|
|
456
|
+
while (task && (task.status === "queued" || task.status === "active")) {
|
|
457
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
458
|
+
return { task, timedOut: true };
|
|
459
|
+
}
|
|
460
|
+
await new Promise((resolve) => setTimeout(resolve, ToolHandlers.TASK_POLL_INTERVAL_MS));
|
|
461
|
+
task = await this.readTask(taskId, projectId);
|
|
462
|
+
}
|
|
463
|
+
if (!task) {
|
|
464
|
+
throw new Error(`Task ${taskId} could not be read`);
|
|
465
|
+
}
|
|
466
|
+
if (task.status === "error") {
|
|
467
|
+
throw new Error(task.failReason || `Task ${taskId} failed`);
|
|
468
|
+
}
|
|
469
|
+
if (task.status === "cancelled" || task.status === "cancelling") {
|
|
470
|
+
throw new Error(`Task ${taskId} was cancelled`);
|
|
471
|
+
}
|
|
472
|
+
if (task.status !== "done") {
|
|
473
|
+
throw new Error(`Task ${taskId} ended with unexpected status "${task.status}"`);
|
|
474
|
+
}
|
|
475
|
+
return { task, timedOut: false };
|
|
476
|
+
}
|
|
477
|
+
resolvePackageFilePath(filePath) {
|
|
478
|
+
const resolvedPath = filePath.startsWith("~")
|
|
479
|
+
? filePath.replace(/^~/, process.env.HOME || "")
|
|
480
|
+
: filePath;
|
|
481
|
+
if (!isAbsolute(resolvedPath)) {
|
|
482
|
+
throw new Error("filePath must be an absolute path to a local .zip file");
|
|
483
|
+
}
|
|
484
|
+
if (!existsSync(resolvedPath)) {
|
|
485
|
+
throw new Error(`File not found: ${resolvedPath}`);
|
|
486
|
+
}
|
|
487
|
+
if (!resolvedPath.toLowerCase().endsWith(".zip")) {
|
|
488
|
+
throw new Error(`Unsupported package file "${resolvedPath}". Only .zip files are supported.`);
|
|
489
|
+
}
|
|
490
|
+
const stats = statSync(resolvedPath);
|
|
491
|
+
if (!stats.isFile()) {
|
|
492
|
+
throw new Error(`Path is not a file: ${resolvedPath}`);
|
|
493
|
+
}
|
|
494
|
+
if (stats.size === 0) {
|
|
495
|
+
throw new Error(`File is empty: ${resolvedPath}`);
|
|
496
|
+
}
|
|
497
|
+
return resolvedPath;
|
|
498
|
+
}
|
|
499
|
+
buildExportPackageName(name) {
|
|
500
|
+
const randomIdentifier = new Date()
|
|
501
|
+
.toISOString()
|
|
502
|
+
.replace(/:/g, "-")
|
|
503
|
+
.slice(0, 19)
|
|
504
|
+
.replace("T", "_");
|
|
505
|
+
return `${name}_${randomIdentifier}`;
|
|
506
|
+
}
|
|
507
|
+
sanitizePackageFileName(name) {
|
|
508
|
+
const cleaned = name
|
|
509
|
+
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-")
|
|
510
|
+
.trim()
|
|
511
|
+
.replace(/\s+/g, " ");
|
|
512
|
+
return cleaned || "export";
|
|
513
|
+
}
|
|
514
|
+
resolvePackageOutputPath(outputPath, suggestedFileName) {
|
|
515
|
+
const resolvedPath = outputPath.startsWith("~")
|
|
516
|
+
? outputPath.replace(/^~/, process.env.HOME || "")
|
|
517
|
+
: outputPath;
|
|
518
|
+
if (!isAbsolute(resolvedPath)) {
|
|
519
|
+
throw new Error("outputPath must be an absolute path to a local file or directory");
|
|
520
|
+
}
|
|
521
|
+
const finalPath = existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()
|
|
522
|
+
? join(resolvedPath, suggestedFileName)
|
|
523
|
+
: resolvedPath.toLowerCase().endsWith(".zip")
|
|
524
|
+
? resolvedPath
|
|
525
|
+
: `${resolvedPath}.zip`;
|
|
526
|
+
mkdirSync(dirname(finalPath), { recursive: true });
|
|
527
|
+
return finalPath;
|
|
528
|
+
}
|
|
529
|
+
buildDefaultPackageOutputPath(suggestedFileName) {
|
|
530
|
+
const exportDir = join(tmpdir(), "cognigy-mcp-packages");
|
|
531
|
+
mkdirSync(exportDir, { recursive: true });
|
|
532
|
+
return join(exportDir, `${randomUUID()}-${suggestedFileName}`);
|
|
533
|
+
}
|
|
534
|
+
describeSavedPackageLocation(finalPath, usedDefaultOutputPath) {
|
|
535
|
+
const savedDirectory = dirname(finalPath);
|
|
536
|
+
return {
|
|
537
|
+
savedTo: finalPath,
|
|
538
|
+
savedToUri: pathToFileURL(finalPath).href,
|
|
539
|
+
savedFileName: basename(finalPath),
|
|
540
|
+
savedDirectory,
|
|
541
|
+
savedDirectoryUri: pathToFileURL(savedDirectory).href,
|
|
542
|
+
openArchiveUri: pathToFileURL(finalPath).href,
|
|
543
|
+
openContainingFolderPath: savedDirectory,
|
|
544
|
+
openContainingFolderUri: pathToFileURL(savedDirectory).href,
|
|
545
|
+
...(usedDefaultOutputPath
|
|
546
|
+
? {
|
|
547
|
+
savedToTemp: true,
|
|
548
|
+
note: "The package download URL requires authentication, so the archive was saved locally instead of returning a raw link.",
|
|
549
|
+
}
|
|
550
|
+
: {}),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async getPackageExportGraph(projectId) {
|
|
554
|
+
return this.apiClient.get(`/new/v2.0/projects/${projectId}/graph`, {
|
|
555
|
+
params: {
|
|
556
|
+
packages: false,
|
|
557
|
+
dependencies: true,
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
async downloadPackageArchive(args) {
|
|
562
|
+
const packageData = await this.apiClient.get(`/new/v2.0/packages/${args.packageId}`);
|
|
563
|
+
const packageName = packageData?.name || "export";
|
|
564
|
+
const suggestedFileName = `${this.sanitizePackageFileName(packageName)}.zip`;
|
|
565
|
+
const downloadResponse = await this.apiClient.post(`/new/v2.0/packages/${args.packageId}/downloadlink`);
|
|
566
|
+
const downloadLink = downloadResponse?.downloadLink;
|
|
567
|
+
if (!downloadLink) {
|
|
568
|
+
throw new Error("Package download link could not be created");
|
|
569
|
+
}
|
|
570
|
+
const usedDefaultOutputPath = !args.outputPath;
|
|
571
|
+
const finalPath = args.outputPath
|
|
572
|
+
? this.resolvePackageOutputPath(args.outputPath, suggestedFileName)
|
|
573
|
+
: this.buildDefaultPackageOutputPath(suggestedFileName);
|
|
574
|
+
const downloadStream = await this.apiClient.get(downloadLink, {
|
|
575
|
+
baseURL: undefined,
|
|
576
|
+
headers: { Accept: "*/*" },
|
|
577
|
+
responseType: "stream",
|
|
578
|
+
});
|
|
579
|
+
await pipeline(downloadStream, createWriteStream(finalPath));
|
|
580
|
+
return {
|
|
581
|
+
package: {
|
|
582
|
+
id: args.packageId,
|
|
583
|
+
name: packageName,
|
|
584
|
+
},
|
|
585
|
+
suggestedFileName,
|
|
586
|
+
...this.describeSavedPackageLocation(finalPath, usedDefaultOutputPath),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
async getPackagePreview(projectId, packageId) {
|
|
590
|
+
const graph = await this.apiClient.get(`/new/v2.0/projects/${projectId}/graph`, {
|
|
591
|
+
params: { packages: true },
|
|
592
|
+
});
|
|
593
|
+
return buildPackageImportPreview(projectId, packageId, {
|
|
594
|
+
[projectId]: graph?.[projectId],
|
|
595
|
+
[packageId]: graph?.[packageId],
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
buildImportPayload(preview, data) {
|
|
599
|
+
const previewResourceMap = new Map(preview.resources.map((resource) => [resource.id, resource]));
|
|
600
|
+
const requestedSelections = new Map((data.resources ?? []).map((resource) => [resource.id, resource]));
|
|
601
|
+
const mergedSelections = preview.resources.map((resource) => {
|
|
602
|
+
const requested = requestedSelections.get(resource.id);
|
|
603
|
+
const shouldImport = requested?.import ?? resource.selectedByDefault;
|
|
604
|
+
const strategy = requested?.strategy ?? resource.defaultStrategy;
|
|
605
|
+
if (resource.disabledReason && shouldImport) {
|
|
606
|
+
throw new Error(`Resource ${resource.id} (${resource.name}) cannot be imported: ${resource.disabledReason}`);
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
id: resource.id,
|
|
610
|
+
type: resource.type,
|
|
611
|
+
import: shouldImport,
|
|
612
|
+
strategy,
|
|
613
|
+
};
|
|
614
|
+
});
|
|
615
|
+
for (const requested of data.resources ?? []) {
|
|
616
|
+
if (!previewResourceMap.has(requested.id)) {
|
|
617
|
+
throw new Error(`Resource ${requested.id} is not present in the package preview`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const selectedResources = mergedSelections.filter((resource) => resource.import);
|
|
621
|
+
if (selectedResources.length === 0) {
|
|
622
|
+
throw new Error("At least one package resource must be selected for import");
|
|
623
|
+
}
|
|
624
|
+
const requiresLocaleMapping = selectedResources.some((resource) => resource.type === "flow") &&
|
|
625
|
+
preview.locales.packageLocales.length > 0;
|
|
626
|
+
const packageLocaleIds = new Set(preview.locales.packageLocales.map((locale) => locale.id));
|
|
627
|
+
const agentLocaleIds = new Set(preview.locales.projectLocales.map((locale) => locale.id));
|
|
628
|
+
const localeMapping = data.localeMapping ?? preview.locales.defaultLocaleMapping;
|
|
629
|
+
for (const mapping of localeMapping) {
|
|
630
|
+
if (!packageLocaleIds.has(mapping.packageLocaleId)) {
|
|
631
|
+
throw new Error(`Unknown packageLocaleId in localeMapping: ${mapping.packageLocaleId}`);
|
|
632
|
+
}
|
|
633
|
+
if (!agentLocaleIds.has(mapping.agentLocaleId)) {
|
|
634
|
+
throw new Error(`Unknown agentLocaleId in localeMapping: ${mapping.agentLocaleId}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const mappedAgentLocaleIds = new Set();
|
|
638
|
+
for (const mapping of localeMapping) {
|
|
639
|
+
if (mappedAgentLocaleIds.has(mapping.agentLocaleId)) {
|
|
640
|
+
throw new Error(`Duplicate locale mapping target: ${mapping.agentLocaleId}`);
|
|
641
|
+
}
|
|
642
|
+
mappedAgentLocaleIds.add(mapping.agentLocaleId);
|
|
643
|
+
}
|
|
644
|
+
if (requiresLocaleMapping && localeMapping.length === 0) {
|
|
645
|
+
throw new Error("localeMapping is required when importing flows from a package with locales");
|
|
646
|
+
}
|
|
647
|
+
const primaryPackageLocale = preview.locales.packageLocales.find((locale) => locale.isPrimary);
|
|
648
|
+
if (requiresLocaleMapping && primaryPackageLocale) {
|
|
649
|
+
const hasPrimaryMapping = localeMapping.some((mapping) => mapping.packageLocaleId === primaryPackageLocale.id);
|
|
650
|
+
if (!hasPrimaryMapping) {
|
|
651
|
+
throw new Error("The primary package locale must be mapped before importing flows");
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
resourceIds: selectedResources.map((resource) => resource.id),
|
|
656
|
+
strategies: selectedResources.map((resource) => ({
|
|
657
|
+
_id: resource.id,
|
|
658
|
+
autoRename: true,
|
|
659
|
+
identityConflictStrategy: resource.strategy,
|
|
660
|
+
})),
|
|
661
|
+
localeMapping,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
// =========================================================================
|
|
665
|
+
// Tool 13: manage_packages
|
|
666
|
+
// =========================================================================
|
|
667
|
+
async handleManagePackages(args) {
|
|
668
|
+
const data = schemas.managePackagesSchema.parse(args);
|
|
669
|
+
switch (data.operation) {
|
|
670
|
+
case "list_exportable": {
|
|
671
|
+
const graph = await this.getPackageExportGraph(data.projectId);
|
|
672
|
+
return {
|
|
673
|
+
operation: "list_exportable",
|
|
674
|
+
projectId: data.projectId,
|
|
675
|
+
...buildPackageExportablePreview(data.projectId, {
|
|
676
|
+
[data.projectId]: graph?.[data.projectId],
|
|
677
|
+
}),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
case "upload_and_inspect": {
|
|
681
|
+
const timeoutMs = data.timeoutMs ?? ToolHandlers.DEFAULT_PACKAGE_TIMEOUT_MS;
|
|
682
|
+
const resolvedPath = this.resolvePackageFilePath(data.filePath);
|
|
683
|
+
const fileName = basename(resolvedPath);
|
|
684
|
+
const uploadResponse = await this.apiClient.uploadFile("/new/v2.0/packages/upload", createReadStream(resolvedPath), fileName, { projectId: data.projectId }, { timeoutMs });
|
|
685
|
+
const taskId = uploadResponse?._id ?? uploadResponse?.id;
|
|
686
|
+
if (!taskId) {
|
|
687
|
+
throw new Error("Package upload did not return a task ID");
|
|
688
|
+
}
|
|
689
|
+
const { task, timedOut } = await this.waitForTask(taskId, data.projectId, timeoutMs);
|
|
690
|
+
const normalizedTask = normalizeTask(task);
|
|
691
|
+
if (timedOut ||
|
|
692
|
+
normalizedTask.status !== "done" ||
|
|
693
|
+
!normalizedTask.data?.packageId) {
|
|
694
|
+
return withHints({
|
|
695
|
+
operation: "upload_and_inspect",
|
|
696
|
+
projectId: data.projectId,
|
|
697
|
+
uploadTaskId: taskId,
|
|
698
|
+
task: normalizedTask,
|
|
699
|
+
timedOutWaiting: timedOut,
|
|
700
|
+
}, {
|
|
701
|
+
warning: "Package upload succeeded, but extraction is still running.",
|
|
702
|
+
action: `Use manage_packages { operation: "read_task", projectId: "${data.projectId}", taskId: "${taskId}" } until the task is done, then call inspect with the packageId.`,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
const preview = await this.getPackagePreview(data.projectId, normalizedTask.data.packageId);
|
|
706
|
+
return {
|
|
707
|
+
operation: "upload_and_inspect",
|
|
708
|
+
projectId: data.projectId,
|
|
709
|
+
uploadTaskId: taskId,
|
|
710
|
+
task: normalizedTask,
|
|
711
|
+
...preview,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
case "inspect": {
|
|
715
|
+
const preview = await this.getPackagePreview(data.projectId, data.packageId);
|
|
716
|
+
return {
|
|
717
|
+
operation: "inspect",
|
|
718
|
+
projectId: data.projectId,
|
|
719
|
+
...preview,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
case "import": {
|
|
723
|
+
const timeoutMs = data.timeoutMs ?? ToolHandlers.DEFAULT_PACKAGE_TIMEOUT_MS;
|
|
724
|
+
const preview = await this.getPackagePreview(data.projectId, data.packageId);
|
|
725
|
+
const payload = this.buildImportPayload(preview, data);
|
|
726
|
+
const response = await this.apiClient.post(`/new/v2.0/packages/${data.packageId}/merge`, payload);
|
|
727
|
+
const taskId = response?._id ?? response?.id;
|
|
728
|
+
if (!taskId) {
|
|
729
|
+
throw new Error("Package import did not return a task ID");
|
|
730
|
+
}
|
|
731
|
+
if (data.waitForCompletion === false) {
|
|
732
|
+
return {
|
|
733
|
+
operation: "import",
|
|
734
|
+
projectId: data.projectId,
|
|
735
|
+
packageId: data.packageId,
|
|
736
|
+
task: {
|
|
737
|
+
id: taskId,
|
|
738
|
+
name: "mergePackage",
|
|
739
|
+
status: "queued",
|
|
740
|
+
currentStep: 0,
|
|
741
|
+
totalStep: 0,
|
|
742
|
+
progress: 0,
|
|
743
|
+
failReason: null,
|
|
744
|
+
data: null,
|
|
745
|
+
},
|
|
746
|
+
selectedResourceCount: payload.resourceIds.length,
|
|
747
|
+
localeMappingCount: payload.localeMapping.length,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const { task, timedOut } = await this.waitForTask(taskId, data.projectId, timeoutMs);
|
|
751
|
+
const normalizedTask = normalizeTask(task);
|
|
752
|
+
const result = {
|
|
753
|
+
operation: "import",
|
|
754
|
+
projectId: data.projectId,
|
|
755
|
+
packageId: data.packageId,
|
|
756
|
+
task: normalizedTask,
|
|
757
|
+
selectedResourceCount: payload.resourceIds.length,
|
|
758
|
+
localeMappingCount: payload.localeMapping.length,
|
|
759
|
+
...(timedOut ? { timedOutWaiting: true } : {}),
|
|
760
|
+
};
|
|
761
|
+
if (timedOut) {
|
|
762
|
+
return withHints(result, {
|
|
763
|
+
warning: "Package import is still running.",
|
|
764
|
+
action: `Use manage_packages { operation: "read_task", projectId: "${data.projectId}", taskId: "${taskId}" } to continue polling the import task.`,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
return result;
|
|
768
|
+
}
|
|
769
|
+
case "export": {
|
|
770
|
+
const timeoutMs = data.timeoutMs ?? ToolHandlers.DEFAULT_PACKAGE_TIMEOUT_MS;
|
|
771
|
+
const graph = await this.getPackageExportGraph(data.projectId);
|
|
772
|
+
const exportPlan = buildPackageExportPlan(data.projectId, {
|
|
773
|
+
[data.projectId]: graph?.[data.projectId],
|
|
774
|
+
}, data.resourceIds, {
|
|
775
|
+
includeDependencies: data.includeDependencies,
|
|
776
|
+
dependencyResourceIds: data.dependencyResourceIds,
|
|
777
|
+
});
|
|
778
|
+
const packageName = this.buildExportPackageName(data.name);
|
|
779
|
+
const response = await this.apiClient.post("/new/v2.0/packages", {
|
|
780
|
+
projectId: data.projectId,
|
|
781
|
+
name: packageName,
|
|
782
|
+
description: data.description,
|
|
783
|
+
resourceIds: exportPlan.resourceIds,
|
|
784
|
+
});
|
|
785
|
+
const taskId = response?._id ?? response?.id;
|
|
786
|
+
if (!taskId) {
|
|
787
|
+
throw new Error("Package export did not return a task ID");
|
|
788
|
+
}
|
|
789
|
+
if (data.waitForCompletion === false) {
|
|
790
|
+
return {
|
|
791
|
+
operation: "export",
|
|
792
|
+
projectId: data.projectId,
|
|
793
|
+
packageName,
|
|
794
|
+
task: {
|
|
795
|
+
id: taskId,
|
|
796
|
+
name: "createPackageNFS",
|
|
797
|
+
status: "queued",
|
|
798
|
+
currentStep: 0,
|
|
799
|
+
totalStep: 0,
|
|
800
|
+
progress: 0,
|
|
801
|
+
failReason: null,
|
|
802
|
+
data: null,
|
|
803
|
+
},
|
|
804
|
+
...exportPlan,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const { task, timedOut } = await this.waitForTask(taskId, data.projectId, timeoutMs);
|
|
808
|
+
const normalizedTask = normalizeTask(task);
|
|
809
|
+
const result = {
|
|
810
|
+
operation: "export",
|
|
811
|
+
projectId: data.projectId,
|
|
812
|
+
packageName,
|
|
813
|
+
task: normalizedTask,
|
|
814
|
+
...exportPlan,
|
|
815
|
+
...(timedOut ? { timedOutWaiting: true } : {}),
|
|
816
|
+
};
|
|
817
|
+
if (timedOut) {
|
|
818
|
+
return withHints(result, {
|
|
819
|
+
warning: "Package export is still running.",
|
|
820
|
+
action: `Use manage_packages { operation: "read_task", projectId: "${data.projectId}", taskId: "${taskId}" } to continue polling the export task.`,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
const packageId = normalizedTask.data?.packageId;
|
|
824
|
+
if (packageId) {
|
|
825
|
+
Object.assign(result, await this.downloadPackageArchive({
|
|
826
|
+
projectId: data.projectId,
|
|
827
|
+
packageId,
|
|
828
|
+
outputPath: data.outputPath,
|
|
829
|
+
}));
|
|
830
|
+
}
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
case "download": {
|
|
834
|
+
return {
|
|
835
|
+
operation: "download",
|
|
836
|
+
projectId: data.projectId,
|
|
837
|
+
...(await this.downloadPackageArchive({
|
|
838
|
+
projectId: data.projectId,
|
|
839
|
+
packageId: data.packageId,
|
|
840
|
+
outputPath: data.outputPath,
|
|
841
|
+
})),
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
case "read_task": {
|
|
845
|
+
const task = await this.readTask(data.taskId, data.projectId);
|
|
846
|
+
return {
|
|
847
|
+
operation: "read_task",
|
|
848
|
+
projectId: data.projectId,
|
|
849
|
+
task: normalizeTask(task),
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// =========================================================================
|
|
855
|
+
// Tool 1: create_ai_agent
|
|
856
|
+
// =========================================================================
|
|
857
|
+
async handleCreateAiAgent(args) {
|
|
858
|
+
const data = schemas.createAiAgentSchema.parse(args);
|
|
859
|
+
let projectId = data.projectId ?? null;
|
|
860
|
+
let createdProject = false;
|
|
861
|
+
let agentId = null;
|
|
862
|
+
let flowId = null;
|
|
863
|
+
let endpointId = null;
|
|
864
|
+
try {
|
|
865
|
+
// Step 0: Auto-create project if none provided
|
|
866
|
+
if (!projectId) {
|
|
867
|
+
const project = await this.apiClient.post("/v2.0/projects", {
|
|
868
|
+
name: data.name,
|
|
869
|
+
color: "blue",
|
|
870
|
+
locale: "en-US",
|
|
871
|
+
});
|
|
872
|
+
projectId = project._id || project.id;
|
|
873
|
+
createdProject = true;
|
|
874
|
+
}
|
|
875
|
+
// Step 1: Create agent resource
|
|
876
|
+
const agentPayload = {
|
|
877
|
+
projectId,
|
|
878
|
+
name: data.name,
|
|
879
|
+
image: DEFAULT_AGENT_IMAGE,
|
|
880
|
+
imageOptimizedFormat: true,
|
|
881
|
+
};
|
|
882
|
+
if (data.description)
|
|
883
|
+
agentPayload.description = data.description;
|
|
884
|
+
const agent = await this.apiClient.post("/v2.0/aiagents", agentPayload);
|
|
885
|
+
agentId = agent._id || agent.id;
|
|
886
|
+
// Step 2: Create flow
|
|
887
|
+
const flow = await this.apiClient.post("/v2.0/flows", {
|
|
888
|
+
projectId,
|
|
889
|
+
name: `${data.name} Flow`,
|
|
890
|
+
description: `Auto-generated flow for ${data.name}`,
|
|
891
|
+
});
|
|
892
|
+
flowId = flow._id || flow.id;
|
|
893
|
+
// Step 3: Find entry node (with retry)
|
|
894
|
+
const entryNode = await retryGetEntryNode(this.apiClient, flowId);
|
|
895
|
+
// Step 4: Create AI Agent Job Node
|
|
896
|
+
const jobNode = await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
897
|
+
mode: "append",
|
|
898
|
+
target: entryNode._id,
|
|
899
|
+
type: "aiAgentJob",
|
|
900
|
+
extension: "@cognigy/basic-nodes",
|
|
901
|
+
label: "AI Agent",
|
|
902
|
+
config: {
|
|
903
|
+
aiAgent: agent.referenceId,
|
|
904
|
+
outputImmediately: true,
|
|
905
|
+
},
|
|
906
|
+
});
|
|
907
|
+
const jobNodeId = jobNode._id || jobNode.id;
|
|
908
|
+
// Step 4a: Auto-assign default LLM to the job node so talk_to_agent works
|
|
909
|
+
// immediately without a separate update_ai_agent call.
|
|
910
|
+
//
|
|
911
|
+
// The node preview (agent avatar + name) is computed server-side from
|
|
912
|
+
// `config.aiAgent`. A config PATCH that omits `aiAgent` makes the backend
|
|
913
|
+
// recompute the preview as a bare string (the job name), wiping the
|
|
914
|
+
// avatar. So we always re-send `aiAgent` alongside any config change to
|
|
915
|
+
// force the backend to regenerate the proper avatar preview object.
|
|
916
|
+
let llmAutoAssigned = false;
|
|
917
|
+
try {
|
|
918
|
+
const llmList = await this.apiClient.get("/v2.0/largelanguagemodels", {
|
|
919
|
+
params: { projectId },
|
|
920
|
+
});
|
|
921
|
+
const llmItems = llmList.items ?? llmList;
|
|
922
|
+
if (Array.isArray(llmItems) && llmItems.length > 0) {
|
|
923
|
+
const defaultLlm = llmItems.find((l) => l.isDefault) ?? llmItems[0];
|
|
924
|
+
const llmRefId = defaultLlm.referenceId ?? defaultLlm._id;
|
|
925
|
+
if (llmRefId) {
|
|
926
|
+
await this.apiClient.patch(`/v2.0/flows/${flowId}/chart/nodes/${jobNodeId}`, {
|
|
927
|
+
config: {
|
|
928
|
+
aiAgent: agent.referenceId,
|
|
929
|
+
llmProviderReferenceId: llmRefId,
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
llmAutoAssigned = true;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
catch (llmErr) {
|
|
937
|
+
logger.warn("Failed to auto-assign LLM to job node — agent may need manual LLM assignment", { error: llmErr.message });
|
|
938
|
+
}
|
|
939
|
+
// Step 4d: Remove backend-created placeholder child tools that are only
|
|
940
|
+
// used for UI preview and should not exist in Cognigy MCP flows.
|
|
941
|
+
try {
|
|
942
|
+
let placeholderTools = [];
|
|
943
|
+
try {
|
|
944
|
+
const chart = await this.apiClient.get(`/new/v2.0/flows/${flowId}/chart`, (flow?.localeReference ?? flow?.localeId)
|
|
945
|
+
? {
|
|
946
|
+
params: {
|
|
947
|
+
preferredLocaleId: flow.localeReference ?? flow.localeId,
|
|
948
|
+
},
|
|
949
|
+
}
|
|
950
|
+
: undefined);
|
|
951
|
+
const chartNodes = chart.nodes ?? [];
|
|
952
|
+
const chartRelations = chart.relations ?? [];
|
|
953
|
+
const jobRelation = (Array.isArray(chartRelations) ? chartRelations : []).find((relation) => relation.node === jobNodeId);
|
|
954
|
+
const childNodeIds = new Set(jobRelation?.children ?? []);
|
|
955
|
+
placeholderTools = (Array.isArray(chartNodes) ? chartNodes : []).filter((n) => childNodeIds.has(n._id || n.id) && n.preview === "unlock_account");
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
// Fall back to the regular node list when the chart endpoint is not available.
|
|
959
|
+
}
|
|
960
|
+
if (placeholderTools.length === 0) {
|
|
961
|
+
const nodeList = await this.apiClient.get(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
962
|
+
params: { limit: 200 },
|
|
963
|
+
});
|
|
964
|
+
const nodeItems = nodeList.items ?? nodeList;
|
|
965
|
+
placeholderTools = (Array.isArray(nodeItems) ? nodeItems : []).filter((n) => (n.parentId === jobNodeId || n.parent === jobNodeId) &&
|
|
966
|
+
(n.label === "unlock_account" ||
|
|
967
|
+
n.config?.toolId === "unlock_account"));
|
|
968
|
+
}
|
|
969
|
+
for (const placeholderTool of placeholderTools) {
|
|
970
|
+
const placeholderToolId = placeholderTool._id || placeholderTool.id;
|
|
971
|
+
if (!placeholderToolId)
|
|
972
|
+
continue;
|
|
973
|
+
await this.apiClient.delete(`/v2.0/flows/${flowId}/chart/nodes/${placeholderToolId}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
catch (placeholderCleanupError) {
|
|
977
|
+
logger.warn("Failed to remove backend-created placeholder tool from agent flow", { error: placeholderCleanupError.message });
|
|
978
|
+
}
|
|
979
|
+
// Step 4e: If knowledge store provided, create a knowledge tool on the job node
|
|
980
|
+
let knowledgeToolId = null;
|
|
981
|
+
if (data.knowledgeStoreReferenceId) {
|
|
982
|
+
try {
|
|
983
|
+
const knowledgeToolNode = await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
984
|
+
type: "knowledgeTool",
|
|
985
|
+
extension: "@cognigy/basic-nodes",
|
|
986
|
+
mode: "appendChild",
|
|
987
|
+
target: jobNodeId,
|
|
988
|
+
label: "Search Knowledge",
|
|
989
|
+
config: {
|
|
990
|
+
knowledgeStoreId: data.knowledgeStoreReferenceId,
|
|
991
|
+
toolId: "search_knowledge",
|
|
992
|
+
description: "Search the knowledge base for relevant information",
|
|
993
|
+
},
|
|
994
|
+
});
|
|
995
|
+
knowledgeToolId = knowledgeToolNode._id || knowledgeToolNode.id;
|
|
996
|
+
}
|
|
997
|
+
catch (knowledgeError) {
|
|
998
|
+
logger.warn("Failed to create knowledge tool — agent was created without it", { error: knowledgeError.message });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// Step 5: Create REST endpoint
|
|
1002
|
+
const endpoint = await this.apiClient.post("/v2.0/endpoints", {
|
|
1003
|
+
projectId,
|
|
1004
|
+
channel: "rest",
|
|
1005
|
+
flowId: flow.referenceId,
|
|
1006
|
+
name: `${data.name} REST Endpoint`,
|
|
1007
|
+
});
|
|
1008
|
+
endpointId = endpoint._id || endpoint.id;
|
|
1009
|
+
// Step 6: LLM status — derived from the auto-assign attempt in Step 4a
|
|
1010
|
+
// If auto-assignment succeeded, we know the LLM is configured.
|
|
1011
|
+
// If it did not, we cannot reliably distinguish "no LLM" from "error",
|
|
1012
|
+
// so we report "unknown" instead of incorrectly claiming "missing".
|
|
1013
|
+
const llmStatus = llmAutoAssigned
|
|
1014
|
+
? "configured"
|
|
1015
|
+
: "unknown";
|
|
1016
|
+
const result = {
|
|
1017
|
+
projectId,
|
|
1018
|
+
projectCreated: createdProject,
|
|
1019
|
+
agent: filterResponse("agent", agent),
|
|
1020
|
+
flow: filterResponse("flow", flow),
|
|
1021
|
+
endpoint: filterResponse("endpoint", endpoint),
|
|
1022
|
+
endpointUrl: endpoint.URLToken
|
|
1023
|
+
? `${this.endpointBaseUrl}/${endpoint.URLToken}`
|
|
1024
|
+
: "URL not available",
|
|
1025
|
+
llmStatus,
|
|
1026
|
+
};
|
|
1027
|
+
if (knowledgeToolId) {
|
|
1028
|
+
result.knowledgeTool = {
|
|
1029
|
+
toolId: knowledgeToolId,
|
|
1030
|
+
knowledgeStoreReferenceId: data.knowledgeStoreReferenceId,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
if (data.knowledgeStoreReferenceId && !knowledgeToolId) {
|
|
1034
|
+
return withHints(result, {
|
|
1035
|
+
warning: "Agent created but knowledge tool failed to provision.",
|
|
1036
|
+
action: `Create it manually: create_tool { aiAgentId: "${agentId}", toolType: "knowledge", name: "Search Knowledge", config: { knowledgeStoreId: "${data.knowledgeStoreReferenceId}", toolId: "search_knowledge", description: "Search the knowledge base" } }`,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
if (llmStatus === "unknown") {
|
|
1040
|
+
const nextAction = createdProject
|
|
1041
|
+
? `A new project was auto-created as "${projectId}". Immediately inspect the other projects with list_resources { resourceType: "project" } and list_resources { resourceType: "llm_model", projectId } for each one. Choose only source-project llm_model entries with a non-empty connectionId, transfer the required LLM resources plus their shared connection resource(s) via manage_packages export/upload_and_inspect/import, verify the import with list_resources { resourceType: "llm_model", projectId: "${projectId}" }, and do not call talk_to_agent until the import is confirmed. If this workflow will use knowledge, transfer the source project's embedding model and exact Knowledge Search model together before calling manage_settings. Only use setup_llm if no reusable LLM with connectionId exists or package transfer fails.`
|
|
1042
|
+
: `Inspect the other projects with list_resources { resourceType: "project" } and list_resources { resourceType: "llm_model", projectId } for each one. Choose only source-project llm_model entries with a non-empty connectionId, transfer the required LLM resources plus their shared connection resource(s) via manage_packages export/upload_and_inspect/import, verify the import with list_resources { resourceType: "llm_model", projectId: "${projectId}" }, and do not call talk_to_agent until the import is confirmed. If this workflow will use knowledge, transfer the source project's embedding model and exact Knowledge Search model together before calling manage_settings. Only use setup_llm if no reusable LLM with connectionId exists or package transfer fails.`;
|
|
1043
|
+
return withHints(result, {
|
|
1044
|
+
warning: "Could not verify LLM resource in project. Agent may not generate responses.",
|
|
1045
|
+
action: nextAction,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
return result;
|
|
1049
|
+
}
|
|
1050
|
+
catch (error) {
|
|
1051
|
+
const rolledBack = [];
|
|
1052
|
+
const rollbackFailed = [];
|
|
1053
|
+
if (endpointId) {
|
|
1054
|
+
try {
|
|
1055
|
+
await this.apiClient.delete(`/v2.0/endpoints/${endpointId}`);
|
|
1056
|
+
rolledBack.push("endpoint");
|
|
1057
|
+
}
|
|
1058
|
+
catch {
|
|
1059
|
+
rollbackFailed.push("endpoint");
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (flowId) {
|
|
1063
|
+
try {
|
|
1064
|
+
await this.apiClient.delete(`/v2.0/flows/${flowId}`);
|
|
1065
|
+
rolledBack.push("flow");
|
|
1066
|
+
}
|
|
1067
|
+
catch {
|
|
1068
|
+
rollbackFailed.push("flow");
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (agentId) {
|
|
1072
|
+
try {
|
|
1073
|
+
await this.apiClient.delete(`/v2.0/aiagents/${agentId}`);
|
|
1074
|
+
rolledBack.push("agent");
|
|
1075
|
+
}
|
|
1076
|
+
catch {
|
|
1077
|
+
rollbackFailed.push("agent");
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (createdProject && projectId) {
|
|
1081
|
+
try {
|
|
1082
|
+
await this.apiClient.delete(`/v2.0/projects/${projectId}`);
|
|
1083
|
+
rolledBack.push("project");
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
rollbackFailed.push("project");
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const likelyCause = rollbackFailed.length > 0
|
|
1090
|
+
? `Orchestration failed. Rolled back: [${rolledBack.join(", ")}]. FAILED to roll back: [${rollbackFailed.join(", ")}] — these are orphaned and should be deleted manually.`
|
|
1091
|
+
: "Orchestration failed. All created resources were rolled back.";
|
|
1092
|
+
const action = rollbackFailed.length > 0
|
|
1093
|
+
? `Delete orphaned resources with delete_resource, then retry create_ai_agent.`
|
|
1094
|
+
: "Read the troubleshooting guide, then retry create_ai_agent.";
|
|
1095
|
+
return withHints({
|
|
1096
|
+
failed: {
|
|
1097
|
+
step: identifyFailedStep(agentId, flowId, endpointId),
|
|
1098
|
+
error: error.message,
|
|
1099
|
+
},
|
|
1100
|
+
}, {
|
|
1101
|
+
likely_cause: likelyCause,
|
|
1102
|
+
action,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
// =========================================================================
|
|
1107
|
+
// Tool 2: update_ai_agent
|
|
1108
|
+
// =========================================================================
|
|
1109
|
+
async handleUpdateAiAgent(args) {
|
|
1110
|
+
const { aiAgentId, jobConfig, ...rest } = schemas.updateAiAgentSchema.parse(args);
|
|
1111
|
+
const updatedParts = [];
|
|
1112
|
+
// Step 1: Patch AI Agent resource if any agent-level fields provided
|
|
1113
|
+
const agentPayload = {};
|
|
1114
|
+
if (rest.name !== undefined)
|
|
1115
|
+
agentPayload.name = rest.name;
|
|
1116
|
+
if (rest.description !== undefined)
|
|
1117
|
+
agentPayload.description = rest.description;
|
|
1118
|
+
if (rest.instructions !== undefined)
|
|
1119
|
+
agentPayload.instructions = rest.instructions;
|
|
1120
|
+
let agentResult;
|
|
1121
|
+
if (Object.keys(agentPayload).length > 0) {
|
|
1122
|
+
agentResult = await this.apiClient.patch(`/v2.0/aiagents/${aiAgentId}`, agentPayload);
|
|
1123
|
+
updatedParts.push("agent");
|
|
1124
|
+
}
|
|
1125
|
+
// Step 2: Patch AI Agent Job Node config if any job-level fields provided
|
|
1126
|
+
const needsJobPatch = jobConfig && Object.keys(jobConfig).length > 0;
|
|
1127
|
+
const needsPreviewPatch = rest.name !== undefined || jobConfig?.jobName !== undefined;
|
|
1128
|
+
let jobNodeResult;
|
|
1129
|
+
if (needsJobPatch || needsPreviewPatch) {
|
|
1130
|
+
const resolved = await resolveFlowForAgent(this.apiClient, aiAgentId);
|
|
1131
|
+
if (!resolved) {
|
|
1132
|
+
if (needsJobPatch) {
|
|
1133
|
+
return withHints({
|
|
1134
|
+
error: "Could not find a flow associated with this agent. Job config was not updated.",
|
|
1135
|
+
}, {
|
|
1136
|
+
action: "Ensure the agent was created via create_ai_agent, which provisions the flow and Job Node.",
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
const nodes = await this.apiClient.get(`/v2.0/flows/${resolved.flowId}/chart/nodes`, {
|
|
1142
|
+
params: { limit: 100 },
|
|
1143
|
+
});
|
|
1144
|
+
const allNodes = nodes.items ?? nodes;
|
|
1145
|
+
const jobNode = (Array.isArray(allNodes) ? allNodes : []).find((n) => n.type === "aiAgentJob");
|
|
1146
|
+
if (!jobNode) {
|
|
1147
|
+
if (needsJobPatch) {
|
|
1148
|
+
return withHints({
|
|
1149
|
+
error: "No AI Agent Job Node found in the flow. Job config was not updated.",
|
|
1150
|
+
}, {
|
|
1151
|
+
action: "Ensure the agent was created via create_ai_agent.",
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
else {
|
|
1156
|
+
const jobNodeId = jobNode._id || jobNode.id;
|
|
1157
|
+
const nodePatch = {};
|
|
1158
|
+
// The node's avatar preview is computed server-side from
|
|
1159
|
+
// `config.aiAgent`. Any config PATCH that omits it makes the backend
|
|
1160
|
+
// recompute the preview as a bare string (the job name), which wipes
|
|
1161
|
+
// the avatar image in the flow editor. So we always include the
|
|
1162
|
+
// existing `aiAgent` reference in the config patch — both when
|
|
1163
|
+
// changing job fields and when only the agent name/avatar changed —
|
|
1164
|
+
// to force the backend to regenerate the proper avatar preview.
|
|
1165
|
+
if (needsJobPatch || needsPreviewPatch) {
|
|
1166
|
+
// The avatar preview is recomputed server-side from `config.aiAgent`;
|
|
1167
|
+
// omitting it wipes the avatar. The `/chart/nodes` index may not
|
|
1168
|
+
// carry `config`, so prefer the authoritative agent reference from
|
|
1169
|
+
// the resolved agent record, falling back to the node's config.
|
|
1170
|
+
const nodeConfigPatch = {
|
|
1171
|
+
aiAgent: resolved.agent?.referenceId ?? jobNode.config?.aiAgent,
|
|
1172
|
+
};
|
|
1173
|
+
if (needsJobPatch) {
|
|
1174
|
+
if (jobConfig.llmProviderReferenceId !== undefined)
|
|
1175
|
+
nodeConfigPatch.llmProviderReferenceId =
|
|
1176
|
+
jobConfig.llmProviderReferenceId;
|
|
1177
|
+
if (jobConfig.jobName !== undefined)
|
|
1178
|
+
nodeConfigPatch.name = jobConfig.jobName;
|
|
1179
|
+
if (jobConfig.jobDescription !== undefined)
|
|
1180
|
+
nodeConfigPatch.description = jobConfig.jobDescription;
|
|
1181
|
+
if (jobConfig.jobInstructions !== undefined)
|
|
1182
|
+
nodeConfigPatch.instructions = jobConfig.jobInstructions;
|
|
1183
|
+
if (jobConfig.temperature !== undefined)
|
|
1184
|
+
nodeConfigPatch.temperature = jobConfig.temperature;
|
|
1185
|
+
if (jobConfig.maxTokens !== undefined)
|
|
1186
|
+
nodeConfigPatch.maxTokens = jobConfig.maxTokens;
|
|
1187
|
+
}
|
|
1188
|
+
nodePatch.config = nodeConfigPatch;
|
|
1189
|
+
}
|
|
1190
|
+
jobNodeResult = await this.apiClient.patch(`/v2.0/flows/${resolved.flowId}/chart/nodes/${jobNodeId}`, nodePatch);
|
|
1191
|
+
updatedParts.push("jobNode");
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (updatedParts.length === 0) {
|
|
1196
|
+
return withHints({
|
|
1197
|
+
error: "Nothing to update. Provide agent-level fields (name, description, instructions) and/or jobConfig fields.",
|
|
1198
|
+
}, { action: "Include at least one field to update." });
|
|
1199
|
+
}
|
|
1200
|
+
// Build response from what was updated
|
|
1201
|
+
const response = { updated: updatedParts };
|
|
1202
|
+
if (agentResult) {
|
|
1203
|
+
Object.assign(response, filterResponse("agent", agentResult));
|
|
1204
|
+
}
|
|
1205
|
+
if (jobNodeResult) {
|
|
1206
|
+
const jobNodeResponse = {
|
|
1207
|
+
id: jobNodeResult._id || jobNodeResult.id,
|
|
1208
|
+
};
|
|
1209
|
+
if (jobConfig && Object.keys(jobConfig).length > 0)
|
|
1210
|
+
jobNodeResponse.configUpdated = Object.keys(jobConfig);
|
|
1211
|
+
if (needsPreviewPatch)
|
|
1212
|
+
jobNodeResponse.previewUpdated = true;
|
|
1213
|
+
response.jobNode = jobNodeResponse;
|
|
1214
|
+
}
|
|
1215
|
+
return response;
|
|
1216
|
+
}
|
|
1217
|
+
// =========================================================================
|
|
1218
|
+
// Tool 3: setup_llm
|
|
1219
|
+
// =========================================================================
|
|
1220
|
+
async handleSetupLlm(args) {
|
|
1221
|
+
const data = schemas.setupLlmSchema.parse(args);
|
|
1222
|
+
if (!data.apiKey && !data.connectionId) {
|
|
1223
|
+
return withHints({ error: "Either apiKey or connectionId must be provided." }, {
|
|
1224
|
+
action: "Read the provider guide for credential requirements.",
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
let connectionRefId = data.connectionId;
|
|
1228
|
+
if (connectionRefId) {
|
|
1229
|
+
try {
|
|
1230
|
+
const connections = await this.apiClient.get("/new/v2.0/connections", {
|
|
1231
|
+
params: { projectId: data.projectId },
|
|
1232
|
+
});
|
|
1233
|
+
const items = connections?.items ?? connections;
|
|
1234
|
+
const match = (Array.isArray(items) ? items : []).find((connection) => connection.referenceId === connectionRefId ||
|
|
1235
|
+
connection._id === connectionRefId ||
|
|
1236
|
+
connection.id === connectionRefId);
|
|
1237
|
+
if (!match) {
|
|
1238
|
+
return withHints({
|
|
1239
|
+
error: "The provided connectionId was not found in the target project. Cognigy connections are project-scoped and cannot be reused across projects directly.",
|
|
1240
|
+
connectionId: connectionRefId,
|
|
1241
|
+
projectId: data.projectId,
|
|
1242
|
+
}, {
|
|
1243
|
+
action: "Import the LLM and its connection into the target project with manage_packages, or provide an apiKey / same-project connectionId.",
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
connectionRefId =
|
|
1247
|
+
match.referenceId ?? match._id ?? match.id ?? connectionRefId;
|
|
1248
|
+
}
|
|
1249
|
+
catch (connectionLookupError) {
|
|
1250
|
+
return withHints({
|
|
1251
|
+
error: `Could not verify the provided connectionId in the target project: ${connectionLookupError.message}`,
|
|
1252
|
+
connectionId: connectionRefId,
|
|
1253
|
+
projectId: data.projectId,
|
|
1254
|
+
}, {
|
|
1255
|
+
action: "Verify the connection exists in the target project, or import it together with the LLM via manage_packages before retrying.",
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
// If apiKey is provided, auto-create a Connection first
|
|
1260
|
+
if (data.apiKey && !connectionRefId) {
|
|
1261
|
+
try {
|
|
1262
|
+
const connection = await this.apiClient.post("/v2.0/connections", {
|
|
1263
|
+
projectId: data.projectId,
|
|
1264
|
+
name: `${data.provider} - auto`,
|
|
1265
|
+
type: PROVIDER_CONNECTION_TYPE[data.provider] ?? data.provider,
|
|
1266
|
+
extension: "@cognigy/generative-ai-provider",
|
|
1267
|
+
fields: { apiKey: data.apiKey },
|
|
1268
|
+
});
|
|
1269
|
+
connectionRefId =
|
|
1270
|
+
connection.referenceId || connection._id || connection.id;
|
|
1271
|
+
}
|
|
1272
|
+
catch (connError) {
|
|
1273
|
+
return withHints({ error: `Failed to create connection: ${connError.message}` }, {
|
|
1274
|
+
action: "Check API key and provider, then retry.",
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const displayName = data.name || data.modelType;
|
|
1279
|
+
let result;
|
|
1280
|
+
try {
|
|
1281
|
+
result = await this.apiClient.post("/v2.0/largelanguagemodels", {
|
|
1282
|
+
projectId: data.projectId,
|
|
1283
|
+
name: displayName,
|
|
1284
|
+
modelType: data.modelType,
|
|
1285
|
+
provider: data.provider,
|
|
1286
|
+
connectionId: connectionRefId,
|
|
1287
|
+
isDefault: data.isDefault ?? true,
|
|
1288
|
+
[data.provider]: {},
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
catch (error) {
|
|
1292
|
+
return withHints({ error: error.message }, {
|
|
1293
|
+
action: "Read the provider guide for valid provider names and model strings.",
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
const llmId = result._id || result.id;
|
|
1297
|
+
if (data.dangerouslySkipConnectionTest) {
|
|
1298
|
+
const filtered = filterResponse("llm_model", result);
|
|
1299
|
+
return {
|
|
1300
|
+
...filtered,
|
|
1301
|
+
warning: "Connection test was skipped. The model may not work correctly — verify manually before use.",
|
|
1302
|
+
connectionTest: { skipped: true },
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
if (!llmId) {
|
|
1306
|
+
logger.error("LLM creation response did not include an id; unable to run connection test or cleanup.", {
|
|
1307
|
+
provider: data.provider,
|
|
1308
|
+
modelType: data.modelType,
|
|
1309
|
+
rawResult: result,
|
|
1310
|
+
});
|
|
1311
|
+
return withHints({
|
|
1312
|
+
error: "Model may have been created but the API response did not include a model id. " +
|
|
1313
|
+
"Connection test and automatic cleanup could not be performed. Please verify the model state in the UI and delete it manually if necessary.",
|
|
1314
|
+
provider: data.provider,
|
|
1315
|
+
modelType: data.modelType,
|
|
1316
|
+
}, {
|
|
1317
|
+
action: "Verify your provider setup and model configuration, then retry.",
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
try {
|
|
1321
|
+
const testResponse = await this.apiClient.post(`/v2.0/largelanguagemodels/${llmId}/test`);
|
|
1322
|
+
if (!testResponse?.isCredentialsValid) {
|
|
1323
|
+
let cleanedUp = false;
|
|
1324
|
+
try {
|
|
1325
|
+
if (result.isDefault) {
|
|
1326
|
+
try {
|
|
1327
|
+
await this.apiClient.patch(`/v2.0/largelanguagemodels/${llmId}`, {
|
|
1328
|
+
isDefault: false,
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
catch (unsetDefaultError) {
|
|
1332
|
+
logger.warn("Failed to unset default flag before deleting broken LLM model", {
|
|
1333
|
+
llmId,
|
|
1334
|
+
error: unsetDefaultError.message,
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
await this.apiClient.delete(`/v2.0/largelanguagemodels/${llmId}`);
|
|
1339
|
+
cleanedUp = true;
|
|
1340
|
+
}
|
|
1341
|
+
catch (cleanupError) {
|
|
1342
|
+
logger.warn("Failed to clean up broken LLM model after failed connection test", {
|
|
1343
|
+
llmId,
|
|
1344
|
+
error: cleanupError.message,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
return withHints({
|
|
1348
|
+
error: `Model created but connection test failed${cleanedUp ? " — the model has been removed to prevent broken references" : " — automatic cleanup failed, delete the model manually"}.`,
|
|
1349
|
+
providerMessage: testResponse?.msg || "No details from provider.",
|
|
1350
|
+
provider: data.provider,
|
|
1351
|
+
modelType: data.modelType,
|
|
1352
|
+
...(cleanedUp ? {} : { modelId: llmId }),
|
|
1353
|
+
}, {
|
|
1354
|
+
action: "Verify your API key and model type are correct, then retry.",
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
const filtered = filterResponse("llm_model", result);
|
|
1358
|
+
return {
|
|
1359
|
+
...filtered,
|
|
1360
|
+
connectionTest: {
|
|
1361
|
+
isCredentialsValid: true,
|
|
1362
|
+
msg: testResponse.msg,
|
|
1363
|
+
},
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
catch (testError) {
|
|
1367
|
+
logger.warn("Connection test request failed, but model was created successfully", {
|
|
1368
|
+
llmId,
|
|
1369
|
+
error: testError.message,
|
|
1370
|
+
});
|
|
1371
|
+
const filtered = filterResponse("llm_model", result);
|
|
1372
|
+
return withHints({
|
|
1373
|
+
...filtered,
|
|
1374
|
+
warning: `Model created but the connection test could not be executed: ${testError.message}. Verify the model works before relying on it.`,
|
|
1375
|
+
connectionTest: { skipped: true, reason: testError.message },
|
|
1376
|
+
}, {
|
|
1377
|
+
action: "Test the model manually or delete and recreate if credentials are wrong.",
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
// =========================================================================
|
|
1382
|
+
// Tool 4: talk_to_agent
|
|
1383
|
+
// =========================================================================
|
|
1384
|
+
async handleTalkToAgent(args) {
|
|
1385
|
+
const data = schemas.talkToAgentSchema.parse(args);
|
|
1386
|
+
const sessionId = data.sessionId || `mcp-session-${randomUUID()}`;
|
|
1387
|
+
const userId = data.userId || "mcp-user";
|
|
1388
|
+
// --- Endpoint resolution ---
|
|
1389
|
+
let endpointUrl = data.endpointUrl;
|
|
1390
|
+
let endpointMeta = {};
|
|
1391
|
+
if (!endpointUrl && data.aiAgentId) {
|
|
1392
|
+
const resolved = await resolveFlowForAgent(this.apiClient, data.aiAgentId);
|
|
1393
|
+
if (!resolved) {
|
|
1394
|
+
return withHints({
|
|
1395
|
+
error: "Could not find a flow associated with this agent.",
|
|
1396
|
+
sessionId,
|
|
1397
|
+
}, {
|
|
1398
|
+
likely_cause: "Agent may not have been created via create_ai_agent, or has no associated flow.",
|
|
1399
|
+
action: "Create the agent with create_ai_agent, or provide endpointUrl directly.",
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
const { flowId, agent } = resolved;
|
|
1403
|
+
let flowReferenceId = null;
|
|
1404
|
+
let flowProjectReference = null;
|
|
1405
|
+
try {
|
|
1406
|
+
const flowObj = await this.apiClient.get(`/v2.0/flows/${flowId}`);
|
|
1407
|
+
flowReferenceId = flowObj.referenceId ?? null;
|
|
1408
|
+
flowProjectReference = flowObj.projectReference ?? null;
|
|
1409
|
+
}
|
|
1410
|
+
catch {
|
|
1411
|
+
// fall through — we'll still match on flowId
|
|
1412
|
+
}
|
|
1413
|
+
const projectId = data.projectId ||
|
|
1414
|
+
agent.projectReference ||
|
|
1415
|
+
agent.projectId ||
|
|
1416
|
+
agent.project?._id ||
|
|
1417
|
+
agent.project?.id ||
|
|
1418
|
+
flowProjectReference;
|
|
1419
|
+
if (!projectId) {
|
|
1420
|
+
return withHints({
|
|
1421
|
+
error: "Could not determine projectId for this agent.",
|
|
1422
|
+
sessionId,
|
|
1423
|
+
}, { action: "Provide projectId explicitly alongside aiAgentId." });
|
|
1424
|
+
}
|
|
1425
|
+
// Search for an existing REST endpoint connected to this flow
|
|
1426
|
+
let existingEndpoint = null;
|
|
1427
|
+
const pageSize = 100;
|
|
1428
|
+
let offset = 0;
|
|
1429
|
+
let hasMore = true;
|
|
1430
|
+
while (hasMore && !existingEndpoint) {
|
|
1431
|
+
const eps = await this.apiClient.get("/v2.0/endpoints", {
|
|
1432
|
+
params: { projectId, limit: pageSize, skip: offset },
|
|
1433
|
+
});
|
|
1434
|
+
const epItems = eps.items ?? eps;
|
|
1435
|
+
if (!Array.isArray(epItems) || epItems.length === 0)
|
|
1436
|
+
break;
|
|
1437
|
+
existingEndpoint = epItems.find((ep) => ep.channel === "rest" &&
|
|
1438
|
+
(ep.flowId === flowId || ep.flowId === flowReferenceId));
|
|
1439
|
+
hasMore = epItems.length >= pageSize;
|
|
1440
|
+
offset += pageSize;
|
|
1441
|
+
}
|
|
1442
|
+
if (existingEndpoint) {
|
|
1443
|
+
endpointUrl = existingEndpoint.URLToken
|
|
1444
|
+
? `${this.endpointBaseUrl}/${existingEndpoint.URLToken}`
|
|
1445
|
+
: undefined;
|
|
1446
|
+
endpointMeta = {
|
|
1447
|
+
resolved: true,
|
|
1448
|
+
endpointId: existingEndpoint._id || existingEndpoint.id,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
else {
|
|
1452
|
+
// Auto-create REST endpoint
|
|
1453
|
+
try {
|
|
1454
|
+
const flowRef = flowReferenceId || flowId;
|
|
1455
|
+
const endpoint = await this.apiClient.post("/v2.0/endpoints", {
|
|
1456
|
+
projectId,
|
|
1457
|
+
channel: "rest",
|
|
1458
|
+
flowId: flowRef,
|
|
1459
|
+
name: `${agent.name} REST Endpoint`,
|
|
1460
|
+
});
|
|
1461
|
+
endpointUrl = endpoint.URLToken
|
|
1462
|
+
? `${this.endpointBaseUrl}/${endpoint.URLToken}`
|
|
1463
|
+
: undefined;
|
|
1464
|
+
endpointMeta = {
|
|
1465
|
+
autoCreated: true,
|
|
1466
|
+
endpointId: endpoint._id || endpoint.id,
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
catch (createErr) {
|
|
1470
|
+
return withHints({
|
|
1471
|
+
error: "Failed to auto-create REST endpoint for agent.",
|
|
1472
|
+
detail: createErr.response?.data?.error || createErr.message,
|
|
1473
|
+
sessionId,
|
|
1474
|
+
}, {
|
|
1475
|
+
likely_cause: "Insufficient permissions or project configuration issue.",
|
|
1476
|
+
action: "Create endpoint manually via create_ai_agent or the Cognigy UI, then provide endpointUrl.",
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (!endpointUrl) {
|
|
1481
|
+
return withHints({
|
|
1482
|
+
error: "Endpoint found/created but URL token not available.",
|
|
1483
|
+
sessionId,
|
|
1484
|
+
}, {
|
|
1485
|
+
action: "Try list_resources { resourceType: 'endpoint', projectId } to check endpoint status.",
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
// --- Message sending ---
|
|
1490
|
+
const payload = { userId, sessionId, text: data.message };
|
|
1491
|
+
if (data.data)
|
|
1492
|
+
payload.data = data.data;
|
|
1493
|
+
try {
|
|
1494
|
+
const response = await axios.post(endpointUrl, payload, {
|
|
1495
|
+
headers: {
|
|
1496
|
+
"Content-Type": "application/json",
|
|
1497
|
+
Accept: "application/json",
|
|
1498
|
+
},
|
|
1499
|
+
timeout: 30000,
|
|
1500
|
+
});
|
|
1501
|
+
let agentResponse = response.data.text || "";
|
|
1502
|
+
const outputStack = response.data.outputStack || [];
|
|
1503
|
+
const textOutputs = outputStack
|
|
1504
|
+
.filter((o) => o.text?.trim())
|
|
1505
|
+
.map((o) => o.text);
|
|
1506
|
+
if (textOutputs.length > 0)
|
|
1507
|
+
agentResponse = textOutputs.join(" ");
|
|
1508
|
+
const result = { agentResponse, sessionId, endpointUrl };
|
|
1509
|
+
if (endpointMeta.autoCreated)
|
|
1510
|
+
result.endpointAutoCreated = true;
|
|
1511
|
+
if (endpointMeta.resolved)
|
|
1512
|
+
result.endpointResolved = true;
|
|
1513
|
+
if (data.verbose) {
|
|
1514
|
+
result.rawResponse = response.data;
|
|
1515
|
+
}
|
|
1516
|
+
if (!agentResponse) {
|
|
1517
|
+
return withHints(result, {
|
|
1518
|
+
likely_cause: "Agent returned no text. Possible causes: 1) no LLM configured, 2) empty agent description, 3) endpoint not connected to flow.",
|
|
1519
|
+
action: "Read the troubleshooting guide for diagnostic steps.",
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
return result;
|
|
1523
|
+
}
|
|
1524
|
+
catch (error) {
|
|
1525
|
+
const detail = error.response?.data?.error ||
|
|
1526
|
+
error.response?.data?.message ||
|
|
1527
|
+
error.message;
|
|
1528
|
+
return withHints({
|
|
1529
|
+
error: `Request failed with status ${error.response?.status ?? "unknown"}`,
|
|
1530
|
+
detail,
|
|
1531
|
+
sessionId,
|
|
1532
|
+
}, {
|
|
1533
|
+
likely_cause: "Endpoint URL invalid or expired.",
|
|
1534
|
+
action: "Verify endpoint with list_resources { resourceType: 'endpoint' }.",
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
// =========================================================================
|
|
1539
|
+
// Tool 5: list_resources
|
|
1540
|
+
// =========================================================================
|
|
1541
|
+
async handleListResources(args) {
|
|
1542
|
+
const data = schemas.listResourcesSchema.parse(args);
|
|
1543
|
+
const { resourceType, projectId, aiAgentId, limit, skip } = data;
|
|
1544
|
+
const paging = { limit: limit ?? 25, skip: skip ?? 0 };
|
|
1545
|
+
// Validate projectId requirement
|
|
1546
|
+
if (resourceType !== "project" && resourceType !== "tool" && !projectId) {
|
|
1547
|
+
return withHints({ error: `projectId is required for resourceType '${resourceType}'.` }, {
|
|
1548
|
+
action: "Use list_resources { resourceType: 'project' } to find projectIds first.",
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
if (resourceType === "tool" && !aiAgentId) {
|
|
1552
|
+
return withHints({ error: "aiAgentId is required for resourceType 'tool'." }, {
|
|
1553
|
+
action: "Use list_resources { resourceType: 'agent', projectId } to find agents first.",
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
let items;
|
|
1557
|
+
let total;
|
|
1558
|
+
switch (resourceType) {
|
|
1559
|
+
case "project": {
|
|
1560
|
+
const res = await this.apiClient.get("/v2.0/projects", {
|
|
1561
|
+
params: paging,
|
|
1562
|
+
});
|
|
1563
|
+
items = res.items ?? res;
|
|
1564
|
+
total = res.total;
|
|
1565
|
+
break;
|
|
1566
|
+
}
|
|
1567
|
+
case "agent": {
|
|
1568
|
+
const res = await this.apiClient.get("/v2.0/aiagents", {
|
|
1569
|
+
params: { projectId, ...paging },
|
|
1570
|
+
});
|
|
1571
|
+
items = res.items ?? res;
|
|
1572
|
+
total = res.total;
|
|
1573
|
+
break;
|
|
1574
|
+
}
|
|
1575
|
+
case "flow": {
|
|
1576
|
+
const res = await this.apiClient.get("/v2.0/flows", {
|
|
1577
|
+
params: { projectId, ...paging },
|
|
1578
|
+
});
|
|
1579
|
+
items = res.items ?? res;
|
|
1580
|
+
total = res.total;
|
|
1581
|
+
break;
|
|
1582
|
+
}
|
|
1583
|
+
case "endpoint": {
|
|
1584
|
+
const res = await this.apiClient.get("/v2.0/endpoints", {
|
|
1585
|
+
params: { projectId, ...paging },
|
|
1586
|
+
});
|
|
1587
|
+
items = res.items ?? res;
|
|
1588
|
+
total = res.total;
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
case "llm_model": {
|
|
1592
|
+
const endpoint = data.useCase
|
|
1593
|
+
? "/new/v2.0/largelanguagemodels"
|
|
1594
|
+
: "/v2.0/largelanguagemodels";
|
|
1595
|
+
const res = await this.apiClient.get(endpoint, {
|
|
1596
|
+
params: {
|
|
1597
|
+
projectId,
|
|
1598
|
+
...(data.useCase ? { useCase: data.useCase } : {}),
|
|
1599
|
+
...paging,
|
|
1600
|
+
},
|
|
1601
|
+
});
|
|
1602
|
+
items = res.items ?? res;
|
|
1603
|
+
total = res.total;
|
|
1604
|
+
break;
|
|
1605
|
+
}
|
|
1606
|
+
case "knowledge_store": {
|
|
1607
|
+
const res = await this.apiClient.get("/v2.0/knowledgestores", {
|
|
1608
|
+
params: { projectId, ...paging },
|
|
1609
|
+
});
|
|
1610
|
+
items = res.items ?? res;
|
|
1611
|
+
total = res.total;
|
|
1612
|
+
break;
|
|
1613
|
+
}
|
|
1614
|
+
case "conversation": {
|
|
1615
|
+
const params = { projectId, ...paging };
|
|
1616
|
+
if (data.startDate)
|
|
1617
|
+
params.startDate = data.startDate;
|
|
1618
|
+
if (data.endDate)
|
|
1619
|
+
params.endDate = data.endDate;
|
|
1620
|
+
if (data.channel)
|
|
1621
|
+
params.channel = data.channel;
|
|
1622
|
+
const res = await this.apiClient.get("/v2.0/conversations", {
|
|
1623
|
+
params,
|
|
1624
|
+
});
|
|
1625
|
+
items = res.items ?? res;
|
|
1626
|
+
total = res.total;
|
|
1627
|
+
break;
|
|
1628
|
+
}
|
|
1629
|
+
case "extension": {
|
|
1630
|
+
const res = await this.apiClient.get(`/v2.0/projects/${projectId}/extensions`, {
|
|
1631
|
+
params: paging,
|
|
1632
|
+
});
|
|
1633
|
+
items = res.items ?? res;
|
|
1634
|
+
total = res.total;
|
|
1635
|
+
break;
|
|
1636
|
+
}
|
|
1637
|
+
case "function": {
|
|
1638
|
+
const res = await this.apiClient.get(`/v2.0/projects/${projectId}/functions`, {
|
|
1639
|
+
params: paging,
|
|
1640
|
+
});
|
|
1641
|
+
items = res.items ?? res;
|
|
1642
|
+
total = res.total;
|
|
1643
|
+
break;
|
|
1644
|
+
}
|
|
1645
|
+
case "tool": {
|
|
1646
|
+
const resolved = await resolveFlowForAgent(this.apiClient, aiAgentId);
|
|
1647
|
+
if (!resolved) {
|
|
1648
|
+
return withHints({ error: "Could not find a flow associated with this agent." }, {
|
|
1649
|
+
likely_cause: "Agent was not created via create_ai_agent.",
|
|
1650
|
+
action: "Create the agent with create_ai_agent first.",
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
const agentFlowId = resolved.flowId;
|
|
1654
|
+
const nodes = await this.apiClient.get(`/v2.0/flows/${agentFlowId}/chart/nodes`, {
|
|
1655
|
+
params: { limit: 100 },
|
|
1656
|
+
});
|
|
1657
|
+
const allNodes = nodes.items ?? nodes;
|
|
1658
|
+
items = (Array.isArray(allNodes) ? allNodes : [])
|
|
1659
|
+
.filter((n) => AI_AGENT_TOOL_TYPES.has(n.type))
|
|
1660
|
+
.map((n) => ({
|
|
1661
|
+
toolId: n._id || n.id,
|
|
1662
|
+
name: n.label || n.name,
|
|
1663
|
+
toolType: n.type,
|
|
1664
|
+
description: n.config?.description,
|
|
1665
|
+
...(n.config?.knowledgeStoreId
|
|
1666
|
+
? { knowledgeStoreId: n.config.knowledgeStoreId }
|
|
1667
|
+
: {}),
|
|
1668
|
+
}));
|
|
1669
|
+
total = items.length;
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
default:
|
|
1673
|
+
throw new Error(`Unknown resourceType: ${resourceType}`);
|
|
1674
|
+
}
|
|
1675
|
+
if (!Array.isArray(items))
|
|
1676
|
+
items = [];
|
|
1677
|
+
const filtered = resourceType === "tool" ? items : filterList(resourceType, items);
|
|
1678
|
+
if (resourceType === "endpoint") {
|
|
1679
|
+
filtered.forEach((ep) => {
|
|
1680
|
+
if (ep.URLToken)
|
|
1681
|
+
ep.endpointUrl = `${this.endpointBaseUrl}/${ep.URLToken}`;
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
const result = { items: filtered, total: total ?? filtered.length };
|
|
1685
|
+
if (filtered.length === 0 && resourceType === "agent") {
|
|
1686
|
+
return withHints(result, {
|
|
1687
|
+
hint: "No agents found.",
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
return result;
|
|
1691
|
+
}
|
|
1692
|
+
// =========================================================================
|
|
1693
|
+
// Tool 6: get_resource
|
|
1694
|
+
// =========================================================================
|
|
1695
|
+
async handleGetResource(args) {
|
|
1696
|
+
const data = schemas.getResourceSchema.parse(args);
|
|
1697
|
+
const { resourceType, id, raw } = data;
|
|
1698
|
+
const endpointMap = {
|
|
1699
|
+
agent: `/v2.0/aiagents/${id}`,
|
|
1700
|
+
flow: `/v2.0/flows/${id}`,
|
|
1701
|
+
endpoint: `/v2.0/endpoints/${id}`,
|
|
1702
|
+
project: `/v2.0/projects/${id}`,
|
|
1703
|
+
conversation: `/v2.0/conversations/${id}`,
|
|
1704
|
+
session_state: `/v2.0/sessions/${id}/state`,
|
|
1705
|
+
llm_model: `/v2.0/largelanguagemodels/${id}`,
|
|
1706
|
+
knowledge_store: `/v2.0/knowledgestores/${id}`,
|
|
1707
|
+
extension: `/v2.0/extensions/${id}`,
|
|
1708
|
+
function: `/v2.0/functions/${id}`,
|
|
1709
|
+
};
|
|
1710
|
+
const url = endpointMap[resourceType];
|
|
1711
|
+
if (!url)
|
|
1712
|
+
throw new Error(`Unknown resourceType: ${resourceType}`);
|
|
1713
|
+
const result = await this.apiClient.get(url);
|
|
1714
|
+
if (raw)
|
|
1715
|
+
return result;
|
|
1716
|
+
const filtered = RESOURCE_FILTERS_GET[resourceType]
|
|
1717
|
+
? RESOURCE_FILTERS_GET[resourceType](result)
|
|
1718
|
+
: filterResponse(resourceType, result);
|
|
1719
|
+
if (resourceType === "endpoint" && result.URLToken) {
|
|
1720
|
+
filtered.endpointUrl = `${this.endpointBaseUrl}/${result.URLToken}`;
|
|
1721
|
+
}
|
|
1722
|
+
return filtered;
|
|
1723
|
+
}
|
|
1724
|
+
// =========================================================================
|
|
1725
|
+
// Tool 7: delete_resource
|
|
1726
|
+
// =========================================================================
|
|
1727
|
+
async handleDeleteResource(args) {
|
|
1728
|
+
const data = schemas.deleteResourceSchema.parse(args);
|
|
1729
|
+
const { resourceType, id, aiAgentId, cascade } = data;
|
|
1730
|
+
if (resourceType === "tool") {
|
|
1731
|
+
if (!aiAgentId) {
|
|
1732
|
+
return withHints({ error: "aiAgentId is required for resourceType 'tool'." }, {
|
|
1733
|
+
action: "Provide aiAgentId so the handler can resolve the agent's flow.",
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
const resolved = await resolveFlowForAgent(this.apiClient, aiAgentId);
|
|
1737
|
+
if (!resolved) {
|
|
1738
|
+
return withHints({ error: "Could not find a flow associated with this agent." }, {
|
|
1739
|
+
action: "Ensure agent was created via create_ai_agent.",
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
await this.apiClient.delete(`/v2.0/flows/${resolved.flowId}/chart/nodes/${id}`);
|
|
1743
|
+
return { deleted: true, resourceType: "tool", id };
|
|
1744
|
+
}
|
|
1745
|
+
// Agent deletion requires cascade: endpoints → flow → agent.
|
|
1746
|
+
// The Cognigy API rejects agent deletion while referencing resources exist.
|
|
1747
|
+
if (resourceType === "agent") {
|
|
1748
|
+
if (cascade === false) {
|
|
1749
|
+
await this.apiClient.delete(`/v2.0/aiagents/${id}`);
|
|
1750
|
+
return { deleted: true, resourceType, id };
|
|
1751
|
+
}
|
|
1752
|
+
return this.cascadeDeleteAgent(id);
|
|
1753
|
+
}
|
|
1754
|
+
const deleteMap = {
|
|
1755
|
+
flow: `/v2.0/flows/${id}`,
|
|
1756
|
+
endpoint: `/v2.0/endpoints/${id}`,
|
|
1757
|
+
llm_model: `/v2.0/largelanguagemodels/${id}`,
|
|
1758
|
+
knowledge_store: `/v2.0/knowledgestores/${id}`,
|
|
1759
|
+
function: `/v2.0/functions/${id}`,
|
|
1760
|
+
};
|
|
1761
|
+
const url = deleteMap[resourceType];
|
|
1762
|
+
if (!url)
|
|
1763
|
+
throw new Error(`Unknown resourceType: ${resourceType}`);
|
|
1764
|
+
await this.apiClient.delete(url);
|
|
1765
|
+
return { deleted: true, resourceType, id };
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Cascade-delete an AI Agent and all resources provisioned alongside it:
|
|
1769
|
+
* 1. Resolve the agent's flow
|
|
1770
|
+
* 2. Delete every endpoint pointing at that flow
|
|
1771
|
+
* 3. Delete the flow itself
|
|
1772
|
+
* 4. Delete the agent resource
|
|
1773
|
+
*/
|
|
1774
|
+
async cascadeDeleteAgent(agentId) {
|
|
1775
|
+
const deleted = [];
|
|
1776
|
+
const failed = [];
|
|
1777
|
+
const resolved = await resolveFlowForAgent(this.apiClient, agentId);
|
|
1778
|
+
const agent = resolved?.agent;
|
|
1779
|
+
const flowId = resolved?.flowId;
|
|
1780
|
+
const projectId = agent?.projectReference ??
|
|
1781
|
+
agent?.projectId ??
|
|
1782
|
+
agent?.project?._id ??
|
|
1783
|
+
agent?.project?.id;
|
|
1784
|
+
// Step 1: delete endpoints that reference the agent's flow
|
|
1785
|
+
if (flowId && projectId) {
|
|
1786
|
+
try {
|
|
1787
|
+
const flowRef = agent?.flowReferenceId ??
|
|
1788
|
+
(await this.apiClient.get(`/v2.0/flows/${flowId}`))
|
|
1789
|
+
.referenceId;
|
|
1790
|
+
if (flowRef) {
|
|
1791
|
+
const limit = 100;
|
|
1792
|
+
let offset = 0;
|
|
1793
|
+
let hasMore = true;
|
|
1794
|
+
while (hasMore) {
|
|
1795
|
+
const eps = await this.apiClient.get("/v2.0/endpoints", {
|
|
1796
|
+
params: { projectId, limit, offset },
|
|
1797
|
+
});
|
|
1798
|
+
const epItems = eps.items ?? eps;
|
|
1799
|
+
if (!Array.isArray(epItems) || epItems.length === 0) {
|
|
1800
|
+
break;
|
|
1801
|
+
}
|
|
1802
|
+
for (const ep of epItems) {
|
|
1803
|
+
if (ep.flowId === flowRef || ep.flowId === flowId) {
|
|
1804
|
+
const epId = ep._id || ep.id;
|
|
1805
|
+
try {
|
|
1806
|
+
await this.apiClient.delete(`/v2.0/endpoints/${epId}`);
|
|
1807
|
+
deleted.push(`endpoint:${epId}`);
|
|
1808
|
+
}
|
|
1809
|
+
catch (e) {
|
|
1810
|
+
failed.push({
|
|
1811
|
+
resource: `endpoint:${epId}`,
|
|
1812
|
+
error: e.message ?? String(e),
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
if (epItems.length < limit) {
|
|
1818
|
+
hasMore = false;
|
|
1819
|
+
}
|
|
1820
|
+
else {
|
|
1821
|
+
offset += limit;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
catch (e) {
|
|
1827
|
+
// best-effort — continue with flow/agent deletion, but record partial failure
|
|
1828
|
+
failed.push({
|
|
1829
|
+
resource: `endpoints:list:${projectId}`,
|
|
1830
|
+
error: e?.message ?? String(e),
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
// Step 2: delete the flow
|
|
1835
|
+
if (flowId) {
|
|
1836
|
+
try {
|
|
1837
|
+
await this.apiClient.delete(`/v2.0/flows/${flowId}`);
|
|
1838
|
+
deleted.push(`flow:${flowId}`);
|
|
1839
|
+
}
|
|
1840
|
+
catch (e) {
|
|
1841
|
+
failed.push({
|
|
1842
|
+
resource: `flow:${flowId}`,
|
|
1843
|
+
error: e.message ?? String(e),
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
// Step 3: delete the agent
|
|
1848
|
+
try {
|
|
1849
|
+
await this.apiClient.delete(`/v2.0/aiagents/${agentId}`);
|
|
1850
|
+
deleted.push(`agent:${agentId}`);
|
|
1851
|
+
}
|
|
1852
|
+
catch (e) {
|
|
1853
|
+
failed.push({
|
|
1854
|
+
resource: `agent:${agentId}`,
|
|
1855
|
+
error: e.message ?? String(e),
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
const allSucceeded = failed.length === 0 && deleted.includes(`agent:${agentId}`);
|
|
1859
|
+
return {
|
|
1860
|
+
deleted: allSucceeded,
|
|
1861
|
+
resourceType: "agent",
|
|
1862
|
+
id: agentId,
|
|
1863
|
+
cascade: { deleted, failed: failed.length > 0 ? failed : undefined },
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
// =========================================================================
|
|
1867
|
+
// Tool 8: manage_knowledge
|
|
1868
|
+
// =========================================================================
|
|
1869
|
+
async handleManageKnowledge(args) {
|
|
1870
|
+
const data = schemas.manageKnowledgeSchema.parse(args);
|
|
1871
|
+
switch (data.operation) {
|
|
1872
|
+
case "create_store": {
|
|
1873
|
+
if (!data.projectId) {
|
|
1874
|
+
return withHints({ error: "projectId is required for create_store" }, {
|
|
1875
|
+
action: "Use list_resources { resourceType: 'project' } to find projectIds.",
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
if (!data.name) {
|
|
1879
|
+
return withHints({ error: "name is required for create_store" }, { action: "Provide a name for the knowledge store." });
|
|
1880
|
+
}
|
|
1881
|
+
const payload = { projectId: data.projectId, name: data.name };
|
|
1882
|
+
if (data.description)
|
|
1883
|
+
payload.description = data.description;
|
|
1884
|
+
const result = await this.apiClient.post("/v2.0/knowledgestores", payload);
|
|
1885
|
+
return filterResponse("knowledge_store", result);
|
|
1886
|
+
}
|
|
1887
|
+
case "create_source": {
|
|
1888
|
+
if (!data.knowledgeStoreId) {
|
|
1889
|
+
return withHints({ error: "knowledgeStoreId is required for create_source" }, {
|
|
1890
|
+
action: "Use list_resources { resourceType: 'knowledge_store', projectId } to find store IDs.",
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
const storeId = data.knowledgeStoreId;
|
|
1894
|
+
const sourceType = data.type ?? (data.url ? "url" : data.filePath ? "file" : "manual");
|
|
1895
|
+
if (sourceType === "file") {
|
|
1896
|
+
if (!data.filePath) {
|
|
1897
|
+
throw new Error('filePath is required for type "file" — provide an absolute path to the local file');
|
|
1898
|
+
}
|
|
1899
|
+
const resolvedPath = data.filePath.startsWith("~")
|
|
1900
|
+
? data.filePath.replace(/^~/, process.env.HOME || "")
|
|
1901
|
+
: data.filePath;
|
|
1902
|
+
if (!existsSync(resolvedPath)) {
|
|
1903
|
+
throw new Error(`File not found: ${resolvedPath}`);
|
|
1904
|
+
}
|
|
1905
|
+
const fileName = basename(resolvedPath);
|
|
1906
|
+
const ext = fileName.split(".").pop()?.toLowerCase();
|
|
1907
|
+
const ALLOWED_EXTS = ["pdf", "txt", "text", "docx", "ctxt", "pptx"];
|
|
1908
|
+
if (!ext || !ALLOWED_EXTS.includes(ext)) {
|
|
1909
|
+
throw new Error(`Unsupported file type ".${ext}" (${fileName}). Supported: ${ALLOWED_EXTS.join(", ")}`);
|
|
1910
|
+
}
|
|
1911
|
+
const fileBuffer = readFileSync(resolvedPath);
|
|
1912
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
1913
|
+
if (fileBuffer.length > MAX_FILE_SIZE) {
|
|
1914
|
+
throw new Error(`File too large: ${(fileBuffer.length / 1024 / 1024).toFixed(1)}MB (max 10MB). File: ${fileName}`);
|
|
1915
|
+
}
|
|
1916
|
+
if (fileBuffer.length === 0) {
|
|
1917
|
+
throw new Error(`File is empty: ${fileName}`);
|
|
1918
|
+
}
|
|
1919
|
+
const result = await this.apiClient.uploadFile(`/v2.0/knowledgestores/${storeId}/sources/upload`, fileBuffer, fileName);
|
|
1920
|
+
return withHints({
|
|
1921
|
+
source: {
|
|
1922
|
+
taskId: result.taskData?.taskId || result._id || result.id,
|
|
1923
|
+
type: "file",
|
|
1924
|
+
fileName,
|
|
1925
|
+
fileSize: `${(fileBuffer.length / 1024).toFixed(0)}KB`,
|
|
1926
|
+
status: "ingesting",
|
|
1927
|
+
},
|
|
1928
|
+
}, {
|
|
1929
|
+
warning: "File ingestion is async — content will be processed and chunked automatically. This may take 10-60 seconds.",
|
|
1930
|
+
action: "Wait, then use list_chunks to verify the content was ingested.",
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
if (sourceType === "url") {
|
|
1934
|
+
if (!data.url) {
|
|
1935
|
+
return withHints({ error: 'url is required for type "url"' }, {
|
|
1936
|
+
action: "Provide the url field with a valid web page URL to scrape.",
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
const payload = {
|
|
1940
|
+
name: data.name || data.url,
|
|
1941
|
+
type: "url",
|
|
1942
|
+
url: data.url,
|
|
1943
|
+
};
|
|
1944
|
+
if (data.description)
|
|
1945
|
+
payload.description = data.description;
|
|
1946
|
+
const result = await this.apiClient.post(`/v2.0/knowledgestores/${storeId}/sources`, payload);
|
|
1947
|
+
return withHints({
|
|
1948
|
+
source: {
|
|
1949
|
+
id: result.taskData?.taskId || result._id || result.id,
|
|
1950
|
+
type: "url",
|
|
1951
|
+
status: "ingesting",
|
|
1952
|
+
},
|
|
1953
|
+
}, {
|
|
1954
|
+
warning: "URL ingestion is async — content may not be searchable for 10-60 seconds.",
|
|
1955
|
+
action: "Wait, then use list_chunks to verify the content was ingested.",
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
// Manual/text source: create source, then add a chunk with the text
|
|
1959
|
+
if (!data.text) {
|
|
1960
|
+
return withHints({ error: "text is required for manual sources" }, {
|
|
1961
|
+
action: "Provide the text field with the content to store as a knowledge chunk.",
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
const sourcePayload = {
|
|
1965
|
+
name: data.name || "Manual source",
|
|
1966
|
+
type: "manual",
|
|
1967
|
+
};
|
|
1968
|
+
if (data.description)
|
|
1969
|
+
sourcePayload.description = data.description;
|
|
1970
|
+
const sourceResult = await this.apiClient.post(`/v2.0/knowledgestores/${storeId}/sources`, sourcePayload);
|
|
1971
|
+
const source = sourceResult.knowledgeSource ?? sourceResult;
|
|
1972
|
+
const sourceId = source._id || source.id;
|
|
1973
|
+
const chunkResult = await this.apiClient.post(`/v2.0/knowledgestores/${storeId}/sources/${sourceId}/chunks`, { text: data.text, order: 1 });
|
|
1974
|
+
return withHints({
|
|
1975
|
+
source: { id: sourceId, type: "manual", name: sourcePayload.name },
|
|
1976
|
+
chunk: { id: chunkResult._id || chunkResult.id },
|
|
1977
|
+
}, {
|
|
1978
|
+
warning: "Chunk created. It may take a few seconds before it becomes searchable.",
|
|
1979
|
+
action: "Wait, then use list_chunks to verify the content was ingested.",
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
case "list_chunks": {
|
|
1983
|
+
if (!data.knowledgeStoreId) {
|
|
1984
|
+
return withHints({ error: "knowledgeStoreId is required for list_chunks" }, {
|
|
1985
|
+
action: "Use list_resources { resourceType: 'knowledge_store', projectId } to find store IDs.",
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
const ksId = data.knowledgeStoreId;
|
|
1989
|
+
let targetSourceId = data.sourceId;
|
|
1990
|
+
if (!targetSourceId) {
|
|
1991
|
+
const sources = await this.apiClient.get(`/v2.0/knowledgestores/${ksId}/sources`);
|
|
1992
|
+
const srcItems = sources.items ?? sources;
|
|
1993
|
+
if (!Array.isArray(srcItems) || srcItems.length === 0) {
|
|
1994
|
+
return withHints({ chunks: [], sources: [] }, {
|
|
1995
|
+
likely_cause: "No sources found in this knowledge store.",
|
|
1996
|
+
action: "Add a source first with create_source.",
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
targetSourceId = srcItems[0]._id || srcItems[0].id;
|
|
2000
|
+
}
|
|
2001
|
+
const params = { limit: data.limit ?? 25 };
|
|
2002
|
+
if (data.filter)
|
|
2003
|
+
params.filter = data.filter;
|
|
2004
|
+
const result = await this.apiClient.get(`/v2.0/knowledgestores/${ksId}/sources/${targetSourceId}/chunks`, { params });
|
|
2005
|
+
const chunks = result.items ?? result;
|
|
2006
|
+
return {
|
|
2007
|
+
chunks: Array.isArray(chunks)
|
|
2008
|
+
? chunks.map((c) => ({
|
|
2009
|
+
id: c._id || c.id,
|
|
2010
|
+
text: c.text,
|
|
2011
|
+
order: c.order,
|
|
2012
|
+
disabled: c.disabled,
|
|
2013
|
+
}))
|
|
2014
|
+
: [],
|
|
2015
|
+
total: result.total ?? (Array.isArray(chunks) ? chunks.length : 0),
|
|
2016
|
+
sourceId: targetSourceId,
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
case "list_sources": {
|
|
2020
|
+
if (!data.knowledgeStoreId) {
|
|
2021
|
+
return withHints({ error: "knowledgeStoreId is required for list_sources" }, {
|
|
2022
|
+
action: "Use list_resources { resourceType: 'knowledge_store' } to find store IDs.",
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
const sources = await this.apiClient.get(`/v2.0/knowledgestores/${data.knowledgeStoreId}/sources`);
|
|
2026
|
+
const items = sources.items ?? sources;
|
|
2027
|
+
return {
|
|
2028
|
+
knowledgeStoreId: data.knowledgeStoreId,
|
|
2029
|
+
sources: (Array.isArray(items) ? items : []).map((s) => ({
|
|
2030
|
+
id: s._id || s.id,
|
|
2031
|
+
name: s.name,
|
|
2032
|
+
type: s.type,
|
|
2033
|
+
status: s.status,
|
|
2034
|
+
description: s.description,
|
|
2035
|
+
})),
|
|
2036
|
+
total: Array.isArray(items) ? items.length : 0,
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
default:
|
|
2040
|
+
throw new Error(`Unknown operation: ${data.operation}`);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
// =========================================================================
|
|
2044
|
+
// Tool 9: create_tool
|
|
2045
|
+
// =========================================================================
|
|
2046
|
+
async handleCreateTool(args) {
|
|
2047
|
+
const data = schemas.createToolSchema.parse(args);
|
|
2048
|
+
// Step 1: Resolve the agent's flow
|
|
2049
|
+
const resolved = await resolveFlowForAgent(this.apiClient, data.aiAgentId);
|
|
2050
|
+
if (!resolved) {
|
|
2051
|
+
return withHints({ error: "Could not find a flow associated with this agent." }, {
|
|
2052
|
+
likely_cause: "create_tool requires an agent created via create_ai_agent (which auto-provisions the flow).",
|
|
2053
|
+
action: "Read the tools guide, ensure agent was created via create_ai_agent, then retry.",
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
const { flowId } = resolved;
|
|
2057
|
+
// Step 2: Find the AI Agent Job Node
|
|
2058
|
+
const nodes = await this.apiClient.get(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2059
|
+
params: { limit: 100 },
|
|
2060
|
+
});
|
|
2061
|
+
const allNodes = nodes.items ?? nodes;
|
|
2062
|
+
const jobNode = (Array.isArray(allNodes) ? allNodes : []).find((n) => n.type === "aiAgentJob");
|
|
2063
|
+
if (!jobNode) {
|
|
2064
|
+
return withHints({
|
|
2065
|
+
error: "No aiAgentJob node found in the flow. Tools must be children of an AI Agent Job node.",
|
|
2066
|
+
}, {
|
|
2067
|
+
action: "Ensure the agent was created via create_ai_agent (which provisions the aiAgentJob node).",
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
const cfg = data.config;
|
|
2071
|
+
const requestedToolId = typeof cfg.toolId === "string" && cfg.toolId.trim().length > 0
|
|
2072
|
+
? cfg.toolId.trim()
|
|
2073
|
+
: undefined;
|
|
2074
|
+
if (requestedToolId) {
|
|
2075
|
+
const duplicateTool = (Array.isArray(allNodes) ? allNodes : []).find((node) => MCP_MANAGED_TOOL_TYPES.has(node.type) &&
|
|
2076
|
+
(node.config?.toolId === requestedToolId ||
|
|
2077
|
+
node.label === requestedToolId ||
|
|
2078
|
+
node.name === requestedToolId));
|
|
2079
|
+
if (duplicateTool) {
|
|
2080
|
+
const duplicateToolNodeId = duplicateTool._id || duplicateTool.id;
|
|
2081
|
+
return withHints({
|
|
2082
|
+
toolId: duplicateToolNodeId,
|
|
2083
|
+
toolNodeId: duplicateToolNodeId,
|
|
2084
|
+
requestedToolId,
|
|
2085
|
+
name: duplicateTool.label || duplicateTool.name || data.name,
|
|
2086
|
+
toolType: duplicateTool.type === "knowledgeTool"
|
|
2087
|
+
? "knowledge"
|
|
2088
|
+
: duplicateTool.type === "sendEmailTool"
|
|
2089
|
+
? "send_email"
|
|
2090
|
+
: duplicateTool.type === "aiAgentJobMCPTool"
|
|
2091
|
+
? "mcp"
|
|
2092
|
+
: data.toolType,
|
|
2093
|
+
reusedExisting: true,
|
|
2094
|
+
}, {
|
|
2095
|
+
warning: `A tool with toolId "${requestedToolId}" already exists in this agent flow, so the existing tool was reused instead of creating a duplicate.`,
|
|
2096
|
+
action: `Continue by adding logic inside that tool with manage_flow_nodes using parentNodeId "${duplicateToolNodeId}", or modify it with update_tool { aiAgentId: "${data.aiAgentId}", toolNodeId: "${duplicateToolNodeId}", ... }.`,
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
// Step 3: Create the tool node
|
|
2101
|
+
const mapping = TOOL_TYPE_MAP[data.toolType];
|
|
2102
|
+
if (!mapping)
|
|
2103
|
+
throw new Error(`Unknown toolType: ${data.toolType}`);
|
|
2104
|
+
const nodeConfig = {};
|
|
2105
|
+
switch (data.toolType) {
|
|
2106
|
+
case "tool":
|
|
2107
|
+
if (cfg.toolId)
|
|
2108
|
+
nodeConfig.toolId = cfg.toolId;
|
|
2109
|
+
if (cfg.description)
|
|
2110
|
+
nodeConfig.description = cfg.description;
|
|
2111
|
+
if (cfg.parameters) {
|
|
2112
|
+
nodeConfig.useParameters = true;
|
|
2113
|
+
nodeConfig.parameters = cfg.parameters;
|
|
2114
|
+
}
|
|
2115
|
+
break;
|
|
2116
|
+
case "knowledge":
|
|
2117
|
+
if (cfg.knowledgeStoreId)
|
|
2118
|
+
nodeConfig.knowledgeStoreId = cfg.knowledgeStoreId;
|
|
2119
|
+
if (cfg.toolId)
|
|
2120
|
+
nodeConfig.toolId = cfg.toolId;
|
|
2121
|
+
if (cfg.description)
|
|
2122
|
+
nodeConfig.description = cfg.description;
|
|
2123
|
+
if (cfg.topK)
|
|
2124
|
+
nodeConfig.topK = cfg.topK;
|
|
2125
|
+
break;
|
|
2126
|
+
case "send_email":
|
|
2127
|
+
if (cfg.toolId)
|
|
2128
|
+
nodeConfig.toolId = cfg.toolId;
|
|
2129
|
+
if (cfg.description)
|
|
2130
|
+
nodeConfig.description = cfg.description;
|
|
2131
|
+
if (cfg.recipient)
|
|
2132
|
+
nodeConfig.recipient = cfg.recipient;
|
|
2133
|
+
break;
|
|
2134
|
+
case "mcp":
|
|
2135
|
+
if (cfg.mcpName)
|
|
2136
|
+
nodeConfig.name = cfg.mcpName;
|
|
2137
|
+
if (cfg.mcpServerUrl)
|
|
2138
|
+
nodeConfig.mcpServerUrl = cfg.mcpServerUrl;
|
|
2139
|
+
if (cfg.timeout)
|
|
2140
|
+
nodeConfig.timeout = cfg.timeout;
|
|
2141
|
+
break;
|
|
2142
|
+
case "http":
|
|
2143
|
+
if (cfg.toolId)
|
|
2144
|
+
nodeConfig.toolId = cfg.toolId;
|
|
2145
|
+
if (cfg.description)
|
|
2146
|
+
nodeConfig.description = cfg.description;
|
|
2147
|
+
if (cfg.parameters) {
|
|
2148
|
+
nodeConfig.useParameters = true;
|
|
2149
|
+
nodeConfig.parameters = cfg.parameters;
|
|
2150
|
+
}
|
|
2151
|
+
break;
|
|
2152
|
+
}
|
|
2153
|
+
// For non-http tools: create the tool node + resolve node (if required by the tool type)
|
|
2154
|
+
const toolLabel = cfg.toolId || data.name;
|
|
2155
|
+
if (data.toolType !== "http") {
|
|
2156
|
+
const createdNodeIds = [];
|
|
2157
|
+
try {
|
|
2158
|
+
const createdNode = await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2159
|
+
type: mapping.type,
|
|
2160
|
+
extension: mapping.extension,
|
|
2161
|
+
mode: "appendChild",
|
|
2162
|
+
target: jobNode._id,
|
|
2163
|
+
label: toolLabel,
|
|
2164
|
+
config: nodeConfig,
|
|
2165
|
+
});
|
|
2166
|
+
const toolNodeId = createdNode._id || createdNode.id;
|
|
2167
|
+
createdNodeIds.push(toolNodeId);
|
|
2168
|
+
const resolveSpec = RESOLVE_NODE_MAP[data.toolType];
|
|
2169
|
+
let resolveNodeId;
|
|
2170
|
+
if (resolveSpec) {
|
|
2171
|
+
const resolveConfig = {};
|
|
2172
|
+
if (resolveSpec.type === "aiAgentToolAnswer") {
|
|
2173
|
+
resolveConfig.answer =
|
|
2174
|
+
cfg.toolResponseValue ?? "{{JSON.stringify(input.result)}}";
|
|
2175
|
+
}
|
|
2176
|
+
const resolveLabel = resolveSpec.type === "aiAgentToolAnswer"
|
|
2177
|
+
? `${toolLabel} - Resolve`
|
|
2178
|
+
: resolveSpec.label;
|
|
2179
|
+
const resolveNode = await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2180
|
+
type: resolveSpec.type,
|
|
2181
|
+
extension: "@cognigy/basic-nodes",
|
|
2182
|
+
mode: "append",
|
|
2183
|
+
target: toolNodeId,
|
|
2184
|
+
label: resolveLabel,
|
|
2185
|
+
config: resolveConfig,
|
|
2186
|
+
});
|
|
2187
|
+
resolveNodeId = resolveNode._id || resolveNode.id;
|
|
2188
|
+
if (resolveNodeId)
|
|
2189
|
+
createdNodeIds.push(resolveNodeId);
|
|
2190
|
+
}
|
|
2191
|
+
return {
|
|
2192
|
+
toolId: toolNodeId,
|
|
2193
|
+
name: data.name,
|
|
2194
|
+
toolType: data.toolType,
|
|
2195
|
+
...(resolveNodeId ? { resolveNodeId } : {}),
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
catch (error) {
|
|
2199
|
+
const rolledBack = [];
|
|
2200
|
+
const rollbackFailed = [];
|
|
2201
|
+
for (const nodeId of createdNodeIds.reverse()) {
|
|
2202
|
+
try {
|
|
2203
|
+
await this.apiClient.delete(`/v2.0/flows/${flowId}/chart/nodes/${nodeId}`);
|
|
2204
|
+
rolledBack.push(nodeId);
|
|
2205
|
+
}
|
|
2206
|
+
catch {
|
|
2207
|
+
rollbackFailed.push(nodeId);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
const action = rollbackFailed.length > 0
|
|
2211
|
+
? `Rollback partially failed — orphaned node IDs: [${rollbackFailed.join(", ")}]. Delete them with delete_resource { resourceType: 'tool', id, aiAgentId }, then retry.`
|
|
2212
|
+
: "Check tool type and config, then retry.";
|
|
2213
|
+
return withHints({ error: error.message }, { action });
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
// HTTP tool: parent aiAgentJobTool + child httpRequest (+ optional Code nodes)
|
|
2217
|
+
if (!cfg.url) {
|
|
2218
|
+
return withHints({ error: "url is required in config for http tool type." }, {
|
|
2219
|
+
action: "Provide config.url and retry.",
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
const createdNodeIds = [];
|
|
2223
|
+
try {
|
|
2224
|
+
// 1. Create the tool node as a child of the Job Node
|
|
2225
|
+
const toolNode = await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2226
|
+
type: mapping.type,
|
|
2227
|
+
extension: mapping.extension,
|
|
2228
|
+
mode: "appendChild",
|
|
2229
|
+
target: jobNode._id,
|
|
2230
|
+
label: toolLabel,
|
|
2231
|
+
config: nodeConfig,
|
|
2232
|
+
});
|
|
2233
|
+
const toolNodeId = toolNode._id || toolNode.id;
|
|
2234
|
+
createdNodeIds.push(toolNodeId);
|
|
2235
|
+
// 2. Create the Resolve Tool node — must be created before the HTTP node
|
|
2236
|
+
// so the flow tree is wired correctly (matches UI creation order).
|
|
2237
|
+
const resolveAnswer = cfg.toolResponseValue || "{{JSON.stringify(input.httprequest)}}";
|
|
2238
|
+
const resolveNode = await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2239
|
+
type: "aiAgentToolAnswer",
|
|
2240
|
+
extension: "@cognigy/basic-nodes",
|
|
2241
|
+
mode: "append",
|
|
2242
|
+
target: toolNodeId,
|
|
2243
|
+
label: `${toolLabel} - Resolve`,
|
|
2244
|
+
config: {
|
|
2245
|
+
answer: resolveAnswer,
|
|
2246
|
+
},
|
|
2247
|
+
});
|
|
2248
|
+
const resolveNodeId = resolveNode._id || resolveNode.id;
|
|
2249
|
+
createdNodeIds.push(resolveNodeId);
|
|
2250
|
+
// 3. Create optional pre-process Code node
|
|
2251
|
+
let preProcessNodeId;
|
|
2252
|
+
if (cfg.preProcessCode) {
|
|
2253
|
+
const preNode = await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2254
|
+
type: "code",
|
|
2255
|
+
extension: "@cognigy/basic-nodes",
|
|
2256
|
+
mode: "append",
|
|
2257
|
+
target: toolNodeId,
|
|
2258
|
+
label: `${toolLabel} - Pre-Process`,
|
|
2259
|
+
config: { code: cfg.preProcessCode },
|
|
2260
|
+
});
|
|
2261
|
+
preProcessNodeId = preNode._id || preNode.id;
|
|
2262
|
+
if (preProcessNodeId)
|
|
2263
|
+
createdNodeIds.push(preProcessNodeId);
|
|
2264
|
+
}
|
|
2265
|
+
// 4. Create the HTTP Request node
|
|
2266
|
+
const httpConfig = buildHttpNodeConfig({
|
|
2267
|
+
url: cfg.url,
|
|
2268
|
+
method: cfg.method,
|
|
2269
|
+
headers: cfg.headers,
|
|
2270
|
+
body: cfg.body,
|
|
2271
|
+
});
|
|
2272
|
+
const httpNode = await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2273
|
+
type: "httpRequest",
|
|
2274
|
+
extension: "@cognigy/basic-nodes",
|
|
2275
|
+
mode: "append",
|
|
2276
|
+
target: preProcessNodeId ?? toolNodeId,
|
|
2277
|
+
label: `${toolLabel} - HTTP Request`,
|
|
2278
|
+
config: httpConfig,
|
|
2279
|
+
});
|
|
2280
|
+
const httpNodeId = httpNode._id || httpNode.id;
|
|
2281
|
+
createdNodeIds.push(httpNodeId);
|
|
2282
|
+
// 5. Create optional post-process Code node
|
|
2283
|
+
let postProcessNodeId;
|
|
2284
|
+
if (cfg.postProcessCode) {
|
|
2285
|
+
const postNode = await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2286
|
+
type: "code",
|
|
2287
|
+
extension: "@cognigy/basic-nodes",
|
|
2288
|
+
mode: "append",
|
|
2289
|
+
target: httpNodeId,
|
|
2290
|
+
label: `${toolLabel} - Post-Process`,
|
|
2291
|
+
config: { code: cfg.postProcessCode },
|
|
2292
|
+
});
|
|
2293
|
+
postProcessNodeId = postNode._id || postNode.id;
|
|
2294
|
+
if (postProcessNodeId)
|
|
2295
|
+
createdNodeIds.push(postProcessNodeId);
|
|
2296
|
+
}
|
|
2297
|
+
return {
|
|
2298
|
+
toolId: toolNodeId,
|
|
2299
|
+
name: data.name,
|
|
2300
|
+
toolType: "http",
|
|
2301
|
+
childNodes: {
|
|
2302
|
+
...(preProcessNodeId ? { preProcessNodeId } : {}),
|
|
2303
|
+
httpNodeId,
|
|
2304
|
+
...(postProcessNodeId ? { postProcessNodeId } : {}),
|
|
2305
|
+
resolveNodeId,
|
|
2306
|
+
},
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
catch (error) {
|
|
2310
|
+
const rolledBack = [];
|
|
2311
|
+
const rollbackFailed = [];
|
|
2312
|
+
for (const nodeId of createdNodeIds.reverse()) {
|
|
2313
|
+
try {
|
|
2314
|
+
await this.apiClient.delete(`/v2.0/flows/${flowId}/chart/nodes/${nodeId}`);
|
|
2315
|
+
rolledBack.push(nodeId);
|
|
2316
|
+
}
|
|
2317
|
+
catch {
|
|
2318
|
+
rollbackFailed.push(nodeId);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
const action = rollbackFailed.length > 0
|
|
2322
|
+
? `Rollback partially failed — orphaned node IDs: [${rollbackFailed.join(", ")}]. Delete them with delete_resource { resourceType: 'tool', id, aiAgentId }, then retry.`
|
|
2323
|
+
: "Check HTTP config and code snippets, then retry.";
|
|
2324
|
+
return withHints({ error: error.message }, { action });
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
// =========================================================================
|
|
2328
|
+
// Tool 10: update_tool
|
|
2329
|
+
// =========================================================================
|
|
2330
|
+
async handleUpdateTool(args) {
|
|
2331
|
+
const data = schemas.updateToolSchema.parse(args);
|
|
2332
|
+
const resolved = await resolveFlowForAgent(this.apiClient, data.aiAgentId);
|
|
2333
|
+
if (!resolved) {
|
|
2334
|
+
return withHints({ error: "Could not find a flow associated with this agent." }, {
|
|
2335
|
+
likely_cause: "update_tool requires an agent created via create_ai_agent.",
|
|
2336
|
+
action: "Ensure agent was created via create_ai_agent, then retry.",
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
const { flowId } = resolved;
|
|
2340
|
+
if (!data.name && !data.config) {
|
|
2341
|
+
return withHints({ error: "Nothing to update. Provide at least name or config." }, { action: "Include fields to update in the request." });
|
|
2342
|
+
}
|
|
2343
|
+
const updatedFields = [];
|
|
2344
|
+
const cfg = data.config;
|
|
2345
|
+
const toolType = data.toolType;
|
|
2346
|
+
// Detect whether config contains HTTP child-node fields
|
|
2347
|
+
const hasHttpUpdates = cfg && (cfg.url || cfg.method || cfg.headers || cfg.body);
|
|
2348
|
+
const hasCodeUpdates = cfg &&
|
|
2349
|
+
(cfg.preProcessCode !== undefined || cfg.postProcessCode !== undefined);
|
|
2350
|
+
const hasResolveUpdate = cfg && cfg.toolResponseValue !== undefined;
|
|
2351
|
+
const hasChildUpdates = hasHttpUpdates || hasCodeUpdates || hasResolveUpdate;
|
|
2352
|
+
// Step 1: Update the tool node itself (label and/or tool-node config)
|
|
2353
|
+
const patchPayload = {};
|
|
2354
|
+
if (data.name)
|
|
2355
|
+
patchPayload.label = data.name;
|
|
2356
|
+
if (cfg) {
|
|
2357
|
+
const nodeConfig = {};
|
|
2358
|
+
if (toolType === "tool" || toolType === "http" || !toolType) {
|
|
2359
|
+
if (cfg.toolId)
|
|
2360
|
+
nodeConfig.toolId = cfg.toolId;
|
|
2361
|
+
if (cfg.description)
|
|
2362
|
+
nodeConfig.description = cfg.description;
|
|
2363
|
+
if (cfg.parameters) {
|
|
2364
|
+
nodeConfig.useParameters = true;
|
|
2365
|
+
nodeConfig.parameters = cfg.parameters;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
if (toolType === "knowledge") {
|
|
2369
|
+
if (cfg.knowledgeStoreId)
|
|
2370
|
+
nodeConfig.knowledgeStoreId = cfg.knowledgeStoreId;
|
|
2371
|
+
if (cfg.toolId)
|
|
2372
|
+
nodeConfig.toolId = cfg.toolId;
|
|
2373
|
+
if (cfg.description)
|
|
2374
|
+
nodeConfig.description = cfg.description;
|
|
2375
|
+
if (cfg.topK)
|
|
2376
|
+
nodeConfig.topK = cfg.topK;
|
|
2377
|
+
}
|
|
2378
|
+
if (toolType === "send_email") {
|
|
2379
|
+
if (cfg.toolId)
|
|
2380
|
+
nodeConfig.toolId = cfg.toolId;
|
|
2381
|
+
if (cfg.description)
|
|
2382
|
+
nodeConfig.description = cfg.description;
|
|
2383
|
+
if (cfg.recipient)
|
|
2384
|
+
nodeConfig.recipient = cfg.recipient;
|
|
2385
|
+
}
|
|
2386
|
+
if (toolType === "mcp") {
|
|
2387
|
+
if (cfg.mcpName)
|
|
2388
|
+
nodeConfig.name = cfg.mcpName;
|
|
2389
|
+
if (cfg.mcpServerUrl)
|
|
2390
|
+
nodeConfig.mcpServerUrl = cfg.mcpServerUrl;
|
|
2391
|
+
if (cfg.timeout)
|
|
2392
|
+
nodeConfig.timeout = cfg.timeout;
|
|
2393
|
+
}
|
|
2394
|
+
if (Object.keys(nodeConfig).length > 0) {
|
|
2395
|
+
patchPayload.config = nodeConfig;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
if (Object.keys(patchPayload).length > 0) {
|
|
2399
|
+
await this.apiClient.patch(`/v2.0/flows/${flowId}/chart/nodes/${data.toolNodeId}`, patchPayload);
|
|
2400
|
+
if (data.name)
|
|
2401
|
+
updatedFields.push("name");
|
|
2402
|
+
if (patchPayload.config)
|
|
2403
|
+
updatedFields.push("config");
|
|
2404
|
+
}
|
|
2405
|
+
// Step 2: Update child nodes for http tools (httpRequest + Code nodes)
|
|
2406
|
+
const skippedUpdates = [];
|
|
2407
|
+
if (hasChildUpdates && cfg) {
|
|
2408
|
+
const nodes = await this.apiClient.get(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2409
|
+
params: { limit: 200 },
|
|
2410
|
+
});
|
|
2411
|
+
const rawNodes = nodes.items ?? nodes;
|
|
2412
|
+
const allNodes = Array.isArray(rawNodes) ? rawNodes : [];
|
|
2413
|
+
const toolNode = allNodes.find((n) => (n._id || n.id) === data.toolNodeId);
|
|
2414
|
+
const toolLabel = toolNode?.label ?? "";
|
|
2415
|
+
const findById = (id) => id ? allNodes.find((n) => (n._id || n.id) === id) : undefined;
|
|
2416
|
+
const findByLabelSuffix = (suffix, type) => {
|
|
2417
|
+
if (!toolLabel)
|
|
2418
|
+
return undefined;
|
|
2419
|
+
const target = `${toolLabel} - ${suffix}`;
|
|
2420
|
+
return allNodes.find((n) => n.type === type && n.label === target);
|
|
2421
|
+
};
|
|
2422
|
+
if (hasHttpUpdates) {
|
|
2423
|
+
const httpNode = findById(cfg.httpNodeId) ??
|
|
2424
|
+
findByLabelSuffix("HTTP Request", "httpRequest");
|
|
2425
|
+
if (httpNode) {
|
|
2426
|
+
const httpPatch = buildHttpNodeConfig({
|
|
2427
|
+
url: cfg.url,
|
|
2428
|
+
method: cfg.method,
|
|
2429
|
+
headers: cfg.headers,
|
|
2430
|
+
body: cfg.body,
|
|
2431
|
+
});
|
|
2432
|
+
if (Object.keys(httpPatch).length > 0) {
|
|
2433
|
+
await this.apiClient.patch(`/v2.0/flows/${flowId}/chart/nodes/${httpNode._id || httpNode.id}`, { config: httpPatch });
|
|
2434
|
+
updatedFields.push("http");
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
else {
|
|
2438
|
+
skippedUpdates.push("HTTP node not found — pass config.httpNodeId (from create_tool's childNodes.httpNodeId) to update it explicitly");
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
if (cfg.preProcessCode !== undefined) {
|
|
2442
|
+
const preNode = findById(cfg.preProcessNodeId) ??
|
|
2443
|
+
findByLabelSuffix("Pre-Process", "code");
|
|
2444
|
+
if (preNode) {
|
|
2445
|
+
await this.apiClient.patch(`/v2.0/flows/${flowId}/chart/nodes/${preNode._id || preNode.id}`, { config: { code: cfg.preProcessCode } });
|
|
2446
|
+
updatedFields.push("preProcessCode");
|
|
2447
|
+
}
|
|
2448
|
+
else if (cfg.preProcessNodeId) {
|
|
2449
|
+
skippedUpdates.push("Pre-process Code node with the provided preProcessNodeId was not found");
|
|
2450
|
+
}
|
|
2451
|
+
else if (toolNode) {
|
|
2452
|
+
await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2453
|
+
type: "code",
|
|
2454
|
+
extension: "@cognigy/basic-nodes",
|
|
2455
|
+
mode: "append",
|
|
2456
|
+
target: data.toolNodeId,
|
|
2457
|
+
label: `${toolLabel} - Pre-Process`,
|
|
2458
|
+
config: { code: cfg.preProcessCode },
|
|
2459
|
+
});
|
|
2460
|
+
updatedFields.push("preProcessCode");
|
|
2461
|
+
}
|
|
2462
|
+
else {
|
|
2463
|
+
skippedUpdates.push("Tool node not found — cannot provision pre-process Code node");
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
if (cfg.postProcessCode !== undefined) {
|
|
2467
|
+
const postNode = findById(cfg.postProcessNodeId) ??
|
|
2468
|
+
findByLabelSuffix("Post-Process", "code");
|
|
2469
|
+
if (postNode) {
|
|
2470
|
+
await this.apiClient.patch(`/v2.0/flows/${flowId}/chart/nodes/${postNode._id || postNode.id}`, { config: { code: cfg.postProcessCode } });
|
|
2471
|
+
updatedFields.push("postProcessCode");
|
|
2472
|
+
}
|
|
2473
|
+
else if (cfg.postProcessNodeId) {
|
|
2474
|
+
skippedUpdates.push("Post-process Code node with the provided postProcessNodeId was not found");
|
|
2475
|
+
}
|
|
2476
|
+
else {
|
|
2477
|
+
const httpAnchor = findById(cfg.httpNodeId) ??
|
|
2478
|
+
findByLabelSuffix("HTTP Request", "httpRequest");
|
|
2479
|
+
if (httpAnchor) {
|
|
2480
|
+
await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2481
|
+
type: "code",
|
|
2482
|
+
extension: "@cognigy/basic-nodes",
|
|
2483
|
+
mode: "append",
|
|
2484
|
+
target: httpAnchor._id || httpAnchor.id,
|
|
2485
|
+
label: `${toolLabel} - Post-Process`,
|
|
2486
|
+
config: { code: cfg.postProcessCode },
|
|
2487
|
+
});
|
|
2488
|
+
updatedFields.push("postProcessCode");
|
|
2489
|
+
}
|
|
2490
|
+
else {
|
|
2491
|
+
skippedUpdates.push("HTTP Request node not found — cannot provision post-process Code node (it is wired after the HTTP Request)");
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
if (cfg.toolResponseValue !== undefined) {
|
|
2496
|
+
const resolveCandidates = allNodes.filter((n) => n.type === "aiAgentToolAnswer");
|
|
2497
|
+
const resolveNode = findById(cfg.resolveNodeId) ??
|
|
2498
|
+
findByLabelSuffix("Resolve", "aiAgentToolAnswer") ??
|
|
2499
|
+
(resolveCandidates.length === 1 ? resolveCandidates[0] : undefined);
|
|
2500
|
+
if (resolveNode) {
|
|
2501
|
+
await this.apiClient.patch(`/v2.0/flows/${flowId}/chart/nodes/${resolveNode._id || resolveNode.id}`, { config: { answer: cfg.toolResponseValue } });
|
|
2502
|
+
updatedFields.push("toolResponseValue");
|
|
2503
|
+
}
|
|
2504
|
+
else if (resolveCandidates.length > 1) {
|
|
2505
|
+
skippedUpdates.push(`Multiple Resolve Tool Action nodes exist and none matched the label "${toolLabel} - Resolve" — pass config.resolveNodeId (from create_tool's childNodes.resolveNodeId) to pick one`);
|
|
2506
|
+
}
|
|
2507
|
+
else {
|
|
2508
|
+
skippedUpdates.push("Resolve Tool Action node not found — pass config.resolveNodeId to update it explicitly");
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
const response = {
|
|
2513
|
+
toolId: data.toolNodeId,
|
|
2514
|
+
name: data.name ?? undefined,
|
|
2515
|
+
updated: true,
|
|
2516
|
+
updatedFields,
|
|
2517
|
+
};
|
|
2518
|
+
if (skippedUpdates.length > 0) {
|
|
2519
|
+
return withHints(response, {
|
|
2520
|
+
warning: `Some updates were skipped: ${skippedUpdates.join("; ")}`,
|
|
2521
|
+
action: "Child nodes may not exist yet. Use create_tool with http type to create the full node tree, or verify the tool structure.",
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
return response;
|
|
2525
|
+
}
|
|
2526
|
+
// =========================================================================
|
|
2527
|
+
// Tool 12: manage_flow_nodes
|
|
2528
|
+
// =========================================================================
|
|
2529
|
+
async handleManageFlowNodes(args) {
|
|
2530
|
+
const data = schemas.manageFlowNodesSchema.parse(args);
|
|
2531
|
+
const { flowId, operation } = data;
|
|
2532
|
+
switch (operation) {
|
|
2533
|
+
// ----- LIST -----
|
|
2534
|
+
case "list": {
|
|
2535
|
+
const nodes = await this.apiClient.get(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2536
|
+
params: { limit: 200 },
|
|
2537
|
+
});
|
|
2538
|
+
const items = nodes.items ?? nodes;
|
|
2539
|
+
if (!Array.isArray(items))
|
|
2540
|
+
return { nodes: [] };
|
|
2541
|
+
return {
|
|
2542
|
+
nodes: items.map((n) => ({
|
|
2543
|
+
id: n._id || n.id,
|
|
2544
|
+
type: n.type,
|
|
2545
|
+
label: n.label,
|
|
2546
|
+
parentId: n.parentId ?? null,
|
|
2547
|
+
isEntryPoint: n.isEntryPoint ?? false,
|
|
2548
|
+
})),
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
// ----- CREATE -----
|
|
2552
|
+
case "create": {
|
|
2553
|
+
if (!data.nodeType) {
|
|
2554
|
+
return withHints({ error: "nodeType is required for create operation." }, {
|
|
2555
|
+
action: "Read the flow-nodes guide for supported node types.",
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
const entry = getNodeEntry(data.nodeType);
|
|
2559
|
+
if (!entry) {
|
|
2560
|
+
return withHints({
|
|
2561
|
+
error: `Unsupported nodeType: "${data.nodeType}". Supported types: ${supportedNodeTypes().join(", ")}`,
|
|
2562
|
+
}, {
|
|
2563
|
+
action: "Read the flow-nodes guide for the full list and config schemas.",
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
if (!data.label) {
|
|
2567
|
+
return withHints({ error: "label is required for create operation." }, { action: "Provide a display label for the node." });
|
|
2568
|
+
}
|
|
2569
|
+
const cfg = data.config ?? {};
|
|
2570
|
+
const aliasMap = {
|
|
2571
|
+
milliseconds: ["milliseconds", "delay"],
|
|
2572
|
+
key: ["key", "contextEntries"],
|
|
2573
|
+
value: ["value", "contextEntries"],
|
|
2574
|
+
};
|
|
2575
|
+
const missingKeys = entry.requiredConfigKeys.filter((k) => {
|
|
2576
|
+
const aliases = aliasMap[k] ?? [k];
|
|
2577
|
+
return !aliases.some((a) => cfg[a] !== undefined);
|
|
2578
|
+
});
|
|
2579
|
+
if (missingKeys.length > 0) {
|
|
2580
|
+
const missingKeyLabels = missingKeys.map((k) => {
|
|
2581
|
+
const aliases = aliasMap[k] ?? [k];
|
|
2582
|
+
return aliases.length > 1 ? aliases.join(" / ") : aliases[0];
|
|
2583
|
+
});
|
|
2584
|
+
return withHints({
|
|
2585
|
+
error: `Missing required config keys for ${data.nodeType}: ${missingKeyLabels.join(", ")}`,
|
|
2586
|
+
}, {
|
|
2587
|
+
action: `Provide the required config fields: ${missingKeyLabels.join(", ")}`,
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
const targetNodeId = data.parentNodeId;
|
|
2591
|
+
let mode = data.mode ?? "append";
|
|
2592
|
+
if (!targetNodeId) {
|
|
2593
|
+
return withHints({ error: "parentNodeId is required for create operation." }, {
|
|
2594
|
+
action: "Specify the parentNodeId of a node inside the appropriate tool branch where the new node should be created.",
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
// Auto-rewrite appendChild → append for node types where appendChild
|
|
2598
|
+
// creates orphaned nodes (parentId: null). This covers:
|
|
2599
|
+
// • aiAgentJobTool — so nodes land in the tool's execution chain
|
|
2600
|
+
// • then / else / case / default — branching children of if and switch
|
|
2601
|
+
const REWRITE_TYPES = new Set([
|
|
2602
|
+
"aiAgentJobTool",
|
|
2603
|
+
"then",
|
|
2604
|
+
"else",
|
|
2605
|
+
"case",
|
|
2606
|
+
"default",
|
|
2607
|
+
]);
|
|
2608
|
+
if (mode === "appendChild" && targetNodeId) {
|
|
2609
|
+
try {
|
|
2610
|
+
const targetCheck = await this.apiClient.get(`/v2.0/flows/${flowId}/chart/nodes/${targetNodeId}`);
|
|
2611
|
+
if (targetCheck && REWRITE_TYPES.has(targetCheck.type)) {
|
|
2612
|
+
mode = "append";
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
catch {
|
|
2616
|
+
// If the check fails, proceed with the original mode.
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
const apiConfig = data.config
|
|
2620
|
+
? transformConfigForApi(entry.type, data.config)
|
|
2621
|
+
: undefined;
|
|
2622
|
+
const createdNode = await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
2623
|
+
type: entry.type,
|
|
2624
|
+
extension: entry.extension,
|
|
2625
|
+
mode,
|
|
2626
|
+
target: targetNodeId,
|
|
2627
|
+
label: data.label,
|
|
2628
|
+
...(apiConfig && Object.keys(apiConfig).length > 0
|
|
2629
|
+
? { config: apiConfig }
|
|
2630
|
+
: {}),
|
|
2631
|
+
});
|
|
2632
|
+
const nodeId = createdNode._id || createdNode.id;
|
|
2633
|
+
const actualParentId = createdNode.parentId ??
|
|
2634
|
+
createdNode.parent_id ??
|
|
2635
|
+
(createdNode.parent &&
|
|
2636
|
+
(createdNode.parent._id || createdNode.parent.id));
|
|
2637
|
+
return {
|
|
2638
|
+
nodeId,
|
|
2639
|
+
type: entry.type,
|
|
2640
|
+
label: data.label,
|
|
2641
|
+
...(actualParentId ? { parentId: actualParentId } : {}),
|
|
2642
|
+
targetNodeId,
|
|
2643
|
+
mode,
|
|
2644
|
+
configApplied: data.config ? Object.keys(data.config) : [],
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
// ----- UPDATE -----
|
|
2648
|
+
case "update": {
|
|
2649
|
+
if (!data.nodeId) {
|
|
2650
|
+
return withHints({ error: "nodeId is required for update operation." }, {
|
|
2651
|
+
action: 'Use manage_flow_nodes { operation: "list", flowId } to find node IDs.',
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
if (!data.config && !data.label) {
|
|
2655
|
+
return withHints({ error: "Nothing to update. Provide at least label or config." }, { action: "Include fields to update in the request." });
|
|
2656
|
+
}
|
|
2657
|
+
const patchPayload = {};
|
|
2658
|
+
if (data.label)
|
|
2659
|
+
patchPayload.label = data.label;
|
|
2660
|
+
if (data.config) {
|
|
2661
|
+
const existingNode = await this.apiClient.get(`/v2.0/flows/${flowId}/chart/nodes/${data.nodeId}`);
|
|
2662
|
+
const nodeType = existingNode?.type ?? "";
|
|
2663
|
+
// Handle case node updates — the Cognigy API expects exactly
|
|
2664
|
+
// { config: { case: { value: "..." } } } with no extra fields merged in.
|
|
2665
|
+
if (nodeType === "case") {
|
|
2666
|
+
if (data.config.value !== undefined) {
|
|
2667
|
+
patchPayload.config = { case: { value: data.config.value } };
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
// Handle switch node updates — if cases array is provided, patch each
|
|
2671
|
+
// child case node with its value, then update the switch node itself.
|
|
2672
|
+
else if (nodeType === "switch" && Array.isArray(data.config.cases)) {
|
|
2673
|
+
const casesToUpdate = data.config.cases;
|
|
2674
|
+
const caseResults = [];
|
|
2675
|
+
for (const c of casesToUpdate) {
|
|
2676
|
+
if (!c.id || c.value === undefined)
|
|
2677
|
+
continue;
|
|
2678
|
+
try {
|
|
2679
|
+
await this.apiClient.patch(`/v2.0/flows/${flowId}/chart/nodes/${c.id}`, { config: { case: { value: c.value } } });
|
|
2680
|
+
caseResults.push({ id: c.id, value: c.value, updated: true });
|
|
2681
|
+
}
|
|
2682
|
+
catch (err) {
|
|
2683
|
+
caseResults.push({
|
|
2684
|
+
id: c.id,
|
|
2685
|
+
value: c.value,
|
|
2686
|
+
updated: false,
|
|
2687
|
+
error: err.message,
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
// Update the switch node itself (without the cases array)
|
|
2692
|
+
const { cases: _cases, ...switchConfig } = data.config;
|
|
2693
|
+
if (Object.keys(switchConfig).length > 0) {
|
|
2694
|
+
const transformed = transformConfigForApi(nodeType, switchConfig);
|
|
2695
|
+
const existingConfig = existingNode?.config ?? {};
|
|
2696
|
+
patchPayload.config = deepMerge(existingConfig, transformed);
|
|
2697
|
+
}
|
|
2698
|
+
if (Object.keys(patchPayload).length > 0) {
|
|
2699
|
+
await this.apiClient.patch(`/v2.0/flows/${flowId}/chart/nodes/${data.nodeId}`, patchPayload);
|
|
2700
|
+
}
|
|
2701
|
+
return {
|
|
2702
|
+
updated: true,
|
|
2703
|
+
nodeId: data.nodeId,
|
|
2704
|
+
...(data.label ? { label: data.label } : {}),
|
|
2705
|
+
...(data.config
|
|
2706
|
+
? { configUpdated: Object.keys(data.config) }
|
|
2707
|
+
: {}),
|
|
2708
|
+
casesUpdated: caseResults,
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
else {
|
|
2712
|
+
const transformed = transformConfigForApi(nodeType, data.config);
|
|
2713
|
+
const existingConfig = existingNode?.config ?? {};
|
|
2714
|
+
patchPayload.config = deepMerge(existingConfig, transformed);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
await this.apiClient.patch(`/v2.0/flows/${flowId}/chart/nodes/${data.nodeId}`, patchPayload);
|
|
2718
|
+
return {
|
|
2719
|
+
updated: true,
|
|
2720
|
+
nodeId: data.nodeId,
|
|
2721
|
+
...(data.label ? { label: data.label } : {}),
|
|
2722
|
+
...(data.config ? { configUpdated: Object.keys(data.config) } : {}),
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
// ----- DELETE -----
|
|
2726
|
+
case "delete": {
|
|
2727
|
+
if (!data.nodeId) {
|
|
2728
|
+
return withHints({ error: "nodeId is required for delete operation." }, {
|
|
2729
|
+
action: 'Use manage_flow_nodes { operation: "list", flowId } to find node IDs.',
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
await this.apiClient.delete(`/v2.0/flows/${flowId}/chart/nodes/${data.nodeId}`);
|
|
2733
|
+
return { deleted: true, nodeId: data.nodeId };
|
|
2734
|
+
}
|
|
2735
|
+
default:
|
|
2736
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
// =========================================================================
|
|
2740
|
+
// Tool 11: manage_webchat
|
|
2741
|
+
// =========================================================================
|
|
2742
|
+
async handleManageWebchat(args) {
|
|
2743
|
+
const data = schemas.manageWebchatSchema.parse(args);
|
|
2744
|
+
const webchatSettings = buildWebchatSettings(data);
|
|
2745
|
+
const settingsKeys = Object.keys(webchatSettings).filter((k) => k !== "demoWebchat");
|
|
2746
|
+
const hasSettings = settingsKeys.length > 0;
|
|
2747
|
+
let endpointId = data.endpointId ?? null;
|
|
2748
|
+
// CREATE when no endpointId provided, UPDATE when endpointId is explicit
|
|
2749
|
+
if (!endpointId) {
|
|
2750
|
+
if (!data.projectId) {
|
|
2751
|
+
return withHints({ error: "projectId is required to create a webchat endpoint." }, {
|
|
2752
|
+
action: "Provide projectId. Use list_resources { resourceType: 'project' } to find one.",
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
if (!data.flowId) {
|
|
2756
|
+
return withHints({
|
|
2757
|
+
error: "flowId is required to create a webchat endpoint. To update an existing one, provide endpointId instead.",
|
|
2758
|
+
}, {
|
|
2759
|
+
action: "Provide flowId. Use list_resources { resourceType: 'flow', projectId } to find one, or create an agent first with create_ai_agent.",
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
let localeId;
|
|
2763
|
+
try {
|
|
2764
|
+
const flow = await this.apiClient.get(`/v2.0/flows/${data.flowId}`);
|
|
2765
|
+
localeId = flow?.localeReference;
|
|
2766
|
+
}
|
|
2767
|
+
catch {
|
|
2768
|
+
// Non-critical
|
|
2769
|
+
}
|
|
2770
|
+
const createPayload = {
|
|
2771
|
+
projectId: data.projectId,
|
|
2772
|
+
entrypoint: data.projectId,
|
|
2773
|
+
channel: "webchat3",
|
|
2774
|
+
flowId: data.flowId,
|
|
2775
|
+
name: data.name || "Webchat",
|
|
2776
|
+
targetType: "flow",
|
|
2777
|
+
agentId: "",
|
|
2778
|
+
};
|
|
2779
|
+
if (localeId)
|
|
2780
|
+
createPayload.localeId = localeId;
|
|
2781
|
+
try {
|
|
2782
|
+
const createdEndpoint = await this.apiClient.post("/v2.0/endpoints", createPayload);
|
|
2783
|
+
endpointId = createdEndpoint._id || createdEndpoint.id;
|
|
2784
|
+
// Re-fetch to guarantee URLToken and full settings are available
|
|
2785
|
+
let endpoint = await this.apiClient.get(`/v2.0/endpoints/${endpointId}`);
|
|
2786
|
+
let settingsApplied = false;
|
|
2787
|
+
if (hasSettings) {
|
|
2788
|
+
try {
|
|
2789
|
+
const mergedSettings = this.mergeWebchatSettings(endpoint.settings ?? {}, webchatSettings);
|
|
2790
|
+
await this.apiClient.patch(`/v2.0/endpoints/${endpointId}`, {
|
|
2791
|
+
settings: mergedSettings,
|
|
2792
|
+
});
|
|
2793
|
+
endpoint = await this.apiClient.get(`/v2.0/endpoints/${endpointId}`);
|
|
2794
|
+
settingsApplied = true;
|
|
2795
|
+
}
|
|
2796
|
+
catch {
|
|
2797
|
+
// Settings patch failed but endpoint was created — continue
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
const response = this.buildWebchatResponse({
|
|
2801
|
+
created: true,
|
|
2802
|
+
endpointId: endpointId,
|
|
2803
|
+
endpoint,
|
|
2804
|
+
settingsKeys: settingsApplied ? settingsKeys : [],
|
|
2805
|
+
});
|
|
2806
|
+
if (hasSettings && !settingsApplied) {
|
|
2807
|
+
return withHints(response, {
|
|
2808
|
+
warning: "Endpoint created but settings failed to apply.",
|
|
2809
|
+
action: `Retry settings by calling manage_webchat { endpointId: "${endpointId}", ...settings }`,
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
return response;
|
|
2813
|
+
}
|
|
2814
|
+
catch (error) {
|
|
2815
|
+
return withHints({ error: `Failed to create webchat endpoint: ${error.message}` }, {
|
|
2816
|
+
action: "Check projectId and flowId, then retry.",
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
// UPDATE: patch existing endpoint
|
|
2821
|
+
if (!data.name && !hasSettings) {
|
|
2822
|
+
const ep = await this.safeGetEndpoint(endpointId);
|
|
2823
|
+
if (ep) {
|
|
2824
|
+
return this.buildWebchatResponse({
|
|
2825
|
+
endpointId: endpointId,
|
|
2826
|
+
endpoint: ep,
|
|
2827
|
+
settingsKeys: [],
|
|
2828
|
+
note: "No changes requested. Returning current endpoint info.",
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
return withHints({
|
|
2832
|
+
error: "Nothing to update. Provide at least one setting group or name.",
|
|
2833
|
+
}, {
|
|
2834
|
+
action: "Include layout, behavior, homeScreen, or other setting groups.",
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
try {
|
|
2838
|
+
// Read-merge-write: fetch full settings, merge our changes, send complete object
|
|
2839
|
+
const fullEndpoint = await this.apiClient.get(`/v2.0/endpoints/${endpointId}`);
|
|
2840
|
+
const existingSettings = fullEndpoint.settings ?? {};
|
|
2841
|
+
const mergedSettings = this.mergeWebchatSettings(existingSettings, webchatSettings);
|
|
2842
|
+
const patchPayload = { settings: mergedSettings };
|
|
2843
|
+
if (data.name)
|
|
2844
|
+
patchPayload.name = data.name;
|
|
2845
|
+
if (data.flowId)
|
|
2846
|
+
patchPayload.flowId = data.flowId;
|
|
2847
|
+
await this.apiClient.patch(`/v2.0/endpoints/${endpointId}`, patchPayload);
|
|
2848
|
+
const endpoint = await this.apiClient.get(`/v2.0/endpoints/${endpointId}`);
|
|
2849
|
+
return this.buildWebchatResponse({
|
|
2850
|
+
updated: true,
|
|
2851
|
+
endpointId: endpointId,
|
|
2852
|
+
endpoint,
|
|
2853
|
+
settingsKeys,
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
catch (error) {
|
|
2857
|
+
return withHints({ error: `Failed to update webchat endpoint: ${error.message}` }, {
|
|
2858
|
+
action: "Verify endpointId and settings, then retry.",
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Build a consistent webchat response. The demo URL is always the top-level
|
|
2864
|
+
* field so the LLM surfaces it by default. Integration details (configUrl,
|
|
2865
|
+
* embeddingSnippet) are nested under _integration so the LLM only mentions
|
|
2866
|
+
* them when the user explicitly asks about embedding.
|
|
2867
|
+
*/
|
|
2868
|
+
buildWebchatResponse(opts) {
|
|
2869
|
+
const { endpoint } = opts;
|
|
2870
|
+
const demoWebchatUrl = this.buildDemoWebchatUrl(endpoint);
|
|
2871
|
+
const configUrl = this.buildConfigUrl(endpoint);
|
|
2872
|
+
const result = {};
|
|
2873
|
+
if (opts.created)
|
|
2874
|
+
result.created = true;
|
|
2875
|
+
if (opts.updated)
|
|
2876
|
+
result.updated = true;
|
|
2877
|
+
result.endpointId = opts.endpointId;
|
|
2878
|
+
result.name = endpoint.name;
|
|
2879
|
+
result.channel = endpoint.channel ?? "webchat3";
|
|
2880
|
+
result.demoWebchatUrl = demoWebchatUrl;
|
|
2881
|
+
if (opts.settingsKeys.length > 0)
|
|
2882
|
+
result.settingsApplied = opts.settingsKeys;
|
|
2883
|
+
if (opts.note)
|
|
2884
|
+
result.note = opts.note;
|
|
2885
|
+
result._integration = {
|
|
2886
|
+
configUrl,
|
|
2887
|
+
embeddingSnippet: `<script src="https://github.com/Cognigy/Webchat/releases/latest/download/webchat.js"></script>\n<script>window.cognigyWebchat.open({ configUrl: "${configUrl}" });</script>`,
|
|
2888
|
+
};
|
|
2889
|
+
result._instruction =
|
|
2890
|
+
"ALWAYS show demoWebchatUrl to the user as a clickable link. This is the live demo page they can open in a browser right now. Only mention _integration details if the user asks about embedding or deploying to their website.";
|
|
2891
|
+
return result;
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Merge partial webchat settings into a full existing settings object.
|
|
2895
|
+
* The v3 API validation destructures nested groups (colors, layout, behavior,
|
|
2896
|
+
* startBehavior, demoWebchat, fileStorageSettings, chatOptions) and crashes
|
|
2897
|
+
* if any top-level group is missing. We must send the complete settings object.
|
|
2898
|
+
*/
|
|
2899
|
+
mergeWebchatSettings(existing, updates) {
|
|
2900
|
+
return deepMerge(existing, updates);
|
|
2901
|
+
}
|
|
2902
|
+
async safeGetEndpoint(endpointId) {
|
|
2903
|
+
try {
|
|
2904
|
+
return await this.apiClient.get(`/v2.0/endpoints/${endpointId}`);
|
|
2905
|
+
}
|
|
2906
|
+
catch {
|
|
2907
|
+
return null;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
buildDemoWebchatUrl(endpoint) {
|
|
2911
|
+
if (!endpoint.URLToken || !this.webchatBaseUrl)
|
|
2912
|
+
return undefined;
|
|
2913
|
+
return `${this.webchatBaseUrl}/v3/${endpoint.URLToken}`;
|
|
2914
|
+
}
|
|
2915
|
+
buildConfigUrl(endpoint) {
|
|
2916
|
+
if (!endpoint.URLToken)
|
|
2917
|
+
return "URL not available";
|
|
2918
|
+
return `${this.endpointBaseUrl}/${endpoint.URLToken}`;
|
|
2919
|
+
}
|
|
2920
|
+
// =========================================================================
|
|
2921
|
+
// Voice Gateway
|
|
2922
|
+
// =========================================================================
|
|
2923
|
+
async handleManageVoiceGateway(args) {
|
|
2924
|
+
const data = schemas.manageVoiceGatewaySchema.parse(args);
|
|
2925
|
+
let endpointId = data.endpointId ?? null;
|
|
2926
|
+
// ---- CREATE ----
|
|
2927
|
+
if (!endpointId) {
|
|
2928
|
+
if (!data.projectId) {
|
|
2929
|
+
return withHints({
|
|
2930
|
+
error: "projectId is required to create a voice gateway endpoint.",
|
|
2931
|
+
}, {
|
|
2932
|
+
action: "Provide projectId. Use list_resources { resourceType: 'project' } to find one.",
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
if (!data.flowId) {
|
|
2936
|
+
return withHints({
|
|
2937
|
+
error: "flowId is required to create a voice gateway endpoint. To update an existing one, provide endpointId instead.",
|
|
2938
|
+
}, {
|
|
2939
|
+
action: "Provide flowId. Use list_resources { resourceType: 'flow', projectId } to find one, or create an agent first with create_ai_agent.",
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
// Resolve locale — try flow first, fall back to project's primary locale
|
|
2943
|
+
let localeId;
|
|
2944
|
+
try {
|
|
2945
|
+
const flow = await this.apiClient.get(`/v2.0/flows/${data.flowId}`);
|
|
2946
|
+
localeId = flow?.localeReference;
|
|
2947
|
+
}
|
|
2948
|
+
catch {
|
|
2949
|
+
// Fall through to project locale
|
|
2950
|
+
}
|
|
2951
|
+
if (!localeId) {
|
|
2952
|
+
try {
|
|
2953
|
+
const locales = await this.apiClient.get("/v2.0/locales", {
|
|
2954
|
+
params: { projectId: data.projectId },
|
|
2955
|
+
});
|
|
2956
|
+
const items = locales?.items ?? locales;
|
|
2957
|
+
if (Array.isArray(items) && items.length > 0) {
|
|
2958
|
+
localeId = items[0].referenceId ?? items[0]._id;
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
catch {
|
|
2962
|
+
// Non-critical — endpoint will be created without locale
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
// Step 1: Create voiceGateway2 endpoint
|
|
2966
|
+
const createPayload = {
|
|
2967
|
+
projectId: data.projectId,
|
|
2968
|
+
entrypoint: data.projectId,
|
|
2969
|
+
channel: "voiceGateway2",
|
|
2970
|
+
flowId: data.flowId,
|
|
2971
|
+
name: data.name || "Voice Gateway",
|
|
2972
|
+
targetType: "flow",
|
|
2973
|
+
agentId: "",
|
|
2974
|
+
};
|
|
2975
|
+
if (localeId)
|
|
2976
|
+
createPayload.localeId = localeId;
|
|
2977
|
+
let endpoint;
|
|
2978
|
+
try {
|
|
2979
|
+
const created = await this.apiClient.post("/new/v2.0/endpoints", createPayload);
|
|
2980
|
+
endpointId = created._id || created.id;
|
|
2981
|
+
endpoint = await this.apiClient.get(`/new/v2.0/endpoints/${endpointId}`);
|
|
2982
|
+
}
|
|
2983
|
+
catch (error) {
|
|
2984
|
+
return withHints({
|
|
2985
|
+
error: `Failed to create voice gateway endpoint: ${error.message}`,
|
|
2986
|
+
}, {
|
|
2987
|
+
action: "Check projectId and flowId, then retry.",
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
// Step 2: Provision WebRTC client
|
|
2991
|
+
const userWidgetConfig = data.webrtcWidgetConfig ?? {};
|
|
2992
|
+
const webrtcWidgetConfig = {
|
|
2993
|
+
label: userWidgetConfig.label ?? "",
|
|
2994
|
+
active: true,
|
|
2995
|
+
theme: userWidgetConfig.theme ?? "DARK_MODE",
|
|
2996
|
+
transcription: {
|
|
2997
|
+
enabled: userWidgetConfig.transcription?.enabled ?? true,
|
|
2998
|
+
backgroundMode: userWidgetConfig.transcription?.backgroundMode ?? "transparent",
|
|
2999
|
+
},
|
|
3000
|
+
demoPage: {
|
|
3001
|
+
background: {
|
|
3002
|
+
mode: userWidgetConfig.demoPage?.background?.mode ?? "color",
|
|
3003
|
+
color: userWidgetConfig.demoPage?.background?.color ?? "#FFFFFF",
|
|
3004
|
+
},
|
|
3005
|
+
position: userWidgetConfig.demoPage?.position ?? "centered",
|
|
3006
|
+
},
|
|
3007
|
+
...(userWidgetConfig.avatarLogoUrl
|
|
3008
|
+
? { avatarLogoUrl: userWidgetConfig.avatarLogoUrl }
|
|
3009
|
+
: {}),
|
|
3010
|
+
...(userWidgetConfig.tagline
|
|
3011
|
+
? { tagline: userWidgetConfig.tagline }
|
|
3012
|
+
: {}),
|
|
3013
|
+
};
|
|
3014
|
+
try {
|
|
3015
|
+
await this.apiClient.patch(`/new/v2.0/endpoints/${endpointId}`, {
|
|
3016
|
+
createWebrtcClient: true,
|
|
3017
|
+
channel: "voiceGateway2",
|
|
3018
|
+
name: endpoint.name,
|
|
3019
|
+
URLToken: endpoint.URLToken,
|
|
3020
|
+
localeId: endpoint.localeId ?? localeId,
|
|
3021
|
+
webrtcWidgetConfig,
|
|
3022
|
+
});
|
|
3023
|
+
endpoint = await this.apiClient.get(`/new/v2.0/endpoints/${endpointId}`);
|
|
3024
|
+
}
|
|
3025
|
+
catch (error) {
|
|
3026
|
+
// Endpoint created but WebRTC failed — still return what we have
|
|
3027
|
+
return withHints(this.buildVoiceGatewayResponse({
|
|
3028
|
+
created: true,
|
|
3029
|
+
endpointId: endpointId,
|
|
3030
|
+
endpoint,
|
|
3031
|
+
webrtcProvisioned: false,
|
|
3032
|
+
}), {
|
|
3033
|
+
warning: `Endpoint created but WebRTC client provisioning failed: ${error.message}`,
|
|
3034
|
+
action: `Retry by calling manage_voice_gateway { endpointId: "${endpointId}" }`,
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
return this.buildVoiceGatewayResponse({
|
|
3038
|
+
created: true,
|
|
3039
|
+
endpointId: endpointId,
|
|
3040
|
+
endpoint,
|
|
3041
|
+
webrtcProvisioned: true,
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
// ---- UPDATE ----
|
|
3045
|
+
try {
|
|
3046
|
+
let endpoint = await this.apiClient.get(`/new/v2.0/endpoints/${endpointId}`);
|
|
3047
|
+
const patchPayload = {};
|
|
3048
|
+
if (data.name)
|
|
3049
|
+
patchPayload.name = data.name;
|
|
3050
|
+
if (data.flowId)
|
|
3051
|
+
patchPayload.flowId = data.flowId;
|
|
3052
|
+
if (data.webrtcWidgetConfig) {
|
|
3053
|
+
const existing = endpoint.webrtcWidgetConfig ?? {};
|
|
3054
|
+
patchPayload.webrtcWidgetConfig = {
|
|
3055
|
+
...existing,
|
|
3056
|
+
...data.webrtcWidgetConfig,
|
|
3057
|
+
transcription: {
|
|
3058
|
+
...(existing.transcription ?? {}),
|
|
3059
|
+
...(data.webrtcWidgetConfig.transcription ?? {}),
|
|
3060
|
+
},
|
|
3061
|
+
demoPage: {
|
|
3062
|
+
...(existing.demoPage ?? {}),
|
|
3063
|
+
...(data.webrtcWidgetConfig.demoPage ?? {}),
|
|
3064
|
+
background: {
|
|
3065
|
+
...(existing.demoPage?.background ?? {}),
|
|
3066
|
+
...(data.webrtcWidgetConfig.demoPage?.background ?? {}),
|
|
3067
|
+
},
|
|
3068
|
+
},
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
// If no WebRTC client yet, provision it
|
|
3072
|
+
if (!endpoint.webrtcClient) {
|
|
3073
|
+
patchPayload.createWebrtcClient = true;
|
|
3074
|
+
patchPayload.channel = "voiceGateway2";
|
|
3075
|
+
patchPayload.URLToken = endpoint.URLToken;
|
|
3076
|
+
if (!patchPayload.webrtcWidgetConfig) {
|
|
3077
|
+
patchPayload.webrtcWidgetConfig = {
|
|
3078
|
+
label: "",
|
|
3079
|
+
active: true,
|
|
3080
|
+
theme: "DARK_MODE",
|
|
3081
|
+
transcription: { enabled: true, backgroundMode: "transparent" },
|
|
3082
|
+
demoPage: {
|
|
3083
|
+
background: { mode: "color", color: "#FFFFFF" },
|
|
3084
|
+
position: "centered",
|
|
3085
|
+
},
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
if (Object.keys(patchPayload).length === 0) {
|
|
3090
|
+
return this.buildVoiceGatewayResponse({
|
|
3091
|
+
endpointId: endpointId,
|
|
3092
|
+
endpoint,
|
|
3093
|
+
webrtcProvisioned: !!endpoint.webrtcClient,
|
|
3094
|
+
note: "No changes requested. Returning current endpoint info.",
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
await this.apiClient.patch(`/new/v2.0/endpoints/${endpointId}`, patchPayload);
|
|
3098
|
+
endpoint = await this.apiClient.get(`/new/v2.0/endpoints/${endpointId}`);
|
|
3099
|
+
return this.buildVoiceGatewayResponse({
|
|
3100
|
+
updated: true,
|
|
3101
|
+
endpointId: endpointId,
|
|
3102
|
+
endpoint,
|
|
3103
|
+
webrtcProvisioned: !!endpoint.webrtcClient,
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
catch (error) {
|
|
3107
|
+
return withHints({ error: `Failed to update voice gateway endpoint: ${error.message}` }, {
|
|
3108
|
+
action: "Verify endpointId and settings, then retry.",
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
// =========================================================================
|
|
3113
|
+
// Settings
|
|
3114
|
+
// =========================================================================
|
|
3115
|
+
static SPEECH_PROVIDER_TYPE_MAP = {
|
|
3116
|
+
microsoft: "MicrosoftSpeechProvider",
|
|
3117
|
+
google: "GoogleSpeechProvider",
|
|
3118
|
+
aws: "AWSSpeechProvider",
|
|
3119
|
+
deepgram: "DeepgramSpeechProvider",
|
|
3120
|
+
elevenlabs: "ElevenLabsSpeechProvider",
|
|
3121
|
+
};
|
|
3122
|
+
async handleManageSettings(args) {
|
|
3123
|
+
const data = schemas.manageSettingsSchema.parse(args);
|
|
3124
|
+
switch (data.operation) {
|
|
3125
|
+
case "set_voice_preview": {
|
|
3126
|
+
const { projectId, provider } = data;
|
|
3127
|
+
let connectionRefId = data.connectionId;
|
|
3128
|
+
// Auto-detect speech connection if not provided
|
|
3129
|
+
if (!connectionRefId) {
|
|
3130
|
+
const providerType = ToolHandlers.SPEECH_PROVIDER_TYPE_MAP[provider] ?? provider;
|
|
3131
|
+
try {
|
|
3132
|
+
const connections = await this.apiClient.get("/new/v2.0/connections", { params: { projectId } });
|
|
3133
|
+
const items = connections?.items ?? connections;
|
|
3134
|
+
const match = (Array.isArray(items) ? items : []).find((c) => c.extension === "@cognigy/audio-preview-provider" &&
|
|
3135
|
+
c.type === providerType);
|
|
3136
|
+
if (match) {
|
|
3137
|
+
connectionRefId = match.referenceId ?? match._id;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
catch {
|
|
3141
|
+
// Fall through — will report missing connection
|
|
3142
|
+
}
|
|
3143
|
+
if (!connectionRefId) {
|
|
3144
|
+
return withHints({
|
|
3145
|
+
error: `No speech connection found for provider "${provider}".`,
|
|
3146
|
+
provider,
|
|
3147
|
+
providerType,
|
|
3148
|
+
}, {
|
|
3149
|
+
action: `Upload a package containing a "${providerType}" speech connection using manage_packages { operation: "upload_and_inspect", projectId: "${projectId}", filePath: "<path>" }, import it, then retry this operation.`,
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
// PATCH project settings
|
|
3154
|
+
try {
|
|
3155
|
+
await this.apiClient.patch(`/new/v2.0/projects/${projectId}/settings`, {
|
|
3156
|
+
audioPreviewSettings: {
|
|
3157
|
+
provider,
|
|
3158
|
+
connections: {
|
|
3159
|
+
[provider]: { connectionId: connectionRefId },
|
|
3160
|
+
},
|
|
3161
|
+
},
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
catch (error) {
|
|
3165
|
+
return withHints({
|
|
3166
|
+
error: `Failed to update voice preview settings: ${error.message}`,
|
|
3167
|
+
}, {
|
|
3168
|
+
action: "Verify projectId and connectionId, then retry.",
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
return {
|
|
3172
|
+
updated: true,
|
|
3173
|
+
provider,
|
|
3174
|
+
connectionId: connectionRefId,
|
|
3175
|
+
_hint: "Voice preview settings configured. You can now use manage_voice_gateway to create a voice endpoint, or test voice preview in the Cognigy UI.",
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
case "set_knowledge_ai": {
|
|
3179
|
+
const patchPayload = {};
|
|
3180
|
+
const updatedFields = [];
|
|
3181
|
+
if (data.knowledgeSearchModelId || data.answerExtractionModelId) {
|
|
3182
|
+
patchPayload.generativeAISettings = {
|
|
3183
|
+
enabled: true,
|
|
3184
|
+
useCasesSettings: {},
|
|
3185
|
+
};
|
|
3186
|
+
if (data.knowledgeSearchModelId) {
|
|
3187
|
+
patchPayload.generativeAISettings.useCasesSettings.knowledgeSearch =
|
|
3188
|
+
{
|
|
3189
|
+
largeLanguageModelId: data.knowledgeSearchModelId,
|
|
3190
|
+
};
|
|
3191
|
+
updatedFields.push("knowledgeSearchModelId");
|
|
3192
|
+
}
|
|
3193
|
+
if (data.answerExtractionModelId) {
|
|
3194
|
+
patchPayload.generativeAISettings.useCasesSettings.answerExtraction =
|
|
3195
|
+
{
|
|
3196
|
+
largeLanguageModelId: data.answerExtractionModelId,
|
|
3197
|
+
};
|
|
3198
|
+
updatedFields.push("answerExtractionModelId");
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
if (data.contentParser !== undefined ||
|
|
3202
|
+
data.azureDIConnectionId !== undefined) {
|
|
3203
|
+
patchPayload.knowledgeAISettings = {};
|
|
3204
|
+
if (data.contentParser !== undefined) {
|
|
3205
|
+
patchPayload.knowledgeAISettings.fileExtractor = data.contentParser;
|
|
3206
|
+
updatedFields.push("contentParser");
|
|
3207
|
+
}
|
|
3208
|
+
if (data.azureDIConnectionId !== undefined) {
|
|
3209
|
+
patchPayload.knowledgeAISettings.azureDIConnectionId =
|
|
3210
|
+
data.azureDIConnectionId;
|
|
3211
|
+
if (!updatedFields.includes("azureDIConnectionId")) {
|
|
3212
|
+
updatedFields.push("azureDIConnectionId");
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
try {
|
|
3217
|
+
await this.apiClient.patch(`/new/v2.0/projects/${data.projectId}/settings`, patchPayload);
|
|
3218
|
+
}
|
|
3219
|
+
catch (error) {
|
|
3220
|
+
let allowedKnowledgeSearchModels;
|
|
3221
|
+
let allowedKnowledgeSearchModelsError;
|
|
3222
|
+
if (data.knowledgeSearchModelId) {
|
|
3223
|
+
try {
|
|
3224
|
+
const res = await this.apiClient.get("/new/v2.0/largelanguagemodels", {
|
|
3225
|
+
params: {
|
|
3226
|
+
projectId: data.projectId,
|
|
3227
|
+
useCase: "knowledgeSearch",
|
|
3228
|
+
limit: 100,
|
|
3229
|
+
},
|
|
3230
|
+
});
|
|
3231
|
+
const items = res.items ?? res;
|
|
3232
|
+
allowedKnowledgeSearchModels = filterList("llm_model", Array.isArray(items) ? items : []);
|
|
3233
|
+
}
|
|
3234
|
+
catch (candidateError) {
|
|
3235
|
+
allowedKnowledgeSearchModelsError = candidateError.message;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
return withHints({
|
|
3239
|
+
error: `Failed to update Knowledge AI settings: ${error.message}`,
|
|
3240
|
+
...(allowedKnowledgeSearchModels
|
|
3241
|
+
? { allowedKnowledgeSearchModels }
|
|
3242
|
+
: {}),
|
|
3243
|
+
...(allowedKnowledgeSearchModelsError
|
|
3244
|
+
? {
|
|
3245
|
+
allowedKnowledgeSearchModelsError,
|
|
3246
|
+
}
|
|
3247
|
+
: {}),
|
|
3248
|
+
}, {
|
|
3249
|
+
action: `Verify the projectId, same-project llm_model referenceIds, and content parser connection details, then retry. For Knowledge Search, call list_resources { resourceType: "llm_model", projectId: "${data.projectId}", useCase: "knowledgeSearch" } to match the Settings UI dropdown before choosing another model. If you are reusing another project's knowledge workflow, ensure the exact source-project Knowledge Search model has already been imported into this project before trying a different model.`,
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
return {
|
|
3253
|
+
updated: true,
|
|
3254
|
+
updatedFields,
|
|
3255
|
+
...(data.knowledgeSearchModelId
|
|
3256
|
+
? { knowledgeSearchModelId: data.knowledgeSearchModelId }
|
|
3257
|
+
: {}),
|
|
3258
|
+
...(data.answerExtractionModelId
|
|
3259
|
+
? { answerExtractionModelId: data.answerExtractionModelId }
|
|
3260
|
+
: {}),
|
|
3261
|
+
...(data.contentParser ? { contentParser: data.contentParser } : {}),
|
|
3262
|
+
...(data.azureDIConnectionId
|
|
3263
|
+
? { azureDIConnectionId: data.azureDIConnectionId }
|
|
3264
|
+
: {}),
|
|
3265
|
+
...(data.knowledgeSearchModelId || data.answerExtractionModelId
|
|
3266
|
+
? { generativeAIEnabled: true }
|
|
3267
|
+
: {}),
|
|
3268
|
+
_hint: "Knowledge AI settings configured. If you are preparing a new project, ensure the referenced LLMs already exist in this project before creating knowledge stores or answer extraction flows.",
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
default:
|
|
3272
|
+
throw new Error(`Unknown operation: ${data.operation}`);
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
buildVoiceGatewayResponse(opts) {
|
|
3276
|
+
const { endpoint } = opts;
|
|
3277
|
+
const webrtcDemoUrl = this.buildWebrtcDemoUrl(endpoint);
|
|
3278
|
+
const wsEndpointUrl = this.buildVoiceGatewayWsUrl(endpoint);
|
|
3279
|
+
const result = {};
|
|
3280
|
+
if (opts.created)
|
|
3281
|
+
result.created = true;
|
|
3282
|
+
if (opts.updated)
|
|
3283
|
+
result.updated = true;
|
|
3284
|
+
result.endpointId = opts.endpointId;
|
|
3285
|
+
result.name = endpoint.name;
|
|
3286
|
+
result.channel = "voiceGateway2";
|
|
3287
|
+
result.webrtcProvisioned = opts.webrtcProvisioned;
|
|
3288
|
+
result.webrtcDemoUrl = webrtcDemoUrl;
|
|
3289
|
+
if (opts.note)
|
|
3290
|
+
result.note = opts.note;
|
|
3291
|
+
result._integration = {
|
|
3292
|
+
wsEndpointUrl,
|
|
3293
|
+
embeddingSnippet: `<script src="https://github.com/Cognigy/WebRTCWidget/releases/latest/download/webRTCWidget.js"></script>\n<script>\n addEventListener("load", (event) => {\n if (window.initWebRTCWidget) {\n window.initWebRTCWidget("${wsEndpointUrl}");\n }\n });\n</script>`,
|
|
3294
|
+
};
|
|
3295
|
+
result._instruction =
|
|
3296
|
+
"ALWAYS show webrtcDemoUrl to the user as a clickable link. This is the live demo page they can open in a browser to talk to the agent via voice. Only mention _integration details if the user asks about embedding.";
|
|
3297
|
+
result._speechProviderHint =
|
|
3298
|
+
"Voice preview requires a speech provider. Ensure one is configured in Settings > Voice Preview Settings > Speech Provider, or use manage_settings { operation: 'set_voice_preview', projectId, provider } to set it via API.";
|
|
3299
|
+
return result;
|
|
3300
|
+
}
|
|
3301
|
+
buildWebrtcDemoUrl(endpoint) {
|
|
3302
|
+
if (!endpoint.URLToken || !this.staticFilesBaseUrl)
|
|
3303
|
+
return undefined;
|
|
3304
|
+
return `${this.staticFilesBaseUrl}/webrtc/?token=${endpoint.URLToken}`;
|
|
3305
|
+
}
|
|
3306
|
+
buildVoiceGatewayWsUrl(endpoint) {
|
|
3307
|
+
if (!endpoint.URLToken || !this.endpointBaseUrl)
|
|
3308
|
+
return undefined;
|
|
3309
|
+
const base = `${this.endpointBaseUrl}/${endpoint.URLToken}/voiceGateway`;
|
|
3310
|
+
return base.replace(/^http/, "ws");
|
|
3311
|
+
}
|
|
3312
|
+
// =========================================================================
|
|
3313
|
+
// Tool 16: audit_voice_agent
|
|
3314
|
+
// =========================================================================
|
|
3315
|
+
async handleAuditVoiceAgent(args) {
|
|
3316
|
+
const data = schemas.auditVoiceAgentSchema.parse(args);
|
|
3317
|
+
const { aiAgentId, endpointId, projectId, apply, only } = data;
|
|
3318
|
+
// Resolve the flow to audit.
|
|
3319
|
+
let flowId = data.flowId;
|
|
3320
|
+
if (!flowId) {
|
|
3321
|
+
const resolved = await resolveFlowForAgent(this.apiClient, aiAgentId);
|
|
3322
|
+
if (!resolved) {
|
|
3323
|
+
return withHints({ error: "Could not resolve a flow for this agent." }, {
|
|
3324
|
+
action: "Provide flowId directly, or ensure the agent was created via create_ai_agent.",
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
flowId = resolved.flowId;
|
|
3328
|
+
}
|
|
3329
|
+
// The `/chart/nodes` index returns NO `config` and NO ordering — only
|
|
3330
|
+
// id/type/label/preview/isEntryPoint/parentId. The checklist reads
|
|
3331
|
+
// `node.config.*` and the auto-fix PATCH merges against existing config, so
|
|
3332
|
+
// we must (a) enrich the inspected nodes with their per-node `config` and
|
|
3333
|
+
// (b) derive the true first node from the chart `next` chain. Using the bare
|
|
3334
|
+
// index would yield false failures and let the fix PATCH clobber config.
|
|
3335
|
+
const CONFIG_RELEVANT_TYPES = new Set(["setSessionConfig", "aiAgentJob"]);
|
|
3336
|
+
const fetchNodeIndex = async () => {
|
|
3337
|
+
const res = await this.apiClient.get(`/v2.0/flows/${flowId}/chart/nodes`, { params: { limit: 200 } });
|
|
3338
|
+
const items = res.items ?? res;
|
|
3339
|
+
return Array.isArray(items) ? items : [];
|
|
3340
|
+
};
|
|
3341
|
+
// Full `config` only comes from the per-node read. Enrich the node types the
|
|
3342
|
+
// checklist actually inspects; leave the rest as cheap index entries.
|
|
3343
|
+
const enrichConfig = async (index) => Promise.all(index.map(async (n) => {
|
|
3344
|
+
if (!CONFIG_RELEVANT_TYPES.has(n?.type))
|
|
3345
|
+
return n;
|
|
3346
|
+
try {
|
|
3347
|
+
const full = await this.apiClient.get(`/v2.0/flows/${flowId}/chart/nodes/${voiceNodeId(n)}`);
|
|
3348
|
+
return { ...n, config: full?.config ?? n.config ?? {} };
|
|
3349
|
+
}
|
|
3350
|
+
catch {
|
|
3351
|
+
return n;
|
|
3352
|
+
}
|
|
3353
|
+
}));
|
|
3354
|
+
// The true first node is the one the `start` node points at in the chart
|
|
3355
|
+
// `next` chain — NOT whatever reports isEntryPoint. Returns undefined if it
|
|
3356
|
+
// cannot be derived (the evaluator then warns instead of guessing).
|
|
3357
|
+
const fetchFirstNodeId = async () => {
|
|
3358
|
+
try {
|
|
3359
|
+
const chart = await this.apiClient.get(`/new/v2.0/flows/${flowId}/chart`);
|
|
3360
|
+
const rels = Array.isArray(chart?.relations) ? chart.relations : [];
|
|
3361
|
+
const chartNodes = Array.isArray(chart?.nodes) ? chart.nodes : [];
|
|
3362
|
+
const startNode = chartNodes.find((n) => n?.type === "start");
|
|
3363
|
+
const startId = startNode ? voiceNodeId(startNode) : undefined;
|
|
3364
|
+
if (!startId)
|
|
3365
|
+
return undefined;
|
|
3366
|
+
const startRel = rels.find((r) => r?.node === startId);
|
|
3367
|
+
const next = startRel?.next;
|
|
3368
|
+
const firstRef = Array.isArray(next) ? next[0] : next;
|
|
3369
|
+
if (!firstRef)
|
|
3370
|
+
return undefined;
|
|
3371
|
+
return typeof firstRef === "string" ? firstRef : voiceNodeId(firstRef);
|
|
3372
|
+
}
|
|
3373
|
+
catch {
|
|
3374
|
+
return undefined;
|
|
3375
|
+
}
|
|
3376
|
+
};
|
|
3377
|
+
const loadFlowState = async () => {
|
|
3378
|
+
const [nodes, firstNodeId] = await Promise.all([
|
|
3379
|
+
fetchNodeIndex().then(enrichConfig),
|
|
3380
|
+
fetchFirstNodeId(),
|
|
3381
|
+
]);
|
|
3382
|
+
return { nodes, firstNodeId };
|
|
3383
|
+
};
|
|
3384
|
+
// Tri-state: undefined = not requested, null = fetch failed, object = resolved.
|
|
3385
|
+
const fetchEndpoint = async () => {
|
|
3386
|
+
if (!endpointId)
|
|
3387
|
+
return undefined;
|
|
3388
|
+
try {
|
|
3389
|
+
return await this.apiClient.get(`/v2.0/endpoints/${endpointId}`);
|
|
3390
|
+
}
|
|
3391
|
+
catch {
|
|
3392
|
+
return null;
|
|
3393
|
+
}
|
|
3394
|
+
};
|
|
3395
|
+
let { nodes, firstNodeId } = await loadFlowState();
|
|
3396
|
+
let endpoint = await fetchEndpoint();
|
|
3397
|
+
// Best-effort LLM resolution for the fallback check (advisory only).
|
|
3398
|
+
// Tri-state: undefined = not requested, null = could not resolve, object = resolved.
|
|
3399
|
+
let llm = undefined;
|
|
3400
|
+
if (projectId) {
|
|
3401
|
+
llm = null;
|
|
3402
|
+
try {
|
|
3403
|
+
const agentNode = nodes.find((n) => n.type === "aiAgentJob");
|
|
3404
|
+
const ref = agentNode?.config?.llmProviderReferenceId;
|
|
3405
|
+
const res = await this.apiClient.get("/v2.0/largelanguagemodels", {
|
|
3406
|
+
params: { projectId, limit: 100 },
|
|
3407
|
+
});
|
|
3408
|
+
const models = res.items ?? res;
|
|
3409
|
+
if (Array.isArray(models) && models.length > 0) {
|
|
3410
|
+
// llmProviderReferenceId may hold either a referenceId or an _id/id
|
|
3411
|
+
// (create_ai_agent can set it to either), so match on all of them —
|
|
3412
|
+
// otherwise the lookup misses and the advisory inspects the wrong model.
|
|
3413
|
+
const matchesRef = (m) => m.referenceId === ref || m._id === ref || m.id === ref;
|
|
3414
|
+
llm =
|
|
3415
|
+
(ref && ref !== "default" ? models.find(matchesRef) : undefined) ??
|
|
3416
|
+
models.find((m) => m.isDefault) ??
|
|
3417
|
+
null;
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
catch {
|
|
3421
|
+
llm = null;
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
const describeFix = (fix) => fix.kind === "patchNode"
|
|
3425
|
+
? { kind: "patchNode", nodeId: fix.nodeId, config: fix.config }
|
|
3426
|
+
: {
|
|
3427
|
+
kind: "createSessionConfig",
|
|
3428
|
+
beforeNodeId: fix.targetNodeId,
|
|
3429
|
+
label: fix.label,
|
|
3430
|
+
config: fix.config,
|
|
3431
|
+
};
|
|
3432
|
+
const formatCheck = (c) => {
|
|
3433
|
+
const out = {
|
|
3434
|
+
id: c.id,
|
|
3435
|
+
section: c.section,
|
|
3436
|
+
title: c.title,
|
|
3437
|
+
status: c.status,
|
|
3438
|
+
detail: c.detail,
|
|
3439
|
+
autoFixable: c.autoFixable,
|
|
3440
|
+
};
|
|
3441
|
+
if (c.fix)
|
|
3442
|
+
out.proposedFix = describeFix(c.fix);
|
|
3443
|
+
return out;
|
|
3444
|
+
};
|
|
3445
|
+
const checks = evaluateChecks({ nodes, firstNodeId, endpoint, llm });
|
|
3446
|
+
if (!apply) {
|
|
3447
|
+
return {
|
|
3448
|
+
flowId,
|
|
3449
|
+
mode: "dry-run",
|
|
3450
|
+
summary: summarize(checks),
|
|
3451
|
+
checks: checks.map(formatCheck),
|
|
3452
|
+
_note: "Dry-run: no changes made. Re-run with apply: true to apply the auto-fixable fixes (the checks with a proposedFix). Use only: [ids] to apply a subset.",
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
// Apply the auto-fixable fixes.
|
|
3456
|
+
//
|
|
3457
|
+
// The cache maps nodeId → its current config snapshot. `undefined` means the
|
|
3458
|
+
// config was never captured (enrichment missed/failed) — distinct from an
|
|
3459
|
+
// empty config — so the apply path re-fetches before patching rather than
|
|
3460
|
+
// PATCHing a partial config that would clobber unrelated fields.
|
|
3461
|
+
const nodeConfigById = new Map(nodes.map((n) => [voiceNodeId(n), n.config]));
|
|
3462
|
+
const toApply = checks.filter((c) => c.autoFixable && c.fix && (!only || only.includes(c.id)));
|
|
3463
|
+
const appliedFixes = [];
|
|
3464
|
+
for (const c of toApply) {
|
|
3465
|
+
const fix = c.fix;
|
|
3466
|
+
try {
|
|
3467
|
+
if (fix.kind === "patchNode") {
|
|
3468
|
+
// Resolve the current config. If it was never captured, re-fetch the
|
|
3469
|
+
// full node so the merge below preserves existing fields.
|
|
3470
|
+
let existing = nodeConfigById.get(fix.nodeId);
|
|
3471
|
+
if (existing === undefined) {
|
|
3472
|
+
try {
|
|
3473
|
+
const full = await this.apiClient.get(`/v2.0/flows/${flowId}/chart/nodes/${fix.nodeId}`);
|
|
3474
|
+
existing = full?.config ?? {};
|
|
3475
|
+
}
|
|
3476
|
+
catch {
|
|
3477
|
+
existing = {};
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
const merged = { ...existing, ...fix.config };
|
|
3481
|
+
await this.apiClient.patch(`/v2.0/flows/${flowId}/chart/nodes/${fix.nodeId}`, { config: merged });
|
|
3482
|
+
// Update the snapshot so a later fix on the SAME node builds on this
|
|
3483
|
+
// merge instead of reverting it to the original config.
|
|
3484
|
+
nodeConfigById.set(fix.nodeId, merged);
|
|
3485
|
+
appliedFixes.push({
|
|
3486
|
+
id: c.id,
|
|
3487
|
+
applied: true,
|
|
3488
|
+
nodeId: fix.nodeId,
|
|
3489
|
+
fields: Object.keys(fix.config),
|
|
3490
|
+
});
|
|
3491
|
+
}
|
|
3492
|
+
else {
|
|
3493
|
+
// `prepend` (not `insertBefore`): a top-level node lives on the chart's
|
|
3494
|
+
// `next` chain (start → agent → …), not in any node's `children`.
|
|
3495
|
+
// insertBefore searches `children` and throws "Error while reading
|
|
3496
|
+
// ChartData" on a top-level target. prepend rewires the next chain so
|
|
3497
|
+
// the new node lands immediately before the AI Agent node.
|
|
3498
|
+
const created = await this.apiClient.post(`/v2.0/flows/${flowId}/chart/nodes`, {
|
|
3499
|
+
type: "setSessionConfig",
|
|
3500
|
+
extension: "@cognigy/voicegateway2",
|
|
3501
|
+
mode: "prepend",
|
|
3502
|
+
target: fix.targetNodeId,
|
|
3503
|
+
label: fix.label,
|
|
3504
|
+
config: fix.config,
|
|
3505
|
+
});
|
|
3506
|
+
appliedFixes.push({
|
|
3507
|
+
id: c.id,
|
|
3508
|
+
applied: true,
|
|
3509
|
+
createdNodeId: created._id || created.id,
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
catch (err) {
|
|
3514
|
+
appliedFixes.push({ id: c.id, applied: false, error: err.message });
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
// Re-audit so the response reflects the post-fix state. Re-derive ordering
|
|
3518
|
+
// too — a created Set Session Config node changes the first node.
|
|
3519
|
+
({ nodes, firstNodeId } = await loadFlowState());
|
|
3520
|
+
endpoint = await fetchEndpoint();
|
|
3521
|
+
const postChecks = evaluateChecks({ nodes, firstNodeId, endpoint, llm });
|
|
3522
|
+
return {
|
|
3523
|
+
flowId,
|
|
3524
|
+
mode: "apply",
|
|
3525
|
+
appliedFixes,
|
|
3526
|
+
summary: summarize(postChecks),
|
|
3527
|
+
checks: postChecks.map(formatCheck),
|
|
3528
|
+
_note: "Applied auto-fixable fixes and re-audited. Verify the flow in the UI — especially node ordering when a Set Session Config node was created. Advisory checks (warn) and manual items are not auto-fixed.",
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3531
|
+
// =========================================================================
|
|
3532
|
+
// Main dispatcher
|
|
3533
|
+
// =========================================================================
|
|
3534
|
+
async handleToolCall(toolName, args) {
|
|
3535
|
+
logger.info(`Handling tool call: ${toolName}`, {
|
|
3536
|
+
args: this.sanitizeArgs(args),
|
|
3537
|
+
});
|
|
3538
|
+
try {
|
|
3539
|
+
let result;
|
|
3540
|
+
switch (toolName) {
|
|
3541
|
+
case "create_ai_agent":
|
|
3542
|
+
result = await this.handleCreateAiAgent(args);
|
|
3543
|
+
break;
|
|
3544
|
+
case "update_ai_agent":
|
|
3545
|
+
result = await this.handleUpdateAiAgent(args);
|
|
3546
|
+
break;
|
|
3547
|
+
case "setup_llm":
|
|
3548
|
+
result = await this.handleSetupLlm(args);
|
|
3549
|
+
break;
|
|
3550
|
+
case "talk_to_agent":
|
|
3551
|
+
result = await this.handleTalkToAgent(args);
|
|
3552
|
+
break;
|
|
3553
|
+
case "list_resources":
|
|
3554
|
+
result = await this.handleListResources(args);
|
|
3555
|
+
break;
|
|
3556
|
+
case "get_resource":
|
|
3557
|
+
result = await this.handleGetResource(args);
|
|
3558
|
+
break;
|
|
3559
|
+
case "delete_resource":
|
|
3560
|
+
result = await this.handleDeleteResource(args);
|
|
3561
|
+
break;
|
|
3562
|
+
case "manage_knowledge":
|
|
3563
|
+
result = await this.handleManageKnowledge(args);
|
|
3564
|
+
break;
|
|
3565
|
+
case "create_tool":
|
|
3566
|
+
result = await this.handleCreateTool(args);
|
|
3567
|
+
break;
|
|
3568
|
+
case "update_tool":
|
|
3569
|
+
result = await this.handleUpdateTool(args);
|
|
3570
|
+
break;
|
|
3571
|
+
case "manage_flow_nodes":
|
|
3572
|
+
result = await this.handleManageFlowNodes(args);
|
|
3573
|
+
break;
|
|
3574
|
+
case "manage_packages":
|
|
3575
|
+
result = await this.handleManagePackages(args);
|
|
3576
|
+
break;
|
|
3577
|
+
case "manage_webchat":
|
|
3578
|
+
result = await this.handleManageWebchat(args);
|
|
3579
|
+
break;
|
|
3580
|
+
case "manage_voice_gateway":
|
|
3581
|
+
result = await this.handleManageVoiceGateway(args);
|
|
3582
|
+
break;
|
|
3583
|
+
case "manage_settings":
|
|
3584
|
+
result = await this.handleManageSettings(args);
|
|
3585
|
+
break;
|
|
3586
|
+
case "audit_voice_agent":
|
|
3587
|
+
result = await this.handleAuditVoiceAgent(args);
|
|
3588
|
+
break;
|
|
3589
|
+
default:
|
|
3590
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
3591
|
+
}
|
|
3592
|
+
logger.info(`Tool call successful: ${toolName}`);
|
|
3593
|
+
return result;
|
|
3594
|
+
}
|
|
3595
|
+
catch (error) {
|
|
3596
|
+
logger.error(`Tool call failed: ${toolName}`, { error: error.message });
|
|
3597
|
+
throw error;
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
// Reserved: per-type detail-view filters for get_resource (falls back to RESOURCE_FILTERS when empty)
|
|
3602
|
+
const RESOURCE_FILTERS_GET = {};
|
|
3603
|
+
//# sourceMappingURL=handlers.js.map
|