@tiledesk/tiledesk-server 2.29.0 → 3.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/CHANGELOG.md +17 -1
- package/app.js +1 -1
- package/config/native_mcp/native_mcp_servers.json +7 -0
- package/controllers/chatbot.controller.js +63 -3
- package/models/faq_kb.js +35 -0
- package/package.json +1 -1
- package/pubmodules/aiGenerations/README.md +154 -0
- package/pubmodules/aiGenerations/index.js +7 -0
- package/pubmodules/aiGenerations/listeners.js +96 -0
- package/pubmodules/aiGenerations/models/faqKbRevision.js +51 -0
- package/pubmodules/aiGenerations/routes/aiGenerations.js +426 -0
- package/pubmodules/aiGenerations/services/agentWriter.js +286 -0
- package/pubmodules/aiGenerations/services/atomic.js +70 -0
- package/pubmodules/aiGenerations/services/references.js +78 -0
- package/pubmodules/aiGenerations/services/revisionService.js +169 -0
- package/pubmodules/aiGenerations/services/snapshot.js +109 -0
- package/pubmodules/aiGenerations/test/aiGenerationsRoute.js +503 -0
- package/pubmodules/pubModulesManager.js +17 -0
- package/routes/faq.js +10 -3
- package/routes/faq_kb.js +86 -1
- package/services/faqService.js +36 -8
- package/services/v3FlowService.js +416 -0
- package/test/faqRoute.js +83 -0
- package/test/faqkbV3Route.js +436 -0
- package/test/nativemcp.test.js +6 -1
- package/test/v3FlowService.js +187 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rotte del modulo aiGenerations, montate su `/:projectid/modules/ai-generations` (ruolo admin).
|
|
3
|
+
*
|
|
4
|
+
* GET / stato del modulo
|
|
5
|
+
* POST /agents crea un agente con l'AI, in modo atomico, con la revisione 1
|
|
6
|
+
* GET /agents/:id_faq_kb/revisions la storia (leggera), a pagine
|
|
7
|
+
* POST /agents/:id_faq_kb/revisions checkpoint della bozza
|
|
8
|
+
* POST /agents/:id_faq_kb/revisions/release collega una release pubblicata
|
|
9
|
+
* GET /agents/:id_faq_kb/hash impronta corrente e ultima revisione
|
|
10
|
+
* POST /agents/:id_faq_kb/restore ripristina una revisione o una release, in place o come copia
|
|
11
|
+
* POST /agents/:id_faq_kb/edit applica operazioni sui blocchi e registra la revisione ai_edit
|
|
12
|
+
* DELETE /agents/:id_faq_kb/revisions cancella la storia
|
|
13
|
+
* GET /revisions/:revision_id una revisione completa (?snapshot=true aggiunge lo snapshot)
|
|
14
|
+
*/
|
|
15
|
+
const express = require('express');
|
|
16
|
+
const mongoose = require('mongoose');
|
|
17
|
+
const winston = require('../../../config/winston');
|
|
18
|
+
const Faq_kb = require('../../../models/faq_kb');
|
|
19
|
+
const revisionService = require('../services/revisionService');
|
|
20
|
+
const agentWriter = require('../services/agentWriter');
|
|
21
|
+
const atomic = require('../services/atomic');
|
|
22
|
+
const snapshotUtil = require('../services/snapshot');
|
|
23
|
+
const references = require('../services/references');
|
|
24
|
+
|
|
25
|
+
const router = express.Router({ mergeParams: true });
|
|
26
|
+
|
|
27
|
+
const MAX_INTENTS = Number(process.env.AI_GENERATIONS_MAX_INTENTS) || 1000;
|
|
28
|
+
const MAX_LIMIT = 50;
|
|
29
|
+
|
|
30
|
+
function isObjectId(id) {
|
|
31
|
+
return typeof id === 'string' && mongoose.Types.ObjectId.isValid(id) && /^[0-9a-fA-F]{24}$/.test(id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fail(res, err, context) {
|
|
35
|
+
const status = (err && err.status) || 500;
|
|
36
|
+
if (status >= 500) winston.error('(aiGenerations) ' + context + ': ' + (err && err.stack || err));
|
|
37
|
+
else winston.debug('(aiGenerations) ' + context + ': ' + (err && err.message));
|
|
38
|
+
return res.status(status).send({ success: false, message: (err && err.message) || 'Error', details: err && err.details });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function badRequest(message, details) {
|
|
42
|
+
const err = new Error(message);
|
|
43
|
+
err.status = 400;
|
|
44
|
+
err.details = details;
|
|
45
|
+
return err;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function conflict(message, details) {
|
|
49
|
+
const err = new Error(message);
|
|
50
|
+
err.status = 409;
|
|
51
|
+
err.details = details;
|
|
52
|
+
return err;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function notFound(message) {
|
|
56
|
+
const err = new Error(message);
|
|
57
|
+
err.status = 404;
|
|
58
|
+
return err;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Il blocco `ai` del body, ripulito: solo request e result, oggetti. */
|
|
62
|
+
function aiBlock(body) {
|
|
63
|
+
if (!body.ai) return undefined;
|
|
64
|
+
if (typeof body.ai !== 'object') throw badRequest('ai must be an object');
|
|
65
|
+
const ai = {};
|
|
66
|
+
if (body.ai.request !== undefined) {
|
|
67
|
+
if (typeof body.ai.request !== 'object') throw badRequest('ai.request must be an object');
|
|
68
|
+
ai.request = body.ai.request;
|
|
69
|
+
}
|
|
70
|
+
if (body.ai.result !== undefined) {
|
|
71
|
+
if (typeof body.ai.result !== 'object') throw badRequest('ai.result must be an object');
|
|
72
|
+
ai.result = body.ai.result;
|
|
73
|
+
}
|
|
74
|
+
return ai;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateIntents(intents) {
|
|
78
|
+
if (!Array.isArray(intents) || !intents.length) throw badRequest('agent.intents must be a non-empty array');
|
|
79
|
+
if (intents.length > MAX_INTENTS) throw badRequest('too many blocks', { max: MAX_INTENTS });
|
|
80
|
+
const names = new Set();
|
|
81
|
+
const ids = new Set();
|
|
82
|
+
intents.forEach((intent, index) => {
|
|
83
|
+
if (!intent || typeof intent !== 'object') throw badRequest('agent.intents[' + index + '] must be an object');
|
|
84
|
+
if (!intent.intent_display_name) throw badRequest('agent.intents[' + index + '].intent_display_name is required');
|
|
85
|
+
if (names.has(intent.intent_display_name)) throw badRequest('duplicate intent_display_name', { intent_display_name: intent.intent_display_name });
|
|
86
|
+
names.add(intent.intent_display_name);
|
|
87
|
+
if (intent.intent_id) {
|
|
88
|
+
if (ids.has(intent.intent_id)) throw badRequest('duplicate intent_id', { intent_id: intent.intent_id });
|
|
89
|
+
ids.add(intent.intent_id);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** L'agente del progetto, non cestinato; 404 o 409 altrimenti. */
|
|
95
|
+
async function loadAgent(req) {
|
|
96
|
+
const id_faq_kb = req.params.id_faq_kb;
|
|
97
|
+
if (!isObjectId(id_faq_kb)) throw badRequest('invalid id_faq_kb');
|
|
98
|
+
const kb = await revisionService.getAgent(req.projectid, id_faq_kb);
|
|
99
|
+
if (!kb) throw notFound('Chatbot not found with id ' + id_faq_kb + ' for project ' + req.projectid);
|
|
100
|
+
if (kb.trashed === true) throw conflict('Chatbot ' + id_faq_kb + ' is trashed');
|
|
101
|
+
return kb;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Se i blocchi correnti sono diversi dall'ultima revisione con snapshot, salva una revisione `auto`
|
|
106
|
+
* della versione corrente. Restituisce la revisione creata, oppure null.
|
|
107
|
+
*/
|
|
108
|
+
async function backupIfChanged(req, state, reason, session) {
|
|
109
|
+
const last = await revisionService.lastRevision(req.projectid, String(state.kb._id), { contentHash: { $ne: null } }, session);
|
|
110
|
+
if (last && last.contentHash === state.contentHash) return null;
|
|
111
|
+
return revisionService.create({
|
|
112
|
+
id_project: req.projectid,
|
|
113
|
+
id_faq_kb: state.kb._id,
|
|
114
|
+
kind: 'auto',
|
|
115
|
+
reason: reason,
|
|
116
|
+
note: last ? 'Modifiche successive alla revisione ' + last.seq : 'Stato precedente',
|
|
117
|
+
parent_id: last ? last.revision_id : null,
|
|
118
|
+
snapshot: state.snapshot,
|
|
119
|
+
contentHash: state.contentHash,
|
|
120
|
+
createdBy: req.user.id
|
|
121
|
+
}, session);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function checkExpectedHash(body, state) {
|
|
125
|
+
if (body.expectedHash && body.expectedHash !== state.contentHash) {
|
|
126
|
+
throw conflict('The chatbot has been modified: expected hash does not match', { expectedHash: body.expectedHash, contentHash: state.contentHash });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
router.get('/', async (req, res) => {
|
|
133
|
+
try {
|
|
134
|
+
res.json({ enabled: true, module: 'aiGenerations', transactions: await atomic.supportsTransactions(), max_intents: MAX_INTENTS });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
fail(res, err, 'status');
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/** Creazione atomica di un agente con l'AI. Body: { agent, ai, note }. */
|
|
141
|
+
router.post('/agents', async (req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
const agent = req.body.agent;
|
|
144
|
+
if (!agent || typeof agent !== 'object') throw badRequest('agent is required');
|
|
145
|
+
if (!agent.name || typeof agent.name !== 'string') throw badRequest('agent.name is required');
|
|
146
|
+
validateIntents(agent.intents);
|
|
147
|
+
const ai = aiBlock(req.body);
|
|
148
|
+
|
|
149
|
+
const created = await agentWriter.createAgent(req.projectid, req.user.id, agent);
|
|
150
|
+
const state = await revisionService.currentState(created.kb);
|
|
151
|
+
let revision;
|
|
152
|
+
try {
|
|
153
|
+
revision = await revisionService.create({
|
|
154
|
+
id_project: req.projectid,
|
|
155
|
+
id_faq_kb: created.kb._id,
|
|
156
|
+
kind: 'ai_create',
|
|
157
|
+
note: req.body.note || 'Creato con l\'AI',
|
|
158
|
+
ai: ai,
|
|
159
|
+
snapshot: state.snapshot,
|
|
160
|
+
contentHash: state.contentHash,
|
|
161
|
+
createdBy: req.user.id
|
|
162
|
+
});
|
|
163
|
+
} catch (err) {
|
|
164
|
+
winston.error('(aiGenerations) revision failed after creation, removing chatbot ' + created.kb._id + ': ' + err.message);
|
|
165
|
+
await mongoose.model('faq').deleteMany({ id_faq_kb: String(created.kb._id) }).exec().catch(() => null);
|
|
166
|
+
await Faq_kb.findByIdAndDelete(created.kb._id).exec().catch(() => null);
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
agentWriter.emitEvents({ createdFaqs: created.faqs, createdBot: { req: req, chatbot: created.kb } });
|
|
171
|
+
res.status(201).send({ bot_id: String(created.kb._id), chatbot: created.kb, revision: revisionService.light(revision) });
|
|
172
|
+
} catch (err) {
|
|
173
|
+
fail(res, err, 'POST /agents');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
router.get('/agents/:id_faq_kb/revisions', async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
const kb = await revisionService.getAgent(req.projectid, req.params.id_faq_kb).catch(() => null);
|
|
180
|
+
if (!kb) throw notFound('Chatbot not found with id ' + req.params.id_faq_kb + ' for project ' + req.projectid);
|
|
181
|
+
const page = Math.max(0, parseInt(req.query.page, 10) || 0);
|
|
182
|
+
const limit = Math.min(MAX_LIMIT, Math.max(1, parseInt(req.query.limit, 10) || 20));
|
|
183
|
+
res.json(await revisionService.list(req.projectid, String(kb._id), page, limit));
|
|
184
|
+
} catch (err) {
|
|
185
|
+
fail(res, err, 'GET revisions');
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/** Checkpoint della bozza. Body: { note }. */
|
|
190
|
+
router.post('/agents/:id_faq_kb/revisions', async (req, res) => {
|
|
191
|
+
try {
|
|
192
|
+
const kb = await loadAgent(req);
|
|
193
|
+
const state = await revisionService.currentState(kb);
|
|
194
|
+
const last = await revisionService.lastRevision(req.projectid, String(kb._id));
|
|
195
|
+
const revision = await revisionService.create({
|
|
196
|
+
id_project: req.projectid,
|
|
197
|
+
id_faq_kb: kb._id,
|
|
198
|
+
kind: 'checkpoint',
|
|
199
|
+
note: req.body.note || 'Versione salvata',
|
|
200
|
+
parent_id: last ? last.revision_id : null,
|
|
201
|
+
snapshot: state.snapshot,
|
|
202
|
+
contentHash: state.contentHash,
|
|
203
|
+
createdBy: req.user.id
|
|
204
|
+
});
|
|
205
|
+
await revisionService.prune(req.projectid, String(kb._id));
|
|
206
|
+
res.status(201).send({ revision: revisionService.light(revision) });
|
|
207
|
+
} catch (err) {
|
|
208
|
+
fail(res, err, 'POST checkpoint');
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
/** Collega una release pubblicata. Body: { release_id }. */
|
|
213
|
+
router.post('/agents/:id_faq_kb/revisions/release', async (req, res) => {
|
|
214
|
+
try {
|
|
215
|
+
const kb = await loadAgent(req);
|
|
216
|
+
const release_id = req.body.release_id;
|
|
217
|
+
if (!isObjectId(release_id)) throw badRequest('release_id is required');
|
|
218
|
+
const release = await Faq_kb.findOne({ _id: release_id, id_project: req.projectid, root_id: String(kb._id) }).exec();
|
|
219
|
+
if (!release) throw notFound('Release ' + release_id + ' not found for chatbot ' + kb._id);
|
|
220
|
+
const existing = await revisionService.lastRevision(req.projectid, String(kb._id), { kind: 'release', release_id: release_id });
|
|
221
|
+
if (existing) return res.status(200).send({ revision: revisionService.light(existing), already_linked: true });
|
|
222
|
+
const last = await revisionService.lastRevision(req.projectid, String(kb._id));
|
|
223
|
+
const revision = await revisionService.create({
|
|
224
|
+
id_project: req.projectid,
|
|
225
|
+
id_faq_kb: kb._id,
|
|
226
|
+
kind: 'release',
|
|
227
|
+
note: req.body.note || release.release_note || undefined,
|
|
228
|
+
parent_id: last ? last.revision_id : null,
|
|
229
|
+
contentHash: last ? last.contentHash : null,
|
|
230
|
+
release_id: release_id,
|
|
231
|
+
publishedAt: release.publishedAt || new Date(),
|
|
232
|
+
createdBy: req.user.id
|
|
233
|
+
});
|
|
234
|
+
res.status(201).send({ revision: revisionService.light(revision) });
|
|
235
|
+
} catch (err) {
|
|
236
|
+
fail(res, err, 'POST release');
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
router.get('/agents/:id_faq_kb/hash', async (req, res) => {
|
|
241
|
+
try {
|
|
242
|
+
const kb = await loadAgent(req);
|
|
243
|
+
const state = await revisionService.currentState(kb);
|
|
244
|
+
const last = await revisionService.lastRevision(req.projectid, String(kb._id), { contentHash: { $ne: null } });
|
|
245
|
+
res.json({
|
|
246
|
+
contentHash: state.contentHash,
|
|
247
|
+
intents: state.faqs.length,
|
|
248
|
+
revision_id: last ? last.revision_id : null,
|
|
249
|
+
revision_seq: last ? last.seq : null,
|
|
250
|
+
modified: last ? last.contentHash !== state.contentHash : null
|
|
251
|
+
});
|
|
252
|
+
} catch (err) {
|
|
253
|
+
fail(res, err, 'GET hash');
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/** Lo snapshot da ripristinare: da una revisione o da una release. */
|
|
258
|
+
async function loadSource(req, kb) {
|
|
259
|
+
const body = req.body || {};
|
|
260
|
+
if (body.revision_id) {
|
|
261
|
+
const revision = await revisionService.getRevision(req.projectid, body.revision_id);
|
|
262
|
+
if (!revision || revision.id_faq_kb !== String(kb._id)) throw notFound('Revision ' + body.revision_id + ' not found for chatbot ' + kb._id);
|
|
263
|
+
const snapshot = snapshotUtil.decompress(revision.snapshot);
|
|
264
|
+
if (!snapshot) throw conflict('Revision ' + body.revision_id + ' has no snapshot (kind ' + revision.kind + ')');
|
|
265
|
+
return { snapshot: snapshot, parent_id: revision.revision_id, label: 'revisione ' + revision.seq };
|
|
266
|
+
}
|
|
267
|
+
if (body.release_id) {
|
|
268
|
+
if (!isObjectId(body.release_id)) throw badRequest('invalid release_id');
|
|
269
|
+
const release = await Faq_kb.findOne({ _id: body.release_id, id_project: req.projectid, root_id: String(kb._id) }).exec();
|
|
270
|
+
if (!release) throw notFound('Release ' + body.release_id + ' not found for chatbot ' + kb._id);
|
|
271
|
+
const state = await revisionService.currentState(release);
|
|
272
|
+
const linked = await revisionService.lastRevision(req.projectid, String(kb._id), { kind: 'release', release_id: body.release_id });
|
|
273
|
+
return { snapshot: state.snapshot, parent_id: linked ? linked.revision_id : null, label: 'release ' + body.release_id };
|
|
274
|
+
}
|
|
275
|
+
throw badRequest('revision_id or release_id is required');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Ripristino. Body: { revision_id | release_id, as_copy, expectedHash, note, name, dry_run }.
|
|
280
|
+
* Con `dry_run: true` non scrive nulla: risponde con i riferimenti esterni dello snapshot che nel progetto
|
|
281
|
+
* non esistono più (dipartimenti, knowledge base, tabelle, agenti) e con l'impronta corrente.
|
|
282
|
+
*/
|
|
283
|
+
router.post('/agents/:id_faq_kb/restore', async (req, res) => {
|
|
284
|
+
try {
|
|
285
|
+
const kb = await loadAgent(req);
|
|
286
|
+
const source = await loadSource(req, kb);
|
|
287
|
+
validateIntents(source.snapshot.intents);
|
|
288
|
+
|
|
289
|
+
if (req.body.dry_run === true) {
|
|
290
|
+
const state = await revisionService.currentState(kb);
|
|
291
|
+
const missing = await references.missingReferences(source.snapshot, req.projectid);
|
|
292
|
+
return res.status(200).send({
|
|
293
|
+
dry_run: true,
|
|
294
|
+
source: source.label,
|
|
295
|
+
intents: source.snapshot.intents.length,
|
|
296
|
+
contentHash: state.contentHash,
|
|
297
|
+
changed: !!req.body.expectedHash && req.body.expectedHash !== state.contentHash,
|
|
298
|
+
missing: missing
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (req.body.as_copy === true) {
|
|
303
|
+
const copy = Object.assign({}, source.snapshot, { name: req.body.name || (source.snapshot.name + ' (copia)') });
|
|
304
|
+
const created = await agentWriter.createAgent(req.projectid, req.user.id, copy);
|
|
305
|
+
const state = await revisionService.currentState(created.kb);
|
|
306
|
+
const revision = await revisionService.create({
|
|
307
|
+
id_project: req.projectid,
|
|
308
|
+
id_faq_kb: created.kb._id,
|
|
309
|
+
kind: 'restore',
|
|
310
|
+
reason: 'as_copy',
|
|
311
|
+
note: req.body.note || ('Copia di ' + kb.name + ', ' + source.label),
|
|
312
|
+
parent_id: source.parent_id,
|
|
313
|
+
snapshot: state.snapshot,
|
|
314
|
+
contentHash: state.contentHash,
|
|
315
|
+
createdBy: req.user.id
|
|
316
|
+
});
|
|
317
|
+
agentWriter.emitEvents({ createdFaqs: created.faqs, createdBot: { req: req, chatbot: created.kb } });
|
|
318
|
+
return res.status(201).send({ bot_id: String(created.kb._id), chatbot: created.kb, revision: revisionService.light(revision), backup: null });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const state = await revisionService.currentState(kb);
|
|
322
|
+
checkExpectedHash(req.body, state);
|
|
323
|
+
|
|
324
|
+
const outcome = await atomic.run(async (session) => {
|
|
325
|
+
const backup = await backupIfChanged(req, state, 'before_restore', session);
|
|
326
|
+
const replaced = await agentWriter.replaceBlocks(kb, source.snapshot, req.projectid, req.user.id, session);
|
|
327
|
+
const updatedBot = await agentWriter.updateAgentFromSnapshot(kb, source.snapshot, session);
|
|
328
|
+
const after = await revisionService.currentState(updatedBot, session);
|
|
329
|
+
const revision = await revisionService.create({
|
|
330
|
+
id_project: req.projectid,
|
|
331
|
+
id_faq_kb: kb._id,
|
|
332
|
+
kind: 'restore',
|
|
333
|
+
reason: 'in_place',
|
|
334
|
+
note: req.body.note || ('Ripristino della ' + source.label),
|
|
335
|
+
parent_id: source.parent_id,
|
|
336
|
+
snapshot: after.snapshot,
|
|
337
|
+
contentHash: after.contentHash,
|
|
338
|
+
createdBy: req.user.id
|
|
339
|
+
}, session);
|
|
340
|
+
return { backup: backup, replaced: replaced, updatedBot: updatedBot, revision: revision };
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
agentWriter.emitEvents({ deletedFaqs: outcome.replaced.removed, createdFaqs: outcome.replaced.inserted, updatedBot: outcome.updatedBot });
|
|
344
|
+
await revisionService.prune(req.projectid, String(kb._id));
|
|
345
|
+
res.status(200).send({ bot_id: String(kb._id), revision: revisionService.light(outcome.revision), backup: revisionService.light(outcome.backup) });
|
|
346
|
+
} catch (err) {
|
|
347
|
+
fail(res, err, 'POST restore');
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
/** Modifica via prompt. Body: { operations, ai, expectedHash, note }. */
|
|
352
|
+
router.post('/agents/:id_faq_kb/edit', async (req, res) => {
|
|
353
|
+
try {
|
|
354
|
+
const kb = await loadAgent(req);
|
|
355
|
+
const operations = req.body.operations;
|
|
356
|
+
if (!Array.isArray(operations) || !operations.length) throw badRequest('operations must be a non-empty array');
|
|
357
|
+
if (operations.length > MAX_INTENTS) throw badRequest('too many operations', { max: MAX_INTENTS });
|
|
358
|
+
const ai = aiBlock(req.body) || {};
|
|
359
|
+
|
|
360
|
+
const state = await revisionService.currentState(kb);
|
|
361
|
+
checkExpectedHash(req.body, state);
|
|
362
|
+
|
|
363
|
+
const outcome = await atomic.run(async (session) => {
|
|
364
|
+
const backup = await backupIfChanged(req, state, 'before_ai_edit', session);
|
|
365
|
+
let applied;
|
|
366
|
+
try {
|
|
367
|
+
applied = await agentWriter.applyOperations(kb, operations, req.projectid, req.user.id, session);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
if (!session) {
|
|
370
|
+
// Senza transazione: si rimettono i blocchi di prima, poi si rilancia.
|
|
371
|
+
await agentWriter.replaceBlocks(kb, state.snapshot, req.projectid, req.user.id, null).catch(e => winston.error('(aiGenerations) edit recovery failed: ' + e.message));
|
|
372
|
+
}
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
375
|
+
const updatedBot = await agentWriter.setModified(kb._id, session);
|
|
376
|
+
const after = await revisionService.currentState(updatedBot, session);
|
|
377
|
+
const last = await revisionService.lastRevision(req.projectid, String(kb._id), null, session);
|
|
378
|
+
const changes = {
|
|
379
|
+
added: applied.created.map(f => f.intent_id),
|
|
380
|
+
updated: applied.updated.map(f => f.intent_id),
|
|
381
|
+
removed: applied.deleted.map(f => f.intent_id)
|
|
382
|
+
};
|
|
383
|
+
const aiWithChanges = Object.assign({}, ai, { result: Object.assign({}, ai.result || {}, { changes: changes }) });
|
|
384
|
+
const revision = await revisionService.create({
|
|
385
|
+
id_project: req.projectid,
|
|
386
|
+
id_faq_kb: kb._id,
|
|
387
|
+
kind: 'ai_edit',
|
|
388
|
+
note: req.body.note || 'Modificato con l\'AI',
|
|
389
|
+
parent_id: last ? last.revision_id : null,
|
|
390
|
+
ai: aiWithChanges,
|
|
391
|
+
snapshot: after.snapshot,
|
|
392
|
+
contentHash: after.contentHash,
|
|
393
|
+
createdBy: req.user.id
|
|
394
|
+
}, session);
|
|
395
|
+
return { backup: backup, applied: applied, updatedBot: updatedBot, revision: revision, changes: changes };
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
agentWriter.emitEvents({ deletedFaqs: outcome.applied.deleted, createdFaqs: outcome.applied.created, updatedFaqs: outcome.applied.updated, updatedBot: outcome.updatedBot });
|
|
399
|
+
await revisionService.prune(req.projectid, String(kb._id));
|
|
400
|
+
res.status(200).send({ bot_id: String(kb._id), revision: revisionService.light(outcome.revision), backup: revisionService.light(outcome.backup), changes: outcome.changes });
|
|
401
|
+
} catch (err) {
|
|
402
|
+
fail(res, err, 'POST edit');
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
router.delete('/agents/:id_faq_kb/revisions', async (req, res) => {
|
|
407
|
+
try {
|
|
408
|
+
if (!isObjectId(req.params.id_faq_kb)) throw badRequest('invalid id_faq_kb');
|
|
409
|
+
const deleted = await revisionService.deleteAll(req.projectid, req.params.id_faq_kb);
|
|
410
|
+
res.json({ success: true, deleted: deleted });
|
|
411
|
+
} catch (err) {
|
|
412
|
+
fail(res, err, 'DELETE revisions');
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
router.get('/revisions/:revision_id', async (req, res) => {
|
|
417
|
+
try {
|
|
418
|
+
const revision = await revisionService.getRevision(req.projectid, req.params.revision_id);
|
|
419
|
+
if (!revision) throw notFound('Revision ' + req.params.revision_id + ' not found');
|
|
420
|
+
res.json(revisionService.full(revision, req.query.snapshot === 'true'));
|
|
421
|
+
} catch (err) {
|
|
422
|
+
fail(res, err, 'GET revision');
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
module.exports = router;
|