@adminforth/bulk-ai-flow 1.27.5 → 1.27.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +15 -0
- package/dist/index.js +101 -80
- package/index.ts +92 -72
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,15 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
10
10
|
rateLimiters: Record<string, RateLimiter>;
|
|
11
11
|
sessionIds: Set<string>;
|
|
12
12
|
constructor(options: PluginOptions);
|
|
13
|
+
/**
|
|
14
|
+
* Compiles Handlebars templates from `source`.
|
|
15
|
+
*
|
|
16
|
+
* `record` is the raw row and is always what `provideAdditionalContextForRecord` sees, but the
|
|
17
|
+
* templates themselves are rendered against `record` only when they come from `PluginOptions`:
|
|
18
|
+
* those are written by the app author and may reference any column. Templates which arrived in
|
|
19
|
+
* the request body are rendered against the record as the caller is allowed to read it, so a
|
|
20
|
+
* hand-written `{{column}}` cannot pull a backendOnly column out of the row.
|
|
21
|
+
*/
|
|
13
22
|
private compileTemplates;
|
|
14
23
|
private compileOutputFieldsTemplates;
|
|
15
24
|
private compileOutputFieldsTemplatesNoImage;
|
|
@@ -48,5 +57,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
48
57
|
* way the core show route drops them.
|
|
49
58
|
*/
|
|
50
59
|
private scopeRecordForFrontend;
|
|
60
|
+
/**
|
|
61
|
+
* The row as the caller is allowed to read it: columns which are not declared on the resource and
|
|
62
|
+
* columns whose `backendOnly` resolves true for this request are dropped, exactly the way the
|
|
63
|
+
* core show route drops them before a row reaches the browser.
|
|
64
|
+
*/
|
|
65
|
+
private stripBackendOnlyColumns;
|
|
51
66
|
setupEndpoints(server: IHttpServer): void;
|
|
52
67
|
}
|
package/dist/index.js
CHANGED
|
@@ -106,40 +106,49 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
106
106
|
// for rate limiting
|
|
107
107
|
this.sessionIds = new Set();
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Compiles Handlebars templates from `source`.
|
|
111
|
+
*
|
|
112
|
+
* `record` is the raw row and is always what `provideAdditionalContextForRecord` sees, but the
|
|
113
|
+
* templates themselves are rendered against `record` only when they come from `PluginOptions`:
|
|
114
|
+
* those are written by the app author and may reference any column. Templates which arrived in
|
|
115
|
+
* the request body are rendered against the record as the caller is allowed to read it, so a
|
|
116
|
+
* hand-written `{{column}}` cannot pull a backendOnly column out of the row.
|
|
117
|
+
*/
|
|
118
|
+
compileTemplates(source_1, _a, valueSelector_1) {
|
|
119
|
+
return __awaiter(this, arguments, void 0, function* (source, { record, ctx, clientSupplied }, valueSelector) {
|
|
120
|
+
let templateContext = clientSupplied ? yield this.stripBackendOnlyColumns(record, ctx) : record;
|
|
112
121
|
if (this.options.provideAdditionalContextForRecord) {
|
|
113
|
-
const additionalFields = yield this.options.provideAdditionalContextForRecord({ record, adminUser:
|
|
114
|
-
|
|
122
|
+
const additionalFields = yield this.options.provideAdditionalContextForRecord({ record, adminUser: ctx.adminUser, resource: this.resourceConfig });
|
|
123
|
+
templateContext = Object.assign(Object.assign({}, templateContext), additionalFields);
|
|
115
124
|
}
|
|
116
125
|
const compiled = {};
|
|
117
126
|
for (const [key, value] of Object.entries(source)) {
|
|
118
127
|
const templateStr = valueSelector(value);
|
|
119
128
|
try {
|
|
120
129
|
const tpl = Handlebars.compile(templateStr);
|
|
121
|
-
compiled[key] = tpl(
|
|
130
|
+
compiled[key] = tpl(templateContext);
|
|
122
131
|
}
|
|
123
|
-
catch (
|
|
132
|
+
catch (_b) {
|
|
124
133
|
compiled[key] = templateStr;
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
return compiled;
|
|
128
137
|
});
|
|
129
138
|
}
|
|
130
|
-
compileOutputFieldsTemplates(
|
|
131
|
-
return __awaiter(this,
|
|
132
|
-
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillFieldsFromImages, record, v => String(v));
|
|
139
|
+
compileOutputFieldsTemplates(record_1, ctx_1, customPrompt_1) {
|
|
140
|
+
return __awaiter(this, arguments, void 0, function* (record, ctx, customPrompt, clientSupplied = !!customPrompt) {
|
|
141
|
+
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillFieldsFromImages, { record, ctx, clientSupplied }, v => String(v));
|
|
133
142
|
});
|
|
134
143
|
}
|
|
135
|
-
compileOutputFieldsTemplatesNoImage(
|
|
136
|
-
return __awaiter(this,
|
|
137
|
-
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillPlainFields, record, v => String(v));
|
|
144
|
+
compileOutputFieldsTemplatesNoImage(record_1, ctx_1, customPrompt_1) {
|
|
145
|
+
return __awaiter(this, arguments, void 0, function* (record, ctx, customPrompt, clientSupplied = !!customPrompt) {
|
|
146
|
+
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillPlainFields, { record, ctx, clientSupplied }, v => String(v));
|
|
138
147
|
});
|
|
139
148
|
}
|
|
140
|
-
compileGenerationFieldTemplates(
|
|
141
|
-
return __awaiter(this,
|
|
142
|
-
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.generateImages, record, v => String(customPrompt ? v : v.prompt));
|
|
149
|
+
compileGenerationFieldTemplates(record_1, ctx_1, customPrompt_1) {
|
|
150
|
+
return __awaiter(this, arguments, void 0, function* (record, ctx, customPrompt, clientSupplied = !!customPrompt) {
|
|
151
|
+
return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.generateImages, { record, ctx, clientSupplied }, v => String(customPrompt ? v : v.prompt));
|
|
143
152
|
});
|
|
144
153
|
}
|
|
145
154
|
removeFromPromptFilledFields(compiledOutputFields, record) {
|
|
@@ -188,19 +197,15 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
188
197
|
Do NOT wrap in \`\`\` or \`\`\`json. Do not add any extra text. Do not return prompt in response`;
|
|
189
198
|
return prompt;
|
|
190
199
|
}
|
|
191
|
-
analyze_image(jobId_1,
|
|
192
|
-
return __awaiter(this, arguments, void 0, function* (jobId,
|
|
200
|
+
analyze_image(jobId_1, record_1, ctx_1, headers_1, customPrompt_1) {
|
|
201
|
+
return __awaiter(this, arguments, void 0, function* (jobId, record, ctx, headers, customPrompt, filterFilledFields = true) {
|
|
193
202
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u;
|
|
194
203
|
// TODO: need to do correct check of rate limit
|
|
195
204
|
// if (this.options.rateLimits && this.options.rateLimits.fillFieldsFromImages && await this.checkRateLimit("fillFieldsFromImages" ,this.options.rateLimits.fillFieldsFromImages, headers)) {
|
|
196
205
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
197
206
|
// return { error: "Rate limit exceeded" };
|
|
198
207
|
// }
|
|
199
|
-
const selectedId = recordId;
|
|
200
208
|
let isError = false;
|
|
201
|
-
// Fetch the record using the provided ID
|
|
202
|
-
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
203
|
-
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)]);
|
|
204
209
|
//recieve image URLs to analyze
|
|
205
210
|
const attachmentFiles = yield this.options.attachFiles({ record: record });
|
|
206
211
|
if (STUB_MODE) {
|
|
@@ -228,7 +233,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
228
233
|
return { ok: false, error: 'One of the image URLs is not valid' };
|
|
229
234
|
}
|
|
230
235
|
//create prompt for OpenAI
|
|
231
|
-
const compiledOutputFields = yield this.compileOutputFieldsTemplates(record, customPrompt);
|
|
236
|
+
const compiledOutputFields = yield this.compileOutputFieldsTemplates(record, ctx, customPrompt);
|
|
232
237
|
const filteredCompiledOutputFields = filterFilledFields ? this.removeFromPromptFilledFields(compiledOutputFields, record) : compiledOutputFields;
|
|
233
238
|
if (Object.keys(filteredCompiledOutputFields).length === 0) {
|
|
234
239
|
jobs.set(jobId, { status: 'completed', result: {} });
|
|
@@ -276,14 +281,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
276
281
|
}
|
|
277
282
|
});
|
|
278
283
|
}
|
|
279
|
-
analyzeNoImages(jobId_1,
|
|
280
|
-
return __awaiter(this, arguments, void 0, function* (jobId,
|
|
284
|
+
analyzeNoImages(jobId_1, record_1, ctx_1, headers_1, customPrompt_1) {
|
|
285
|
+
return __awaiter(this, arguments, void 0, function* (jobId, record, ctx, headers, customPrompt, filterFilledFields = true) {
|
|
281
286
|
// TODO: need to do correct check of rate limit
|
|
282
287
|
// if (this.options.rateLimits && this.options.rateLimits.fillPlainFields && await this.checkRateLimit("fillPlainFields" ,this.options.rateLimits.fillPlainFields, headers)) {
|
|
283
288
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
284
289
|
// return { error: "Rate limit exceeded" };
|
|
285
290
|
// }
|
|
286
|
-
const selectedId = recordId;
|
|
287
291
|
let isError = false;
|
|
288
292
|
if (STUB_MODE) {
|
|
289
293
|
const fakeError = Math.random() < 0.005; // 0.05% chance of error
|
|
@@ -298,9 +302,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
298
302
|
}
|
|
299
303
|
}
|
|
300
304
|
else {
|
|
301
|
-
const
|
|
302
|
-
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)]);
|
|
303
|
-
const compiledOutputFields = yield this.compileOutputFieldsTemplatesNoImage(record, customPrompt);
|
|
305
|
+
const compiledOutputFields = yield this.compileOutputFieldsTemplatesNoImage(record, ctx, customPrompt);
|
|
304
306
|
const filteredCompiledOutputFields = filterFilledFields ? this.removeFromPromptFilledFields(compiledOutputFields, record) : compiledOutputFields;
|
|
305
307
|
if (Object.keys(filteredCompiledOutputFields).length === 0) {
|
|
306
308
|
jobs.set(jobId, { status: 'completed', result: {} });
|
|
@@ -342,18 +344,16 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
342
344
|
}
|
|
343
345
|
});
|
|
344
346
|
}
|
|
345
|
-
initialImageGenerate(jobId_1,
|
|
346
|
-
return __awaiter(this, arguments, void 0, function* (jobId,
|
|
347
|
-
var _a
|
|
347
|
+
initialImageGenerate(jobId_1, record_1, ctx_1, headers_1, customPrompt_1) {
|
|
348
|
+
return __awaiter(this, arguments, void 0, function* (jobId, record, ctx, headers, customPrompt, filterFilledFields = true) {
|
|
349
|
+
var _a;
|
|
348
350
|
// TODO: need to do correct check of rate limit
|
|
349
351
|
// if (this.options.rateLimits && this.options.rateLimits.generateImages && await this.checkRateLimit("generateImages" ,this.options.rateLimits.generateImages, headers)) {
|
|
350
352
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
351
353
|
// return { error: "Rate limit exceeded" };
|
|
352
354
|
// }
|
|
353
|
-
const selectedId = recordId;
|
|
354
355
|
let isError = false;
|
|
355
356
|
const start = +new Date();
|
|
356
|
-
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ((_a = this.resourceConfig.columns.find(c => c.primaryKey)) === null || _a === void 0 ? void 0 : _a.name, selectedId)]);
|
|
357
357
|
let attachmentFiles;
|
|
358
358
|
if (!this.options.attachFiles) {
|
|
359
359
|
attachmentFiles = [];
|
|
@@ -370,7 +370,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
370
370
|
return { ok: false, error: 'One of the image URLs is not valid' };
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
|
-
const fieldTasks = Object.keys(((
|
|
373
|
+
const fieldTasks = Object.keys(((_a = this.options) === null || _a === void 0 ? void 0 : _a.generateImages) || {}).map((key) => __awaiter(this, void 0, void 0, function* () {
|
|
374
374
|
if (record[key] && filterFilledFields) {
|
|
375
375
|
const plugin = this.adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.resourceConfig.resourceId &&
|
|
376
376
|
p.pluginOptions.pathColumnName === key);
|
|
@@ -384,7 +384,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
384
384
|
}
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
|
-
const prompt = (yield this.compileGenerationFieldTemplates(record, customPrompt))[key];
|
|
387
|
+
const prompt = (yield this.compileGenerationFieldTemplates(record, ctx, customPrompt))[key];
|
|
388
388
|
let images;
|
|
389
389
|
if (this.options.attachFiles && attachmentFiles.length === 0) {
|
|
390
390
|
isError = true;
|
|
@@ -456,10 +456,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
456
456
|
}
|
|
457
457
|
});
|
|
458
458
|
}
|
|
459
|
-
regenerateImage(jobId,
|
|
459
|
+
regenerateImage(jobId, record, fieldName, prompt, headers) {
|
|
460
460
|
return __awaiter(this, void 0, void 0, function* () {
|
|
461
|
-
var _a;
|
|
462
|
-
const Id = recordId;
|
|
463
461
|
let isError = false;
|
|
464
462
|
// TODO: need to do correct check of rate limit
|
|
465
463
|
// if (await this.checkRateLimit(fieldName, this.options.generateImages[fieldName].rateLimit, headers)) {
|
|
@@ -467,7 +465,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
467
465
|
// return { error: "Rate limit exceeded" };
|
|
468
466
|
// }
|
|
469
467
|
const start = +new Date();
|
|
470
|
-
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ((_a = this.resourceConfig.columns.find(c => c.primaryKey)) === null || _a === void 0 ? void 0 : _a.name, Id)]);
|
|
471
468
|
let attachmentFiles;
|
|
472
469
|
if (!this.options.attachFiles) {
|
|
473
470
|
attachmentFiles = [];
|
|
@@ -525,13 +522,16 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
525
522
|
}
|
|
526
523
|
});
|
|
527
524
|
}
|
|
528
|
-
regenerateCell(jobId, fieldToRegenerate,
|
|
525
|
+
regenerateCell(jobId, fieldToRegenerate, record, ctx, actionType, prompt) {
|
|
529
526
|
return __awaiter(this, void 0, void 0, function* () {
|
|
530
527
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
531
|
-
if (!fieldToRegenerate || !
|
|
528
|
+
if (!fieldToRegenerate || !actionType) {
|
|
532
529
|
jobs.set(jobId, { status: 'failed', error: 'Missing parameters' });
|
|
533
530
|
//return { ok: false, error: "Missing parameters" };
|
|
534
531
|
}
|
|
532
|
+
// a prompt which arrived in the request body is client-authored even though it is wrapped into
|
|
533
|
+
// `promptToPass` below, while the PluginOptions fallback is written by the app author
|
|
534
|
+
const clientSupplied = !!prompt;
|
|
535
535
|
if (!prompt) {
|
|
536
536
|
if (actionType === 'analyze') {
|
|
537
537
|
prompt = this.options.fillFieldsFromImages ? this.options.fillFieldsFromImages[fieldToRegenerate] : null;
|
|
@@ -540,8 +540,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
540
540
|
prompt = this.options.fillPlainFields ? this.options.fillPlainFields[fieldToRegenerate] : null;
|
|
541
541
|
}
|
|
542
542
|
}
|
|
543
|
-
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
544
|
-
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, recordId)]);
|
|
545
543
|
let promptToPass = JSON.stringify({ [fieldToRegenerate]: prompt });
|
|
546
544
|
if (STUB_MODE) {
|
|
547
545
|
yield new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
|
|
@@ -550,7 +548,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
550
548
|
}
|
|
551
549
|
else {
|
|
552
550
|
if (actionType === 'analyze') {
|
|
553
|
-
const compiledPropmt = yield this.compileOutputFieldsTemplates(record, promptToPass);
|
|
551
|
+
const compiledPropmt = yield this.compileOutputFieldsTemplates(record, ctx, promptToPass, clientSupplied);
|
|
554
552
|
const finalPrompt = this.getPromptForImageAnalysis(compiledPropmt);
|
|
555
553
|
const attachmentFiles = yield this.options.attachFiles({ record: record });
|
|
556
554
|
if (attachmentFiles.length === 0) {
|
|
@@ -587,7 +585,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
587
585
|
jobs.set(jobId, { status: 'completed', result: resData });
|
|
588
586
|
}
|
|
589
587
|
else if (actionType === 'analyze_no_images') {
|
|
590
|
-
const compiledPropmt = yield this.compileOutputFieldsTemplatesNoImage(record, promptToPass);
|
|
588
|
+
const compiledPropmt = yield this.compileOutputFieldsTemplatesNoImage(record, ctx, promptToPass, clientSupplied);
|
|
591
589
|
const finalPrompt = this.getPromptForPlainFields(compiledPropmt);
|
|
592
590
|
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
|
|
593
591
|
let resp;
|
|
@@ -987,6 +985,23 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
987
985
|
return scoped;
|
|
988
986
|
});
|
|
989
987
|
}
|
|
988
|
+
/**
|
|
989
|
+
* The row as the caller is allowed to read it: columns which are not declared on the resource and
|
|
990
|
+
* columns whose `backendOnly` resolves true for this request are dropped, exactly the way the
|
|
991
|
+
* core show route drops them before a row reaches the browser.
|
|
992
|
+
*/
|
|
993
|
+
stripBackendOnlyColumns(record, ctx) {
|
|
994
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
995
|
+
const visible = {};
|
|
996
|
+
for (const column of this.resourceConfig.columns) {
|
|
997
|
+
if (yield resolveBoolOrFn(column.backendOnly, ctx)) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
visible[column.name] = record[column.name];
|
|
1001
|
+
}
|
|
1002
|
+
return visible;
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
990
1005
|
setupEndpoints(server) {
|
|
991
1006
|
server.endpoint({
|
|
992
1007
|
method: 'POST',
|
|
@@ -1159,7 +1174,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1159
1174
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl }) {
|
|
1160
1175
|
const data = body;
|
|
1161
1176
|
const customPrompt = data.customPrompt || null;
|
|
1162
|
-
const { record, error } = yield this.getRecordForShow({
|
|
1177
|
+
const { record, ctx, error } = yield this.getRecordForShow({
|
|
1163
1178
|
adminUser,
|
|
1164
1179
|
recordId: data.recordId,
|
|
1165
1180
|
extra: { body, query, headers, cookies, requestUrl },
|
|
@@ -1167,7 +1182,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1167
1182
|
if (error) {
|
|
1168
1183
|
return { ok: false, error };
|
|
1169
1184
|
}
|
|
1170
|
-
const compiledGenerationOptions = yield this.compileGenerationFieldTemplates(record, JSON.stringify({ "prompt": customPrompt }));
|
|
1185
|
+
const compiledGenerationOptions = yield this.compileGenerationFieldTemplates(record, ctx, JSON.stringify({ "prompt": customPrompt }));
|
|
1171
1186
|
return compiledGenerationOptions;
|
|
1172
1187
|
})
|
|
1173
1188
|
});
|
|
@@ -1186,7 +1201,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1186
1201
|
method: 'POST',
|
|
1187
1202
|
path: `/plugin/${this.pluginInstanceId}/create-job`,
|
|
1188
1203
|
request_schema: createJobBodySchema,
|
|
1189
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, response }) {
|
|
1204
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl, response }) {
|
|
1190
1205
|
const data = body;
|
|
1191
1206
|
const { actionType, recordId, customPrompt, filterFilledFields, sessionIds } = data;
|
|
1192
1207
|
if (this.options.rateLimits) {
|
|
@@ -1202,41 +1217,47 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1202
1217
|
return { ok: false, error: "Missing session ids" };
|
|
1203
1218
|
}
|
|
1204
1219
|
}
|
|
1205
|
-
const jobId = randomUUID();
|
|
1206
|
-
jobs.set(jobId, { status: "in_progress" });
|
|
1207
1220
|
if (!actionType) {
|
|
1208
|
-
|
|
1209
|
-
//return { error: "Missing action type" };
|
|
1221
|
+
return { ok: false, error: "Missing action type" };
|
|
1210
1222
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
//return { error: "Missing record id" };
|
|
1223
|
+
if (!recordId && typeof recordId !== 'number') {
|
|
1224
|
+
return { ok: false, error: "Missing record id" };
|
|
1214
1225
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1226
|
+
// every worker below compiles this record into an AI prompt whose answer is handed back
|
|
1227
|
+
// through get-job-status, so the record is read here, behind the same show check and
|
|
1228
|
+
// row-level scoping the other read endpoints use, and the row itself is what gets dispatched
|
|
1229
|
+
const { record, ctx, error } = yield this.getRecordForShow({
|
|
1230
|
+
adminUser,
|
|
1231
|
+
recordId,
|
|
1232
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1233
|
+
});
|
|
1234
|
+
if (error) {
|
|
1235
|
+
return { ok: false, error };
|
|
1236
|
+
}
|
|
1237
|
+
const jobId = randomUUID();
|
|
1238
|
+
jobs.set(jobId, { status: "in_progress" });
|
|
1239
|
+
switch (actionType) {
|
|
1240
|
+
case 'generate_images':
|
|
1241
|
+
this.initialImageGenerate(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1242
|
+
break;
|
|
1243
|
+
case 'analyze_no_images':
|
|
1244
|
+
this.analyzeNoImages(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1245
|
+
break;
|
|
1246
|
+
case 'analyze':
|
|
1247
|
+
this.analyze_image(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1248
|
+
break;
|
|
1249
|
+
case 'regenerate_images':
|
|
1250
|
+
if (!data.prompt || !data.fieldName) {
|
|
1251
|
+
jobs.set(jobId, { status: "failed", error: "Missing prompt or field name" });
|
|
1236
1252
|
break;
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1253
|
+
}
|
|
1254
|
+
this.regenerateImage(jobId, record, data.fieldName, data.prompt, headers);
|
|
1255
|
+
break;
|
|
1256
|
+
case 'regenerate_cell':
|
|
1257
|
+
this.regenerateCell(jobId, data.fieldToRegenerate, record, ctx, data.action, data.prompt);
|
|
1258
|
+
break;
|
|
1259
|
+
default:
|
|
1260
|
+
jobs.set(jobId, { status: "failed", error: "Unknown action type" });
|
|
1240
1261
|
}
|
|
1241
1262
|
setTimeout(() => jobs.delete(jobId), 1800000);
|
|
1242
1263
|
setTimeout(() => jobs.set(jobId, { status: "failed", error: "Job timed out" }), 180000);
|
package/index.ts
CHANGED
|
@@ -123,22 +123,31 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
123
123
|
this.sessionIds = new Set();
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Compiles Handlebars templates from `source`.
|
|
128
|
+
*
|
|
129
|
+
* `record` is the raw row and is always what `provideAdditionalContextForRecord` sees, but the
|
|
130
|
+
* templates themselves are rendered against `record` only when they come from `PluginOptions`:
|
|
131
|
+
* those are written by the app author and may reference any column. Templates which arrived in
|
|
132
|
+
* the request body are rendered against the record as the caller is allowed to read it, so a
|
|
133
|
+
* hand-written `{{column}}` cannot pull a backendOnly column out of the row.
|
|
134
|
+
*/
|
|
127
135
|
private async compileTemplates<T extends Record<string, any>>(
|
|
128
136
|
source: T,
|
|
129
|
-
record: any,
|
|
137
|
+
{ record, ctx, clientSupplied }: { record: any, ctx: AccessCheckContext, clientSupplied: boolean },
|
|
130
138
|
valueSelector: (value: T[keyof T]) => string
|
|
131
139
|
): Promise<Record<string, string>> {
|
|
140
|
+
let templateContext = clientSupplied ? await this.stripBackendOnlyColumns(record, ctx) : record;
|
|
132
141
|
if (this.options.provideAdditionalContextForRecord) {
|
|
133
|
-
const additionalFields = await this.options.provideAdditionalContextForRecord({ record, adminUser:
|
|
134
|
-
|
|
142
|
+
const additionalFields = await this.options.provideAdditionalContextForRecord({ record, adminUser: ctx.adminUser, resource: this.resourceConfig });
|
|
143
|
+
templateContext = { ...templateContext, ...additionalFields };
|
|
135
144
|
}
|
|
136
145
|
const compiled: Record<string, string> = {};
|
|
137
146
|
for (const [key, value] of Object.entries(source)) {
|
|
138
147
|
const templateStr = valueSelector(value);
|
|
139
148
|
try {
|
|
140
149
|
const tpl = Handlebars.compile(templateStr);
|
|
141
|
-
compiled[key] = tpl(
|
|
150
|
+
compiled[key] = tpl(templateContext);
|
|
142
151
|
} catch {
|
|
143
152
|
compiled[key] = templateStr;
|
|
144
153
|
}
|
|
@@ -146,16 +155,16 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
146
155
|
return compiled;
|
|
147
156
|
}
|
|
148
157
|
|
|
149
|
-
private async compileOutputFieldsTemplates(record: any, customPrompt? : string) {
|
|
150
|
-
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) :this.options.fillFieldsFromImages, record, v => String(v));
|
|
158
|
+
private async compileOutputFieldsTemplates(record: any, ctx: AccessCheckContext, customPrompt? : string, clientSupplied: boolean = !!customPrompt) {
|
|
159
|
+
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) :this.options.fillFieldsFromImages, { record, ctx, clientSupplied }, v => String(v));
|
|
151
160
|
}
|
|
152
161
|
|
|
153
|
-
private async compileOutputFieldsTemplatesNoImage(record: any, customPrompt? : string) {
|
|
154
|
-
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillPlainFields, record, v => String(v));
|
|
162
|
+
private async compileOutputFieldsTemplatesNoImage(record: any, ctx: AccessCheckContext, customPrompt? : string, clientSupplied: boolean = !!customPrompt) {
|
|
163
|
+
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillPlainFields, { record, ctx, clientSupplied }, v => String(v));
|
|
155
164
|
}
|
|
156
165
|
|
|
157
|
-
private async compileGenerationFieldTemplates(record: any, customPrompt? : string) {
|
|
158
|
-
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.generateImages, record, v => String(customPrompt ? v : v.prompt));
|
|
166
|
+
private async compileGenerationFieldTemplates(record: any, ctx: AccessCheckContext, customPrompt? : string, clientSupplied: boolean = !!customPrompt) {
|
|
167
|
+
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.generateImages, { record, ctx, clientSupplied }, v => String(customPrompt ? v : v.prompt));
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
private removeFromPromptFilledFields(compiledOutputFields: Record<string, string>, record: Record<string, any>): Record<string, string> {
|
|
@@ -206,17 +215,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
206
215
|
return prompt;
|
|
207
216
|
}
|
|
208
217
|
|
|
209
|
-
private async analyze_image(jobId: string,
|
|
218
|
+
private async analyze_image(jobId: string, record: any, ctx: AccessCheckContext, headers: Record<string, string | string[] | undefined>, customPrompt? : string, filterFilledFields: boolean = true) {
|
|
210
219
|
// TODO: need to do correct check of rate limit
|
|
211
220
|
// if (this.options.rateLimits && this.options.rateLimits.fillFieldsFromImages && await this.checkRateLimit("fillFieldsFromImages" ,this.options.rateLimits.fillFieldsFromImages, headers)) {
|
|
212
221
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
213
222
|
// return { error: "Rate limit exceeded" };
|
|
214
223
|
// }
|
|
215
|
-
const selectedId = recordId;
|
|
216
224
|
let isError = false;
|
|
217
|
-
// Fetch the record using the provided ID
|
|
218
|
-
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
219
|
-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)] );
|
|
220
225
|
|
|
221
226
|
//recieve image URLs to analyze
|
|
222
227
|
const attachmentFiles = await this.options.attachFiles({ record: record });
|
|
@@ -241,7 +246,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
241
246
|
return { ok: false, error: 'One of the image URLs is not valid' };
|
|
242
247
|
}
|
|
243
248
|
//create prompt for OpenAI
|
|
244
|
-
const compiledOutputFields = await this.compileOutputFieldsTemplates(record, customPrompt);
|
|
249
|
+
const compiledOutputFields = await this.compileOutputFieldsTemplates(record, ctx, customPrompt);
|
|
245
250
|
const filteredCompiledOutputFields = filterFilledFields ? this.removeFromPromptFilledFields(compiledOutputFields, record) : compiledOutputFields;
|
|
246
251
|
|
|
247
252
|
if (Object.keys(filteredCompiledOutputFields).length === 0) {
|
|
@@ -291,13 +296,12 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
291
296
|
|
|
292
297
|
}
|
|
293
298
|
|
|
294
|
-
private async analyzeNoImages(jobId: string,
|
|
299
|
+
private async analyzeNoImages(jobId: string, record: any, ctx: AccessCheckContext, headers: Record<string, string | string[] | undefined>, customPrompt? : string, filterFilledFields: boolean = true) {
|
|
295
300
|
// TODO: need to do correct check of rate limit
|
|
296
301
|
// if (this.options.rateLimits && this.options.rateLimits.fillPlainFields && await this.checkRateLimit("fillPlainFields" ,this.options.rateLimits.fillPlainFields, headers)) {
|
|
297
302
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
298
303
|
// return { error: "Rate limit exceeded" };
|
|
299
304
|
// }
|
|
300
|
-
const selectedId = recordId;
|
|
301
305
|
let isError = false;
|
|
302
306
|
if (STUB_MODE) {
|
|
303
307
|
const fakeError = Math.random() < 0.005; // 0.05% chance of error
|
|
@@ -310,10 +314,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
310
314
|
return { ok: true };
|
|
311
315
|
}
|
|
312
316
|
} else {
|
|
313
|
-
const
|
|
314
|
-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, selectedId)] );
|
|
315
|
-
|
|
316
|
-
const compiledOutputFields = await this.compileOutputFieldsTemplatesNoImage(record, customPrompt);
|
|
317
|
+
const compiledOutputFields = await this.compileOutputFieldsTemplatesNoImage(record, ctx, customPrompt);
|
|
317
318
|
|
|
318
319
|
const filteredCompiledOutputFields = filterFilledFields ? this.removeFromPromptFilledFields(compiledOutputFields, record) : compiledOutputFields;
|
|
319
320
|
|
|
@@ -355,16 +356,14 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
355
356
|
}
|
|
356
357
|
}
|
|
357
358
|
|
|
358
|
-
private async initialImageGenerate(jobId: string,
|
|
359
|
+
private async initialImageGenerate(jobId: string, record: any, ctx: AccessCheckContext, headers: Record<string, string | string[] | undefined>, customPrompt? : string, filterFilledFields: boolean = true) {
|
|
359
360
|
// TODO: need to do correct check of rate limit
|
|
360
361
|
// if (this.options.rateLimits && this.options.rateLimits.generateImages && await this.checkRateLimit("generateImages" ,this.options.rateLimits.generateImages, headers)) {
|
|
361
362
|
// jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
|
|
362
363
|
// return { error: "Rate limit exceeded" };
|
|
363
364
|
// }
|
|
364
|
-
const selectedId = recordId;
|
|
365
365
|
let isError = false;
|
|
366
366
|
const start = +new Date();
|
|
367
|
-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, selectedId)]);
|
|
368
367
|
let attachmentFiles
|
|
369
368
|
if(!this.options.attachFiles){
|
|
370
369
|
attachmentFiles = [];
|
|
@@ -395,7 +394,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
395
394
|
}
|
|
396
395
|
};
|
|
397
396
|
}
|
|
398
|
-
const prompt = (await this.compileGenerationFieldTemplates(record, customPrompt))[key];
|
|
397
|
+
const prompt = (await this.compileGenerationFieldTemplates(record, ctx, customPrompt))[key];
|
|
399
398
|
let images;
|
|
400
399
|
if (this.options.attachFiles && attachmentFiles.length === 0) {
|
|
401
400
|
isError = true;
|
|
@@ -467,8 +466,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
467
466
|
}
|
|
468
467
|
}
|
|
469
468
|
|
|
470
|
-
private async regenerateImage(jobId: string,
|
|
471
|
-
const Id = recordId;
|
|
469
|
+
private async regenerateImage(jobId: string, record: any, fieldName: string, prompt: string, headers: Record<string, string | string[] | undefined>) {
|
|
472
470
|
let isError = false;
|
|
473
471
|
// TODO: need to do correct check of rate limit
|
|
474
472
|
// if (await this.checkRateLimit(fieldName, this.options.generateImages[fieldName].rateLimit, headers)) {
|
|
@@ -476,7 +474,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
476
474
|
// return { error: "Rate limit exceeded" };
|
|
477
475
|
// }
|
|
478
476
|
const start = +new Date();
|
|
479
|
-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, Id)]);
|
|
480
477
|
let attachmentFiles
|
|
481
478
|
if(!this.options.attachFiles){
|
|
482
479
|
attachmentFiles = [];
|
|
@@ -536,11 +533,14 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
536
533
|
}
|
|
537
534
|
}
|
|
538
535
|
|
|
539
|
-
private async regenerateCell(jobId, fieldToRegenerate,
|
|
540
|
-
if (!fieldToRegenerate || !
|
|
536
|
+
private async regenerateCell(jobId, fieldToRegenerate, record: any, ctx: AccessCheckContext, actionType, prompt) {
|
|
537
|
+
if (!fieldToRegenerate || !actionType ) {
|
|
541
538
|
jobs.set(jobId, { status: 'failed', error: 'Missing parameters' });
|
|
542
539
|
//return { ok: false, error: "Missing parameters" };
|
|
543
540
|
}
|
|
541
|
+
// a prompt which arrived in the request body is client-authored even though it is wrapped into
|
|
542
|
+
// `promptToPass` below, while the PluginOptions fallback is written by the app author
|
|
543
|
+
const clientSupplied = !!prompt;
|
|
544
544
|
if ( !prompt ) {
|
|
545
545
|
if (actionType === 'analyze') {
|
|
546
546
|
prompt = this.options.fillFieldsFromImages ? (this.options.fillFieldsFromImages as any)[fieldToRegenerate] : null;
|
|
@@ -548,9 +548,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
548
548
|
prompt = this.options.fillPlainFields ? (this.options.fillPlainFields as any)[fieldToRegenerate] : null;
|
|
549
549
|
}
|
|
550
550
|
}
|
|
551
|
-
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
552
|
-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, recordId)] );
|
|
553
|
-
|
|
554
551
|
let promptToPass = JSON.stringify({[fieldToRegenerate]: prompt});
|
|
555
552
|
if (STUB_MODE) {
|
|
556
553
|
await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
|
|
@@ -558,7 +555,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
558
555
|
jobs.set(jobId, { status: 'completed', result: {[fieldToRegenerate]: "stub value"} });
|
|
559
556
|
} else {
|
|
560
557
|
if ( actionType === 'analyze') {
|
|
561
|
-
const compiledPropmt = await this.compileOutputFieldsTemplates(record, promptToPass);
|
|
558
|
+
const compiledPropmt = await this.compileOutputFieldsTemplates(record, ctx, promptToPass, clientSupplied);
|
|
562
559
|
const finalPrompt = this.getPromptForImageAnalysis(compiledPropmt);
|
|
563
560
|
const attachmentFiles = await this.options.attachFiles({ record: record });
|
|
564
561
|
if (attachmentFiles.length === 0) {
|
|
@@ -595,7 +592,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
595
592
|
jobs.set(jobId, { status: 'completed', result: resData });
|
|
596
593
|
|
|
597
594
|
} else if ( actionType === 'analyze_no_images') {
|
|
598
|
-
const compiledPropmt = await this.compileOutputFieldsTemplatesNoImage(record, promptToPass);
|
|
595
|
+
const compiledPropmt = await this.compileOutputFieldsTemplatesNoImage(record, ctx, promptToPass, clientSupplied);
|
|
599
596
|
const finalPrompt = this.getPromptForPlainFields(compiledPropmt);
|
|
600
597
|
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
|
|
601
598
|
let resp;
|
|
@@ -1039,6 +1036,22 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1039
1036
|
return scoped;
|
|
1040
1037
|
}
|
|
1041
1038
|
|
|
1039
|
+
/**
|
|
1040
|
+
* The row as the caller is allowed to read it: columns which are not declared on the resource and
|
|
1041
|
+
* columns whose `backendOnly` resolves true for this request are dropped, exactly the way the
|
|
1042
|
+
* core show route drops them before a row reaches the browser.
|
|
1043
|
+
*/
|
|
1044
|
+
private async stripBackendOnlyColumns(record: any, ctx: AccessCheckContext): Promise<Record<string, any>> {
|
|
1045
|
+
const visible: Record<string, any> = {};
|
|
1046
|
+
for (const column of this.resourceConfig.columns) {
|
|
1047
|
+
if (await resolveBoolOrFn(column.backendOnly, ctx)) {
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
visible[column.name] = record[column.name];
|
|
1051
|
+
}
|
|
1052
|
+
return visible;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1042
1055
|
setupEndpoints(server: IHttpServer) {
|
|
1043
1056
|
server.endpoint({
|
|
1044
1057
|
method: 'POST',
|
|
@@ -1225,7 +1238,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1225
1238
|
handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
|
|
1226
1239
|
const data = body as z.infer<typeof getImageGenerationPromptsBodySchema>;
|
|
1227
1240
|
const customPrompt = data.customPrompt || null;
|
|
1228
|
-
const { record, error } = await this.getRecordForShow({
|
|
1241
|
+
const { record, ctx, error } = await this.getRecordForShow({
|
|
1229
1242
|
adminUser,
|
|
1230
1243
|
recordId: data.recordId,
|
|
1231
1244
|
extra: { body, query, headers, cookies, requestUrl },
|
|
@@ -1233,7 +1246,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1233
1246
|
if (error) {
|
|
1234
1247
|
return { ok: false, error };
|
|
1235
1248
|
}
|
|
1236
|
-
const compiledGenerationOptions = await this.compileGenerationFieldTemplates(record, JSON.stringify({"prompt": customPrompt}));
|
|
1249
|
+
const compiledGenerationOptions = await this.compileGenerationFieldTemplates(record, ctx, JSON.stringify({"prompt": customPrompt}));
|
|
1237
1250
|
return compiledGenerationOptions;
|
|
1238
1251
|
}
|
|
1239
1252
|
});
|
|
@@ -1256,7 +1269,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1256
1269
|
method: 'POST',
|
|
1257
1270
|
path: `/plugin/${this.pluginInstanceId}/create-job`,
|
|
1258
1271
|
request_schema: createJobBodySchema,
|
|
1259
|
-
handler: async ({ body, adminUser, headers, response }) => {
|
|
1272
|
+
handler: async ({ body, adminUser, headers, query, cookies, requestUrl, response }) => {
|
|
1260
1273
|
const data = body as z.infer<typeof createJobBodySchema>;
|
|
1261
1274
|
const { actionType, recordId, customPrompt, filterFilledFields, sessionIds } = data;
|
|
1262
1275
|
if (this.options.rateLimits) {
|
|
@@ -1271,40 +1284,47 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1271
1284
|
return { ok: false, error: "Missing session ids" };
|
|
1272
1285
|
}
|
|
1273
1286
|
}
|
|
1274
|
-
const jobId = randomUUID();
|
|
1275
|
-
jobs.set(jobId, { status: "in_progress" });
|
|
1276
1287
|
if (!actionType) {
|
|
1277
|
-
|
|
1278
|
-
//return { error: "Missing action type" };
|
|
1288
|
+
return { ok: false, error: "Missing action type" };
|
|
1279
1289
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1290
|
+
if (!recordId && typeof recordId !== 'number') {
|
|
1291
|
+
return { ok: false, error: "Missing record id" };
|
|
1292
|
+
}
|
|
1293
|
+
// every worker below compiles this record into an AI prompt whose answer is handed back
|
|
1294
|
+
// through get-job-status, so the record is read here, behind the same show check and
|
|
1295
|
+
// row-level scoping the other read endpoints use, and the row itself is what gets dispatched
|
|
1296
|
+
const { record, ctx, error } = await this.getRecordForShow({
|
|
1297
|
+
adminUser,
|
|
1298
|
+
recordId,
|
|
1299
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1300
|
+
});
|
|
1301
|
+
if (error) {
|
|
1302
|
+
return { ok: false, error };
|
|
1303
|
+
}
|
|
1304
|
+
const jobId = randomUUID();
|
|
1305
|
+
jobs.set(jobId, { status: "in_progress" });
|
|
1306
|
+
switch(actionType) {
|
|
1307
|
+
case 'generate_images':
|
|
1308
|
+
this.initialImageGenerate(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1309
|
+
break;
|
|
1310
|
+
case 'analyze_no_images':
|
|
1311
|
+
this.analyzeNoImages(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1312
|
+
break;
|
|
1313
|
+
case 'analyze':
|
|
1314
|
+
this.analyze_image(jobId, record, ctx, headers, customPrompt, filterFilledFields);
|
|
1315
|
+
break;
|
|
1316
|
+
case 'regenerate_images':
|
|
1317
|
+
if (!data.prompt || !data.fieldName) {
|
|
1318
|
+
jobs.set(jobId, { status: "failed", error: "Missing prompt or field name" });
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
this.regenerateImage(jobId, record, data.fieldName, data.prompt, headers);
|
|
1322
|
+
break;
|
|
1323
|
+
case 'regenerate_cell':
|
|
1324
|
+
this.regenerateCell(jobId, data.fieldToRegenerate, record, ctx, data.action, data.prompt);
|
|
1325
|
+
break;
|
|
1326
|
+
default:
|
|
1327
|
+
jobs.set(jobId, { status: "failed", error: "Unknown action type" });
|
|
1308
1328
|
}
|
|
1309
1329
|
setTimeout(() => jobs.delete(jobId), 1_800_000);
|
|
1310
1330
|
setTimeout(() => jobs.set(jobId, { status: "failed", error: "Job timed out" }), 180_000);
|