diff --git a/frontend/src/pages/Admin/SystemPromptVariables/VariableRow/index.jsx b/frontend/src/pages/Admin/SystemPromptVariables/VariableRow/index.jsx
index ac4c3ea8120..05a95c0b69a 100644
--- a/frontend/src/pages/Admin/SystemPromptVariables/VariableRow/index.jsx
+++ b/frontend/src/pages/Admin/SystemPromptVariables/VariableRow/index.jsx
@@ -45,11 +45,16 @@ export default function VariableRow({ variable, onRefresh }) {
bg: "bg-blue-600/20",
text: "text-blue-400 light:text-blue-800",
};
- case "dynamic":
+ case "user":
return {
bg: "bg-green-600/20",
text: "text-green-400 light:text-green-800",
};
+ case "workspace":
+ return {
+ bg: "bg-cyan-600/20",
+ text: "text-cyan-400 light:text-cyan-800",
+ };
default:
return {
bg: "bg-yellow-600/20",
@@ -81,7 +86,7 @@ export default function VariableRow({ variable, onRefresh }) {
- {titleCase(variable.type)}
+ {titleCase(variable?.type ?? "static")}
diff --git a/server/__tests__/models/systemPromptVariables.test.js b/server/__tests__/models/systemPromptVariables.test.js
new file mode 100644
index 00000000000..2220406e649
--- /dev/null
+++ b/server/__tests__/models/systemPromptVariables.test.js
@@ -0,0 +1,61 @@
+const { SystemPromptVariables } = require("../../models/systemPromptVariables");
+const prisma = require("../../utils/prisma");
+
+const mockUser = {
+ id: 1,
+ username: "john.doe",
+ bio: "I am a test user",
+};
+
+const mockWorkspace = {
+ id: 1,
+ name: "Test Workspace",
+ slug: 'test-workspace',
+};
+
+const mockSystemPromptVariables = [
+ {
+ id: 1,
+ key: "mystaticvariable",
+ value: "AnythingLLM testing runtime",
+ description: "A test variable",
+ type: "static",
+ userId: null,
+ },
+];
+
+describe("SystemPromptVariables.expandSystemPromptVariables", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ // Mock just the Prisma actions since that is what is used by default values
+ prisma.system_prompt_variables.findMany = jest.fn().mockResolvedValue(mockSystemPromptVariables);
+ prisma.workspaces.findUnique = jest.fn().mockResolvedValue(mockWorkspace);
+ prisma.users.findUnique = jest.fn().mockResolvedValue(mockUser);
+ });
+
+ it("should expand user-defined system prompt variables", async () => {
+ const variables = await SystemPromptVariables.expandSystemPromptVariables("Hello {mystaticvariable}");
+ expect(variables).toBe(`Hello ${mockSystemPromptVariables[0].value}`);
+ });
+
+ it("should expand workspace-defined system prompt variables", async () => {
+ const variables = await SystemPromptVariables.expandSystemPromptVariables("Hello {workspace.name}", null, mockWorkspace.id);
+ expect(variables).toBe(`Hello ${mockWorkspace.name}`);
+ });
+
+ it("should expand user-defined system prompt variables", async () => {
+ const variables = await SystemPromptVariables.expandSystemPromptVariables("Hello {user.name}", mockUser.id);
+ expect(variables).toBe(`Hello ${mockUser.username}`);
+ });
+
+ it("should work with any combination of variables", async () => {
+ const variables = await SystemPromptVariables.expandSystemPromptVariables("Hello {mystaticvariable} {workspace.name} {user.name}", mockUser.id, mockWorkspace.id);
+ expect(variables).toBe(`Hello ${mockSystemPromptVariables[0].value} ${mockWorkspace.name} ${mockUser.username}`);
+ });
+
+ it('should fail gracefully with invalid variables that are undefined for any reason', async () => {
+ // Undefined sub-fields on valid classes are push to a placeholder [Class prop]. This is expected behavior.
+ const variables = await SystemPromptVariables.expandSystemPromptVariables("Hello {invalid.variable} {user.password} the current user is {user.name} on workspace id #{workspace.id}", null, null);
+ expect(variables).toBe("Hello {invalid.variable} [User password] the current user is [User name] on workspace id #[Workspace ID]");
+ });
+});
\ No newline at end of file
diff --git a/server/models/systemPromptVariables.js b/server/models/systemPromptVariables.js
index bfe94f19621..5e63c8a19d5 100644
--- a/server/models/systemPromptVariables.js
+++ b/server/models/systemPromptVariables.js
@@ -7,13 +7,13 @@ const moment = require("moment");
* @property {string} key
* @property {string|function} value
* @property {string} description
- * @property {'system'|'user'|'static'} type
+ * @property {'system'|'user'|'workspace'|'static'} type
* @property {number} userId
* @property {boolean} multiUserRequired
*/
const SystemPromptVariables = {
- VALID_TYPES: ["user", "system", "static"],
+ VALID_TYPES: ["user", "workspace", "system", "static"],
DEFAULT_VARIABLES: [
{
key: "time",
@@ -36,6 +36,16 @@ const SystemPromptVariables = {
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) => {
@@ -74,6 +84,30 @@ const SystemPromptVariables = {
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,
+ },
],
/**
@@ -183,12 +217,17 @@ const SystemPromptVariables = {
},
/**
- * Injects variables into a string based on the user ID (if provided) and the variables available
+ * 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}
*/
- expandSystemPromptVariables: async function (str, userId = null) {
+ expandSystemPromptVariables: async function (
+ str,
+ userId = null,
+ workspaceId = null
+ ) {
if (!str) return str;
try {
@@ -202,26 +241,62 @@ const SystemPromptVariables = {
for (const match of matches) {
const key = match.substring(1, match.length - 1); // Remove { and }
- // Handle `user.X` variables with current user's data
- if (key.startsWith("user.")) {
- const userProp = key.split(".")[1];
+ // 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 {
- const value = await variable.value(userId);
- result = result.replace(match, value);
+ 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 user variable ${key}:`, error);
- result = result.replace(match, `[User ${userProp}]`);
+ console.error(
+ `Error processing ${variableTypeDisplay} variable ${key}:`,
+ error
+ );
+ value = `[${variableTypeDisplay} ${prop}]`;
}
+ result = result.replace(match, value);
} else {
- const value = variable.value();
+ 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 {
- result = result.replace(match, `[User ${userProp}]`);
+ // If the variable is not a function, replace the match with the variable value
+ result = result.replace(match, `[${variableTypeDisplay} ${prop}]`);
}
continue;
}
diff --git a/server/utils/chats/index.js b/server/utils/chats/index.js
index 658302f7bb4..aaf46bc2657 100644
--- a/server/utils/chats/index.js
+++ b/server/utils/chats/index.js
@@ -94,7 +94,8 @@ async function chatPrompt(workspace, user = null) {
"Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.";
return await SystemPromptVariables.expandSystemPromptVariables(
basePrompt,
- user?.id
+ user?.id,
+ workspace?.id
);
}
|