@shopnex/ai-plugin
Version:
<p align="center"> <img alt="Payload AI Plugin" src="assets/payload-ai-intro.gif" width="100%" /> </p>
187 lines (186 loc) • 9.05 kB
JavaScript
import * as process from 'node:process';
import { defaultPrompts } from '../ai/prompts';
import { filterEditorSchemaByNodes } from '../ai/utils/filterEditorSchemaByNodes';
import { PLUGIN_API_ENDPOINT_GENERATE, PLUGIN_API_ENDPOINT_GENERATE_UPLOAD, PLUGIN_INSTRUCTIONS_TABLE, PLUGIN_NAME } from '../defaults';
import { registerEditorHelper } from '../libraries/handlebars/helpers';
import { handlebarsHelpersMap } from '../libraries/handlebars/helpersMap';
import { replacePlaceholders } from '../libraries/handlebars/replacePlaceholders';
import { extractImageData } from '../utilities/extractImageData';
import { getGenerationModels } from '../utilities/getGenerationModels';
const assignPrompt = async (action, { type, actionParams, context, field, layout, systemPrompt = '', template })=>{
const prompt = await replacePlaceholders(template, context);
const toLexicalHTML = type === 'richText' ? handlebarsHelpersMap.toHTML.name : '';
const assignedPrompts = {
layout: type === 'richText' ? layout : undefined,
prompt,
//TODO: Define only once on a collection level
system: type === 'richText' ? systemPrompt : undefined
};
if (action === 'Compose') {
return assignedPrompts;
}
const { layout: getLayout, system: getSystemPrompt } = defaultPrompts.find((p)=>p.name === action);
let updatedLayout = layout;
if (getLayout) {
updatedLayout = getLayout();
}
const system = getSystemPrompt({
...actionParams || {},
prompt,
systemPrompt
});
return {
layout: updatedLayout,
// TODO: revisit this toLexicalHTML
prompt: await replacePlaceholders(`{{${toLexicalHTML} ${field}}}`, context),
system
};
};
export const endpoints = (pluginConfig)=>({
textarea: {
//TODO: This is the main endpoint for generating content - its just needs to be renamed to 'generate' or something.
handler: async (req)=>{
const data = await req.json?.();
const { allowedEditorNodes = [], locale = 'en', options } = data;
const { action, actionParams, instructionId } = options;
const contextData = data.doc;
if (!instructionId) {
throw new Error(`Instruction ID is required for "${PLUGIN_NAME}" to work, please check your configuration`);
}
const instructions = await req.payload.findByID({
id: instructionId,
collection: PLUGIN_INSTRUCTIONS_TABLE
});
const { collections } = req.payload.config;
const collection = collections.find((collection)=>collection.slug === PLUGIN_INSTRUCTIONS_TABLE);
const { custom: { [PLUGIN_NAME]: { editorConfig = {} } = {} } = {} } = collection.admin;
const { schema: editorSchema = {} } = editorConfig;
const { prompt: promptTemplate = '' } = instructions;
let allowedEditorSchema = editorSchema;
if (allowedEditorNodes.length) {
allowedEditorSchema = filterEditorSchemaByNodes(editorSchema, allowedEditorNodes);
}
const schemaPath = instructions['schema-path'];
const fieldName = schemaPath?.split('.').pop();
registerEditorHelper(req.payload, schemaPath);
const { defaultLocale, locales = [] } = req.payload.config.localization || {};
const localeData = locales.find((l)=>{
return l.code === locale;
});
const localeInfo = localeData?.label[defaultLocale] || locale;
const model = getGenerationModels(pluginConfig).find((model)=>model.id === instructions['model-id']);
const settingsName = model.settings?.name;
const modelOptions = instructions[settingsName] || {};
const prompts = await assignPrompt(action, {
type: instructions['field-type'],
actionParams,
context: contextData,
field: fieldName,
layout: instructions.layout,
systemPrompt: instructions.system,
template: promptTemplate
});
try {
return model.handler?.(prompts.prompt, {
...modelOptions,
editorSchema: allowedEditorSchema,
layout: prompts.layout,
locale: localeInfo,
system: prompts.system
});
} catch (error) {
req.payload.logger.error('Error generating content: ', error);
return new Response(JSON.stringify(error.message), {
status: 500
});
}
},
method: 'post',
path: PLUGIN_API_ENDPOINT_GENERATE
},
upload: {
handler: async (req)=>{
const data = await req.json?.();
const { collectionSlug, documentId, options } = data;
const { instructionId } = options;
const docData = await req.payload.findByID({
id: documentId,
collection: collectionSlug,
draft: true
});
const contextData = {
...data.doc,
...docData
};
let instructions = {
images: [],
'model-id': '',
prompt: ''
};
if (instructionId) {
// @ts-expect-error
instructions = await req.payload.findByID({
id: instructionId,
collection: PLUGIN_INSTRUCTIONS_TABLE
});
}
const { images: sampleImages = [], prompt: promptTemplate = '' } = instructions;
const schemaPath = instructions['schema-path'];
registerEditorHelper(req.payload, schemaPath);
const text = await replacePlaceholders(promptTemplate, contextData);
const modelId = instructions['model-id'];
const uploadCollectionSlug = instructions['relation-to'];
const images = [
...extractImageData(text),
...sampleImages
];
const editImages = [];
for (const img of images){
try {
const serverURL = req.payload.config?.serverURL || process.env.SERVER_URL || process.env.NEXT_PUBLIC_SERVER_URL;
const response = await fetch(`${serverURL}${img.image.url}`, {
headers: {
//TODO: Further testing needed or so find a proper way.
Authorization: `Bearer ${req.headers.get('Authorization')?.split('Bearer ')[1] || ''}`
},
method: 'GET'
});
const blob = await response.blob();
editImages.push({
name: img.image.name,
type: img.image.type,
data: blob,
size: blob.size,
url: `${serverURL}${img.image.url}`
});
} catch (e) {
req.payload.logger.error('Error fetching reference images!');
console.error(e);
throw Error("We couldn't fetch the images. Please ensure the images are accessible and hosted publicly.");
}
}
const model = getGenerationModels(pluginConfig).find((model)=>model.id === modelId);
const settingsName = model.settings?.name;
let modelOptions = instructions[settingsName] || {};
modelOptions = {
...modelOptions,
images: editImages
};
const result = await model.handler?.(text, modelOptions);
const assetData = await req.payload.create({
collection: uploadCollectionSlug,
data: result.data,
file: result.file
});
return new Response(JSON.stringify({
result: {
id: assetData.id,
alt: assetData.alt
}
}));
},
method: 'post',
path: PLUGIN_API_ENDPOINT_GENERATE_UPLOAD
}
});
//# sourceMappingURL=index.js.map