UNPKG

@websolutespa/payload-plugin-bowl-llm

Version:

LLM plugin for Bowl PayloadCms plugin

250 lines (249 loc) 9.41 kB
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