@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/build.log +2 -2
- package/custom/ImageGenerationCarousel.vue +42 -13
- package/custom/VisionAction.vue +153 -64
- package/custom/VisionTable.vue +3 -1
- package/dist/custom/ImageGenerationCarousel.vue +42 -13
- package/dist/custom/VisionAction.vue +153 -64
- package/dist/custom/VisionTable.vue +3 -1
- package/dist/index.js +310 -195
- package/index.ts +315 -207
- 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);
|
|
@@ -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
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
|