@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/build.log +2 -2
- package/custom/ImageGenerationCarousel.vue +48 -19
- package/custom/VisionAction.vue +185 -136
- package/custom/VisionTable.vue +33 -25
- package/dist/custom/ImageGenerationCarousel.vue +48 -19
- package/dist/custom/VisionAction.vue +185 -136
- package/dist/custom/VisionTable.vue +33 -25
- package/dist/index.js +317 -198
- package/index.ts +321 -209
- package/package.json +1 -1
- package/types.ts +9 -0
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
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
|