362 lines
12 KiB
JavaScript
362 lines
12 KiB
JavaScript
const prisma = require("../utils/prisma");
|
|
const moment = require("moment");
|
|
|
|
/**
|
|
* @typedef {Object} SystemPromptVariable
|
|
* @property {number} id
|
|
* @property {string} key
|
|
* @property {string|function} value
|
|
* @property {string} description
|
|
* @property {'system'|'user'|'workspace'|'static'} type
|
|
* @property {number} userId
|
|
* @property {boolean} multiUserRequired
|
|
*/
|
|
|
|
const SystemPromptVariables = {
|
|
VALID_TYPES: ["user", "workspace", "system", "static"],
|
|
DEFAULT_VARIABLES: [
|
|
{
|
|
key: "time",
|
|
value: () => moment().format("LTS"),
|
|
description: "Current time",
|
|
type: "system",
|
|
multiUserRequired: false,
|
|
},
|
|
{
|
|
key: "date",
|
|
value: () => moment().format("LL"),
|
|
description: "Current date",
|
|
type: "system",
|
|
multiUserRequired: false,
|
|
},
|
|
{
|
|
key: "datetime",
|
|
value: () => moment().format("LLLL"),
|
|
description: "Current date and time",
|
|
type: "system",
|
|
multiUserRequired: false,
|
|
},
|
|
{
|
|
key: "user.id",
|
|
value: (userId = null) => {
|
|
if (!userId) return "[User ID]";
|
|
return userId;
|
|
},
|
|
description: "Current user's ID",
|
|
type: "user",
|
|
multiUserRequired: true,
|
|
},
|
|
{
|
|
key: "user.name",
|
|
value: async (userId = null) => {
|
|
if (!userId) return "[User name]";
|
|
try {
|
|
const user = await prisma.users.findUnique({
|
|
where: { id: Number(userId) },
|
|
select: { username: true },
|
|
});
|
|
return user?.username || "[User name is empty or unknown]";
|
|
} catch (error) {
|
|
console.error("Error fetching user name:", error);
|
|
return "[User name is empty or unknown]";
|
|
}
|
|
},
|
|
description: "Current user's username",
|
|
type: "user",
|
|
multiUserRequired: true,
|
|
},
|
|
{
|
|
key: "user.bio",
|
|
value: async (userId = null) => {
|
|
if (!userId) return "[User bio]";
|
|
try {
|
|
const user = await prisma.users.findUnique({
|
|
where: { id: Number(userId) },
|
|
select: { bio: true },
|
|
});
|
|
return user?.bio || "[User bio is empty]";
|
|
} catch (error) {
|
|
console.error("Error fetching user bio:", error);
|
|
return "[User bio is empty]";
|
|
}
|
|
},
|
|
description: "Current user's bio field from their profile",
|
|
type: "user",
|
|
multiUserRequired: true,
|
|
},
|
|
{
|
|
key: "workspace.id",
|
|
value: (workspaceId = null) => {
|
|
if (!workspaceId) return "[Workspace ID]";
|
|
return workspaceId;
|
|
},
|
|
description: "Current workspace's ID",
|
|
type: "workspace",
|
|
multiUserRequired: false,
|
|
},
|
|
{
|
|
key: "workspace.name",
|
|
value: async (workspaceId = null) => {
|
|
if (!workspaceId) return "[Workspace name]";
|
|
const workspace = await prisma.workspaces.findUnique({
|
|
where: { id: Number(workspaceId) },
|
|
select: { name: true },
|
|
});
|
|
return workspace?.name || "[Workspace name is empty or unknown]";
|
|
},
|
|
description: "Current workspace's name",
|
|
type: "workspace",
|
|
multiUserRequired: false,
|
|
},
|
|
],
|
|
|
|
/**
|
|
* Gets a system prompt variable by its key
|
|
* @param {string} key
|
|
* @returns {Promise<SystemPromptVariable>}
|
|
*/
|
|
get: async function (key = null) {
|
|
if (!key) return null;
|
|
const variable = await prisma.system_prompt_variables.findUnique({
|
|
where: { key: String(key) },
|
|
});
|
|
return variable;
|
|
},
|
|
|
|
/**
|
|
* Retrieves all system prompt variables with dynamic variables as well
|
|
* as user defined variables
|
|
* @param {number|null} userId - the current user ID (determines if in multi-user mode)
|
|
* @returns {Promise<SystemPromptVariable[]>}
|
|
*/
|
|
getAll: async function (userId = null) {
|
|
// All user-defined system variables are available to everyone globally since only admins can create them.
|
|
const userDefinedSystemVariables =
|
|
await prisma.system_prompt_variables.findMany();
|
|
const formattedDbVars = userDefinedSystemVariables.map((v) => ({
|
|
id: v.id,
|
|
key: v.key,
|
|
value: v.value,
|
|
description: v.description,
|
|
type: v.type,
|
|
userId: v.userId,
|
|
}));
|
|
|
|
// If userId is not provided, filter the default variables to only include non-multiUserRequired variables
|
|
// since we wont be able to dynamically inject user-related content.
|
|
const defaultSystemVariables = !userId
|
|
? this.DEFAULT_VARIABLES.filter((v) => !v.multiUserRequired)
|
|
: this.DEFAULT_VARIABLES;
|
|
|
|
return [...defaultSystemVariables, ...formattedDbVars];
|
|
},
|
|
|
|
/**
|
|
* Creates a new system prompt variable
|
|
* @param {{ key: string, value: string, description: string, type: string, userId: number }} data
|
|
* @returns {Promise<SystemPromptVariable>}
|
|
*/
|
|
create: async function ({
|
|
key,
|
|
value,
|
|
description = null,
|
|
type = "static",
|
|
userId = null,
|
|
}) {
|
|
await this._checkVariableKey(key, true);
|
|
return await prisma.system_prompt_variables.create({
|
|
data: {
|
|
key: String(key),
|
|
value: String(value),
|
|
description: description ? String(description) : null,
|
|
type: type ? String(type) : "static",
|
|
userId: userId ? Number(userId) : null,
|
|
},
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Updates a system prompt variable by its unique database ID
|
|
* @param {number} id
|
|
* @param {{ key: string, value: string, description: string }} data
|
|
* @returns {Promise<SystemPromptVariable>}
|
|
*/
|
|
update: async function (id, { key, value, description = null }) {
|
|
if (!id || !key || !value) return null;
|
|
const existingRecord = await prisma.system_prompt_variables.findFirst({
|
|
where: { id: Number(id) },
|
|
});
|
|
if (!existingRecord) throw new Error("System prompt variable not found");
|
|
await this._checkVariableKey(key, false);
|
|
|
|
return await prisma.system_prompt_variables.update({
|
|
where: { id: existingRecord.id },
|
|
data: {
|
|
key: String(key),
|
|
value: String(value),
|
|
description: description ? String(description) : null,
|
|
},
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Deletes a system prompt variable by its unique database ID
|
|
* @param {number} id
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
delete: async function (id = null) {
|
|
try {
|
|
await prisma.system_prompt_variables.delete({
|
|
where: { id: Number(id) },
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Error deleting variable:", error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Injects variables into a string based on the user ID and workspace ID (if provided) and the variables available
|
|
* @param {string} str - the input string to expand variables into
|
|
* @param {number|null} userId - the user ID to use for dynamic variables
|
|
* @param {number|null} workspaceId - the workspace ID to use for workspace variables
|
|
* @returns {Promise<string>}
|
|
*/
|
|
expandSystemPromptVariables: async function (
|
|
str,
|
|
userId = null,
|
|
workspaceId = null
|
|
) {
|
|
if (!str) return str;
|
|
|
|
try {
|
|
const allVariables = await this.getAll(userId);
|
|
let result = str;
|
|
|
|
// Find all variable patterns in the string
|
|
const matches = str.match(/\{([^}]+)\}/g) || [];
|
|
|
|
// Process each match
|
|
for (const match of matches) {
|
|
const key = match.substring(1, match.length - 1); // Remove { and }
|
|
|
|
// Determine if the variable is a class-based variable (workspace.X or user.X)
|
|
const isWorkspaceOrUserVariable = ["workspace.", "user."].some(
|
|
(prefix) => key.startsWith(prefix)
|
|
);
|
|
|
|
// Handle class-based variables with current workspace's or user's data
|
|
if (isWorkspaceOrUserVariable) {
|
|
let variableTypeDisplay;
|
|
if (key.startsWith("workspace.")) variableTypeDisplay = "Workspace";
|
|
else if (key.startsWith("user.")) variableTypeDisplay = "User";
|
|
else throw new Error(`Invalid class-based variable: ${key}`);
|
|
|
|
// Get the property name after the prefix
|
|
const prop = key.split(".")[1];
|
|
const variable = allVariables.find((v) => v.key === key);
|
|
|
|
// If the variable is a function, call it to get the current value
|
|
if (variable && typeof variable.value === "function") {
|
|
// If the variable is an async function, call it to get the current value
|
|
if (variable.value.constructor.name === "AsyncFunction") {
|
|
let value;
|
|
try {
|
|
if (variableTypeDisplay === "Workspace")
|
|
value = await variable.value(workspaceId);
|
|
else if (variableTypeDisplay === "User")
|
|
value = await variable.value(userId);
|
|
else throw new Error(`Invalid class-based variable: ${key}`);
|
|
} catch (error) {
|
|
console.error(
|
|
`Error processing ${variableTypeDisplay} variable ${key}:`,
|
|
error
|
|
);
|
|
value = `[${variableTypeDisplay} ${prop}]`;
|
|
}
|
|
result = result.replace(match, value);
|
|
} else {
|
|
let value;
|
|
try {
|
|
// Call the variable function with the appropriate workspace or user ID
|
|
if (variableTypeDisplay === "Workspace")
|
|
value = variable.value(workspaceId);
|
|
else if (variableTypeDisplay === "User")
|
|
value = variable.value(userId);
|
|
else throw new Error(`Invalid class-based variable: ${key}`);
|
|
} catch (error) {
|
|
console.error(
|
|
`Error processing ${variableTypeDisplay} variable ${key}:`,
|
|
error
|
|
);
|
|
value = `[${variableTypeDisplay} ${prop}]`;
|
|
}
|
|
result = result.replace(match, value);
|
|
}
|
|
} else {
|
|
// If the variable is not a function, replace the match with the variable value
|
|
result = result.replace(match, `[${variableTypeDisplay} ${prop}]`);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Handle regular variables (static types)
|
|
const variable = allVariables.find((v) => v.key === key);
|
|
if (!variable) continue;
|
|
|
|
// For dynamic and system variables, call the function to get the current value
|
|
if (
|
|
["system"].includes(variable.type) &&
|
|
typeof variable.value === "function"
|
|
) {
|
|
try {
|
|
if (variable.value.constructor.name === "AsyncFunction") {
|
|
const value = await variable.value(userId);
|
|
result = result.replace(match, value);
|
|
} else {
|
|
const value = variable.value();
|
|
result = result.replace(match, value);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error processing dynamic variable ${key}:`, error);
|
|
result = result.replace(match, match);
|
|
}
|
|
} else {
|
|
result = result.replace(match, variable.value || match);
|
|
}
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Error in expandSystemPromptVariables:", error);
|
|
return str;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Internal function to check if a variable key is valid
|
|
* @param {string} key
|
|
* @param {boolean} checkExisting
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
_checkVariableKey: async function (key = null, checkExisting = true) {
|
|
if (!key) throw new Error("Key is required");
|
|
if (typeof key !== "string") throw new Error("Key must be a string");
|
|
if (!/^[a-zA-Z0-9_]+$/.test(key))
|
|
throw new Error("Key must contain only letters, numbers and underscores");
|
|
if (key.length > 255)
|
|
throw new Error("Key must be less than 255 characters");
|
|
if (key.length < 3) throw new Error("Key must be at least 3 characters");
|
|
if (key.startsWith("user."))
|
|
throw new Error("Key cannot start with 'user.'");
|
|
if (key.startsWith("system."))
|
|
throw new Error("Key cannot start with 'system.'");
|
|
if (checkExisting && (await this.get(key)) !== null)
|
|
throw new Error("System prompt variable with this key already exists");
|
|
|
|
return true;
|
|
},
|
|
};
|
|
|
|
module.exports = { SystemPromptVariables };
|