New Default System Prompt Variables (User ID, Workspace ID, & Workspace Name) (#4414)

* Fix system prompt variable color logic by removing unused variable type from switch statement and adding new types.

* Add workspace id, name and user id as default system prompt variables

* Combine user and workspace  variable evaluations into a single if statment, reducing redundant code.

* minor refactor

* add systemPromptVariable expandSystemPromptVariables test cases

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
Marcello Fitton
2025-09-29 14:32:56 -07:00
committed by GitHub
parent 7ca2753c24
commit 96bf127696
4 changed files with 158 additions and 16 deletions

View File

@@ -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 }) {
<span
className={`rounded-full ${colorTheme.bg} px-2 py-0.5 text-xs leading-5 font-semibold ${colorTheme.text} shadow-sm`}
>
{titleCase(variable.type)}
{titleCase(variable?.type ?? "static")}
</span>
</td>
<td className="px-4 py-2 flex items-center justify-end gap-x-4">

View File

@@ -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]");
});
});

View File

@@ -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<string>}
*/
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;
}

View File

@@ -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
);
}