@adminforth/bulk-ai-flow 1.8.1 → 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);
@@ -140,6 +379,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
140
379
  isFieldsForAnalizePlain: this.options.fillPlainFields ? Object.keys(this.options.fillPlainFields).length > 0 : false,
141
380
  isImageGeneration: this.options.generateImages ? Object.keys(this.options.generateImages).length > 0 : false,
142
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
+ }
143
389
  }
144
390
  }
145
391
 
@@ -222,96 +468,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
222
468
  }
223
469
 
224
470
  setupEndpoints(server: IHttpServer) {
225
- server.endpoint({
226
- method: 'POST',
227
- path: `/plugin/${this.pluginInstanceId}/analyze`,
228
- handler: async ({ body, adminUser, headers }) => {
229
- const selectedIds = body.selectedIds || [];
230
- if (typeof(this.options.rateLimits?.fillFieldsFromImages) === 'string'){
231
- if (this.checkRateLimit("fillFieldsFromImages" ,this.options.rateLimits.fillFieldsFromImages, headers)) {
232
- return { error: "Rate limit exceeded" };
233
- }
234
- }
235
- const tasks = selectedIds.map(async (ID) => {
236
- // Fetch the record using the provided ID
237
- const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
238
- const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, ID)] );
239
-
240
- //recieve image URLs to analyze
241
- const attachmentFiles = await this.options.attachFiles({ record: record });
242
- if (attachmentFiles.length !== 0) {
243
- //create prompt for OpenAI
244
- const compiledOutputFields = this.compileOutputFieldsTemplates(record);
245
- const prompt = `Analyze the following image(s) and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
246
- Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
247
- Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names. If it's number field - return only number.
248
- Image URLs:`;
249
-
250
- //send prompt to OpenAI and get response
251
- const chatResponse = await this.options.visionAdapter.generate({ prompt, inputFileUrls: attachmentFiles });
252
-
253
- const resp: any = (chatResponse as any).response;
254
- const topLevelError = (chatResponse as any).error;
255
- if (topLevelError || resp?.error) {
256
- throw new Error(`ERROR: ${JSON.stringify(topLevelError || resp?.error)}`);
257
- }
258
-
259
- const textOutput = resp?.output?.[0]?.content?.[0]?.text ?? resp?.output_text ?? resp?.choices?.[0]?.message?.content;
260
- if (!textOutput || typeof textOutput !== 'string') {
261
- throw new Error('Unexpected AI response format');
262
- }
263
-
264
- //parse response and update record
265
- const resData = JSON.parse(textOutput);
266
-
267
- return resData;
268
- };
269
- });
270
-
271
- const result = await Promise.all(tasks);
272
-
273
- return { result };
274
- }
275
- });
276
-
277
- server.endpoint({
278
- method: 'POST',
279
- path: `/plugin/${this.pluginInstanceId}/analyze_no_images`,
280
- handler: async ({ body, adminUser, headers }) => {
281
- const selectedIds = body.selectedIds || [];
282
- if (typeof(this.options.rateLimits?.fillPlainFields) === 'string'){
283
- if (this.checkRateLimit("fillPlainFields", this.options.rateLimits.fillPlainFields, headers)) {
284
- return { error: "Rate limit exceeded" };
285
- }
286
- }
287
- const tasks = selectedIds.map(async (ID) => {
288
- const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
289
- const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, ID)] );
290
-
291
- const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
292
- const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
293
- Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
294
- Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
295
- If it's number field - return only number.`;
296
- //send prompt to OpenAI and get response
297
- const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
298
- const { content: chatResponse } = await this.options.textCompleteAdapter.complete(prompt, [], numberOfTokens);
299
-
300
- const resp: any = (chatResponse as any).response;
301
- const topLevelError = (chatResponse as any).error;
302
- if (topLevelError || resp?.error) {
303
- throw new Error(`ERROR: ${JSON.stringify(topLevelError || resp?.error)}`);
304
- }
305
- const resData = JSON.parse(chatResponse);
306
-
307
- return resData;
308
- });
309
-
310
- const result = await Promise.all(tasks);
311
-
312
- return { result };
313
- }
314
- });
315
471
  server.endpoint({
316
472
  method: 'POST',
317
473
  path: `/plugin/${this.pluginInstanceId}/get_records`,
@@ -332,6 +488,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
332
488
  };
333
489
  }
334
490
  });
491
+
492
+
335
493
  server.endpoint({
336
494
  method: 'POST',
337
495
  path: `/plugin/${this.pluginInstanceId}/get_images`,
@@ -349,6 +507,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
349
507
  };
350
508
  }
351
509
  });
510
+
511
+
352
512
  server.endpoint({
353
513
  method: 'POST',
354
514
  path: `/plugin/${this.pluginInstanceId}/update_fields`,
@@ -432,124 +592,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
432
592
  }
433
593
  }
434
594
  });
435
- server.endpoint({
436
- method: 'POST',
437
- path: `/plugin/${this.pluginInstanceId}/regenerate_images`,
438
- handler: async ({ body, headers }) => {
439
- const Id = body.recordId || [];
440
- const prompt = body.prompt || '';
441
- const fieldName = body.fieldName || '';
442
- if (this.checkRateLimit(fieldName, this.options.generateImages[fieldName].rateLimit, headers)) {
443
- return { error: "Rate limit exceeded" };
444
- }
445
- const start = +new Date();
446
- const STUB_MODE = false;
447
- const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, Id)]);
448
- let attachmentFiles
449
- if(!this.options.attachFiles){
450
- attachmentFiles = [];
451
- } else {
452
- attachmentFiles = await this.options.attachFiles({ record });
453
- }
454
- const images = await Promise.all(
455
- (new Array(this.options.generateImages[fieldName].countToGenerate)).fill(0).map(async () => {
456
- if (this.options.attachFiles && attachmentFiles.length === 0) {
457
- return null;
458
- }
459
- if (STUB_MODE) {
460
- await new Promise((resolve) => setTimeout(resolve, 2000));
461
- return `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
462
- }
463
595
 
464
- let generationAdapter;
465
- if (this.options.generateImages[fieldName].adapter) {
466
- generationAdapter = this.options.generateImages[fieldName].adapter;
467
- } else {
468
- generationAdapter = this.options.imageGenerationAdapter;
469
- }
470
- const resp = await generationAdapter.generate(
471
- {
472
- prompt,
473
- inputFiles: attachmentFiles,
474
- n: 1,
475
- size: this.options.generateImages[fieldName].outputSize,
476
- }
477
- )
478
- return resp.imageURLs[0]
479
- })
480
- );
481
- this.totalCalls++;
482
- this.totalDuration += (+new Date() - start) / 1000;
483
- return { images };
484
- }
485
- });
486
- server.endpoint({
487
- method: 'POST',
488
- path: `/plugin/${this.pluginInstanceId}/initial_image_generate`,
489
- handler: async ({ body, headers }) => {
490
- const selectedIds = body.selectedIds || [];
491
- const STUB_MODE = false;
492
- if (typeof(this.options.rateLimits?.generateImages) === 'string'){
493
- if (this.checkRateLimit("generateImages", this.options.rateLimits.generateImages, headers)) {
494
- return { error: "Rate limit exceeded" };
495
- }
496
- }
497
- const start = +new Date();
498
- const tasks = selectedIds.map(async (ID) => {
499
- const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, ID)]);
500
- let attachmentFiles
501
- if(!this.options.attachFiles){
502
- attachmentFiles = [];
503
- } else {
504
- attachmentFiles = await this.options.attachFiles({ record });
505
- }
506
- const fieldTasks = Object.keys(this.options?.generateImages || {}).map(async (key) => {
507
- const prompt = this.compileGenerationFieldTemplates(record)[key];
508
- let images;
509
- if (this.options.attachFiles && attachmentFiles.length === 0) {
510
- return { key, images: [] };
511
- } else {
512
- if (STUB_MODE) {
513
- await new Promise((resolve) => setTimeout(resolve, 2000));
514
- images = `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
515
- } else {
516
- let generationAdapter;
517
- if (this.options.generateImages[key].adapter) {
518
- generationAdapter = this.options.generateImages[key].adapter;
519
- } else {
520
- generationAdapter = this.options.imageGenerationAdapter;
521
- }
522
- const resp = await generationAdapter.generate(
523
- {
524
- prompt,
525
- inputFiles: attachmentFiles,
526
- n: 1,
527
- size: this.options.generateImages[key].outputSize,
528
- }
529
- )
530
- images = resp.imageURLs[0];
531
- }
532
- return { key, images };
533
- }
534
- });
535
596
 
536
- const fieldResults = await Promise.all(fieldTasks);
537
- const recordResult: Record<string, string[]> = {};
538
-
539
- fieldResults.forEach(({ key, images }) => {
540
- recordResult[key] = images;
541
- });
542
-
543
- return recordResult;
544
- });
545
- const result = await Promise.all(tasks);
546
-
547
- this.totalCalls++;
548
- this.totalDuration += (+new Date() - start) / 1000;
549
-
550
- return { result };
551
- }
552
- });
553
597
  server.endpoint({
554
598
  method: 'POST',
555
599
  path: `/plugin/${this.pluginInstanceId}/get_generation_prompts`,
@@ -560,6 +604,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
560
604
  return { generationOptions: compiledGenerationOptions };
561
605
  }
562
606
  });
607
+
608
+
563
609
  server.endpoint({
564
610
  method: 'GET',
565
611
  path: `/plugin/${this.pluginInstanceId}/averageDuration`,
@@ -571,5 +617,67 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
571
617
  };
572
618
  }
573
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
+
574
682
  }
575
683
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/bulk-ai-flow",
3
- "version": "1.8.1",
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