@adminforth/bulk-ai-flow 1.7.4 → 1.8.1

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
@@ -73,58 +73,30 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
73
73
  const columns = this.resourceConfig.columns;
74
74
  let columnEnums = [];
75
75
  if (this.options.fillFieldsFromImages) {
76
- if (!this.options.attachFiles) {
77
- throw new Error('⚠️ attachFiles function must be provided in options when fillFieldsFromImages is used');
78
- }
79
- if (!this.options.visionAdapter) {
80
- throw new Error('⚠️ visionAdapter must be provided in options when fillFieldsFromImages is used');
81
- }
82
-
83
76
  for (const [key, value] of Object.entries((this.options.fillFieldsFromImages ))) {
84
77
  const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
85
- if (column) {
86
- if(column.enum){
87
- (this.options.fillFieldsFromImages as any)[key] = `${value} Select ${key} from the list (USE ONLY VALUE FIELD. USE ONLY VALUES FROM THIS LIST): ${JSON.stringify(column.enum)}`;
88
- columnEnums.push({
89
- name: key,
90
- enum: column.enum,
91
- });
92
- }
93
- } else {
94
- throw new Error(`⚠️ No column found for key "${key}"`);
78
+ if (column && column.enum) {
79
+ (this.options.fillFieldsFromImages as any)[key] = `${value} Select ${key} from the list (USE ONLY VALUE FIELD. USE ONLY VALUES FROM THIS LIST): ${JSON.stringify(column.enum)}`;
80
+ columnEnums.push({
81
+ name: key,
82
+ enum: column.enum,
83
+ });
95
84
  }
96
85
  }
97
86
  }
98
87
 
99
88
  if (this.options.fillPlainFields) {
100
- if (!this.options.textCompleteAdapter) {
101
- throw new Error('⚠️ textCompleteAdapter must be provided in options when fillPlainFields is used');
102
- }
103
-
104
89
  for (const [key, value] of Object.entries((this.options.fillPlainFields))) {
105
90
  const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
106
- if (column) {
107
- if(column.enum){
108
- (this.options.fillPlainFields as any)[key] = `${value} Select ${key} from the list (USE ONLY VALUE FIELD. USE ONLY VALUES FROM THIS LIST): ${JSON.stringify(column.enum)}`;
109
- columnEnums.push({
110
- name: key,
111
- enum: column.enum,
112
- });
113
- }
114
- } else {
115
- throw new Error(`⚠️ No column found for key "${key}"`);
116
- }
117
- }
118
- }
119
-
120
- if (this.options.generateImages && !this.options.imageGenerationAdapter) {
121
- for (const [key, value] of Object.entries(this.options.generateImages)) {
122
- if (!this.options.generateImages[key].adapter) {
123
- throw new Error(`⚠️ No image generation adapter found for key "${key}"`);
91
+ if (column && column.enum) {
92
+ (this.options.fillPlainFields as any)[key] = `${value} Select ${key} from the list (USE ONLY VALUE FIELD. USE ONLY VALUES FROM THIS LIST): ${JSON.stringify(column.enum)}`;
93
+ columnEnums.push({
94
+ name: key,
95
+ enum: column.enum,
96
+ });
124
97
  }
125
98
  }
126
- }
127
-
99
+ }
128
100
 
129
101
  const outputImageFields = [];
130
102
  if (this.options.generateImages) {
@@ -136,25 +108,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
136
108
  //check if Upload plugin is installed on all attachment fields
137
109
  if (this.options.generateImages) {
138
110
  for (const [key, value] of Object.entries(this.options.generateImages)) {
139
- const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
140
- if (!column) {
141
- throw new Error(`⚠️ No column found for key "${key}"`);
142
- }
143
111
  const plugin = adminforth.activatedPlugins.find(p =>
144
112
  p.resourceConfig!.resourceId === this.resourceConfig.resourceId &&
145
113
  p.pluginOptions.pathColumnName === key
146
114
  );
147
- if (!plugin) {
148
- 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}`);
149
- }
150
- if (!plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
151
- throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}'
152
- 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.
153
- Please configure adapter in such way that it will store objects publicly (e.g. for S3 use 'public-read' ACL).
154
- `);
155
- }
156
-
157
- outputImagesPluginInstanceIds[key] = plugin.pluginInstanceId;
115
+ outputImagesPluginInstanceIds[key] = plugin.pluginInstanceId;
158
116
  }
159
117
  }
160
118
 
@@ -168,7 +126,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
168
126
  const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
169
127
 
170
128
  const pageInjection = {
171
- file: this.componentPath('visionAction.vue'),
129
+ file: this.componentPath('VisionAction.vue'),
172
130
  meta: {
173
131
  pluginInstanceId: this.pluginInstanceId,
174
132
  outputFields: outputFields,
@@ -199,18 +157,71 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
199
157
  }
200
158
 
201
159
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
202
- // optional method where you can safely check field types after database discovery was performed
160
+ const columns = this.resourceConfig.columns;
161
+ if (this.options.fillFieldsFromImages) {
162
+ if (!this.options.attachFiles) {
163
+ throw new Error('⚠️ attachFiles function must be provided when fillFieldsFromImages is used');
164
+ }
165
+ if (!this.options.visionAdapter) {
166
+ throw new Error('⚠️ visionAdapter must be provided when fillFieldsFromImages is used');
167
+ }
168
+ for (const key of Object.keys(this.options.fillFieldsFromImages)) {
169
+ const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
170
+ if (!column) {
171
+ throw new Error(`⚠️ No column found for key "${key}"`);
172
+ }
173
+ }
174
+ }
175
+ if (this.options.fillPlainFields) {
176
+ if (!this.options.textCompleteAdapter) {
177
+ throw new Error('⚠️ textCompleteAdapter must be provided when fillPlainFields is used');
178
+ }
179
+ for (const key of Object.keys(this.options.fillPlainFields)) {
180
+ const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
181
+ if (!column) {
182
+ throw new Error(`⚠️ No column found for key "${key}"`);
183
+ }
184
+ }
185
+ }
186
+ if (this.options.generateImages) {
187
+ for (const key of Object.keys(this.options.generateImages)) {
188
+ const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
189
+ if (!column) {
190
+ throw new Error(`⚠️ No column found for key "${key}"`);
191
+ }
192
+ const perKeyAdapter = this.options.generateImages[key].adapter;
193
+ if (!perKeyAdapter && !this.options.imageGenerationAdapter) {
194
+ throw new Error(`⚠️ No image generation adapter provided for key "${key}"`);
195
+ }
196
+
197
+ const plugin = adminforth.activatedPlugins.find(p =>
198
+ p.resourceConfig!.resourceId === this.resourceConfig.resourceId &&
199
+ p.pluginOptions.pathColumnName === key
200
+ );
201
+ if (!plugin) {
202
+ 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}`);
203
+ }
204
+ if (!plugin.pluginOptions || !plugin.pluginOptions.storageAdapter) {
205
+ throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}' is missing a storageAdapter configuration.`);
206
+ }
207
+ if (typeof plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly !== 'function') {
208
+ throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}' uses a storage adapter without 'objectCanBeAccesedPublicly' method.`);
209
+ }
210
+ if (!plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
211
+ throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}'
212
+ 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.
213
+ Please configure adapter in such way that it will store objects publicly (e.g. for S3 use 'public-read' ACL).
214
+ `);
215
+ }
216
+ }
217
+ }
203
218
  }
204
219
 
205
220
  instanceUniqueRepresentation(pluginOptions: any) : string {
206
- // optional method to return unique string representation of plugin instance.
207
- // Needed if plugin can have multiple instances on one resource
208
221
  return `${this.pluginOptions.actionName}`;
209
222
  }
210
223
 
211
224
  setupEndpoints(server: IHttpServer) {
212
-
213
-
214
225
  server.endpoint({
215
226
  method: 'POST',
216
227
  path: `/plugin/${this.pluginInstanceId}/analyze`,
@@ -224,7 +235,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
224
235
  const tasks = selectedIds.map(async (ID) => {
225
236
  // Fetch the record using the provided ID
226
237
  const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
227
- const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, ID)] );
238
+ const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, ID)] );
228
239
 
229
240
  //recieve image URLs to analyze
230
241
  const attachmentFiles = await this.options.attachFiles({ record: record });
@@ -274,26 +285,23 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
274
285
  }
275
286
  }
276
287
  const tasks = selectedIds.map(async (ID) => {
277
- // Fetch the record using the provided ID
278
288
  const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
279
289
  const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, ID)] );
280
290
 
281
- //create prompt for OpenAI
282
291
  const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
283
292
  const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
284
293
  Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
285
294
  Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
286
295
  If it's number field - return only number.`;
287
296
  //send prompt to OpenAI and get response
288
- const { content: chatResponse } = await this.options.textCompleteAdapter.complete(prompt, [], 500);
297
+ const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
298
+ const { content: chatResponse } = await this.options.textCompleteAdapter.complete(prompt, [], numberOfTokens);
289
299
 
290
300
  const resp: any = (chatResponse as any).response;
291
301
  const topLevelError = (chatResponse as any).error;
292
302
  if (topLevelError || resp?.error) {
293
303
  throw new Error(`ERROR: ${JSON.stringify(topLevelError || resp?.error)}`);
294
304
  }
295
-
296
- //parse response and update record
297
305
  const resData = JSON.parse(chatResponse);
298
306
 
299
307
  return resData;
@@ -304,8 +312,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
304
312
  return { result };
305
313
  }
306
314
  });
307
-
308
-
309
315
  server.endpoint({
310
316
  method: 'POST',
311
317
  path: `/plugin/${this.pluginInstanceId}/get_records`,
@@ -316,13 +322,16 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
316
322
  for( const [index, record] of records.entries() ) {
317
323
  records[index]._label = this.resourceConfig.recordLabel(records[index]);
318
324
  }
325
+ const order = Object.fromEntries(body.body.record.map((id, i) => [id, i]));
326
+
327
+ const sortedRecords = records.sort(
328
+ (a, b) => order[a.id] - order[b.id]
329
+ );
319
330
  return {
320
- records,
331
+ records: sortedRecords,
321
332
  };
322
333
  }
323
334
  });
324
-
325
-
326
335
  server.endpoint({
327
336
  method: 'POST',
328
337
  path: `/plugin/${this.pluginInstanceId}/get_images`,
@@ -340,8 +349,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
340
349
  };
341
350
  }
342
351
  });
343
-
344
-
345
352
  server.endpoint({
346
353
  method: 'POST',
347
354
  path: `/plugin/${this.pluginInstanceId}/update_fields`,
@@ -387,6 +394,35 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
387
394
  }
388
395
  }
389
396
  }
397
+ try {
398
+ const AuditLogPlugin:any = this.adminforth.getPluginByClassName('AuditLogPlugin');
399
+ if (AuditLogPlugin) {
400
+
401
+ for (const [key, value] of Object.entries(oldRecord)) {
402
+ if (!(key in fieldsToUpdate[idx])) {
403
+ delete oldRecord[key];
404
+ }
405
+ }
406
+
407
+ const reorderedOldRecord = Object.keys(fieldsToUpdate[idx]).reduce((acc, key) => {
408
+ if (key in oldRecord) {
409
+ acc[key] = oldRecord[key];
410
+ }
411
+ return acc;
412
+ }, {} as Record<string, unknown>);
413
+
414
+ AuditLogPlugin.logCustomAction({
415
+ resourceId: this.resourceConfig.resourceId,
416
+ recordId: ID,
417
+ actionId: 'Bulk-ai-flow',
418
+ oldData: reorderedOldRecord,
419
+ data: fieldsToUpdate[idx],
420
+ user: adminUser,
421
+ headers: headers
422
+ });
423
+ }
424
+ } catch (error) { }
425
+
390
426
  return this.adminforth.resource(this.resourceConfig.resourceId).update(ID, fieldsToUpdate[idx])
391
427
  });
392
428
  await Promise.all(updates);
@@ -396,8 +432,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
396
432
  }
397
433
  }
398
434
  });
399
-
400
-
401
435
  server.endpoint({
402
436
  method: 'POST',
403
437
  path: `/plugin/${this.pluginInstanceId}/regenerate_images`,
@@ -449,8 +483,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
449
483
  return { images };
450
484
  }
451
485
  });
452
-
453
-
454
486
  server.endpoint({
455
487
  method: 'POST',
456
488
  path: `/plugin/${this.pluginInstanceId}/initial_image_generate`,
@@ -518,8 +550,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
518
550
  return { result };
519
551
  }
520
552
  });
521
-
522
-
523
553
  server.endpoint({
524
554
  method: 'POST',
525
555
  path: `/plugin/${this.pluginInstanceId}/get_generation_prompts`,
@@ -530,8 +560,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
530
560
  return { generationOptions: compiledGenerationOptions };
531
561
  }
532
562
  });
533
-
534
-
535
563
  server.endpoint({
536
564
  method: 'GET',
537
565
  path: `/plugin/${this.pluginInstanceId}/averageDuration`,
@@ -543,8 +571,5 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
543
571
  };
544
572
  }
545
573
  });
546
-
547
-
548
-
549
574
  }
550
575
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/bulk-ai-flow",
3
- "version": "1.7.4",
3
+ "version": "1.8.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/types.ts CHANGED
@@ -1,20 +1,52 @@
1
- import { ImageVisionAdapter, AdminUser, IAdminForth, StorageAdapter, ImageGenerationAdapter, CompletionAdapter } from "adminforth";
1
+ import { ImageVisionAdapter, ImageGenerationAdapter, CompletionAdapter } from "adminforth";
2
2
 
3
3
 
4
4
  export interface PluginOptions {
5
+ /**
6
+ * Name of the action in three dots menu.
7
+ */
5
8
  actionName: string,
9
+
10
+ /**
11
+ * The adapter to use for scaning images and filling fields basing on the image content.
12
+ */
6
13
  visionAdapter?: ImageVisionAdapter,
14
+
15
+ /**
16
+ * The adapter to use for text->text generation.
17
+ */
7
18
  textCompleteAdapter?: CompletionAdapter,
19
+
8
20
  /**
9
21
  * The adapter to use for image generation.
10
22
  */
11
23
  imageGenerationAdapter?: ImageGenerationAdapter,
24
+
25
+ /**
26
+ * List of fields that should be filled based on the image content analysis.
27
+ */
12
28
  fillFieldsFromImages?: Record<string, string>, // can analyze what is on image and fill fields, typical tasks "find dominant color", "describe what is on image", "clasify to one enum item, e.g. what is on image dog/cat/plant"
29
+
30
+ /**
31
+ * List of fields that should be filled based on the text.
32
+ */
13
33
  fillPlainFields?: Record<string, string>,
34
+
35
+ /**
36
+ * Number of tokens to generate (Only for text completion adapter). Default is 1000. 1 token ~= ¾ words
37
+ */
38
+ fillPlainFieldsMaxTokens?: number,
39
+
40
+ /**
41
+ * If you want to generate fields or images based on the image content attached images
42
+ */
14
43
  attachFiles?: ({ record }: {
15
44
  record: any,
16
45
  }) => string[] | Promise<string[]>,
17
46
 
47
+ /**
48
+ * List of image fields, that should be filled.
49
+ */
18
50
  generateImages?: Record<
19
51
  string, {
20
52
  // can generate from images or just from another fields, e.g. "remove text from images", "improve image quality", "turn image into ghibli style"
@@ -43,6 +75,10 @@ export interface PluginOptions {
43
75
  */
44
76
  countToGenerate: number,
45
77
  }>,
78
+
79
+ /**
80
+ * Rate limits for each action.
81
+ */
46
82
  rateLimits?: {
47
83
  fillFieldsFromImages?: string, // e.g. 5/1d - 5 requests per day
48
84
  fillPlainFields?: string,