Skip to content

Commit 4a96ce3

Browse files
committed
feat: add support for analyzing fields without images
1 parent 975aafb commit 4a96ce3

File tree

4 files changed

+182
-29
lines changed

4 files changed

+182
-29
lines changed

custom/visionAction.vue

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
:customFieldNames="customFieldNames"
2828
:tableColumnsIndexes="tableColumnsIndexes"
2929
:selected="selected"
30-
:isAiResponseReceived="isAiResponseReceived"
30+
:isAiResponseReceivedAnalize="isAiResponseReceivedAnalize"
3131
:isAiResponseReceivedImage="isAiResponseReceivedImage"
3232
:primaryKey="primaryKey"
3333
:openGenerationCarousel="openGenerationCarousel"
@@ -83,7 +83,7 @@ const tableColumns = ref([]);
8383
const tableColumnsIndexes = ref([]);
8484
const customFieldNames = ref([]);
8585
const selected = ref<any[]>([]);
86-
const isAiResponseReceived = ref([]);
86+
const isAiResponseReceivedAnalize = ref([]);
8787
const isAiResponseReceivedImage = ref([]);
8888
const primaryKey = props.meta.primaryKey;
8989
const openGenerationCarousel = ref([]);
@@ -97,7 +97,7 @@ const openDialog = async () => {
9797
const result = generateTableColumns();
9898
tableColumns.value = result.tableData;
9999
tableColumnsIndexes.value = result.indexes;
100-
customFieldNames.value = tableHeaders.value.slice(3).map(h => h.fieldName);
100+
customFieldNames.value = tableHeaders.value.slice(props.meta.isFieldsForAnalizeFromImages ? 3 : 2).map(h => h.fieldName);
101101
setSelected();
102102
for (let i = 0; i < selected.value?.length; i++) {
103103
openGenerationCarousel.value[i] = props.meta.outputImageFields?.reduce((acc,key) =>{
@@ -107,10 +107,17 @@ const openDialog = async () => {
107107
},{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
108108
}
109109
isLoading.value = true;
110-
await Promise.all([
111-
analyzeFields(),
112-
generateImages()
113-
]);
110+
const tasks = [];
111+
if (props.meta.isFieldsForAnalizeFromImages) {
112+
tasks.push(analyzeFields());
113+
}
114+
if (props.meta.isFieldsForAnalizePlain) {
115+
tasks.push(analyzeFieldsNoImages());
116+
}
117+
if (props.meta.isImageGeneration) {
118+
tasks.push(generateImages());
119+
}
120+
await Promise.all(tasks);
114121
isLoading.value = false;
115122
}
116123
@@ -120,7 +127,7 @@ const openDialog = async () => {
120127
121128
const closeDialog = () => {
122129
confirmDialog.value.close();
123-
isAiResponseReceived.value = [];
130+
isAiResponseReceivedAnalize.value = [];
124131
isAiResponseReceivedImage.value = [];
125132
126133
records.value = [];
@@ -192,7 +199,7 @@ function setSelected() {
192199
}
193200
selected.value[index].isChecked = true;
194201
selected.value[index][primaryKey] = record[primaryKey];
195-
isAiResponseReceived.value[index] = true;
202+
isAiResponseReceivedAnalize.value[index] = true;
196203
});
197204
}
198205
@@ -287,7 +294,7 @@ async function convertImages(fieldName, img) {
287294
288295
async function analyzeFields() {
289296
try {
290-
isAiResponseReceived.value = props.checkboxes.map(() => false);
297+
isAiResponseReceivedAnalize.value = props.checkboxes.map(() => false);
291298
292299
const res = await callAdminForthApi({
293300
path: `/plugin/${props.meta.pluginInstanceId}/analyze`,
@@ -297,7 +304,7 @@ async function analyzeFields() {
297304
},
298305
});
299306
300-
isAiResponseReceived.value = props.checkboxes.map(() => true);
307+
isAiResponseReceivedAnalize.value = props.checkboxes.map(() => true);
301308
302309
res.result.forEach((item, idx) => {
303310
const pk = selected.value[idx]?.[primaryKey]
@@ -317,6 +324,43 @@ async function analyzeFields() {
317324
}
318325
}
319326
327+
328+
async function analyzeFieldsNoImages() {
329+
try {
330+
isAiResponseReceivedAnalize.value = props.checkboxes.map(() => false);
331+
332+
const res = await callAdminForthApi({
333+
path: `/plugin/${props.meta.pluginInstanceId}/analyze_no_images`,
334+
method: 'POST',
335+
body: {
336+
selectedIds: props.checkboxes,
337+
},
338+
});
339+
if(!props.meta.isFieldsForAnalizeFromImages) {
340+
isAiResponseReceivedAnalize.value = props.checkboxes.map(() => true);
341+
}
342+
343+
res.result.forEach((item, idx) => {
344+
const pk = selected.value[idx]?.[primaryKey]
345+
346+
if (pk) {
347+
selected.value[idx] = {
348+
...selected.value[idx],
349+
...item,
350+
isChecked: true,
351+
[primaryKey]: pk
352+
}
353+
}
354+
})
355+
} catch (error) {
356+
console.error('Failed to get records:', error);
357+
358+
}
359+
}
360+
361+
362+
363+
320364
async function saveData() {
321365
if (!selected.value?.length) {
322366
adminforth.alert({ message: 'No items selected', variant: 'warning' });

custom/visionTable.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
</template>
4545
<!-- CUSTOM FIELD TEMPLATES -->
4646
<template v-for="n in customFieldNames" :key="n" #[`cell:${n}`]="{ item, column }">
47-
<div v-if="isAiResponseReceived[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] && !isInColumnImage(n)">
47+
<div v-if="isAiResponseReceivedAnalize[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] && !isInColumnImage(n)">
4848
<div v-if="isInColumnEnum(n)">
4949
<Select
5050
:options="convertColumnEnumToSelectOptions(props.meta.columnEnums, n)"
@@ -126,7 +126,7 @@ const props = defineProps<{
126126
customFieldNames: any,
127127
tableColumnsIndexes: any,
128128
selected: any,
129-
isAiResponseReceived: boolean[],
129+
isAiResponseReceivedAnalize: boolean[],
130130
isAiResponseReceivedImage: boolean[],
131131
primaryKey: any,
132132
openGenerationCarousel: any

index.ts

Lines changed: 119 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
3535
return compiled;
3636
}
3737

38+
private compileOutputFieldsTemplatesNoImage(record: any): Record<string, string> {
39+
const compiled: Record<string, string> = {};
40+
for (const [key, templateStr] of Object.entries(this.options.fillPlainFields)) {
41+
try {
42+
const tpl = Handlebars.compile(String(templateStr));
43+
compiled[key] = tpl(record);
44+
} catch {
45+
compiled[key] = String(templateStr);
46+
}
47+
}
48+
return compiled;
49+
}
50+
3851
private compileGenerationFieldTemplates(record: any) {
3952
const compiled: Record<string, any> = {};
4053
for (const key in this.options.generateImages) {
@@ -68,20 +81,59 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
6881
//check if options names are provided
6982
const columns = this.resourceConfig.columns;
7083
let columnEnums = [];
71-
for (const [key, value] of Object.entries(this.options.fillFieldsFromImages)) {
72-
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
73-
if (column) {
74-
if(column.enum){
75-
(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)}`;
76-
columnEnums.push({
77-
name: key,
78-
enum: column.enum,
79-
});
84+
if (this.options.fillFieldsFromImages) {
85+
if (!this.options.attachFiles) {
86+
throw new Error('⚠️ attachFiles function must be provided in options when fillFieldsFromImages is used');
87+
}
88+
if (!this.options.visionAdapter) {
89+
throw new Error('⚠️ visionAdapter must be provided in options when fillFieldsFromImages is used');
90+
}
91+
92+
for (const [key, value] of Object.entries((this.options.fillFieldsFromImages ))) {
93+
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
94+
if (column) {
95+
if(column.enum){
96+
(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)}`;
97+
columnEnums.push({
98+
name: key,
99+
enum: column.enum,
100+
});
101+
}
102+
} else {
103+
throw new Error(`⚠️ No column found for key "${key}"`);
104+
}
105+
}
106+
}
107+
108+
if (this.options.fillPlainFields) {
109+
if (!this.options.textCompleteAdapter) {
110+
throw new Error('⚠️ textCompleteAdapter must be provided in options when fillPlainFields is used');
111+
}
112+
113+
for (const [key, value] of Object.entries((this.options.fillPlainFields))) {
114+
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
115+
if (column) {
116+
if(column.enum){
117+
(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)}`;
118+
columnEnums.push({
119+
name: key,
120+
enum: column.enum,
121+
});
122+
}
123+
} else {
124+
throw new Error(`⚠️ No column found for key "${key}"`);
125+
}
126+
}
127+
}
128+
129+
if (this.options.generateImages && !this.options.imageGenerationAdapter) {
130+
for (const [key, value] of Object.entries(this.options.generateImages)) {
131+
if (!this.options.generateImages[key].adapter) {
132+
throw new Error(`⚠️ No image generation adapter found for key "${key}"`);
80133
}
81-
} else {
82-
throw new Error(`⚠️ No column found for key "${key}"`);
83134
}
84135
}
136+
85137

86138
const outputImageFields = [];
87139
if (this.options.generateImages) {
@@ -117,6 +169,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
117169

118170
const outputFields = {
119171
...this.options.fillFieldsFromImages,
172+
...this.options.fillPlainFields,
120173
...(this.options.generateImages || {})
121174
};
122175

@@ -131,8 +184,12 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
131184
actionName: this.options.actionName,
132185
columnEnums: columnEnums,
133186
outputImageFields: outputImageFields,
187+
outputPlainFields: this.options.fillPlainFields,
134188
primaryKey: primaryKeyColumn.name,
135189
outputImagesPluginInstanceIds: outputImagesPluginInstanceIds,
190+
isFieldsForAnalizeFromImages: this.options.fillFieldsFromImages ? Object.keys(this.options.fillFieldsFromImages).length > 0 : false,
191+
isFieldsForAnalizePlain: this.options.fillPlainFields ? Object.keys(this.options.fillPlainFields).length > 0 : false,
192+
isImageGeneration: this.options.generateImages ? Object.keys(this.options.generateImages).length > 0 : false
136193
}
137194
}
138195

@@ -204,6 +261,43 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
204261
}
205262
});
206263

264+
server.endpoint({
265+
method: 'POST',
266+
path: `/plugin/${this.pluginInstanceId}/analyze_no_images`,
267+
handler: async ({ body, adminUser, headers }) => {
268+
const selectedIds = body.selectedIds || [];
269+
const tasks = selectedIds.map(async (ID) => {
270+
// Fetch the record using the provided ID
271+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
272+
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, ID)] );
273+
274+
//create prompt for OpenAI
275+
const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
276+
const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
277+
Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
278+
Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
279+
If it's number field - return only number.`;
280+
//send prompt to OpenAI and get response
281+
const { content: chatResponse, finishReason } = await this.options.textCompleteAdapter.complete(prompt, [], 500);
282+
283+
const resp: any = (chatResponse as any).response;
284+
const topLevelError = (chatResponse as any).error;
285+
if (topLevelError || resp?.error) {
286+
throw new Error(`ERROR: ${JSON.stringify(topLevelError || resp?.error)}`);
287+
}
288+
289+
//parse response and update record
290+
const resData = JSON.parse(chatResponse);
291+
292+
return resData;
293+
});
294+
295+
const result = await Promise.all(tasks);
296+
297+
return { result };
298+
}
299+
});
300+
207301

208302
server.endpoint({
209303
method: 'POST',
@@ -308,7 +402,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
308402
return `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
309403
}
310404

311-
const resp = await this.options.generateImages[fieldName].adapter.generate(
405+
let generationAdapter;
406+
if (this.options.generateImages[fieldName].adapter) {
407+
generationAdapter = this.options.generateImages[fieldName].adapter;
408+
} else {
409+
generationAdapter = this.options.imageGenerationAdapter;
410+
}
411+
const resp = await generationAdapter.generate(
312412
{
313413
prompt,
314414
inputFiles: attachmentFiles,
@@ -348,7 +448,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
348448
//images = `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
349449
images = "https://cdn.rafled.com/anime-icons/images/6d5f85159da70f7cb29c1121248031fb4a649588a9cd71ff1784653a1f64be31.jpg";
350450
} else {
351-
const resp = await this.options.generateImages[key].adapter.generate(
451+
let generationAdapter;
452+
if (this.options.generateImages[key].adapter) {
453+
generationAdapter = this.options.generateImages[key].adapter;
454+
} else {
455+
generationAdapter = this.options.imageGenerationAdapter;``
456+
}
457+
const resp = await generationAdapter.generate(
352458
{
353459
prompt,
354460
inputFiles: attachmentFiles,

types.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { ImageVisionAdapter, AdminUser, IAdminForth, StorageAdapter, ImageGenerationAdapter } from "adminforth";
1+
import { ImageVisionAdapter, AdminUser, IAdminForth, StorageAdapter, ImageGenerationAdapter, CompletionAdapter } from "adminforth";
22

33

44
export interface PluginOptions {
55
actionName: string,
6-
visionAdapter: ImageVisionAdapter,
6+
visionAdapter?: ImageVisionAdapter,
7+
textCompleteAdapter?: CompletionAdapter,
8+
imageGenerationAdapter?: ImageGenerationAdapter,
79
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"
10+
fillPlainFields?: Record<string, string>,
811
attachFiles?: ({ record }: {
912
record: any,
1013
}) => string[] | Promise<string[]>,
@@ -14,7 +17,7 @@ export interface PluginOptions {
1417
// can generate from images or just from another fields, e.g. "remove text from images", "improve image quality", "turn image into ghibli style"
1518
prompt: string,
1619

17-
adapter: ImageGenerationAdapter,
20+
adapter?: ImageGenerationAdapter,
1821

1922
/**
2023
* The size of the generated image.

0 commit comments

Comments
 (0)