@websolutespa/payload-plugin-bowl-llm
Version:
LLM plugin for Bowl PayloadCms plugin
392 lines (391 loc) • 15.4 kB
JavaScript
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