From 27f7c19933060580050fe0c663255911abbab9f7 Mon Sep 17 00:00:00 2001 From: angelplusultra Date: Fri, 19 Sep 2025 11:21:57 -0700 Subject: [PATCH 1/5] Fix system prompt variable color logic by removing unused variable type from switch statement and adding new types. --- .../Admin/SystemPromptVariables/VariableRow/index.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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")} From c5092a2882f1312eaabc2c8174c2ffeabc80d549 Mon Sep 17 00:00:00 2001 From: angelplusultra Date: Fri, 19 Sep 2025 11:23:34 -0700 Subject: [PATCH 2/5] Add workspace id, name and user id as default system prompt variables --- server/models/systemPromptVariables.js | 73 ++++++++++++++++++++++++-- server/utils/chats/index.js | 3 +- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/server/models/systemPromptVariables.js b/server/models/systemPromptVariables.js index bfe94f19621..e3f2c69e917 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,6 +241,32 @@ const SystemPromptVariables = { for (const match of matches) { const key = match.substring(1, match.length - 1); // Remove { and } + // Handle `workspace.X` variables with current workspace's data + if (key.startsWith("workspace.")) { + const workspaceProp = key.split(".")[1]; + const variable = allVariables.find((v) => v.key === key); + + if (variable && typeof variable.value === "function") { + if (variable.value.constructor.name === "AsyncFunction") { + try { + const value = await variable.value(workspaceId); + result = result.replace(match, value); + } catch (error) { + console.error( + `Error processing workspace variable ${key}:`, + error + ); + result = result.replace(match, `[Workspace ${workspaceProp}]`); + } + } else { + const value = variable.value(workspaceId); + result = result.replace(match, value); + } + } else { + result = result.replace(match, `[Workspace ${workspaceProp}]`); + } + continue; + } // Handle `user.X` variables with current user's data if (key.startsWith("user.")) { const userProp = key.split(".")[1]; 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 ); } From 4d2d696c08ba9f60964e4fc88dff3ee223b07302 Mon Sep 17 00:00:00 2001 From: angelplusultra Date: Fri, 19 Sep 2025 11:56:41 -0700 Subject: [PATCH 3/5] Combine user and workspace variable evaluations into a single if statment, reducing redundant code. --- server/models/systemPromptVariables.js | 58 ++++++++++++-------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/server/models/systemPromptVariables.js b/server/models/systemPromptVariables.js index e3f2c69e917..012345abfa1 100644 --- a/server/models/systemPromptVariables.js +++ b/server/models/systemPromptVariables.js @@ -241,52 +241,48 @@ const SystemPromptVariables = { for (const match of matches) { const key = match.substring(1, match.length - 1); // Remove { and } - // Handle `workspace.X` variables with current workspace's data - if (key.startsWith("workspace.")) { - const workspaceProp = key.split(".")[1]; + // Determine if the variable is a workspace or user variable + const isWorkspaceOrUserVariable = + key.startsWith("workspace.") || key.startsWith("user."); + + // Handle `workspace.X` or `user.X` variables with current workspace's or user's data + if (isWorkspaceOrUserVariable) { + // Determine the type of variable (Workspace or User) + const variableTypeDisplay = key.startsWith("workspace.") + ? "Workspace" + : "User"; + // 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(workspaceId); - result = result.replace(match, value); + value = await variable.value( + variableTypeDisplay === "Workspace" ? workspaceId : userId + ); } catch (error) { console.error( - `Error processing workspace variable ${key}:`, + `Error processing ${variableTypeDisplay} variable ${key}:`, error ); - result = result.replace(match, `[Workspace ${workspaceProp}]`); + value = `[${variableTypeDisplay} ${prop}]`; } - } else { - const value = variable.value(workspaceId); result = result.replace(match, value); - } - } else { - result = result.replace(match, `[Workspace ${workspaceProp}]`); - } - continue; - } - // Handle `user.X` variables with current user's data - if (key.startsWith("user.")) { - const userProp = key.split(".")[1]; - const variable = allVariables.find((v) => v.key === key); - - if (variable && typeof variable.value === "function") { - if (variable.value.constructor.name === "AsyncFunction") { - try { - const value = await variable.value(userId); - result = result.replace(match, value); - } catch (error) { - console.error(`Error processing user variable ${key}:`, error); - result = result.replace(match, `[User ${userProp}]`); - } } else { - const value = variable.value(); + let value; + // Call the variable function with the appropriate workspace or user ID + value = variable.value( + variableTypeDisplay === "Workspace" ? workspaceId : userId + ); 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; } From 9955e7dbeec184c942da5c982da9f0a606bf361d Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Mon, 29 Sep 2025 14:09:59 -0700 Subject: [PATCH 4/5] minor refactor --- server/models/systemPromptVariables.js | 44 +++++++++++++++++--------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/server/models/systemPromptVariables.js b/server/models/systemPromptVariables.js index 012345abfa1..5e63c8a19d5 100644 --- a/server/models/systemPromptVariables.js +++ b/server/models/systemPromptVariables.js @@ -241,16 +241,18 @@ const SystemPromptVariables = { for (const match of matches) { const key = match.substring(1, match.length - 1); // Remove { and } - // Determine if the variable is a workspace or user variable - const isWorkspaceOrUserVariable = - key.startsWith("workspace.") || key.startsWith("user."); + // Determine if the variable is a class-based variable (workspace.X or user.X) + const isWorkspaceOrUserVariable = ["workspace.", "user."].some( + (prefix) => key.startsWith(prefix) + ); - // Handle `workspace.X` or `user.X` variables with current workspace's or user's data + // Handle class-based variables with current workspace's or user's data if (isWorkspaceOrUserVariable) { - // Determine the type of variable (Workspace or User) - const variableTypeDisplay = key.startsWith("workspace.") - ? "Workspace" - : "User"; + 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); @@ -261,9 +263,11 @@ const SystemPromptVariables = { if (variable.value.constructor.name === "AsyncFunction") { let value; try { - value = await variable.value( - variableTypeDisplay === "Workspace" ? workspaceId : userId - ); + 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}:`, @@ -274,10 +278,20 @@ const SystemPromptVariables = { result = result.replace(match, value); } else { let value; - // Call the variable function with the appropriate workspace or user ID - value = variable.value( - variableTypeDisplay === "Workspace" ? workspaceId : userId - ); + 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 { From d6142620d54a313eb513249ec012308554f612c4 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Mon, 29 Sep 2025 14:28:12 -0700 Subject: [PATCH 5/5] add systemPromptVariable expandSystemPromptVariables test cases --- .../models/systemPromptVariables.test.js | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 server/__tests__/models/systemPromptVariables.test.js 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