UNPKG

@websolutespa/payload-plugin-bowl-llm

Version:

LLM plugin for Bowl PayloadCms plugin

392 lines (391 loc) 15.4 kB
import { CronLogStatus, options as cronOptions } from '@websolutespa/payload-plugin-cron-job'; import { ResponseSuccess } from '@websolutespa/payload-utils/server'; import fs from 'fs'; import path from 'path'; import { addDataAndFileToRequest } from 'payload'; import { v4 as uuid } from 'uuid'; import { options } from '../options'; import { deleteFolderRecursive, getKnowledgebasePath } from '../utils/knowledgebase'; import { KbFileNoToolOption, LlmKnowledgeBaseCreationMode } from './consts'; import { errorHandler } from './error.handler'; import { createTaskLog, updateTaskLog } from './llmKbWebhook.handler'; import { pythonKnowledgeBaseHandler } from './python/pythonKnowledgeBase.handler'; export const kbPollEndpointHandler = async (req)=>{ try { const traceId = req.routeParams?.traceId; // get all logs for the traceId const logs = await req.payload.find({ collection: cronOptions.slug.cronLog, where: { traceId: { equals: traceId } }, overrideAccess: true }); let response; const processing = logs.docs.some((x)=>x.status === CronLogStatus.Processing); if (processing) { // generation is still in progress response = { processing: true }; } else { const errorLogs = logs.docs.filter((x)=>x.status !== CronLogStatus.Succeeded); if (errorLogs.length === 0) { // generation is complete and there are no errors response = { processing: false, success: true }; } else { // generation is complete but there are errors response = { processing: false, success: false, errors: errorLogs.map((log)=>{ const taskData = JSON.parse(log.message); let error; if (taskData.taskResponse?.status === 'completed') { error = taskData.taskResponse.result.error || 'Unknown error'; } else { error = taskData.taskResponse?.error || taskData.error || 'Unknown error'; } return { job: log.job, taskStatus: taskData.taskStatus || 'Unknown taskStatus', error: error }; }) }; } } return ResponseSuccess(response); } catch (error) { return errorHandler(error); } }; export const kbEndpointHandler = async (req)=>{ try { await addDataAndFileToRequest(req); // get the app for which the knowledge base should be generated const { appId, mode, toolFunctionId } = req.data; const { payload } = req; const app = await payload.findByID({ collection: options.slug.llmApp, id: appId, overrideAccess: true }); const traceId = await llmKnowledgeBase(payload, app, { mode: mode, toolFunctionId: toolFunctionId, forceKbGeneration: true }); return ResponseSuccess({ traceId: traceId }); } catch (error) { return errorHandler(error); } }; const createAppVectorDb = async (payload, app, creationOptions, tempKnowledgebasePath, traceId)=>{ if (app.settings.nightlyKbGeneration === false && !creationOptions.forceKbGeneration) { // skip KB generation if nightly generation is disabled and forceKbGeneration is false return; } // get files and endpoints associated with the app const llmKbFiles = await payload.find({ collection: options.slug.llmKbFile, pagination: false, where: { and: [ { 'isActive': { equals: true } }, { llmApp: { equals: app.id } }, { or: [ { appTool: { equals: KbFileNoToolOption } }, { appTool: { equals: null } }, { appTool: { exists: false } } ] } ] }, overrideAccess: true }); const filenames = llmKbFiles.docs.filter((x)=>x.url != null).map((x)=>path.basename(x.url)) || []; const endpoints = app?.settings?.knowledgeBase?.externalEndpoints?.filter((x)=>x.endpointUrl !== '') || []; // if there are no files or endpoints associated with the tool, skip the generation of the KB if (endpoints.length === 0 && filenames.length === 0) { return; } // avoid generating KB if the app has no endpoints and the VectorDb is more recent than all the KB files // (if forceKbGeneration is true, ignore this check) if (endpoints.length === 0 && !creationOptions.forceKbGeneration) { // get the latest VectorDb const vectorDbs = await payload.find({ collection: options.slug.llmVectorDb, limit: 1, pagination: false, sort: '-createdAt', where: { and: [ { llmApp: { equals: app.id } }, { or: [ { appTool: { equals: KbFileNoToolOption } }, { appTool: { equals: null } }, { appTool: { exists: false } } ] } ] }, overrideAccess: true }); if (vectorDbs.docs.length > 0) { const latestVectorDb = vectorDbs.docs[0]; // get the latest file const latestFile = llmKbFiles.docs.reduce((prev, current)=>prev.updatedAt > current.updatedAt ? prev : current); // if the latest vectorDb is more recent than the latest file, skip the KB generation if (latestVectorDb && latestVectorDb.createdAt > latestFile.updatedAt) { return; } } } const jobName = `KB generation of app "${app.name}"`; const log = await createTaskLog(jobName, traceId, { taskStatus: 'Preparing to generate knowledge base' }); try { // abort KB generation if the same job was executed less than a minute ago (fix multi-instance issue) const lastMinute = new Date(); lastMinute.setMinutes(lastMinute.getMinutes() - 1); const existingLogs = await payload.find({ collection: cronOptions.slug.cronLog, where: { id: { not_equals: log.id }, job: { equals: jobName }, status: { equals: CronLogStatus.Processing }, createdAt: { greater_than_equal: lastMinute } }, overrideAccess: true }); if (existingLogs.docs.length > 0) { throw new Error('Job execution aborted: same job was started (and is still processing) less than a minute ago'); } // create app temp directory const appPath = getKnowledgebasePath(tempKnowledgebasePath, app.name); fs.mkdirSync(appPath, { recursive: true }); // trigger kb generation task const taskRequest = { taskId: log.id, model: app.settings.llmConfig.model, provider: app.settings.llmConfig.provider, files: filenames, secrets: app.settings.llmConfig.secrets, endpoints: endpoints, downloadPath: appPath, metadata: { traceId: traceId, appId: app.id } }; await pythonKnowledgeBaseHandler(taskRequest); await updateTaskLog(log.id, { taskStatus: 'ROBOT task started, waiting for webhook', taskRequest: taskRequest }); } catch (error) { await updateTaskLog(log.id, { error: error.message }, CronLogStatus.Failed); } }; const createAppToolsVectorDb = async (payload, app, creationOptions, tempKnowledgebasePath, traceId)=>{ for (const tool of app.settings.appTools.filter((x)=>x.isActive && (!creationOptions.toolFunctionId || x.functionId === creationOptions.toolFunctionId))){ if ((app.settings.nightlyKbGeneration === false || tool.nightlyKbGeneration === false) && !creationOptions.forceKbGeneration) { continue; } // get files and endpoints associated with the tool const llmKbFiles = await payload.find({ collection: options.slug.llmKbFile, pagination: false, where: { 'isActive': { equals: true }, 'llmApp': { equals: app.id }, 'appTool': { equals: tool.functionId } }, overrideAccess: true }); const filenames = llmKbFiles.docs.filter((x)=>x.url != null).map((x)=>path.basename(x.url)) || []; const endpoints = tool.knowledgeBase?.externalEndpoints?.filter((x)=>x.endpointUrl !== '') || []; const integrations = tool.knowledgeBase?.integrations || []; // if there are no files or endpoints or integrations associated with the tool, skip the tool if (endpoints.length === 0 && filenames.length === 0 && integrations.length === 0) { continue; } const jobName = `KB generation of tool "${tool.functionName} - ${tool.name}" [app "${app.name}"]"`; const log = await createTaskLog(jobName, traceId, { taskStatus: 'Preparing to generate knowledge base' }); try { // abort KB generation if the same job was executed less than a minute ago (fix multi-instance issue) const lastMinute = new Date(); lastMinute.setMinutes(lastMinute.getMinutes() - 1); const existingLogs = await payload.find({ collection: cronOptions.slug.cronLog, where: { id: { not_equals: log.id }, job: { equals: jobName }, status: { equals: CronLogStatus.Processing }, createdAt: { greater_than_equal: lastMinute } }, overrideAccess: true }); if (existingLogs.docs.length > 0) { throw new Error('Job execution aborted: same job was started (and is still processing) less than a minute ago'); } // create app tool temp directory const appPath = getKnowledgebasePath(tempKnowledgebasePath, app.name, tool.name); fs.mkdirSync(appPath, { recursive: true }); // trigger kb generation task const taskRequest = { taskId: log.id, model: app.settings.llmConfig.model, provider: app.settings.llmConfig.provider, files: filenames, vectorDbType: tool.knowledgeBase?.vectorDbType, dataSource: tool.dataSource, integrations: tool.knowledgeBase?.integrations, endpoints: endpoints, secrets: app.settings.llmConfig.secrets, downloadPath: appPath, chunkingMethod: tool.knowledgeBase?.chunkingMethod, chunkSize: tool.knowledgeBase?.chunkSize, chunkOverlap: tool.knowledgeBase?.chunkOverlap, deepLevel: tool.knowledgeBase?.deepLevel, metadata: { traceId: traceId, appId: app.id, toolId: tool.id } }; await pythonKnowledgeBaseHandler(taskRequest); await updateTaskLog(log.id, { taskStatus: 'ROBOT task started, waiting for webhook', taskRequest: taskRequest }); } catch (error) { await updateTaskLog(log.id, { error: error.message }, CronLogStatus.Failed); } } }; export async function llmKnowledgeBase(payload, app, creationOptions = { mode: LlmKnowledgeBaseCreationMode.appAndTools, forceKbGeneration: false }) { let apps; if (app) { // single app kb generation (from the UI) apps = [ app ]; } else { // all apps kb generation (via cronjob) const response = await payload.find({ collection: options.slug.llmApp, where: { 'isActive': { equals: true } }, pagination: false, overrideAccess: true }); apps = response.docs; } const traceId = uuid(); const tempKnowledgebasePath = path.join(process.cwd(), 'lib', 'knowledge-base', uuid()); for (const app of apps){ switch(creationOptions.mode){ case LlmKnowledgeBaseCreationMode.appAndTools: createAppToolsVectorDb(payload, app, creationOptions, tempKnowledgebasePath, traceId); createAppVectorDb(payload, app, creationOptions, tempKnowledgebasePath, traceId); break; case LlmKnowledgeBaseCreationMode.appOnly: createAppVectorDb(payload, app, creationOptions, tempKnowledgebasePath, traceId); break; case LlmKnowledgeBaseCreationMode.toolsOnly: case LlmKnowledgeBaseCreationMode.specificTool: createAppToolsVectorDb(payload, app, creationOptions, tempKnowledgebasePath, traceId); break; } } // delete temp directory deleteFolderRecursive(tempKnowledgebasePath); return traceId; } //# sourceMappingURL=knowledgeBase.handler.js.map