@tiledesk/tiledesk-server 2.19.7 → 2.19.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -2
- package/lib/analyticsClient.js +7 -4
- package/package.json +1 -1
- package/pubmodules/analytics-publisher/index.js +118 -72
- package/pubmodules/analytics-publisher/test/analytics-publisher.test.js +247 -0
- package/routes/answered.js +2 -1
- package/routes/kb.js +11 -6
- package/routes/request.js +21 -1
- package/routes/webhook.js +7 -2
- package/services/aiManager.js +2 -1
- package/test/kbRoute.js +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,16 @@
|
|
|
5
5
|
🚀 IN PRODUCTION 🚀
|
|
6
6
|
(https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
|
|
7
7
|
|
|
8
|
-
# 2.19.
|
|
8
|
+
# 2.19.10
|
|
9
|
+
- Fixed bug on add answered question route.
|
|
10
|
+
|
|
11
|
+
# 2.19.9
|
|
12
|
+
- Fixed an issue with the handling of situated_context during namespace import
|
|
13
|
+
|
|
14
|
+
# 2.19.8
|
|
15
|
+
- Fix analytics integration
|
|
16
|
+
|
|
17
|
+
# 2.19.7
|
|
9
18
|
- Added TTL for events collection
|
|
10
19
|
|
|
11
20
|
# 2.19.6
|
|
@@ -2606,4 +2615,4 @@ Added email notification setting for each teammate (also in 2.1.14.3)
|
|
|
2606
2615
|
- Websocket performance fix with lean and removing populate
|
|
2607
2616
|
- Added kubernetes sample config file
|
|
2608
2617
|
- Added required firstname and lastname to signup endpoint
|
|
2609
|
-
- Removed message.request.messages and message.request.department.bot for message.create event
|
|
2618
|
+
- Removed message.request.messages and message.request.department.bot for message.create event
|
package/lib/analyticsClient.js
CHANGED
|
@@ -21,9 +21,9 @@ const client = axios.create({
|
|
|
21
21
|
* produce identical envelope shapes, enabling event_id-based deduplication
|
|
22
22
|
* in the consumer.
|
|
23
23
|
*/
|
|
24
|
-
function _buildEnvelope(eventType, idProject, payload) {
|
|
24
|
+
function _buildEnvelope(eventType, idProject, payload, eventId) {
|
|
25
25
|
return {
|
|
26
|
-
event_id: crypto.randomUUID(),
|
|
26
|
+
event_id: eventId || crypto.randomUUID(),
|
|
27
27
|
event_type: eventType,
|
|
28
28
|
timestamp: new Date().toISOString(),
|
|
29
29
|
id_project: idProject,
|
|
@@ -39,12 +39,15 @@ function _buildEnvelope(eventType, idProject, payload) {
|
|
|
39
39
|
* @param {string} eventType - e.g. 'conversation.created'
|
|
40
40
|
* @param {string} idProject - Tiledesk project ID
|
|
41
41
|
* @param {object} payload - event-specific fields
|
|
42
|
+
* @param {string} [eventId] - optional caller-supplied UUID for idempotency
|
|
43
|
+
* (deterministic id so live + backfill events for
|
|
44
|
+
* the same entity dedupe). Random when omitted.
|
|
42
45
|
*/
|
|
43
|
-
function track(eventType, idProject, payload) {
|
|
46
|
+
function track(eventType, idProject, payload, eventId) {
|
|
44
47
|
if (!INGEST_BASE_URL) return;
|
|
45
48
|
if (!idProject) return;
|
|
46
49
|
|
|
47
|
-
var body = _buildEnvelope(eventType, idProject, payload);
|
|
50
|
+
var body = _buildEnvelope(eventType, idProject, payload, eventId);
|
|
48
51
|
|
|
49
52
|
client.post("/events", body).catch(function (err) {
|
|
50
53
|
var status = err.response && err.response.status;
|
package/package.json
CHANGED
|
@@ -22,6 +22,17 @@ var kbEvent = require("../../event/kbEvent");
|
|
|
22
22
|
var departmentEvent = require("../../event/departmentEvent");
|
|
23
23
|
var botEvent = require("../../event/botEvent");
|
|
24
24
|
var { track } = require("../../lib/analyticsClient");
|
|
25
|
+
var uuidv5 = require("uuid/v5");
|
|
26
|
+
|
|
27
|
+
// Deterministic event_id namespace — MUST match apps/backfill
|
|
28
|
+
// BACKFILL_UUID_NAMESPACE so a live event and its backfilled twin collapse to a
|
|
29
|
+
// single row (idempotency / no double-count). Applied only to the event types
|
|
30
|
+
// the backfill also produces.
|
|
31
|
+
var ANALYTICS_UUID_NAMESPACE = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
|
|
32
|
+
|
|
33
|
+
function eventIdFor(key) {
|
|
34
|
+
return uuidv5(String(key), ANALYTICS_UUID_NAMESPACE);
|
|
35
|
+
}
|
|
25
36
|
|
|
26
37
|
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
27
38
|
|
|
@@ -46,6 +57,7 @@ function availabilityLabel(boolVal) {
|
|
|
46
57
|
* to 'user', misclassifying agent messages.
|
|
47
58
|
*/
|
|
48
59
|
function senderType(sender, request) {
|
|
60
|
+
if (!request) return "user"; // direct/group messages carry no request
|
|
49
61
|
let bot, agent = undefined;
|
|
50
62
|
if(request.participantsBots)
|
|
51
63
|
bot = request.participantsBots.find(participant => participant.includes(sender));
|
|
@@ -101,6 +113,18 @@ function toStringId(doc) {
|
|
|
101
113
|
return (doc._id || doc.id || doc).toString() || null;
|
|
102
114
|
}
|
|
103
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Extract the tag string from a request.updated TAG_ADD patch.
|
|
118
|
+
* The tag may be a bare string or a { tag: "..." } sub-document — the shape
|
|
119
|
+
* varies by call site (see requestService.addTagByRequestId).
|
|
120
|
+
*/
|
|
121
|
+
function tagString(t) {
|
|
122
|
+
if (!t) return null;
|
|
123
|
+
if (typeof t === "string") return t;
|
|
124
|
+
if (typeof t.tag === "string") return t.tag;
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
104
128
|
// ─── listeners ──────────────────────────────────────────────────────────────
|
|
105
129
|
|
|
106
130
|
function listen() {
|
|
@@ -120,16 +144,17 @@ function listen() {
|
|
|
120
144
|
if (request.preflight === true) return;
|
|
121
145
|
|
|
122
146
|
var dept = request.department;
|
|
147
|
+
var requestId = request.request_id || toStringId(request);
|
|
123
148
|
|
|
124
149
|
track("conversation.created", request.id_project, {
|
|
125
|
-
id_request:
|
|
126
|
-
request_id:
|
|
150
|
+
id_request: requestId,
|
|
151
|
+
request_id: requestId,
|
|
127
152
|
department: departmentId(dept),
|
|
128
153
|
channel: (request.channel && request.channel.name) || "web",
|
|
129
154
|
first_response_time: null,
|
|
130
155
|
with_bot: request.hasBot || false,
|
|
131
156
|
visitor_id: firstVisitorId(request.participants),
|
|
132
|
-
});
|
|
157
|
+
}, eventIdFor(requestId));
|
|
133
158
|
}
|
|
134
159
|
|
|
135
160
|
requestEvent.on("request.update.preflight", function (request) {
|
|
@@ -149,6 +174,10 @@ function listen() {
|
|
|
149
174
|
// duration_seconds number
|
|
150
175
|
// waiting_time_seconds number|null
|
|
151
176
|
// satisfaction_rating number 1-5 | null
|
|
177
|
+
// Satisfaction is intentionally NOT included here: ratings are submitted
|
|
178
|
+
// asynchronously (after close) and emitted as a separate conversation.satisfaction
|
|
179
|
+
// event below. The conversation-closed contract has no satisfaction field, so
|
|
180
|
+
// any rating sent here would be stripped by the ingest validator anyway.
|
|
152
181
|
requestEvent.on("request.close", function (request) {
|
|
153
182
|
var createdAt = new Date(request.createdAt);
|
|
154
183
|
var closedAt = request.closed_at ? new Date(request.closed_at) : new Date();
|
|
@@ -157,18 +186,10 @@ function listen() {
|
|
|
157
186
|
? Math.round(request.duration / 1000)
|
|
158
187
|
: Math.round((closedAt - createdAt) / 1000);
|
|
159
188
|
|
|
160
|
-
|
|
161
|
-
var rawRating = request.rating;
|
|
162
|
-
var satisfactionRating = null;
|
|
163
|
-
if (rawRating != null) {
|
|
164
|
-
var r = parseInt(rawRating, 10);
|
|
165
|
-
satisfactionRating = r >= 1 && r <= 5 ? r : null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// TODO: da splittare rispetto al rating
|
|
189
|
+
var requestId = request.request_id || toStringId(request);
|
|
169
190
|
track("conversation.closed", request.id_project, {
|
|
170
|
-
id_request:
|
|
171
|
-
request_id:
|
|
191
|
+
id_request: requestId,
|
|
192
|
+
request_id: requestId,
|
|
172
193
|
closed_by: request.closed_by || "system",
|
|
173
194
|
close_reason: null,
|
|
174
195
|
duration_seconds: durationSeconds,
|
|
@@ -176,8 +197,52 @@ function listen() {
|
|
|
176
197
|
request.waiting_time != null
|
|
177
198
|
? Math.round(request.waiting_time / 1000)
|
|
178
199
|
: null,
|
|
179
|
-
|
|
180
|
-
|
|
200
|
+
}, eventIdFor(requestId + ":closed"));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── conversation.satisfaction ─────────────────────────────────────────────
|
|
204
|
+
// Emitted by routes/request.js ("request.satisfaction") when a visitor submits
|
|
205
|
+
// a rating — asynchronously, after the conversation is already closed.
|
|
206
|
+
// Contract: packages/contracts/src/payloads/conversation-satisfaction.ts
|
|
207
|
+
// id_request string (required)
|
|
208
|
+
// request_id string (required)
|
|
209
|
+
// rating number (required, integer 1-5)
|
|
210
|
+
// rating_message string|null
|
|
211
|
+
requestEvent.on("request.satisfaction", function (data) {
|
|
212
|
+
var request = (data && data.request) || {};
|
|
213
|
+
if (request.rating == null) return;
|
|
214
|
+
|
|
215
|
+
var r = parseInt(request.rating, 10);
|
|
216
|
+
if (!(r >= 1 && r <= 5)) return; // contract requires an integer 1-5
|
|
217
|
+
|
|
218
|
+
var requestId = request.request_id || toStringId(request);
|
|
219
|
+
track("conversation.satisfaction", request.id_project, {
|
|
220
|
+
id_request: requestId,
|
|
221
|
+
request_id: requestId,
|
|
222
|
+
rating: r,
|
|
223
|
+
rating_message: request.rating_message || null,
|
|
224
|
+
}, eventIdFor(requestId + ":satisfaction"));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ── conversation.tag_added ────────────────────────────────────────────────
|
|
228
|
+
// Tags are added via requestService.addTagByRequestId, which emits the generic
|
|
229
|
+
// "request.updated" event with comment === "TAG_ADD" and the added tag in
|
|
230
|
+
// patch.tags. One analytics event per added tag.
|
|
231
|
+
// Contract: packages/contracts/src/payloads/conversation-tag-added.ts
|
|
232
|
+
// id_request string (required)
|
|
233
|
+
// tag string (required)
|
|
234
|
+
requestEvent.on("request.updated", function (data) {
|
|
235
|
+
if (!data || data.comment !== "TAG_ADD") return;
|
|
236
|
+
|
|
237
|
+
var request = data.request || {};
|
|
238
|
+
var tag = tagString(data.patch && data.patch.tags);
|
|
239
|
+
if (!tag) return; // nothing usable to record
|
|
240
|
+
|
|
241
|
+
var requestId = request.request_id || toStringId(request);
|
|
242
|
+
track("conversation.tag_added", request.id_project, {
|
|
243
|
+
id_request: requestId,
|
|
244
|
+
tag: tag,
|
|
245
|
+
}, eventIdFor(requestId + ":" + tag));
|
|
181
246
|
});
|
|
182
247
|
|
|
183
248
|
// ── 3. message.sent ────────────────────────────────────────────────────────
|
|
@@ -198,64 +263,37 @@ function listen() {
|
|
|
198
263
|
var idRequest = messageJson.recipient || null;
|
|
199
264
|
if (!idRequest) return; // cannot emit without a conversation reference
|
|
200
265
|
|
|
266
|
+
// Denormalise department (id) and channel (name) from the parent request so
|
|
267
|
+
// the messages MVs can filter without a query-time join. Mirrors the
|
|
268
|
+
// backfill message mapper (department = id, channel = name). Optional in the
|
|
269
|
+
// contract, so null is fine when the message carries no populated request.
|
|
270
|
+
var msgRequest = messageJson.request || null;
|
|
271
|
+
var msgChannel =
|
|
272
|
+
(msgRequest && msgRequest.channel && msgRequest.channel.name) ||
|
|
273
|
+
(messageJson.channel && messageJson.channel.name) ||
|
|
274
|
+
null;
|
|
275
|
+
|
|
276
|
+
var idMessage = (messageJson._id || messageJson.id || "").toString();
|
|
201
277
|
track("message.sent", messageJson.id_project, {
|
|
202
|
-
id_message:
|
|
278
|
+
id_message: idMessage,
|
|
203
279
|
id_request: idRequest,
|
|
204
280
|
sender_id: sender || "unknown", // required non-null — fallback to 'unknown'
|
|
205
|
-
sender_type: senderType(sender,
|
|
281
|
+
sender_type: senderType(sender, msgRequest),
|
|
206
282
|
message_type: messageJson.type || "text", // required non-null — fallback to 'text'
|
|
207
283
|
has_attachment: !!(messageJson.metadata && messageJson.metadata.src),
|
|
208
284
|
language: messageJson.language || null,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
// ── 5. handover_to_human ──────────────────────────────────────────────────
|
|
213
|
-
// Contract: packages/contracts/src/payloads/handover-to-human.ts
|
|
214
|
-
// id_request string (required)
|
|
215
|
-
// human_id string|null
|
|
216
|
-
// reason string|null
|
|
217
|
-
// department_id string|null
|
|
218
|
-
// waiting_time_seconds number int>=0 | null
|
|
219
|
-
// agent_id string|null (optional)
|
|
220
|
-
// trigger_intent string|null (optional)
|
|
221
|
-
requestEvent.on("request.participants.update", function (data) {
|
|
222
|
-
var request = data.request || {};
|
|
223
|
-
var removedParticipants = data.removedParticipants || [];
|
|
224
|
-
var addedParticipants = data.addedParticipants || [];
|
|
225
|
-
|
|
226
|
-
var botRemoved = removedParticipants.some(function (p) {
|
|
227
|
-
return p.startsWith("bot_");
|
|
228
|
-
});
|
|
229
|
-
var humanAdded = addedParticipants.some(function (p) {
|
|
230
|
-
return !p.startsWith("bot_");
|
|
231
|
-
});
|
|
232
|
-
if (!botRemoved || !humanAdded) return;
|
|
233
|
-
|
|
234
|
-
var botId =
|
|
235
|
-
removedParticipants.find(function (p) {
|
|
236
|
-
return p.startsWith("bot_");
|
|
237
|
-
}) || null;
|
|
238
|
-
var humanId =
|
|
239
|
-
addedParticipants.find(function (p) {
|
|
240
|
-
return !p.startsWith("bot_");
|
|
241
|
-
}) || null;
|
|
242
|
-
|
|
243
|
-
var waitingTimeSecs = null;
|
|
244
|
-
if (request.waiting_time != null) {
|
|
245
|
-
waitingTimeSecs = Math.round(request.waiting_time / 1000);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
track("handover_to_human", request.id_project, {
|
|
249
|
-
id_request: request.request_id || toStringId(request),
|
|
250
|
-
human_id: humanId,
|
|
251
|
-
reason: null,
|
|
252
|
-
department_id: departmentId(request.department),
|
|
253
|
-
waiting_time_seconds: waitingTimeSecs,
|
|
254
|
-
agent_id: null,
|
|
255
|
-
trigger_intent: null,
|
|
256
|
-
});
|
|
285
|
+
department: departmentId(msgRequest && msgRequest.department),
|
|
286
|
+
channel: msgChannel,
|
|
287
|
+
}, eventIdFor(idMessage));
|
|
257
288
|
});
|
|
258
289
|
|
|
290
|
+
// ── handover_to_human: intentionally NOT emitted here ─────────────────────
|
|
291
|
+
// Owned by tiledesk-chatbot (DirMoveToAgent), which fires it reliably at
|
|
292
|
+
// handover time with full context (reason / agent_id / trigger_intent). The
|
|
293
|
+
// participants-diff signal that used to live here was routing-dependent — it
|
|
294
|
+
// required the bot to be removed and a human added in the SAME update, so
|
|
295
|
+
// queue/pool handovers (bot removed first, agent assigned later) were missed —
|
|
296
|
+
// and it double-counted bot escalations the chatbot already records.
|
|
259
297
|
|
|
260
298
|
// ── 5. project_user.activated ─────────────────────────────────────────────
|
|
261
299
|
// Contract: packages/contracts/src/payloads/project-user-activated.ts
|
|
@@ -284,7 +322,7 @@ function listen() {
|
|
|
284
322
|
user_email: email,
|
|
285
323
|
role: pu.role || "agent",
|
|
286
324
|
invited_by: (event.req && event.req.user && event.req.user.id) || null,
|
|
287
|
-
});
|
|
325
|
+
}, eventIdFor(pu.id_project + ":" + toStringId(pu) + ":activated"));
|
|
288
326
|
});
|
|
289
327
|
|
|
290
328
|
// ── 7. human.status_changed ───────────────────────────────────────────────
|
|
@@ -413,22 +451,30 @@ function listen() {
|
|
|
413
451
|
// ── 13. agent.metadata_updated ────────────────────────────────────────────
|
|
414
452
|
// Emitted by routes/faq_kb.js on bot create and update (rename, attribute
|
|
415
453
|
// changes, language, etc.).
|
|
416
|
-
// agent_id = Faq_kb.
|
|
454
|
+
// agent_id = Faq_kb.root_id (the canonical/root agent id)
|
|
417
455
|
// agent_name = Faq_kb.name
|
|
418
456
|
// The consumer writes these into the agent_dimensions ReplacingMergeTree so
|
|
419
457
|
// that dashboard queries always resolve the current bot name.
|
|
458
|
+
//
|
|
459
|
+
// Only PUBLISHED chatbots are tracked. Publishing forks the chatbot into a
|
|
460
|
+
// trashed faq_kb that carries root_id (-> the editable draft); the draft/root
|
|
461
|
+
// copy never has root_id. Gating on root_id therefore: (a) excludes drafts
|
|
462
|
+
// that were never published, (b) keys the dimension on the canonical root id
|
|
463
|
+
// — the same id the chatbot stamps on its runtime events — and (c) makes the
|
|
464
|
+
// name reflect the *published* name (a draft rename only lands here on the
|
|
465
|
+
// next publish, when a fresh fork fires faqbot.update).
|
|
420
466
|
botEvent.on("faqbot.create", function (savedBot) {
|
|
421
|
-
if (!savedBot || !savedBot.id_project || !savedBot.name) return;
|
|
467
|
+
if (!savedBot || !savedBot.id_project || !savedBot.name || !savedBot.root_id) return;
|
|
422
468
|
track("agent.metadata_updated", savedBot.id_project, {
|
|
423
|
-
agent_id: savedBot.root_id
|
|
469
|
+
agent_id: savedBot.root_id,
|
|
424
470
|
agent_name: savedBot.name,
|
|
425
471
|
});
|
|
426
472
|
});
|
|
427
473
|
|
|
428
474
|
botEvent.on("faqbot.update", function (updatedBot) {
|
|
429
|
-
if (!updatedBot || !updatedBot.id_project || !updatedBot.name) return;
|
|
475
|
+
if (!updatedBot || !updatedBot.id_project || !updatedBot.name || !updatedBot.root_id) return;
|
|
430
476
|
track("agent.metadata_updated", updatedBot.id_project, {
|
|
431
|
-
agent_id: updatedBot.root_id
|
|
477
|
+
agent_id: updatedBot.root_id,
|
|
432
478
|
agent_name: updatedBot.name,
|
|
433
479
|
});
|
|
434
480
|
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// During the test the env variable is set to test
|
|
2
|
+
process.env.NODE_ENV = 'test';
|
|
3
|
+
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
|
|
6
|
+
// Intercept analyticsClient.track BEFORE requiring the publisher so the
|
|
7
|
+
// reference destructured inside the module body is our spy. No DB or broker
|
|
8
|
+
// is touched: the publisher's listeners are pure (event in -> track() out).
|
|
9
|
+
const analyticsClient = require('../../../lib/analyticsClient');
|
|
10
|
+
const uuidv5 = require('uuid/v5');
|
|
11
|
+
|
|
12
|
+
// Must match apps/backfill BACKFILL_UUID_NAMESPACE.
|
|
13
|
+
const NS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
|
14
|
+
|
|
15
|
+
const calls = [];
|
|
16
|
+
analyticsClient.track = function (eventType, idProject, payload, eventId) {
|
|
17
|
+
calls.push({ eventType: eventType, idProject: idProject, payload: payload, eventId: eventId });
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Fresh require so the module captures the patched track().
|
|
21
|
+
delete require.cache[require.resolve('../index')];
|
|
22
|
+
const publisher = require('../index');
|
|
23
|
+
|
|
24
|
+
const requestEvent = require('../../../event/requestEvent');
|
|
25
|
+
const messageEvent = require('../../../event/messageEvent');
|
|
26
|
+
const authEvent = require('../../../event/authEvent');
|
|
27
|
+
|
|
28
|
+
function callsFor(type) {
|
|
29
|
+
return calls.filter((c) => c.eventType === type);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('analytics-publisher', function () {
|
|
33
|
+
before(function () {
|
|
34
|
+
publisher.listen();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
beforeEach(function () {
|
|
38
|
+
calls.length = 0;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('message.sent', function () {
|
|
42
|
+
it('propagates department (id) and channel (name) from the populated request', function () {
|
|
43
|
+
messageEvent.emit('message.create', {
|
|
44
|
+
_id: 'msg-1',
|
|
45
|
+
id_project: 'proj-1',
|
|
46
|
+
recipient: 'req-1',
|
|
47
|
+
sender: 'agent-1',
|
|
48
|
+
type: 'text',
|
|
49
|
+
request: {
|
|
50
|
+
participantsAgents: ['agent-1'],
|
|
51
|
+
participantsBots: [],
|
|
52
|
+
department: { _id: 'dept-9', name: 'Sales' },
|
|
53
|
+
channel: { name: 'whatsapp' },
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const sent = callsFor('message.sent');
|
|
58
|
+
expect(sent).to.have.length(1);
|
|
59
|
+
expect(sent[0].payload.department).to.equal('dept-9');
|
|
60
|
+
expect(sent[0].payload.channel).to.equal('whatsapp');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('sets department/channel to null when the message has no request', function () {
|
|
64
|
+
messageEvent.emit('message.create', {
|
|
65
|
+
_id: 'msg-2',
|
|
66
|
+
id_project: 'proj-1',
|
|
67
|
+
recipient: 'req-2',
|
|
68
|
+
sender: 'visitor-1',
|
|
69
|
+
type: 'text',
|
|
70
|
+
// no .request (direct/group message)
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const sent = callsFor('message.sent');
|
|
74
|
+
expect(sent).to.have.length(1);
|
|
75
|
+
expect(sent[0].payload.department).to.equal(null);
|
|
76
|
+
expect(sent[0].payload.channel).to.equal(null);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('conversation.satisfaction', function () {
|
|
81
|
+
it('emits on request.satisfaction with a valid 1-5 rating', function () {
|
|
82
|
+
requestEvent.emit('request.satisfaction', {
|
|
83
|
+
request: {
|
|
84
|
+
request_id: 'req-3',
|
|
85
|
+
id_project: 'proj-1',
|
|
86
|
+
rating: 4,
|
|
87
|
+
rating_message: 'good',
|
|
88
|
+
},
|
|
89
|
+
patch: { rating: 4 },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const sat = callsFor('conversation.satisfaction');
|
|
93
|
+
expect(sat).to.have.length(1);
|
|
94
|
+
expect(sat[0].idProject).to.equal('proj-1');
|
|
95
|
+
expect(sat[0].payload).to.deep.equal({
|
|
96
|
+
id_request: 'req-3',
|
|
97
|
+
request_id: 'req-3',
|
|
98
|
+
rating: 4,
|
|
99
|
+
rating_message: 'good',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('does not emit for an out-of-range rating', function () {
|
|
104
|
+
requestEvent.emit('request.satisfaction', {
|
|
105
|
+
request: { request_id: 'req-4', id_project: 'proj-1', rating: 9 },
|
|
106
|
+
patch: { rating: 9 },
|
|
107
|
+
});
|
|
108
|
+
expect(callsFor('conversation.satisfaction')).to.have.length(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('conversation.closed', function () {
|
|
113
|
+
it('omits the (stripped, superseded) satisfaction_rating field', function () {
|
|
114
|
+
requestEvent.emit('request.close', {
|
|
115
|
+
request_id: 'req-5',
|
|
116
|
+
id_project: 'proj-1',
|
|
117
|
+
createdAt: new Date(Date.now() - 60000),
|
|
118
|
+
closed_at: new Date(),
|
|
119
|
+
rating: 5,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const closed = callsFor('conversation.closed');
|
|
123
|
+
expect(closed).to.have.length(1);
|
|
124
|
+
expect(closed[0].payload).to.not.have.property('satisfaction_rating');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('conversation.tag_added', function () {
|
|
129
|
+
it('emits on request.updated with comment TAG_ADD (object tag)', function () {
|
|
130
|
+
requestEvent.emit('request.updated', {
|
|
131
|
+
comment: 'TAG_ADD',
|
|
132
|
+
request: { request_id: 'req-6', id_project: 'proj-1' },
|
|
133
|
+
patch: { tags: { tag: 'urgent' } },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const tagged = callsFor('conversation.tag_added');
|
|
137
|
+
expect(tagged).to.have.length(1);
|
|
138
|
+
expect(tagged[0].payload).to.deep.equal({ id_request: 'req-6', tag: 'urgent' });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('extracts a plain string tag from the patch', function () {
|
|
142
|
+
requestEvent.emit('request.updated', {
|
|
143
|
+
comment: 'TAG_ADD',
|
|
144
|
+
request: { request_id: 'req-7', id_project: 'proj-1' },
|
|
145
|
+
patch: { tags: 'vip' },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const tagged = callsFor('conversation.tag_added');
|
|
149
|
+
expect(tagged).to.have.length(1);
|
|
150
|
+
expect(tagged[0].payload.tag).to.equal('vip');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('does not emit for TAG_REMOVE or other comments', function () {
|
|
154
|
+
requestEvent.emit('request.updated', {
|
|
155
|
+
comment: 'TAG_REMOVE',
|
|
156
|
+
request: { request_id: 'req-8', id_project: 'proj-1' },
|
|
157
|
+
patch: { tags: { tag: 'urgent' } },
|
|
158
|
+
});
|
|
159
|
+
requestEvent.emit('request.updated', {
|
|
160
|
+
comment: 'PATCH',
|
|
161
|
+
request: { request_id: 'req-9', id_project: 'proj-1' },
|
|
162
|
+
patch: { status: 200 },
|
|
163
|
+
});
|
|
164
|
+
expect(callsFor('conversation.tag_added')).to.have.length(0);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// handover_to_human is owned by tiledesk-chatbot (DirMoveToAgent), which emits
|
|
169
|
+
// it reliably with full context (reason / agent_id / trigger_intent). The
|
|
170
|
+
// server's participants-diff signal was routing-dependent (missed queue/pool
|
|
171
|
+
// handovers) and double-counted bot escalations, so it is intentionally NOT
|
|
172
|
+
// emitted here.
|
|
173
|
+
describe('handover_to_human (owned by tiledesk-chatbot)', function () {
|
|
174
|
+
it('does not emit handover_to_human on a bot->human participants update', function () {
|
|
175
|
+
requestEvent.emit('request.participants.update', {
|
|
176
|
+
request: { request_id: 'req-h1', id_project: 'proj-1', department: 'dept-1' },
|
|
177
|
+
removedParticipants: ['bot_5e9d'],
|
|
178
|
+
addedParticipants: ['agent_7f2a'],
|
|
179
|
+
});
|
|
180
|
+
expect(callsFor('handover_to_human')).to.have.length(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Deterministic event_ids let live + backfill events for the same entity
|
|
185
|
+
// collapse to one row (idempotency). Keys must match the backfill mappers.
|
|
186
|
+
describe('deterministic event_ids', function () {
|
|
187
|
+
it('conversation.created -> uuidv5(request_id)', function () {
|
|
188
|
+
requestEvent.emit('request.create', {
|
|
189
|
+
request_id: 'req-c1', id_project: 'proj-1', participants: [], hasBot: false,
|
|
190
|
+
channel: { name: 'web' },
|
|
191
|
+
});
|
|
192
|
+
const c = callsFor('conversation.created');
|
|
193
|
+
expect(c).to.have.length(1);
|
|
194
|
+
expect(c[0].eventId).to.equal(uuidv5('req-c1', NS));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('conversation.closed -> uuidv5(request_id:closed)', function () {
|
|
198
|
+
requestEvent.emit('request.close', {
|
|
199
|
+
request_id: 'req-c2', id_project: 'proj-1',
|
|
200
|
+
createdAt: new Date(Date.now() - 1000), closed_at: new Date(),
|
|
201
|
+
});
|
|
202
|
+
const c = callsFor('conversation.closed');
|
|
203
|
+
expect(c).to.have.length(1);
|
|
204
|
+
expect(c[0].eventId).to.equal(uuidv5('req-c2:closed', NS));
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('conversation.satisfaction -> uuidv5(request_id:satisfaction)', function () {
|
|
208
|
+
requestEvent.emit('request.satisfaction', {
|
|
209
|
+
request: { request_id: 'req-c3', id_project: 'proj-1', rating: 5 },
|
|
210
|
+
});
|
|
211
|
+
const c = callsFor('conversation.satisfaction');
|
|
212
|
+
expect(c).to.have.length(1);
|
|
213
|
+
expect(c[0].eventId).to.equal(uuidv5('req-c3:satisfaction', NS));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('conversation.tag_added -> uuidv5(request_id:tag)', function () {
|
|
217
|
+
requestEvent.emit('request.updated', {
|
|
218
|
+
comment: 'TAG_ADD', request: { request_id: 'req-c4', id_project: 'proj-1' },
|
|
219
|
+
patch: { tags: { tag: 'vip' } },
|
|
220
|
+
});
|
|
221
|
+
const c = callsFor('conversation.tag_added');
|
|
222
|
+
expect(c).to.have.length(1);
|
|
223
|
+
expect(c[0].eventId).to.equal(uuidv5('req-c4:vip', NS));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('message.sent -> uuidv5(id_message)', function () {
|
|
227
|
+
messageEvent.emit('message.create', {
|
|
228
|
+
_id: 'msg-c5', id_project: 'proj-1', recipient: 'req-c5', sender: 'u1', type: 'text',
|
|
229
|
+
});
|
|
230
|
+
const c = callsFor('message.sent');
|
|
231
|
+
expect(c).to.have.length(1);
|
|
232
|
+
expect(c[0].eventId).to.equal(uuidv5('msg-c5', NS));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('project_user.activated -> uuidv5(id_project:pu_id:activated)', function () {
|
|
236
|
+
authEvent.emit('project_user.invite', {
|
|
237
|
+
savedProject_userPopulated: {
|
|
238
|
+
_id: 'pu-c6', id_project: 'proj-1', role: 'agent',
|
|
239
|
+
id_user: { _id: 'user-c6', email: 'a@b.com' },
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
const c = callsFor('project_user.activated');
|
|
243
|
+
expect(c).to.have.length(1);
|
|
244
|
+
expect(c[0].eventId).to.equal(uuidv5('proj-1:pu-c6:activated', NS));
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
package/routes/answered.js
CHANGED
|
@@ -3,6 +3,7 @@ const router = express.Router();
|
|
|
3
3
|
const { Namespace, AnsweredQuestion } = require('../models/kb_setting');
|
|
4
4
|
const winston = require('../config/winston');
|
|
5
5
|
var fastCsv = require('fast-csv');
|
|
6
|
+
const kbQuestionService = require('../services/kbQuestionService');
|
|
6
7
|
|
|
7
8
|
// Add a new unanswerd question
|
|
8
9
|
router.post('/', async (req, res) => {
|
|
@@ -11,7 +12,7 @@ router.post('/', async (req, res) => {
|
|
|
11
12
|
const data = req.body;
|
|
12
13
|
const id_project = req.projectid;
|
|
13
14
|
|
|
14
|
-
const savedQuestion = await
|
|
15
|
+
const savedQuestion = await kbQuestionService.createAnsweredQuestion(
|
|
15
16
|
id_project,
|
|
16
17
|
data
|
|
17
18
|
);
|
package/routes/kb.js
CHANGED
|
@@ -1267,6 +1267,10 @@ router.post('/namespace/import/:id', upload.single('uploadFile'), async (req, re
|
|
|
1267
1267
|
}
|
|
1268
1268
|
}
|
|
1269
1269
|
|
|
1270
|
+
if (e.situated_context === true && (e.type !== "url" || content.scrape_type === 0)) {
|
|
1271
|
+
content.situated_context = true;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1270
1274
|
addingContents.push(content);
|
|
1271
1275
|
})
|
|
1272
1276
|
|
|
@@ -1291,8 +1295,6 @@ router.post('/namespace/import/:id', upload.single('uploadFile'), async (req, re
|
|
|
1291
1295
|
let engine = ns.engine || default_engine;
|
|
1292
1296
|
let embedding = aiManager.normalizeEmbedding(ns.embedding);
|
|
1293
1297
|
let hybrid = ns.hybrid;
|
|
1294
|
-
const situated_context = normalizeSituatedContext();
|
|
1295
|
-
|
|
1296
1298
|
|
|
1297
1299
|
if (process.env.NODE_ENV !== "test") {
|
|
1298
1300
|
await aiService.deleteNamespace({
|
|
@@ -1327,14 +1329,15 @@ router.post('/namespace/import/:id', upload.single('uploadFile'), async (req, re
|
|
|
1327
1329
|
return res.status(500).send({ success: false, error: "Unable to get new content" });
|
|
1328
1330
|
}
|
|
1329
1331
|
|
|
1330
|
-
let resources = new_contents.map(({ name, status, __v, createdAt, updatedAt,
|
|
1331
|
-
resources = resources.map(({ _id, scrape_options, ...rest }) => {
|
|
1332
|
+
let resources = new_contents.map(({ name, status, __v, createdAt, updatedAt, ...keepAttrs }) => keepAttrs)
|
|
1333
|
+
resources = resources.map(({ _id, scrape_options, situated_context, ...rest }) => {
|
|
1334
|
+
const situated_context_obj = aiManager.normalizeSituatedContext(situated_context);
|
|
1332
1335
|
return {
|
|
1333
1336
|
id: _id,
|
|
1334
1337
|
parameters_scrape_type_4: scrape_options,
|
|
1335
1338
|
embedding: embedding,
|
|
1336
1339
|
engine: engine,
|
|
1337
|
-
...(
|
|
1340
|
+
...(situated_context_obj && { situated_context: situated_context_obj }),
|
|
1338
1341
|
...rest
|
|
1339
1342
|
}
|
|
1340
1343
|
});
|
|
@@ -1731,6 +1734,7 @@ router.post('/', async (req, res) => {
|
|
|
1731
1734
|
|
|
1732
1735
|
const json = {
|
|
1733
1736
|
id: saved_kb._id,
|
|
1737
|
+
id_project: saved_kb.id_project,
|
|
1734
1738
|
type: saved_kb.type,
|
|
1735
1739
|
source: saved_kb.source,
|
|
1736
1740
|
content: saved_kb.content || "",
|
|
@@ -1909,7 +1913,7 @@ router.post('/csv', upload.single('uploadFile'), async (req, res) => {
|
|
|
1909
1913
|
situated_context_obj = normalizeSituatedContext(situated_context);
|
|
1910
1914
|
}
|
|
1911
1915
|
|
|
1912
|
-
let resources = result.map(({ name, status, __v, createdAt, updatedAt,
|
|
1916
|
+
let resources = result.map(({ name, status, __v, createdAt, updatedAt, situated_context, ...keepAttrs }) => keepAttrs)
|
|
1913
1917
|
resources = resources.map(({ _id, ...rest }) => {
|
|
1914
1918
|
return {
|
|
1915
1919
|
id: _id,
|
|
@@ -2200,6 +2204,7 @@ router.put('/:kb_id', async (req, res) => {
|
|
|
2200
2204
|
|
|
2201
2205
|
const json = {
|
|
2202
2206
|
id: updated_content._id,
|
|
2207
|
+
id_project: updated_content.id_project,
|
|
2203
2208
|
type: updated_content.type,
|
|
2204
2209
|
source: updated_content.source,
|
|
2205
2210
|
content: updated_content.content || "",
|
package/routes/request.js
CHANGED
|
@@ -456,6 +456,12 @@ router.put('/:requestid/replace', async (req, res) => {
|
|
|
456
456
|
let id;
|
|
457
457
|
let name;
|
|
458
458
|
let slug;
|
|
459
|
+
// Canonical (root) id of the target bot, returned to the caller so analytics
|
|
460
|
+
// (agent.bot_switched.to_agent_id) can attribute the switch to the root agent
|
|
461
|
+
// — matching agent_dimensions and the target bot's own runtime events. A
|
|
462
|
+
// published bot is forked to a trashed copy with its own _id; root_id points
|
|
463
|
+
// back to the editable root, so root_id || _id is the canonical agent id.
|
|
464
|
+
let replacedRootId = null;
|
|
459
465
|
|
|
460
466
|
if (req.body.id) {
|
|
461
467
|
id = "bot_" + req.body.id;
|
|
@@ -478,6 +484,7 @@ router.put('/:requestid/replace', async (req, res) => {
|
|
|
478
484
|
}
|
|
479
485
|
|
|
480
486
|
id = "bot_" + chatbot._id;
|
|
487
|
+
replacedRootId = (chatbot.root_id || chatbot._id).toString();
|
|
481
488
|
winston.verbose("Chatbot found: ", id);
|
|
482
489
|
}
|
|
483
490
|
|
|
@@ -492,16 +499,29 @@ router.put('/:requestid/replace', async (req, res) => {
|
|
|
492
499
|
}
|
|
493
500
|
|
|
494
501
|
id = "bot_" + chatbot._id;
|
|
502
|
+
replacedRootId = (chatbot.root_id || chatbot._id).toString();
|
|
495
503
|
winston.verbose("Chatbot found: " + id);
|
|
496
504
|
}
|
|
497
505
|
|
|
506
|
+
// Replacing by raw id: normalize a possibly-published (fork) id to its root.
|
|
507
|
+
if (req.body.id) {
|
|
508
|
+
let chatbot = await faq_kb.findById(req.body.id).catch((err) => {
|
|
509
|
+
winston.error("Error finding bot by id ", err);
|
|
510
|
+
return null;
|
|
511
|
+
});
|
|
512
|
+
replacedRootId = ((chatbot && (chatbot.root_id || chatbot._id)) || req.body.id).toString();
|
|
513
|
+
}
|
|
514
|
+
|
|
498
515
|
let participants = [];
|
|
499
516
|
participants.push(id);
|
|
500
517
|
winston.verbose("participants to be set: ", participants);
|
|
501
518
|
|
|
502
519
|
requestService.setParticipantsByRequestId(req.params.requestid, req.projectid, participants).then((updatedRequest) => {
|
|
503
520
|
winston.debug("SetParticipant response: ", updatedRequest);
|
|
504
|
-
|
|
521
|
+
// Additive: include the resolved canonical (root) bot id for analytics
|
|
522
|
+
// attribution. Existing consumers ignore the extra field.
|
|
523
|
+
const requestObj = (updatedRequest && updatedRequest.toJSON) ? updatedRequest.toJSON() : updatedRequest;
|
|
524
|
+
res.status(200).send({ ...requestObj, replaced_bot_root_id: replacedRootId });
|
|
505
525
|
}).catch((err) => {
|
|
506
526
|
winston.error("Error setting participants ", err);
|
|
507
527
|
res.status(500).send({ success: false, error: "Error setting participants to request"})
|
package/routes/webhook.js
CHANGED
|
@@ -166,6 +166,7 @@ router.post('/kb/reindex', async (req, res) => {
|
|
|
166
166
|
|
|
167
167
|
let json = {
|
|
168
168
|
id: kb._id,
|
|
169
|
+
id_project: kb.id_project,
|
|
169
170
|
type: kb.type,
|
|
170
171
|
source: kb.source,
|
|
171
172
|
content: "",
|
|
@@ -311,7 +312,11 @@ router.all('/:webhook_id', async (req, res) => {
|
|
|
311
312
|
//webhookService.run(webhook, payload)
|
|
312
313
|
// To delete - End
|
|
313
314
|
webhookService.run(webhook, payload, dev, redis_client).then((response) => {
|
|
314
|
-
|
|
315
|
+
// Only count production runs: a dev/test invocation (?dev=true here, or the
|
|
316
|
+
// dedicated /:webhook_id/dev route below) must not emit analytics.
|
|
317
|
+
if (dev !== true && dev !== 'true') {
|
|
318
|
+
webhookEvent.emit("webhook.triggered", { webhook: webhook, payload: payload });
|
|
319
|
+
}
|
|
315
320
|
return res.status(200).send(response);
|
|
316
321
|
}).catch((err) => {
|
|
317
322
|
if (err.code === errorCodes.WEBHOOK.ERRORS.NO_PRELOADED_DEV_REQUEST) {
|
|
@@ -321,7 +326,7 @@ router.all('/:webhook_id', async (req, res) => {
|
|
|
321
326
|
return res.status(status).send(err.data);
|
|
322
327
|
}
|
|
323
328
|
})
|
|
324
|
-
|
|
329
|
+
|
|
325
330
|
})
|
|
326
331
|
|
|
327
332
|
router.all('/:webhook_id/dev', async (req, res) => {
|
package/services/aiManager.js
CHANGED
|
@@ -104,7 +104,7 @@ class AiManager {
|
|
|
104
104
|
|
|
105
105
|
let webhook = apiUrl + '/webhook/kb/status?token=' + KB_WEBHOOK_TOKEN;
|
|
106
106
|
|
|
107
|
-
let resources = result.map(({ name, status, __v, createdAt, updatedAt,
|
|
107
|
+
let resources = result.map(({ name, status, __v, createdAt, updatedAt, situated_context, ...keepAttrs }) => keepAttrs)
|
|
108
108
|
resources = resources.map(({ _id, scrape_options, ...rest }) => {
|
|
109
109
|
return {
|
|
110
110
|
id: _id,
|
|
@@ -139,6 +139,7 @@ class AiManager {
|
|
|
139
139
|
|
|
140
140
|
let kb = {
|
|
141
141
|
id: sitemap_content._id,
|
|
142
|
+
id_project: namespace.id_project,
|
|
142
143
|
source: sitemap_content.source,
|
|
143
144
|
type: 'sitemap',
|
|
144
145
|
content: "",
|
package/test/kbRoute.js
CHANGED
|
@@ -244,7 +244,7 @@ describe('KbRoute', () => {
|
|
|
244
244
|
expect(res.body.embedding.name).to.equal('text-embedding-ada-002')
|
|
245
245
|
expect(res.body.embedding.dimension).to.equal(1536)
|
|
246
246
|
expect(res.body.embedding.api_key).to.equal('')
|
|
247
|
-
|
|
247
|
+
|
|
248
248
|
done();
|
|
249
249
|
})
|
|
250
250
|
})
|
|
@@ -321,7 +321,7 @@ describe('KbRoute', () => {
|
|
|
321
321
|
.end((err, res) => {
|
|
322
322
|
if (err) { console.error("err: ", err); }
|
|
323
323
|
if (log) { console.log("get all namespaces res.body: ", res.body); }
|
|
324
|
-
|
|
324
|
+
|
|
325
325
|
res.should.have.status(200);
|
|
326
326
|
res.body.should.be.a('array');
|
|
327
327
|
expect(res.body.length).to.equal(1);
|
|
@@ -633,6 +633,7 @@ describe('KbRoute', () => {
|
|
|
633
633
|
|
|
634
634
|
let scheduleJson = res.body.schedule_json;
|
|
635
635
|
expect(scheduleJson.namespace).to.equal(namespace_id)
|
|
636
|
+
expect(scheduleJson.id_project).to.equal(namespace_id)
|
|
636
637
|
expect(scheduleJson.type).to.equal("url")
|
|
637
638
|
expect(scheduleJson.source).to.equal("https://www.exampleurl5.com")
|
|
638
639
|
expect(scheduleJson.hybrid).to.equal(false);
|
|
@@ -1180,6 +1181,7 @@ describe('KbRoute', () => {
|
|
|
1180
1181
|
scheduleJson.should.be.a('array');
|
|
1181
1182
|
expect(scheduleJson.length).to.equal(2);
|
|
1182
1183
|
expect(scheduleJson[0].namespace).to.equal(namespace_id);
|
|
1184
|
+
expect(scheduleJson[0].id_project).to.equal(namespace_id);
|
|
1183
1185
|
expect(scheduleJson[0].type).to.equal('faq');
|
|
1184
1186
|
expect(scheduleJson[0].source).to.equal('Question 1');
|
|
1185
1187
|
should.exist(scheduleJson[0].engine)
|
|
@@ -1344,6 +1346,7 @@ describe('KbRoute', () => {
|
|
|
1344
1346
|
let scheduleJson = res.body.schedule_json;
|
|
1345
1347
|
expect(scheduleJson.length).to.equal(4);
|
|
1346
1348
|
expect(scheduleJson[0].namespace).to.equal(namespace_id);
|
|
1349
|
+
expect(scheduleJson[0].id_project).to.equal(namespace_id);
|
|
1347
1350
|
expect(scheduleJson[0].source).to.equal('https://gethelp.tiledesk.com/articles/article1');
|
|
1348
1351
|
expect(scheduleJson[0].tags.length).to.equal(2);
|
|
1349
1352
|
expect(scheduleJson[0].tags[0]).to.equal('tag1');
|
|
@@ -1546,6 +1549,7 @@ describe('KbRoute', () => {
|
|
|
1546
1549
|
scheduleJson.should.be.a('array');
|
|
1547
1550
|
should.exist(scheduleJson[0].engine)
|
|
1548
1551
|
should.exist(scheduleJson[0].embedding)
|
|
1552
|
+
expect(scheduleJson[0].id_project).to.equal(namespace_id)
|
|
1549
1553
|
expect(scheduleJson[0].embedding.api_key).to.equal('fakegptkey');
|
|
1550
1554
|
|
|
1551
1555
|
done();
|