@websolutespa/payload-plugin-bowl-llm
Version:
LLM plugin for Bowl PayloadCms plugin
250 lines (249 loc) • 9.41 kB
JavaScript
import { isEntity } from '@websolutespa/bom-core';
import { createCronLog, CronLogStatus, options as cronOptions, updateCronLog } from '@websolutespa/payload-plugin-cron-job';
import { ResponseSuccess } from '@websolutespa/payload-utils/server';
import fs from 'fs';
import mime from 'mime-types';
import path from 'path';
import payload, { addDataAndFileToRequest } from 'payload';
// import getRawBody from 'raw-body';
import { Webhook } from 'standardwebhooks';
import { v4 as uuid } from 'uuid';
import { options } from '../options';
import { deleteFolderRecursive, getKnowledgebasePath } from '../utils';
import { downloadRobot } from '../utils/robot';
import { errorHandler } from './error.handler';
export const createTaskLog = async (job, traceId, logData)=>{
const log = await createCronLog({
job: job,
message: JSON.stringify(logData),
traceId: traceId
});
return log;
};
export const updateTaskLog = async (logId, logData, newStatus)=>{
const result = await payload.find({
collection: cronOptions.slug.cronLog,
where: {
id: {
equals: logId
}
},
limit: 1
});
const log = result.docs.length ? result.docs[0] : undefined;
if (log) {
updateCronLog(log.id, {
status: newStatus || log.status,
message: JSON.stringify(logData)
});
}
};
export const getTaskLog = async (taskId)=>{
const result = await payload.find({
collection: cronOptions.slug.cronLog,
where: {
id: {
equals: taskId
}
},
limit: 1
});
return result.docs.length ? result.docs[0] : undefined;
};
const verifyWebhook = async (req)=>{
const wh = new Webhook(btoa(`${process.env.ROBOT_USER}:${process.env.ROBOT_PASSWORD}`));
const headers = {
'webhook-id': req.headers.get('webhook-id'),
'webhook-timestamp': req.headers.get('webhook-timestamp'),
'webhook-signature': req.headers.get('webhook-signature')
};
if (req.body && req.arrayBuffer) {
// !!! todo verify
wh.verify(req.body, headers);
}
};
async function saveAppVectorDb(appId, robotDbVectorFilename) {
const app = await payload.findByID({
collection: options.slug.llmApp,
id: appId
});
// create app temp directory
const tempKnowledgebasePath = path.join(process.cwd(), 'lib', 'knowledge-base', uuid());
const appPath = getKnowledgebasePath(tempKnowledgebasePath, app.name);
fs.mkdirSync(appPath, {
recursive: true
});
const dbVectorFilename = await downloadRobot(robotDbVectorFilename, appPath);
// save db vector file in the LlmApp tool
const dbVectorPath = path.join(appPath, dbVectorFilename);
const dbVectorDoc = await payload.create({
collection: options.slug.llmVectorDb,
data: {
notes: `Knowledgebase app "${app.name}"`,
llmApp: app.id
},
file: {
data: fs.readFileSync(dbVectorPath),
mimetype: mime.lookup(dbVectorPath),
name: dbVectorFilename,
size: fs.statSync(dbVectorPath).size
},
overrideAccess: true
});
await payload.update({
collection: options.slug.llmApp,
id: app.id,
data: {
settings: {
knowledgeBase: {
vectorDbFile: dbVectorDoc.id
}
}
},
overrideAccess: true
});
// delete obsolete knowledge-base directory
const knowledgebasePath = getKnowledgebasePath(process.cwd(), app.name);
deleteFolderRecursive(knowledgebasePath);
return dbVectorDoc;
}
async function saveToolVectorDb(appId, toolId, robotDbVectorFilename) {
const app = await payload.findByID({
collection: options.slug.llmApp,
id: appId
});
const appTools = app.settings.appTools;
const tool = appTools.find((x)=>x.id === toolId);
if (tool === undefined) {
throw new Error(`Tool with id ${toolId} not found in app ${app.name}`);
}
// create app tool temp directory
const tempKnowledgebasePath = path.join(process.cwd(), 'lib', 'knowledge-base', uuid());
const appPath = getKnowledgebasePath(tempKnowledgebasePath, app.name, tool.name);
fs.mkdirSync(appPath, {
recursive: true
});
const dbVectorFilename = await downloadRobot(robotDbVectorFilename, appPath);
// save db vector file in the LlmApp tool
const dbVectorPath = path.join(appPath, dbVectorFilename);
const dbVectorDoc = await payload.create({
collection: options.slug.llmVectorDb,
data: {
notes: `Knowledgebase tool "${tool.functionId} - ${app.name}"`,
llmApp: app.id,
appTool: tool.functionId
},
file: {
data: fs.readFileSync(dbVectorPath),
mimetype: mime.lookup(dbVectorPath),
name: dbVectorFilename,
size: fs.statSync(dbVectorPath).size
},
overrideAccess: true
});
const appToolIndex = appTools.findIndex((x)=>x.id === tool.id);
if (appToolIndex !== -1) {
const appTool = appTools[appToolIndex];
appTool.knowledgeBase.vectorDbFile = dbVectorDoc.id;
// avoid ValidationError on field settings.appTools.0.knowledgeBase.files.0.media :O
delete appTool.knowledgeBase.files;
}
// delete obsolete knowledge-base directory
const knowledgebasePath = getKnowledgebasePath(process.cwd(), app.name, tool.name);
deleteFolderRecursive(knowledgebasePath);
// "sanitize" appTools before updating the LlmApp (avoid validation error "This field is not a valid upload ID")
for (const tool of appTools){
if (isEntity(tool?.knowledgeBase?.vectorDbFile)) {
tool.knowledgeBase.vectorDbFile = tool.knowledgeBase.vectorDbFile.id;
}
for (const file of tool?.knowledgeBase?.files || []){
if (isEntity(file?.media)) {
file.media = file.media.id;
}
}
}
await payload.update({
collection: options.slug.llmApp,
id: app.id,
data: {
settings: {
appTools: appTools
}
},
overrideAccess: true
});
return dbVectorDoc;
}
export const llmKbWebhookHandler = async (req)=>{
let taskResponse = null;
let logId;
try {
await addDataAndFileToRequest(req);
let data = req.data;
const contentType = req.headers.get('content-type');
if (contentType === undefined || contentType === 'text/plain') {
// !!! todo verify
// data = (await getRawBody(req)).toString('utf8');
} else if (contentType === 'application/json') {
data = JSON.stringify(data);
}
verifyWebhook(req);
taskResponse = JSON.parse(String(data));
if (!taskResponse || typeof taskResponse !== 'object') {
throw new Error('Invalid task response format');
}
logId = taskResponse.id;
if (taskResponse.status === 'failed' || !taskResponse.result.success || !taskResponse.result.file) {
updateTaskLog(logId, {
taskStatus: 'webhook verified, but robot generation failed',
taskResponse: taskResponse
}, CronLogStatus.Failed);
} else {
// get log associated with task
const log = await getTaskLog(logId);
// check if log exists and was not already processed
if (log === undefined) {
throw new Error(`No processing log found for task ${logId}`);
} else if (log.status === CronLogStatus.Succeeded) {
// task already processed, tell ROBOT to stop retrying
return ResponseSuccess({
'success': true
});
} else if (log.status === CronLogStatus.Failed) {
// task already processed, let's retry processing the task
// do nothing?
}
const appId = taskResponse.metadata.extra?.appId;
const toolId = taskResponse.metadata.extra?.toolId;
// save vectordb
let vectorDb;
if (appId && toolId) {
// for tool
vectorDb = await saveToolVectorDb(appId, toolId, taskResponse.result.file);
} else if (appId) {
// for app
vectorDb = await saveAppVectorDb(appId, taskResponse.result.file);
} else {
throw new Error('No appId or toolId found in task metadata');
}
updateTaskLog(log.id, {
taskStatus: 'vectorDb successfully saved',
savedVectorDb: vectorDb?.id,
taskResponse: taskResponse
}, CronLogStatus.Succeeded);
}
return ResponseSuccess({
'success': true
});
} catch (error) {
if (logId) {
updateTaskLog(logId, {
taskStatus: 'an exception occurred while processing the webhook',
taskResponse: taskResponse ?? undefined,
error: error.message
}, CronLogStatus.Failed);
}
return errorHandler(error);
}
};
//# sourceMappingURL=llmKbWebhook.handler.js.map