Skip to content

Commit f5f8fb1

Browse files
Agent workspace system prompt with variable expansion (#4526)
* agent workspace system prompt with variable expansion * cleanup --------- Co-authored-by: timothycarambat <[email protected]>
1 parent 985527c commit f5f8fb1

File tree

5 files changed

+191
-16
lines changed

5 files changed

+191
-16
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Set required env vars before requiring modules
2+
process.env.STORAGE_DIR = __dirname;
3+
process.env.NODE_ENV = "test";
4+
5+
const { SystemPromptVariables } = require("../../../models/systemPromptVariables");
6+
const Provider = require("../../../utils/agents/aibitat/providers/ai-provider");
7+
8+
jest.mock("../../../models/systemPromptVariables");
9+
jest.mock("../../../models/systemSettings");
10+
jest.mock("../../../utils/agents/imported", () => ({
11+
activeImportedPlugins: jest.fn().mockReturnValue([]),
12+
}));
13+
jest.mock("../../../utils/agentFlows", () => ({
14+
AgentFlows: {
15+
activeFlowPlugins: jest.fn().mockReturnValue([]),
16+
},
17+
}));
18+
jest.mock("../../../utils/MCP", () => {
19+
return jest.fn().mockImplementation(() => ({
20+
activeMCPServers: jest.fn().mockResolvedValue([]),
21+
}));
22+
});
23+
24+
const { WORKSPACE_AGENT } = require("../../../utils/agents/defaults");
25+
26+
describe("WORKSPACE_AGENT.getDefinition", () => {
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
// Mock SystemSettings to return empty arrays for agent skills
30+
const { SystemSettings } = require("../../../models/systemSettings");
31+
SystemSettings.getValueOrFallback = jest.fn().mockResolvedValue("[]");
32+
});
33+
34+
it("should use provider default system prompt when workspace has no openAiPrompt", async () => {
35+
const workspace = {
36+
id: 1,
37+
name: "Test Workspace",
38+
openAiPrompt: null,
39+
};
40+
const user = { id: 1 };
41+
const provider = "openai";
42+
const expectedPrompt = await Provider.systemPrompt({ provider, workspace, user });
43+
const definition = await WORKSPACE_AGENT.getDefinition(
44+
provider,
45+
workspace,
46+
user
47+
);
48+
expect(definition.role).toBe(expectedPrompt);
49+
expect(SystemPromptVariables.expandSystemPromptVariables).not.toHaveBeenCalled();
50+
});
51+
52+
it("should use workspace system prompt with variable expansion when openAiPrompt exists", async () => {
53+
const workspace = {
54+
id: 1,
55+
name: "Test Workspace",
56+
openAiPrompt: "You are a helpful assistant for {workspace.name}. The current user is {user.name}.",
57+
};
58+
const user = { id: 1 };
59+
const provider = "openai";
60+
61+
const expandedPrompt = "You are a helpful assistant for Test Workspace. The current user is John Doe.";
62+
SystemPromptVariables.expandSystemPromptVariables.mockResolvedValue(expandedPrompt);
63+
64+
const definition = await WORKSPACE_AGENT.getDefinition(
65+
provider,
66+
workspace,
67+
user
68+
);
69+
70+
expect(SystemPromptVariables.expandSystemPromptVariables).toHaveBeenCalledWith(
71+
workspace.openAiPrompt,
72+
user.id,
73+
workspace.id
74+
);
75+
expect(definition.role).toBe(expandedPrompt);
76+
});
77+
78+
it("should handle workspace system prompt without user context", async () => {
79+
const workspace = {
80+
id: 1,
81+
name: "Test Workspace",
82+
openAiPrompt: "You are a helpful assistant. Today is {date}.",
83+
};
84+
const user = null;
85+
const provider = "lmstudio";
86+
const expandedPrompt = "You are a helpful assistant. Today is January 1, 2024.";
87+
SystemPromptVariables.expandSystemPromptVariables.mockResolvedValue(expandedPrompt);
88+
89+
const definition = await WORKSPACE_AGENT.getDefinition(
90+
provider,
91+
workspace,
92+
user
93+
);
94+
95+
expect(SystemPromptVariables.expandSystemPromptVariables).toHaveBeenCalledWith(
96+
workspace.openAiPrompt,
97+
null,
98+
workspace.id
99+
);
100+
expect(definition.role).toBe(expandedPrompt);
101+
});
102+
103+
it("should return functions array in definition", async () => {
104+
const workspace = { id: 1, openAiPrompt: null };
105+
const provider = "openai";
106+
107+
const definition = await WORKSPACE_AGENT.getDefinition(
108+
provider,
109+
workspace,
110+
null
111+
);
112+
113+
expect(definition).toHaveProperty("functions");
114+
expect(Array.isArray(definition.functions)).toBe(true);
115+
});
116+
117+
it("should use LMStudio specific prompt when workspace has no openAiPrompt", async () => {
118+
const workspace = { id: 1, openAiPrompt: null };
119+
const user = null;
120+
const provider = "lmstudio";
121+
const definition = await WORKSPACE_AGENT.getDefinition(
122+
provider,
123+
workspace,
124+
null
125+
);
126+
127+
expect(definition.role).toBe(await Provider.systemPrompt({ provider, workspace, user }));
128+
expect(definition.role).toContain("helpful ai assistant");
129+
});
130+
});
131+

server/utils/agents/aibitat/providers/ai-provider.js

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const { toValidNumber, safeJsonParse } = require("../../../http");
1919
const { getLLMProviderClass } = require("../../../helpers");
2020
const { parseLMStudioBasePath } = require("../../../AiProviders/lmStudio");
2121
const { parseFoundryBasePath } = require("../../../AiProviders/foundry");
22+
const {
23+
SystemPromptVariables,
24+
} = require("../../../../models/systemPromptVariables");
2225

2326
const DEFAULT_WORKSPACE_PROMPT =
2427
"You are a helpful ai assistant who can assist the user and use tools available to help answer the users prompts and questions.";
@@ -288,10 +291,7 @@ class Provider {
288291
return llm.promptWindowLimit(modelName);
289292
}
290293

291-
// For some providers we may want to override the system prompt to be more verbose.
292-
// Currently we only do this for lmstudio, but we probably will want to expand this even more
293-
// to any Untooled LLM.
294-
static systemPrompt(provider = null) {
294+
static defaultSystemPromptForProvider(provider = null) {
295295
switch (provider) {
296296
case "lmstudio":
297297
return "You are a helpful ai assistant who can assist the user and use tools available to help answer the users prompts and questions. Tools will be handled by another assistant and you will simply receive their responses to help answer the user prompt - always try to answer the user's prompt the best you can with the context available to you and your general knowledge.";
@@ -300,6 +300,27 @@ class Provider {
300300
}
301301
}
302302

303+
/**
304+
* Get the system prompt for a provider.
305+
* @param {string} provider
306+
* @param {import("@prisma/client").workspaces | null} workspace
307+
* @param {import("@prisma/client").users | null} user
308+
* @returns {Promise<string>}
309+
*/
310+
static async systemPrompt({
311+
provider = null,
312+
workspace = null,
313+
user = null,
314+
}) {
315+
if (!workspace?.openAiPrompt)
316+
return Provider.defaultSystemPromptForProvider(provider);
317+
return await SystemPromptVariables.expandSystemPromptVariables(
318+
workspace.openAiPrompt,
319+
user?.id || null,
320+
workspace.id
321+
);
322+
}
323+
303324
/**
304325
* Whether the provider supports agent streaming.
305326
* Disabled by default and needs to be explicitly enabled in the provider

server/utils/agents/defaults.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const Provider = require("./aibitat/providers/ai-provider");
55
const ImportedPlugin = require("./imported");
66
const { AgentFlows } = require("../agentFlows");
77
const MCPCompatibilityLayer = require("../MCP");
8+
const { SystemPromptVariables } = require("../../models/systemPromptVariables");
89

910
// This is a list of skills that are built-in and default enabled.
1011
const DEFAULT_SKILLS = [
@@ -15,7 +16,7 @@ const DEFAULT_SKILLS = [
1516

1617
const USER_AGENT = {
1718
name: "USER",
18-
getDefinition: async () => {
19+
getDefinition: () => {
1920
return {
2021
interrupt: "ALWAYS",
2122
role: "I am the human monitor and oversee this chat. Any questions on action or decision making should be directed to me.",
@@ -25,9 +26,16 @@ const USER_AGENT = {
2526

2627
const WORKSPACE_AGENT = {
2728
name: "@agent",
28-
getDefinition: async (provider = null) => {
29+
/**
30+
* Get the definition for the workspace agent with its role (prompt) and functions in Aibitat format
31+
* @param {string} provider
32+
* @param {import("@prisma/client").workspaces | null} workspace
33+
* @param {import("@prisma/client").users | null} user
34+
* @returns {Promise<{ role: string, functions: object[] }>}
35+
*/
36+
getDefinition: async (provider = null, workspace = null, user = null) => {
2937
return {
30-
role: Provider.systemPrompt(provider),
38+
role: await Provider.systemPrompt({ provider, workspace, user }),
3139
functions: [
3240
...(await agentSkillsFromSystemSettings()),
3341
...ImportedPlugin.activeImportedPlugins(),

server/utils/agents/ephemeral.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const ImportedPlugin = require("./imported");
44
const MCPCompatibilityLayer = require("../MCP");
55
const { AgentFlows } = require("../agentFlows");
66
const { httpSocket } = require("./aibitat/plugins/http-socket.js");
7+
const { User } = require("../../models/user");
78
const { WorkspaceChats } = require("../../models/workspaceChats");
89
const { safeJsonParse } = require("../http");
910
const {
@@ -26,7 +27,7 @@ class EphemeralAgentHandler extends AgentHandler {
2627
#invocationUUID = null;
2728
/** @type {import("@prisma/client").workspaces|null} the workspace to use for the agent */
2829
#workspace = null;
29-
/** @type {import("@prisma/client").users|null} the user id to use for the agent */
30+
/** @type {import("@prisma/client").users["id"]|null} the user id to use for the agent */
3031
#userId = null;
3132
/** @type {import("@prisma/client").workspace_threads|null} the workspace thread id to use for the agent */
3233
#threadId = null;
@@ -69,6 +70,9 @@ class EphemeralAgentHandler extends AgentHandler {
6970
this.#workspace = workspace;
7071
this.#prompt = prompt;
7172

73+
// Note: userId for ephemeral agent is only available
74+
// via the workspace-thread chat endpoints for the API
75+
// since workspaces can belong to multiple users.
7276
this.#userId = userId;
7377
this.#threadId = threadId;
7478
this.#sessionId = sessionId;
@@ -319,10 +323,14 @@ class EphemeralAgentHandler extends AgentHandler {
319323
async #loadAgents() {
320324
// Default User agent and workspace agent
321325
this.log(`Attaching user and default agent to Agent cluster.`);
322-
this.aibitat.agent(USER_AGENT.name, await USER_AGENT.getDefinition());
326+
this.aibitat.agent(USER_AGENT.name, USER_AGENT.getDefinition());
327+
const user = this.#userId
328+
? await User.get({ id: Number(this.#userId) })
329+
: null;
330+
323331
this.aibitat.agent(
324332
WORKSPACE_AGENT.name,
325-
await WORKSPACE_AGENT.getDefinition(this.provider)
333+
await WORKSPACE_AGENT.getDefinition(this.provider, this.#workspace, user)
326334
);
327335

328336
this.#funcsToLoad = [

server/utils/agents/index.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const AgentPlugins = require("./aibitat/plugins");
33
const {
44
WorkspaceAgentInvocation,
55
} = require("../../models/workspaceAgentInvocation");
6+
const { User } = require("../../models/user");
67
const { WorkspaceChats } = require("../../models/workspaceChats");
78
const { safeJsonParse } = require("../http");
89
const { USER_AGENT, WORKSPACE_AGENT } = require("./defaults");
@@ -523,15 +524,21 @@ class AgentHandler {
523524
async #loadAgents() {
524525
// Default User agent and workspace agent
525526
this.log(`Attaching user and default agent to Agent cluster.`);
526-
this.aibitat.agent(USER_AGENT.name, await USER_AGENT.getDefinition());
527-
this.aibitat.agent(
528-
WORKSPACE_AGENT.name,
529-
await WORKSPACE_AGENT.getDefinition(this.provider)
527+
const user = this.invocation.user_id
528+
? await User.get({ id: Number(this.invocation.user_id) })
529+
: null;
530+
const userAgentDef = await USER_AGENT.getDefinition();
531+
const workspaceAgentDef = await WORKSPACE_AGENT.getDefinition(
532+
this.provider,
533+
this.invocation.workspace,
534+
user
530535
);
531536

537+
this.aibitat.agent(USER_AGENT.name, userAgentDef);
538+
this.aibitat.agent(WORKSPACE_AGENT.name, workspaceAgentDef);
532539
this.#funcsToLoad = [
533-
...((await USER_AGENT.getDefinition())?.functions || []),
534-
...((await WORKSPACE_AGENT.getDefinition())?.functions || []),
540+
...(userAgentDef?.functions || []),
541+
...(workspaceAgentDef?.functions || []),
535542
];
536543
}
537544

0 commit comments

Comments
 (0)