@adminforth/bulk-ai-flow 1.8.0 → 1.9.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/index.ts CHANGED
@@ -3,8 +3,10 @@ import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminFor
3
3
  import type { PluginOptions } from './types.js';
4
4
  import Handlebars from 'handlebars';
5
5
  import { RateLimiter } from "adminforth";
6
+ import { randomUUID } from "crypto";
6
7
 
7
-
8
+ const STUB_MODE = false;
9
+ const jobs = new Map();
8
10
  export default class BulkAiFlowPlugin extends AdminForthPlugin {
9
11
  options: PluginOptions;
10
12
  uploadPlugin: AdminForthPlugin;
@@ -65,6 +67,243 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
65
67
  }
66
68
  }
67
69
 
70
+ private async analyze_image(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>) {
71
+ const selectedId = recordId;
72
+ let isError = false;
73
+ if (typeof(this.options.rateLimits?.fillFieldsFromImages) === 'string'){
74
+ if (this.checkRateLimit("fillFieldsFromImages" ,this.options.rateLimits.fillFieldsFromImages, headers)) {
75
+ jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
76
+ return { error: "Rate limit exceeded" };
77
+ }
78
+ }
79
+ // Fetch the record using the provided ID
80
+ const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
81
+ const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)] );
82
+
83
+ //recieve image URLs to analyze
84
+ const attachmentFiles = await this.options.attachFiles({ record: record });
85
+ if (STUB_MODE) {
86
+ await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 8000) + 1000));
87
+ jobs.set(jobId, { status: 'completed', result: {} });
88
+ return {};
89
+ } else if (attachmentFiles.length !== 0) {
90
+ //create prompt for OpenAI
91
+ const compiledOutputFields = this.compileOutputFieldsTemplates(record);
92
+ const prompt = `Analyze the following image(s) and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
93
+ Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
94
+ Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names. If it's number field - return only number.
95
+ Image URLs:`;
96
+
97
+ //send prompt to OpenAI and get response
98
+ let chatResponse;
99
+ try {
100
+ chatResponse = await this.options.visionAdapter.generate({ prompt, inputFileUrls: attachmentFiles });
101
+ } catch (e) {
102
+ isError = true;
103
+ jobs.set(jobId, { status: 'failed', error: 'AI provider refused to analyze images' });
104
+ return { ok: false, error: 'AI provider refused to analyze images' };
105
+ }
106
+ if (!isError) {
107
+ const resp: any = (chatResponse as any).response;
108
+ const topLevelError = (chatResponse as any).error;
109
+ if (topLevelError || resp?.error) {
110
+ jobs.set(jobId, { status: 'failed', error: `ERROR: ${JSON.stringify(topLevelError || resp?.error)}` });
111
+ }
112
+
113
+ const textOutput = resp?.output?.[0]?.content?.[0]?.text ?? resp?.output_text ?? resp?.choices?.[0]?.message?.content;
114
+ if (!textOutput || typeof textOutput !== 'string') {
115
+ jobs.set(jobId, { status: 'failed', error: 'Unexpected AI response format' });
116
+ }
117
+
118
+ //parse response and update record
119
+ const resData = JSON.parse(textOutput);
120
+ const result = resData;
121
+ jobs.set(jobId, { status: 'completed', result });
122
+ return { ok: true };
123
+ }
124
+ };
125
+ }
126
+
127
+ private async analyzeNoImages(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>) {
128
+ const selectedId = recordId;
129
+ let isError = false;
130
+ if (typeof(this.options.rateLimits?.fillPlainFields) === 'string'){
131
+ if (this.checkRateLimit("fillPlainFields", this.options.rateLimits.fillPlainFields, headers)) {
132
+ jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
133
+ return { error: "Rate limit exceeded" };
134
+ }
135
+ }
136
+ if (STUB_MODE) {
137
+ await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
138
+ jobs.set(jobId, { status: 'completed', result: {} });
139
+ return {};
140
+ } else {
141
+ const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
142
+ const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, selectedId)] );
143
+
144
+ const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
145
+ const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
146
+ Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
147
+ Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
148
+ If it's number field - return only number.`;
149
+ //send prompt to OpenAI and get response
150
+ const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
151
+ let resp: any;
152
+ try {
153
+ const { content: chatResponse } = await this.options.textCompleteAdapter.complete(prompt, [], numberOfTokens);
154
+ resp = (chatResponse as any).response;
155
+ const topLevelError = (chatResponse as any).error;
156
+ if (topLevelError || resp?.error) {
157
+ isError = true;
158
+ jobs.set(jobId, { status: 'failed', error: `ERROR: ${JSON.stringify(topLevelError || resp?.error)}` });
159
+ }
160
+ resp = chatResponse
161
+ } catch (e) {
162
+ isError = true;
163
+ jobs.set(jobId, { status: 'failed', error: 'AI provider refused to fill fields' });
164
+ return { ok: false, error: 'AI provider refused to fill fields' };
165
+ }
166
+ const resData = JSON.parse(resp);
167
+ const result = resData;
168
+ jobs.set(jobId, { status: 'completed', result });
169
+ return { ok: true };
170
+ }
171
+ }
172
+
173
+ private async initialImageGenerate(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>) {
174
+ const selectedId = recordId;
175
+ let isError = false;
176
+ if (typeof(this.options.rateLimits?.generateImages) === 'string'){
177
+ if (this.checkRateLimit("generateImages", this.options.rateLimits.generateImages, headers)) {
178
+ jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
179
+ return { error: "Rate limit exceeded" };
180
+ }
181
+ }
182
+ const start = +new Date();
183
+ const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, selectedId)]);
184
+ let attachmentFiles
185
+ if(!this.options.attachFiles){
186
+ attachmentFiles = [];
187
+ } else {
188
+ attachmentFiles = await this.options.attachFiles({ record });
189
+ }
190
+ const fieldTasks = Object.keys(this.options?.generateImages || {}).map(async (key) => {
191
+ const prompt = this.compileGenerationFieldTemplates(record)[key];
192
+ let images;
193
+ if (this.options.attachFiles && attachmentFiles.length === 0) {
194
+ jobs.set(jobId, { status: 'failed', error: "No attachment files found" });
195
+ return { key, images: [] };
196
+ } else {
197
+ if (STUB_MODE) {
198
+ await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
199
+ images = `https://pic.re/image`;
200
+ } else {
201
+ let generationAdapter;
202
+ if (this.options.generateImages[key].adapter) {
203
+ generationAdapter = this.options.generateImages[key].adapter;
204
+ } else {
205
+ generationAdapter = this.options.imageGenerationAdapter;
206
+ }
207
+ let resp;
208
+ try {
209
+ resp = await generationAdapter.generate(
210
+ {
211
+ prompt,
212
+ inputFiles: attachmentFiles,
213
+ n: 1,
214
+ size: this.options.generateImages[key].outputSize,
215
+ }
216
+ )
217
+ } catch (e) {
218
+ jobs.set(jobId, { status: 'failed', error: "AI provider refused to generate image" });
219
+ isError = true;
220
+ return { key, images: [] };
221
+ }
222
+ images = resp.imageURLs[0];
223
+ }
224
+ return { key, images };
225
+ }
226
+ });
227
+
228
+ const fieldResults = await Promise.all(fieldTasks);
229
+ const recordResult: Record<string, string[]> = {};
230
+
231
+ fieldResults.forEach(({ key, images }) => {
232
+ recordResult[key] = images;
233
+ });
234
+
235
+ const result = recordResult;
236
+
237
+ this.totalCalls++;
238
+ this.totalDuration += (+new Date() - start) / 1000;
239
+ if (!isError) {
240
+ jobs.set(jobId, { status: 'completed', result });
241
+ return { ok: true }
242
+ } else {
243
+ return { ok: false, error: 'Error during image generation' };
244
+ }
245
+ }
246
+
247
+ private async regenerateImage(jobId: string, recordId: string, fieldName: string, prompt: string, adminUser: any, headers: Record<string, string | string[] | undefined>) {
248
+ const Id = recordId;
249
+ let isError = false;
250
+ if (this.checkRateLimit(fieldName, this.options.generateImages[fieldName].rateLimit, headers)) {
251
+ jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
252
+ return { error: "Rate limit exceeded" };
253
+ }
254
+ const start = +new Date();
255
+ const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, Id)]);
256
+ let attachmentFiles
257
+ if(!this.options.attachFiles){
258
+ attachmentFiles = [];
259
+ } else {
260
+ attachmentFiles = await this.options.attachFiles({ record });
261
+ }
262
+ const images = await Promise.all(
263
+ (new Array(this.options.generateImages[fieldName].countToGenerate)).fill(0).map(async () => {
264
+ if (this.options.attachFiles && attachmentFiles.length === 0) {
265
+ jobs.set(jobId, { status: 'failed', error: "No attachment files found" });
266
+ return null;
267
+ }
268
+ if (STUB_MODE) {
269
+ await new Promise((resolve) => setTimeout(resolve, 2000));
270
+ jobs.set(jobId, { status: 'completed', result: {} });
271
+ return `https://pic.re/image`;
272
+ }
273
+
274
+ let generationAdapter;
275
+ if (this.options.generateImages[fieldName].adapter) {
276
+ generationAdapter = this.options.generateImages[fieldName].adapter;
277
+ } else {
278
+ generationAdapter = this.options.imageGenerationAdapter;
279
+ }
280
+ let resp;
281
+ try {
282
+ resp = await generationAdapter.generate(
283
+ {
284
+ prompt,
285
+ inputFiles: attachmentFiles,
286
+ n: 1,
287
+ size: this.options.generateImages[fieldName].outputSize,
288
+ }
289
+ )
290
+ } catch (e) {
291
+ jobs.set(jobId, { status: 'failed', error: "AI provider refused to generate image" });
292
+ isError = true;
293
+ return [];
294
+ }
295
+ return resp.imageURLs[0]
296
+ })
297
+ );
298
+ this.totalCalls++;
299
+ this.totalDuration += (+new Date() - start) / 1000;
300
+ if (!isError) {
301
+ jobs.set(jobId, { status: 'completed', result: { [fieldName]: images } });
302
+ return { ok: true };
303
+ } else {
304
+ return { ok: false, error: 'Error during image generation' };
305
+ }
306
+ }
68
307
 
69
308
  async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
70
309
  super.modifyResourceConfig(adminforth, resourceConfig);
@@ -112,9 +351,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
112
351
  p.resourceConfig!.resourceId === this.resourceConfig.resourceId &&
113
352
  p.pluginOptions.pathColumnName === key
114
353
  );
115
- if (plugin && plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
116
354
  outputImagesPluginInstanceIds[key] = plugin.pluginInstanceId;
117
- }
118
355
  }
119
356
  }
120
357
 
@@ -142,6 +379,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
142
379
  isFieldsForAnalizePlain: this.options.fillPlainFields ? Object.keys(this.options.fillPlainFields).length > 0 : false,
143
380
  isImageGeneration: this.options.generateImages ? Object.keys(this.options.generateImages).length > 0 : false,
144
381
  isAttachFiles: this.options.attachFiles ? true : false,
382
+ disabledWhenNoCheckboxes: true,
383
+ refreshRates: {
384
+ fillFieldsFromImages: this.options.refreshRates?.fillFieldsFromImages || 2_000,
385
+ fillPlainFields: this.options.refreshRates?.fillPlainFields || 1_000,
386
+ generateImages: this.options.refreshRates?.generateImages || 5_000,
387
+ regenerateImages: this.options.refreshRates?.regenerateImages || 5_000,
388
+ }
145
389
  }
146
390
  }
147
391
 
@@ -203,6 +447,12 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
203
447
  if (!plugin) {
204
448
  throw new Error(`Plugin for attachment field '${key}' not found in resource '${this.resourceConfig.resourceId}', please check if Upload Plugin is installed on the field ${key}`);
205
449
  }
450
+ if (!plugin.pluginOptions || !plugin.pluginOptions.storageAdapter) {
451
+ throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}' is missing a storageAdapter configuration.`);
452
+ }
453
+ if (typeof plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly !== 'function') {
454
+ throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}' uses a storage adapter without 'objectCanBeAccesedPublicly' method.`);
455
+ }
206
456
  if (!plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
207
457
  throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}'
208
458
  uses adapter which is not configured to store objects in public way, so it will produce only signed private URLs which can not be used in HTML text of blog posts.
@@ -218,96 +468,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
218
468
  }
219
469
 
220
470
  setupEndpoints(server: IHttpServer) {
221
- server.endpoint({
222
- method: 'POST',
223
- path: `/plugin/${this.pluginInstanceId}/analyze`,
224
- handler: async ({ body, adminUser, headers }) => {
225
- const selectedIds = body.selectedIds || [];
226
- if (typeof(this.options.rateLimits?.fillFieldsFromImages) === 'string'){
227
- if (this.checkRateLimit("fillFieldsFromImages" ,this.options.rateLimits.fillFieldsFromImages, headers)) {
228
- return { error: "Rate limit exceeded" };
229
- }
230
- }
231
- const tasks = selectedIds.map(async (ID) => {
232
- // Fetch the record using the provided ID
233
- const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
234
- const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, ID)] );
235
-
236
- //recieve image URLs to analyze
237
- const attachmentFiles = await this.options.attachFiles({ record: record });
238
- if (attachmentFiles.length !== 0) {
239
- //create prompt for OpenAI
240
- const compiledOutputFields = this.compileOutputFieldsTemplates(record);
241
- const prompt = `Analyze the following image(s) and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
242
- Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
243
- Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names. If it's number field - return only number.
244
- Image URLs:`;
245
-
246
- //send prompt to OpenAI and get response
247
- const chatResponse = await this.options.visionAdapter.generate({ prompt, inputFileUrls: attachmentFiles });
248
-
249
- const resp: any = (chatResponse as any).response;
250
- const topLevelError = (chatResponse as any).error;
251
- if (topLevelError || resp?.error) {
252
- throw new Error(`ERROR: ${JSON.stringify(topLevelError || resp?.error)}`);
253
- }
254
-
255
- const textOutput = resp?.output?.[0]?.content?.[0]?.text ?? resp?.output_text ?? resp?.choices?.[0]?.message?.content;
256
- if (!textOutput || typeof textOutput !== 'string') {
257
- throw new Error('Unexpected AI response format');
258
- }
259
-
260
- //parse response and update record
261
- const resData = JSON.parse(textOutput);
262
-
263
- return resData;
264
- };
265
- });
266
-
267
- const result = await Promise.all(tasks);
268
-
269
- return { result };
270
- }
271
- });
272
-
273
- server.endpoint({
274
- method: 'POST',
275
- path: `/plugin/${this.pluginInstanceId}/analyze_no_images`,
276
- handler: async ({ body, adminUser, headers }) => {
277
- const selectedIds = body.selectedIds || [];
278
- if (typeof(this.options.rateLimits?.fillPlainFields) === 'string'){
279
- if (this.checkRateLimit("fillPlainFields", this.options.rateLimits.fillPlainFields, headers)) {
280
- return { error: "Rate limit exceeded" };
281
- }
282
- }
283
- const tasks = selectedIds.map(async (ID) => {
284
- const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
285
- const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, ID)] );
286
-
287
- const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
288
- const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
289
- Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
290
- Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
291
- If it's number field - return only number.`;
292
- //send prompt to OpenAI and get response
293
- const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
294
- const { content: chatResponse } = await this.options.textCompleteAdapter.complete(prompt, [], numberOfTokens);
295
-
296
- const resp: any = (chatResponse as any).response;
297
- const topLevelError = (chatResponse as any).error;
298
- if (topLevelError || resp?.error) {
299
- throw new Error(`ERROR: ${JSON.stringify(topLevelError || resp?.error)}`);
300
- }
301
- const resData = JSON.parse(chatResponse);
302
-
303
- return resData;
304
- });
305
-
306
- const result = await Promise.all(tasks);
307
-
308
- return { result };
309
- }
310
- });
311
471
  server.endpoint({
312
472
  method: 'POST',
313
473
  path: `/plugin/${this.pluginInstanceId}/get_records`,
@@ -328,6 +488,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
328
488
  };
329
489
  }
330
490
  });
491
+
492
+
331
493
  server.endpoint({
332
494
  method: 'POST',
333
495
  path: `/plugin/${this.pluginInstanceId}/get_images`,
@@ -345,6 +507,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
345
507
  };
346
508
  }
347
509
  });
510
+
511
+
348
512
  server.endpoint({
349
513
  method: 'POST',
350
514
  path: `/plugin/${this.pluginInstanceId}/update_fields`,
@@ -428,124 +592,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
428
592
  }
429
593
  }
430
594
  });
431
- server.endpoint({
432
- method: 'POST',
433
- path: `/plugin/${this.pluginInstanceId}/regenerate_images`,
434
- handler: async ({ body, headers }) => {
435
- const Id = body.recordId || [];
436
- const prompt = body.prompt || '';
437
- const fieldName = body.fieldName || '';
438
- if (this.checkRateLimit(fieldName, this.options.generateImages[fieldName].rateLimit, headers)) {
439
- return { error: "Rate limit exceeded" };
440
- }
441
- const start = +new Date();
442
- const STUB_MODE = false;
443
- const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, Id)]);
444
- let attachmentFiles
445
- if(!this.options.attachFiles){
446
- attachmentFiles = [];
447
- } else {
448
- attachmentFiles = await this.options.attachFiles({ record });
449
- }
450
- const images = await Promise.all(
451
- (new Array(this.options.generateImages[fieldName].countToGenerate)).fill(0).map(async () => {
452
- if (this.options.attachFiles && attachmentFiles.length === 0) {
453
- return null;
454
- }
455
- if (STUB_MODE) {
456
- await new Promise((resolve) => setTimeout(resolve, 2000));
457
- return `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
458
- }
459
-
460
- let generationAdapter;
461
- if (this.options.generateImages[fieldName].adapter) {
462
- generationAdapter = this.options.generateImages[fieldName].adapter;
463
- } else {
464
- generationAdapter = this.options.imageGenerationAdapter;
465
- }
466
- const resp = await generationAdapter.generate(
467
- {
468
- prompt,
469
- inputFiles: attachmentFiles,
470
- n: 1,
471
- size: this.options.generateImages[fieldName].outputSize,
472
- }
473
- )
474
- return resp.imageURLs[0]
475
- })
476
- );
477
- this.totalCalls++;
478
- this.totalDuration += (+new Date() - start) / 1000;
479
- return { images };
480
- }
481
- });
482
- server.endpoint({
483
- method: 'POST',
484
- path: `/plugin/${this.pluginInstanceId}/initial_image_generate`,
485
- handler: async ({ body, headers }) => {
486
- const selectedIds = body.selectedIds || [];
487
- const STUB_MODE = false;
488
- if (typeof(this.options.rateLimits?.generateImages) === 'string'){
489
- if (this.checkRateLimit("generateImages", this.options.rateLimits.generateImages, headers)) {
490
- return { error: "Rate limit exceeded" };
491
- }
492
- }
493
- const start = +new Date();
494
- const tasks = selectedIds.map(async (ID) => {
495
- const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, ID)]);
496
- let attachmentFiles
497
- if(!this.options.attachFiles){
498
- attachmentFiles = [];
499
- } else {
500
- attachmentFiles = await this.options.attachFiles({ record });
501
- }
502
- const fieldTasks = Object.keys(this.options?.generateImages || {}).map(async (key) => {
503
- const prompt = this.compileGenerationFieldTemplates(record)[key];
504
- let images;
505
- if (this.options.attachFiles && attachmentFiles.length === 0) {
506
- return { key, images: [] };
507
- } else {
508
- if (STUB_MODE) {
509
- await new Promise((resolve) => setTimeout(resolve, 2000));
510
- images = `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
511
- } else {
512
- let generationAdapter;
513
- if (this.options.generateImages[key].adapter) {
514
- generationAdapter = this.options.generateImages[key].adapter;
515
- } else {
516
- generationAdapter = this.options.imageGenerationAdapter;
517
- }
518
- const resp = await generationAdapter.generate(
519
- {
520
- prompt,
521
- inputFiles: attachmentFiles,
522
- n: 1,
523
- size: this.options.generateImages[key].outputSize,
524
- }
525
- )
526
- images = resp.imageURLs[0];
527
- }
528
- return { key, images };
529
- }
530
- });
531
595
 
532
- const fieldResults = await Promise.all(fieldTasks);
533
- const recordResult: Record<string, string[]> = {};
534
596
 
535
- fieldResults.forEach(({ key, images }) => {
536
- recordResult[key] = images;
537
- });
538
-
539
- return recordResult;
540
- });
541
- const result = await Promise.all(tasks);
542
-
543
- this.totalCalls++;
544
- this.totalDuration += (+new Date() - start) / 1000;
545
-
546
- return { result };
547
- }
548
- });
549
597
  server.endpoint({
550
598
  method: 'POST',
551
599
  path: `/plugin/${this.pluginInstanceId}/get_generation_prompts`,
@@ -556,6 +604,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
556
604
  return { generationOptions: compiledGenerationOptions };
557
605
  }
558
606
  });
607
+
608
+
559
609
  server.endpoint({
560
610
  method: 'GET',
561
611
  path: `/plugin/${this.pluginInstanceId}/averageDuration`,
@@ -567,5 +617,67 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
567
617
  };
568
618
  }
569
619
  });
620
+
621
+
622
+ server.endpoint({
623
+ method: 'POST',
624
+ path: `/plugin/${this.pluginInstanceId}/create-job`,
625
+ handler: async ({ body, adminUser, headers }) => {
626
+ const { actionType, recordId } = body;
627
+ const jobId = randomUUID();
628
+ jobs.set(jobId, { status: "in_progress" });
629
+
630
+ if (!actionType) {
631
+ jobs.set(jobId, { status: "failed", error: "Missing action type" });
632
+ return { error: "Missing action type" };
633
+ }
634
+ if (!recordId) {
635
+ jobs.set(jobId, { status: "failed", error: "Missing record id" });
636
+ return { error: "Missing record id" };
637
+ }
638
+
639
+ switch(actionType) {
640
+ case 'generate_images':
641
+ setTimeout(async () => await this.initialImageGenerate(jobId, recordId, adminUser, headers), 100);
642
+ break;
643
+ case 'analyze_no_images':
644
+ setTimeout(async () => await this.analyzeNoImages(jobId, recordId, adminUser, headers), 100);
645
+ break;
646
+ case 'analyze':
647
+ setTimeout(async () => await this.analyze_image(jobId, recordId, adminUser, headers), 100);
648
+ break;
649
+ case 'regenerate_images':
650
+ if (!body.prompt || !body.fieldName) {
651
+ jobs.set(jobId, { status: "failed", error: "Missing prompt or field name" });
652
+ break;
653
+ }
654
+ setTimeout(async () => await this.regenerateImage(jobId, recordId, body.fieldName, body.prompt, adminUser, headers), 100);
655
+ break;
656
+ default:
657
+ jobs.set(jobId, { status: "failed", error: "Unknown action type" });
658
+ }
659
+ setTimeout(() => jobs.delete(jobId), 300_000);
660
+ return { ok: true, jobId };
661
+ }
662
+ });
663
+
664
+
665
+ server.endpoint({
666
+ method: 'POST',
667
+ path: `/plugin/${this.pluginInstanceId}/get-job-status`,
668
+ handler: async ({ body, adminUser, headers }) => {
669
+ const jobId = body.jobId;
670
+ if (!jobId) {
671
+ return { error: "Can't find job id" };
672
+ }
673
+ const job = jobs.get(jobId);
674
+ if (!job) {
675
+ return { error: "Job not found" };
676
+ }
677
+ return { ok: true, job };
678
+ }
679
+ });
680
+
681
+
570
682
  }
571
683
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/bulk-ai-flow",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/types.ts CHANGED
@@ -85,6 +85,15 @@ export interface PluginOptions {
85
85
  generateImages?: string,
86
86
  },
87
87
 
88
+ /**
89
+ * Job refresh rate for each ai flow job in milliseconds
90
+ */
91
+ refreshRates?: {
92
+ fillFieldsFromImages?: number,
93
+ fillPlainFields?: number,
94
+ generateImages?: number,
95
+ regenerateImages?: number,
96
+ },
88
97
 
89
98
  /**
90
99
  * Whether the user is allowed to save the generated images