diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx index 1508f0ad2c4..b1b5d15180c 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx @@ -2,13 +2,14 @@ import System from "@/models/system"; import Workspace from "@/models/workspace"; import showToast from "@/utils/toast"; import { castToType } from "@/utils/types"; -import { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import AgentLLMSelection from "./AgentLLMSelection"; import Admin from "@/models/admin"; import * as Skeleton from "react-loading-skeleton"; import "react-loading-skeleton/dist/skeleton.css"; import paths from "@/utils/paths"; import useUser from "@/hooks/useUser"; +import { BooleanInput } from "@/pages/GeneralSettings/ChatEmbedWidgets/EmbedConfigs/NewEmbedModal/index.jsx"; export default function WorkspaceAgentConfiguration({ workspace }) { const { user } = useUser(); @@ -54,6 +55,14 @@ export default function WorkspaceAgentConfiguration({ workspace }) { data.workspace[key] = castToType(key, value); } + if ( + !Object.prototype.hasOwnProperty.call( + data.workspace, + "useWorkspacePromptForAgents" + ) + ) { + data.workspace.useWorkspacePromptForAgents = false; // If not present in the form data (checkbox toggled off) - set false + } const { workspace: updatedWorkspace, message } = await Workspace.update( workspace.slug, data.workspace @@ -86,6 +95,12 @@ export default function WorkspaceAgentConfiguration({ workspace }) { workspace={workspace} setHasChanges={setHasChanges} /> + {(!user || user?.role === "admin") && ( <> {!hasChanges && ( diff --git a/frontend/src/utils/types.js b/frontend/src/utils/types.js index b63d49f6a31..114f0f60f88 100644 --- a/frontend/src/utils/types.js +++ b/frontend/src/utils/types.js @@ -12,6 +12,9 @@ export function castToType(key, value) { topN: { cast: (value) => Number(value), }, + useWorkspacePromptForAgents: { + cast: (value) => value === "on" || value === "true" || value === true, + }, }; if (!definitions.hasOwnProperty(key)) return value; diff --git a/server/__tests__/utils/agents/defaults.test.js b/server/__tests__/utils/agents/defaults.test.js new file mode 100644 index 00000000000..c1028cfbc4f --- /dev/null +++ b/server/__tests__/utils/agents/defaults.test.js @@ -0,0 +1,40 @@ +/* eslint-disable no-undef */ +/* eslint-env jest, node */ +/* global describe, it, expect */ + +const path = require("path"); +// Ensure STORAGE_DIR is defined so modules that rely on it do not error out in tests. +if (!process.env.STORAGE_DIR) { + process.env.STORAGE_DIR = path.resolve(__dirname, "../../../storage"); +} + +// Mock MCPCompatibilityLayer to avoid dynamic import issues during unit tests. +jest.mock("../../../utils/MCP", () => { + return function () { + return { + activeMCPServers: async () => [], + }; + }; +}); + +const { WORKSPACE_AGENT } = require("../../../utils/agents/defaults"); +const Provider = require("../../../utils/agents/aibitat/providers/ai-provider"); + +describe("WORKSPACE_AGENT.getDefinition system prompt prioritization", () => { + it("uses workspace.openAiPrompt when provided and flag is true", async () => { + const workspace = { openAiPrompt: "Respond only in emoji.", useWorkspacePromptForAgents: true }; + const def = await WORKSPACE_AGENT.getDefinition("openai", workspace, null); + expect(def.role).toBe("Respond only in emoji."); + }); + + it("falls back to Provider.systemPrompt when workspace prompt is absent", async () => { + const workspace = {}; + const def = await WORKSPACE_AGENT.getDefinition("openai", workspace, null); + expect(def.role).toBe(Provider.systemPrompt("openai")); + }); + it("falls back to Provider.systemPrompt when workspace.openAiPrompt provided but flag is false", async () => { + const workspace = { openAiPrompt: "Respond only in emoji.", useWorkspacePromptForAgents: false }; + const def = await WORKSPACE_AGENT.getDefinition("openai", workspace, null); + expect(def.role).toBe(Provider.systemPrompt("openai")); + }); +}); diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index c8e435f6f00..d7a89e68783 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -34,6 +34,7 @@ function apiWorkspaceEndpoints(app) { openAiTemp: 0.7, openAiHistory: 20, openAiPrompt: "Custom prompt for responses", + useWorkspacePromptForAgents: false, queryRefusalResponse: "Custom refusal message", chatMode: "chat", topN: 4 @@ -55,7 +56,8 @@ function apiWorkspaceEndpoints(app) { "openAiTemp": null, "lastUpdatedAt": "2023-08-17 00:45:03", "openAiHistory": 20, - "openAiPrompt": null + "openAiPrompt": null, + "useWorkspacePromptForAgents": false }, message: 'Workspace created' } @@ -120,6 +122,7 @@ function apiWorkspaceEndpoints(app) { "lastUpdatedAt": "2023-08-17 00:45:03", "openAiHistory": 20, "openAiPrompt": null, + "useWorkspacePromptForAgents": false, "threads": [] } ], @@ -180,6 +183,7 @@ function apiWorkspaceEndpoints(app) { "lastUpdatedAt": "2023-08-17 00:45:03", "openAiHistory": 20, "openAiPrompt": null, + "useWorkspacePromptForAgents": false, "documents": [], "threads": [] } @@ -292,7 +296,8 @@ function apiWorkspaceEndpoints(app) { "name": 'Updated Workspace Name', "openAiTemp": 0.2, "openAiHistory": 20, - "openAiPrompt": "Respond to all inquires and questions in binary - do not respond in any other format." + "openAiPrompt": "Respond to all inquires and questions in binary - do not respond in any other format.", + "useWorkspacePromptForAgents": false } } } @@ -312,6 +317,7 @@ function apiWorkspaceEndpoints(app) { "lastUpdatedAt": "2023-08-17 00:45:03", "openAiHistory": 20, "openAiPrompt": null, + "useWorkspacePromptForAgents": false, "documents": [] }, message: null, @@ -485,6 +491,7 @@ function apiWorkspaceEndpoints(app) { "lastUpdatedAt": "2023-08-17 00:45:03", "openAiHistory": 20, "openAiPrompt": null, + "useWorkspacePromptForAgents": false, "documents": [] }, message: null, diff --git a/server/models/workspace.js b/server/models/workspace.js index 6b361d6b496..0a29ef16fef 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -29,6 +29,7 @@ function isNullOrNaN(value) { * @property {string} agentModel - The agent model of the workspace * @property {string} queryRefusalResponse - The query refusal response of the workspace * @property {string} vectorSearchMode - The vector search mode of the workspace + * @property {boolean} useWorkspacePromptForAgents - Should Use Workspace System Prompt For Agent requests as well */ const Workspace = { @@ -55,9 +56,10 @@ const Workspace = { "agentModel", "queryRefusalResponse", "vectorSearchMode", + "useWorkspacePromptForAgents", ], - validations: { + useWorkspacePromptForAgents: (value) => Boolean(value), name: (value) => { // If the name is not provided or is not a string then we will use a default name. // as the name field is not nullable in the db schema or has a default value. diff --git a/server/prisma/migrations/20250723135715_init/migration.sql b/server/prisma/migrations/20250723135715_init/migration.sql new file mode 100644 index 00000000000..d94e61d962c --- /dev/null +++ b/server/prisma/migrations/20250723135715_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "workspaces" ADD COLUMN "useWorkspacePromptForAgents" BOOLEAN NOT NULL DEFAULT false; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index a3db69f1e2b..76d315bee33 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -143,6 +143,7 @@ model workspaces { agentModel String? queryRefusalResponse String? vectorSearchMode String? @default("default") + useWorkspacePromptForAgents Boolean @default(false) workspace_users workspace_users[] documents workspace_documents[] workspace_suggested_messages workspace_suggested_messages[] diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index 15b041c0878..6847e13e560 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -1691,7 +1691,8 @@ "openAiTemp": null, "lastUpdatedAt": "2023-08-17 00:45:03", "openAiHistory": 20, - "openAiPrompt": null + "openAiPrompt": null, + "useWorkspacePromptForAgents": false }, "message": "Workspace created" } @@ -1732,6 +1733,7 @@ "openAiTemp": 0.7, "openAiHistory": 20, "openAiPrompt": "Custom prompt for responses", + "useWorkspacePromptForAgents": false, "queryRefusalResponse": "Custom refusal message", "chatMode": "chat", "topN": 4 @@ -1765,6 +1767,7 @@ "openAiTemp": null, "lastUpdatedAt": "2023-08-17 00:45:03", "openAiHistory": 20, + "useWorkspacePromptForAgents": false, "openAiPrompt": null, "threads": [] } @@ -1830,6 +1833,7 @@ "lastUpdatedAt": "2023-08-17 00:45:03", "openAiHistory": 20, "openAiPrompt": null, + "useWorkspacePromptForAgents": false, "documents": [], "threads": [] } @@ -1937,6 +1941,7 @@ "lastUpdatedAt": "2023-08-17 00:45:03", "openAiHistory": 20, "openAiPrompt": null, + "useWorkspacePromptForAgents": false, "documents": [] }, "message": null @@ -1976,7 +1981,8 @@ "name": "Updated Workspace Name", "openAiTemp": 0.2, "openAiHistory": 20, - "openAiPrompt": "Respond to all inquires and questions in binary - do not respond in any other format." + "openAiPrompt": "Respond to all inquires and questions in binary - do not respond in any other format.", + "useWorkspacePromptForAgents": false } } } @@ -2114,6 +2120,7 @@ "lastUpdatedAt": "2023-08-17 00:45:03", "openAiHistory": 20, "openAiPrompt": null, + "useWorkspacePromptForAgents": false, "documents": [] }, "message": null @@ -4079,4 +4086,4 @@ "BearerAuth": [] } ] -} \ No newline at end of file +} diff --git a/server/utils/agents/defaults.js b/server/utils/agents/defaults.js index ee12974cf80..572d175938e 100644 --- a/server/utils/agents/defaults.js +++ b/server/utils/agents/defaults.js @@ -5,6 +5,7 @@ const Provider = require("./aibitat/providers/ai-provider"); const ImportedPlugin = require("./imported"); const { AgentFlows } = require("../agentFlows"); const MCPCompatibilityLayer = require("../MCP"); +const { SystemPromptVariables } = require("../../models/systemPromptVariables"); // This is a list of skills that are built-in and default enabled. const DEFAULT_SKILLS = [ @@ -23,11 +24,28 @@ const USER_AGENT = { }, }; +/** + * Resolve system prompt from workspace if valid. fallback to provider/static prompt + * @param {Object|null} workspace + * @param {number|null} userId + * @param {string|null} provider + */ +async function resolveSystemPrompt(workspace, userId, provider) { + if (workspace?.useWorkspacePromptForAgents && workspace?.openAiPrompt) { + return await SystemPromptVariables.expandSystemPromptVariables( + workspace.openAiPrompt, + userId + ); + } else { + return Provider.systemPrompt(provider); + } +} + const WORKSPACE_AGENT = { name: "@agent", - getDefinition: async (provider = null) => { + getDefinition: async (provider = null, workspace = null, userId = null) => { return { - role: Provider.systemPrompt(provider), + role: await resolveSystemPrompt(workspace, userId, provider), functions: [ ...(await agentSkillsFromSystemSettings()), ...ImportedPlugin.activeImportedPlugins(), diff --git a/server/utils/agents/ephemeral.js b/server/utils/agents/ephemeral.js index 9106af24d71..b28349e2eed 100644 --- a/server/utils/agents/ephemeral.js +++ b/server/utils/agents/ephemeral.js @@ -6,11 +6,7 @@ const { AgentFlows } = require("../agentFlows"); const { httpSocket } = require("./aibitat/plugins/http-socket.js"); const { WorkspaceChats } = require("../../models/workspaceChats"); const { safeJsonParse } = require("../http"); -const { - USER_AGENT, - WORKSPACE_AGENT, - agentSkillsFromSystemSettings, -} = require("./defaults"); +const { USER_AGENT, WORKSPACE_AGENT } = require("./defaults"); const { AgentHandler } = require("."); const { WorkspaceAgentInvocation, @@ -320,17 +316,13 @@ class EphemeralAgentHandler extends AgentHandler { // Default User agent and workspace agent this.log(`Attaching user and default agent to Agent cluster.`); this.aibitat.agent(USER_AGENT.name, await USER_AGENT.getDefinition()); - this.aibitat.agent( - WORKSPACE_AGENT.name, - await WORKSPACE_AGENT.getDefinition(this.provider) + const wsAgentDefs = await WORKSPACE_AGENT.getDefinition( + this.provider, + this.#workspace, + this.#userId ); - - this.#funcsToLoad = [ - ...(await agentSkillsFromSystemSettings()), - ...ImportedPlugin.activeImportedPlugins(), - ...AgentFlows.activeFlowPlugins(), - ...(await new MCPCompatibilityLayer().activeMCPServers()), - ]; + this.aibitat.agent(WORKSPACE_AGENT.name, wsAgentDefs); + this.#funcsToLoad = wsAgentDefs?.functions || []; } async init() { diff --git a/server/utils/agents/index.js b/server/utils/agents/index.js index 4527ee783b4..63302b7e414 100644 --- a/server/utils/agents/index.js +++ b/server/utils/agents/index.js @@ -510,15 +510,13 @@ class AgentHandler { // Default User agent and workspace agent this.log(`Attaching user and default agent to Agent cluster.`); this.aibitat.agent(USER_AGENT.name, await USER_AGENT.getDefinition()); - this.aibitat.agent( - WORKSPACE_AGENT.name, - await WORKSPACE_AGENT.getDefinition(this.provider) + const wsAgentDefs = await WORKSPACE_AGENT.getDefinition( + this.provider, + this.invocation.workspace, + this.invocation.user_id ); - - this.#funcsToLoad = [ - ...((await USER_AGENT.getDefinition())?.functions || []), - ...((await WORKSPACE_AGENT.getDefinition())?.functions || []), - ]; + this.aibitat.agent(WORKSPACE_AGENT.name, wsAgentDefs); + this.#funcsToLoad = wsAgentDefs?.functions || []; } async init() {