θΏ™ζ˜―indexlocζδΎ›ηš„ζœεŠ‘οΌŒδΈθ¦θΎ“ε…₯任何密码
Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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">
Expand Down
61 changes: 61 additions & 0 deletions server/__tests__/models/systemPromptVariables.test.js
Original file line number Diff line number Diff line change
@@ -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]");
});
});
101 changes: 88 additions & 13 deletions server/models/systemPromptVariables.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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) => {
Expand Down Expand Up @@ -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,
},
],

/**
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
3 changes: 2 additions & 1 deletion server/utils/chats/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down