From 346ad9667448b4d953f311ba5ab61801457cb2bd Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Mon, 16 Dec 2024 12:06:49 -0800 Subject: [PATCH 01/14] add writible fields to dev api new workspace endpoint --- server/endpoints/api/workspace/index.js | 20 +++++++++++++++++--- server/models/workspace.js | 22 ++++++++++++++++++---- server/swagger/openapi.json | 15 +++++++++++++-- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index cf9c5618c8a..c0935b81acb 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -23,12 +23,20 @@ function apiWorkspaceEndpoints(app) { #swagger.tags = ['Workspaces'] #swagger.description = 'Create a new workspace' #swagger.requestBody = { - description: 'JSON object containing new display name of workspace.', + description: 'JSON object containing workspace configuration.', required: true, content: { "application/json": { example: { name: "My New Workspace", + slug: "custom-slug", + similarityThreshold: 0.7, + openAiTemp: 0.7, + openAiHistory: 20, + openAiPrompt: "Custom prompt for responses", + queryRefusalResponse: "Custom refusal message", + chatMode: "chat", + topN: 4 } } } @@ -62,8 +70,14 @@ function apiWorkspaceEndpoints(app) { } */ try { - const { name = null } = reqBody(request); - const { workspace, message } = await Workspace.new(name); + const { name = null, ...additionalFields } = reqBody(request); + const { workspace, message } = await Workspace.new(name, null, additionalFields); + + if (!workspace) { + response.status(400).json({ workspace: null, message }); + return; + } + await Telemetry.sendTelemetry("workspace_created", { multiUserMode: multiUserMode(response), LLMSelection: process.env.LLM_PROVIDER || "openai", diff --git a/server/models/workspace.js b/server/models/workspace.js index 5bc93019487..2b9485df979 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -53,9 +53,9 @@ const Workspace = { return slugifyModule(...args); }, - new: async function (name = null, creatorId = null) { - if (!name) return { result: null, message: "name cannot be null" }; - var slug = this.slugify(name, { lower: true }); + new: async function (name = null, creatorId = null, additionalFields = {}) { + if (!name) return { workspace: null, message: "name cannot be null" }; + var slug = additionalFields.slug || this.slugify(name, { lower: true }); slug = slug || uuidv4(); const existingBySlug = await this.get({ slug }); @@ -64,9 +64,23 @@ const Workspace = { slug = this.slugify(`${name}-${slugSeed}`, { lower: true }); } + // Filter only valid fields + const validFields = Object.keys(additionalFields).filter((key) => + this.writable.includes(key) + ); + + const validAdditionalFields = {}; + validFields.forEach((key) => { + validAdditionalFields[key] = additionalFields[key]; + }); + try { const workspace = await prisma.workspaces.create({ - data: { name, slug }, + data: { + name, + slug, + ...validAdditionalFields, + }, }); // If created with a user then we need to create the relationship as well. diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index 505d579628b..a56e574052e 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -1451,6 +1451,9 @@ } } }, + "400": { + "description": "Bad Request" + }, "403": { "description": "Forbidden", "content": { @@ -1471,12 +1474,20 @@ } }, "requestBody": { - "description": "JSON object containing new display name of workspace.", + "description": "JSON object containing workspace configuration.", "required": true, "content": { "application/json": { "example": { - "name": "My New Workspace" + "name": "My New Workspace", + "slug": "custom-slug", + "similarityThreshold": 0.7, + "openAiTemp": 0.7, + "openAiHistory": 20, + "openAiPrompt": "Custom prompt for responses", + "queryRefusalResponse": "Custom refusal message", + "chatMode": "chat", + "topN": 4 } } } From c0462c3b8e7c387975e60c3032f1be73207b14bf Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Mon, 16 Dec 2024 12:10:15 -0800 Subject: [PATCH 02/14] lint --- server/endpoints/api/admin/index.js | 48 +++++++++++-------------- server/endpoints/api/workspace/index.js | 6 +++- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/server/endpoints/api/admin/index.js b/server/endpoints/api/admin/index.js index 18f59ee872c..93fbff7669e 100644 --- a/server/endpoints/api/admin/index.js +++ b/server/endpoints/api/admin/index.js @@ -610,24 +610,20 @@ function apiAdminEndpoints(app) { const workspaceUsers = await Workspace.workspaceUsers(workspace.id); if (!workspace) { - response - .status(404) - .json({ - success: false, - error: `Workspace ${workspaceSlug} not found`, - users: workspaceUsers, - }); + response.status(404).json({ + success: false, + error: `Workspace ${workspaceSlug} not found`, + users: workspaceUsers, + }); return; } if (userIds.length === 0) { - response - .status(404) - .json({ - success: false, - error: `No valid user IDs provided.`, - users: workspaceUsers, - }); + response.status(404).json({ + success: false, + error: `No valid user IDs provided.`, + users: workspaceUsers, + }); return; } @@ -637,13 +633,11 @@ function apiAdminEndpoints(app) { workspace.id, userIds ); - return response - .status(200) - .json({ - success, - error, - users: await Workspace.workspaceUsers(workspace.id), - }); + return response.status(200).json({ + success, + error, + users: await Workspace.workspaceUsers(workspace.id), + }); } // Add new users to the workspace if they are not already in the workspace @@ -653,13 +647,11 @@ function apiAdminEndpoints(app) { ); if (usersToAdd.length > 0) await WorkspaceUser.createManyUsers(usersToAdd, workspace.id); - response - .status(200) - .json({ - success: true, - error: null, - users: await Workspace.workspaceUsers(workspace.id), - }); + response.status(200).json({ + success: true, + error: null, + users: await Workspace.workspaceUsers(workspace.id), + }); } catch (e) { console.error(e); response.sendStatus(500).end(); diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index c0935b81acb..67aa1e5d50c 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -71,7 +71,11 @@ function apiWorkspaceEndpoints(app) { */ try { const { name = null, ...additionalFields } = reqBody(request); - const { workspace, message } = await Workspace.new(name, null, additionalFields); + const { workspace, message } = await Workspace.new( + name, + null, + additionalFields + ); if (!workspace) { response.status(400).json({ workspace: null, message }); From e719740bddda23ff193456517f5688a4e03af514 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Mon, 16 Dec 2024 13:25:05 -0800 Subject: [PATCH 03/14] implement validations for workspace model --- server/models/workspace.js | 107 +++++++++++++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 11 deletions(-) diff --git a/server/models/workspace.js b/server/models/workspace.js index 2b9485df979..13e322b62aa 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -6,11 +6,36 @@ const { ROLES } = require("../utils/middleware/multiUserProtected"); const { v4: uuidv4 } = require("uuid"); const { User } = require("./user"); +function isNullOrNaN(value) { + if (value === null) return true; + return isNaN(value); +} + const Workspace = { defaultPrompt: "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.", + + // Internal fields that should not be configurable via API + systemManaged: ["slug", "vectorTag", "pfpFilename", "lastUpdatedAt"], + + // Fields that can be configured via API by users/developers + configurableFields: [ + "name", + "openAiTemp", + "openAiHistory", + "openAiPrompt", + "similarityThreshold", + "chatProvider", + "chatModel", + "topN", + "chatMode", + "agentProvider", + "agentModel", + "queryRefusalResponse", + ], + + // Used for generic updates so we can validate keys in request body writable: [ - // Used for generic updates so we can validate keys in request body "name", "slug", "vectorTag", @@ -28,6 +53,66 @@ const Workspace = { "agentModel", "queryRefusalResponse", ], + + validations: { + name: (value) => { + if (!value || typeof value !== "string") return null; + return String(value).slice(0, 255); + }, + openAiTemp: (value) => { + if (value === null || value === undefined) return null; + const temp = parseFloat(value); + if (isNullOrNaN(temp) || temp < 0 || temp > 1) return null; + return temp; + }, + openAiHistory: (value) => { + if (value === null || value === undefined) return 20; + const history = parseInt(value); + if (isNullOrNaN(history) || history < 0) return 20; + return history; + }, + similarityThreshold: (value) => { + if (value === null || value === undefined) return 0.25; + const threshold = parseFloat(value); + if (isNullOrNaN(threshold) || threshold < 0 || threshold > 1) return 0.25; + return threshold; + }, + topN: (value) => { + if (value === null || value === undefined) return 4; + const n = parseInt(value); + if (isNullOrNaN(n) || n < 1) return 4; + return n; + }, + chatMode: (value) => { + if (!value || !["chat", "query"].includes(value)) return "chat"; + return value; + }, + chatProvider: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + chatModel: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + agentProvider: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + agentModel: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + queryRefusalResponse: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + openAiPrompt: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + }, + /** * The default Slugify module requires some additional mapping to prevent downstream issues * with some vector db providers and instead of building a normalization method for every provider @@ -55,7 +140,7 @@ const Workspace = { new: async function (name = null, creatorId = null, additionalFields = {}) { if (!name) return { workspace: null, message: "name cannot be null" }; - var slug = additionalFields.slug || this.slugify(name, { lower: true }); + var slug = this.slugify(name, { lower: true }); slug = slug || uuidv4(); const existingBySlug = await this.get({ slug }); @@ -64,22 +149,22 @@ const Workspace = { slug = this.slugify(`${name}-${slugSeed}`, { lower: true }); } - // Filter only valid fields - const validFields = Object.keys(additionalFields).filter((key) => - this.writable.includes(key) - ); + // Validate all configurable fields + const validatedFields = { + name: this.validations.name(name), + }; - const validAdditionalFields = {}; - validFields.forEach((key) => { - validAdditionalFields[key] = additionalFields[key]; + Object.keys(additionalFields).forEach((key) => { + if (this.configurableFields.includes(key) && this.validations[key]) { + validatedFields[key] = this.validations[key](additionalFields[key]); + } }); try { const workspace = await prisma.workspaces.create({ data: { - name, + ...validatedFields, slug, - ...validAdditionalFields, }, }); From 0170e06a9e779032c45abbe631513ac08da087ba Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Mon, 16 Dec 2024 13:26:21 -0800 Subject: [PATCH 04/14] update swagger comments --- server/endpoints/api/workspace/index.js | 1 - server/swagger/openapi.json | 1 - 2 files changed, 2 deletions(-) diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index 67aa1e5d50c..d816fae51a8 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -29,7 +29,6 @@ function apiWorkspaceEndpoints(app) { "application/json": { example: { name: "My New Workspace", - slug: "custom-slug", similarityThreshold: 0.7, openAiTemp: 0.7, openAiHistory: 20, diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index a56e574052e..19d14766ce6 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -1480,7 +1480,6 @@ "application/json": { "example": { "name": "My New Workspace", - "slug": "custom-slug", "similarityThreshold": 0.7, "openAiTemp": 0.7, "openAiHistory": 20, From 3d2b036397af877d69001e3616302907d038a6d1 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Mon, 16 Dec 2024 14:58:57 -0800 Subject: [PATCH 05/14] simplify validations for workspace on frontend and API --- server/models/workspace.js | 103 ++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/server/models/workspace.js b/server/models/workspace.js index 13e322b62aa..5ab0f89488e 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -15,30 +15,12 @@ const Workspace = { defaultPrompt: "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.", - // Internal fields that should not be configurable via API - systemManaged: ["slug", "vectorTag", "pfpFilename", "lastUpdatedAt"], - - // Fields that can be configured via API by users/developers - configurableFields: [ - "name", - "openAiTemp", - "openAiHistory", - "openAiPrompt", - "similarityThreshold", - "chatProvider", - "chatModel", - "topN", - "chatMode", - "agentProvider", - "agentModel", - "queryRefusalResponse", - ], - // Used for generic updates so we can validate keys in request body + // commented fields are not writable, but are available on the db object writable: [ "name", - "slug", - "vectorTag", + // "slug", + // "vectorTag", "openAiTemp", "openAiHistory", "lastUpdatedAt", @@ -48,7 +30,7 @@ const Workspace = { "chatModel", "topN", "chatMode", - "pfpFilename", + // "pfpFilename", "agentProvider", "agentModel", "queryRefusalResponse", @@ -62,7 +44,7 @@ const Workspace = { openAiTemp: (value) => { if (value === null || value === undefined) return null; const temp = parseFloat(value); - if (isNullOrNaN(temp) || temp < 0 || temp > 1) return null; + if (isNullOrNaN(temp)) return null; return temp; }, openAiHistory: (value) => { @@ -138,6 +120,32 @@ const Workspace = { return slugifyModule(...args); }, + /** + * Validate the fields for a workspace update. + * @param {Object} updates - The updates to validate - should be writable fields + * @returns {Object} The validated updates. Only valid fields are returned. + */ + validateFields: function (updates = {}) { + const validatedFields = {}; + for (const [key, value] of Object.entries(updates)) { + if (!this.writable.includes(key)) continue; + if (this.validations[key]) { + validatedFields[key] = this.validations[key](value); + } else { + // If there is no validation for the field then we will just pass it through. + validatedFields[key] = value; + } + } + return validatedFields; + }, + + /** + * Create a new workspace. + * @param {string} name - The name of the workspace. + * @param {number} creatorId - The ID of the user creating the workspace. + * @param {Object} additionalFields - Additional fields to apply to the workspace - will be validated. + * @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the created workspace and an error message if applicable. + */ new: async function (name = null, creatorId = null, additionalFields = {}) { if (!name) return { workspace: null, message: "name cannot be null" }; var slug = this.slugify(name, { lower: true }); @@ -149,21 +157,11 @@ const Workspace = { slug = this.slugify(`${name}-${slugSeed}`, { lower: true }); } - // Validate all configurable fields - const validatedFields = { - name: this.validations.name(name), - }; - - Object.keys(additionalFields).forEach((key) => { - if (this.configurableFields.includes(key) && this.validations[key]) { - validatedFields[key] = this.validations[key](additionalFields[key]); - } - }); - try { const workspace = await prisma.workspaces.create({ data: { - ...validatedFields, + name: this.validations.name(name), + ...this.validateFields(additionalFields), slug, }, }); @@ -179,35 +177,36 @@ const Workspace = { } }, + /** + * Update the settings for a workspace. Applies validations to the updates provided. + * @param {number} id - The ID of the workspace to update. + * @param {Object} updates - The data to update. + * @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the updated workspace and an error message if applicable. + */ update: async function (id = null, updates = {}) { if (!id) throw new Error("No workspace id provided for update"); - const validFields = Object.keys(updates).filter((key) => - this.writable.includes(key) - ); - - Object.entries(updates).forEach(([key]) => { - if (validFields.includes(key)) return; - delete updates[key]; - }); - - if (Object.keys(updates).length === 0) + const validatedUpdates = this.validateFields(updates); + if (Object.keys(validatedUpdates).length === 0) return { workspace: { id }, message: "No valid fields to update!" }; // If the user unset the chatProvider we will need // to then clear the chatModel as well to prevent confusion during // LLM loading. - if (updates?.chatProvider === "default") { - updates.chatProvider = null; - updates.chatModel = null; + if (validatedUpdates?.chatProvider === "default") { + validatedUpdates.chatProvider = null; + validatedUpdates.chatModel = null; } - return this._update(id, updates); + return this._update(id, validatedUpdates); }, - // Explicit update of settings + key validations. - // Only use this method when directly setting a key value - // that takes no user input for the keys being modified. + /** + * Direct update of workspace settings without any validation. + * @param {number} id - The ID of the workspace to update. + * @param {Object} data - The data to update. + * @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the updated workspace and an error message if applicable. + */ _update: async function (id = null, data = {}) { if (!id) throw new Error("No workspace id provided for update"); From 6907348130b450675732d3987ece221ce812dde4 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Mon, 16 Dec 2024 15:11:26 -0800 Subject: [PATCH 06/14] cleanup validations --- server/models/workspace.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/server/models/workspace.js b/server/models/workspace.js index 5ab0f89488e..4471f67e7f5 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -38,31 +38,37 @@ const Workspace = { validations: { name: (value) => { - if (!value || typeof value !== "string") return null; + // 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. + if (!value || typeof value !== "string") return "My Workspace"; return String(value).slice(0, 255); }, openAiTemp: (value) => { if (value === null || value === undefined) return null; const temp = parseFloat(value); - if (isNullOrNaN(temp)) return null; + if (isNullOrNaN(temp) || temp < 0) return null; return temp; }, openAiHistory: (value) => { if (value === null || value === undefined) return 20; const history = parseInt(value); - if (isNullOrNaN(history) || history < 0) return 20; + if (isNullOrNaN(history)) return 20; + if (history < 0) return 0; return history; }, similarityThreshold: (value) => { if (value === null || value === undefined) return 0.25; const threshold = parseFloat(value); - if (isNullOrNaN(threshold) || threshold < 0 || threshold > 1) return 0.25; + if (isNullOrNaN(threshold)) return 0.25; + if (threshold < 0) return 0.0; + if (threshold > 1) return 1.0; return threshold; }, topN: (value) => { if (value === null || value === undefined) return 4; const n = parseInt(value); - if (isNullOrNaN(n) || n < 1) return 4; + if (isNullOrNaN(n)) return 4; + if (n < 1) return 1; return n; }, chatMode: (value) => { @@ -70,7 +76,7 @@ const Workspace = { return value; }, chatProvider: (value) => { - if (!value || typeof value !== "string") return null; + if (!value || typeof value !== "string" || value === "none") return null; return String(value); }, chatModel: (value) => { @@ -78,7 +84,7 @@ const Workspace = { return String(value); }, agentProvider: (value) => { - if (!value || typeof value !== "string") return null; + if (!value || typeof value !== "string" || value === "none") return null; return String(value); }, agentModel: (value) => { From e8d31e971f9679d0425d959165907dd73606efaf Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Mon, 16 Dec 2024 15:24:42 -0800 Subject: [PATCH 07/14] Enable default users to be able to add attachments, but not files to chats --- .../ChatContainer/DnDWrapper/index.jsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx index 32b58560fa9..32999e11c64 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx @@ -30,7 +30,6 @@ export function DnDFileUploaderProvider({ workspace, children }) { const { user } = useUser(); useEffect(() => { - if (!!user && user.role === "default") return; System.checkDocumentProcessorOnline().then((status) => setReady(status)); }, [user]); @@ -111,6 +110,8 @@ export function DnDFileUploaderProvider({ workspace, children }) { type: "attachment", }); } else { + // If the user is a default user, we do not want to allow them to upload files. + if (!!user && user.role === "default") continue; newAccepted.push({ uid: v4(), file, @@ -146,6 +147,8 @@ export function DnDFileUploaderProvider({ workspace, children }) { type: "attachment", }); } else { + // If the user is a default user, we do not want to allow them to upload files. + if (!!user && user.role === "default") continue; newAccepted.push({ uid: v4(), file, @@ -216,6 +219,8 @@ export default function DnDFileUploaderWrapper({ children }) { onDragEnter: () => setDragging(true), onDragLeave: () => setDragging(false), }); + const { user } = useUser(); + const canUploadAll = !user || user?.role !== "default"; return (
-

Add anything

+

+ Add {canUploadAll ? "anything" : "an image"} +

- Drop your file here to embed it into your
- workspace auto-magically. + {canUploadAll ? ( + <> + Drop your file here to embed it into your
+ workspace auto-magically. + + ) : ( + <> + Drop your image here to chat with it
+ auto-magically. + + )}

From 9f961992b52d8dd052913caf23f6b5cd729f4c13 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Mon, 16 Dec 2024 12:06:49 -0800 Subject: [PATCH 08/14] add writible fields to dev api new workspace endpoint --- server/endpoints/api/workspace/index.js | 20 +++++++++++++++++--- server/models/workspace.js | 22 ++++++++++++++++++---- server/swagger/openapi.json | 15 +++++++++++++-- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index cf9c5618c8a..c0935b81acb 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -23,12 +23,20 @@ function apiWorkspaceEndpoints(app) { #swagger.tags = ['Workspaces'] #swagger.description = 'Create a new workspace' #swagger.requestBody = { - description: 'JSON object containing new display name of workspace.', + description: 'JSON object containing workspace configuration.', required: true, content: { "application/json": { example: { name: "My New Workspace", + slug: "custom-slug", + similarityThreshold: 0.7, + openAiTemp: 0.7, + openAiHistory: 20, + openAiPrompt: "Custom prompt for responses", + queryRefusalResponse: "Custom refusal message", + chatMode: "chat", + topN: 4 } } } @@ -62,8 +70,14 @@ function apiWorkspaceEndpoints(app) { } */ try { - const { name = null } = reqBody(request); - const { workspace, message } = await Workspace.new(name); + const { name = null, ...additionalFields } = reqBody(request); + const { workspace, message } = await Workspace.new(name, null, additionalFields); + + if (!workspace) { + response.status(400).json({ workspace: null, message }); + return; + } + await Telemetry.sendTelemetry("workspace_created", { multiUserMode: multiUserMode(response), LLMSelection: process.env.LLM_PROVIDER || "openai", diff --git a/server/models/workspace.js b/server/models/workspace.js index 5bc93019487..2b9485df979 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -53,9 +53,9 @@ const Workspace = { return slugifyModule(...args); }, - new: async function (name = null, creatorId = null) { - if (!name) return { result: null, message: "name cannot be null" }; - var slug = this.slugify(name, { lower: true }); + new: async function (name = null, creatorId = null, additionalFields = {}) { + if (!name) return { workspace: null, message: "name cannot be null" }; + var slug = additionalFields.slug || this.slugify(name, { lower: true }); slug = slug || uuidv4(); const existingBySlug = await this.get({ slug }); @@ -64,9 +64,23 @@ const Workspace = { slug = this.slugify(`${name}-${slugSeed}`, { lower: true }); } + // Filter only valid fields + const validFields = Object.keys(additionalFields).filter((key) => + this.writable.includes(key) + ); + + const validAdditionalFields = {}; + validFields.forEach((key) => { + validAdditionalFields[key] = additionalFields[key]; + }); + try { const workspace = await prisma.workspaces.create({ - data: { name, slug }, + data: { + name, + slug, + ...validAdditionalFields, + }, }); // If created with a user then we need to create the relationship as well. diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index 505d579628b..a56e574052e 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -1451,6 +1451,9 @@ } } }, + "400": { + "description": "Bad Request" + }, "403": { "description": "Forbidden", "content": { @@ -1471,12 +1474,20 @@ } }, "requestBody": { - "description": "JSON object containing new display name of workspace.", + "description": "JSON object containing workspace configuration.", "required": true, "content": { "application/json": { "example": { - "name": "My New Workspace" + "name": "My New Workspace", + "slug": "custom-slug", + "similarityThreshold": 0.7, + "openAiTemp": 0.7, + "openAiHistory": 20, + "openAiPrompt": "Custom prompt for responses", + "queryRefusalResponse": "Custom refusal message", + "chatMode": "chat", + "topN": 4 } } } From af1f8229807cddaa227ae6ae8ae76a91a739cff4 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Mon, 16 Dec 2024 12:10:15 -0800 Subject: [PATCH 09/14] lint --- server/endpoints/api/workspace/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index c0935b81acb..67aa1e5d50c 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -71,7 +71,11 @@ function apiWorkspaceEndpoints(app) { */ try { const { name = null, ...additionalFields } = reqBody(request); - const { workspace, message } = await Workspace.new(name, null, additionalFields); + const { workspace, message } = await Workspace.new( + name, + null, + additionalFields + ); if (!workspace) { response.status(400).json({ workspace: null, message }); From e330087eae53b7128d8108e6c40e22e32261987d Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Mon, 16 Dec 2024 13:25:05 -0800 Subject: [PATCH 10/14] implement validations for workspace model --- server/models/workspace.js | 107 +++++++++++++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 11 deletions(-) diff --git a/server/models/workspace.js b/server/models/workspace.js index 2b9485df979..13e322b62aa 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -6,11 +6,36 @@ const { ROLES } = require("../utils/middleware/multiUserProtected"); const { v4: uuidv4 } = require("uuid"); const { User } = require("./user"); +function isNullOrNaN(value) { + if (value === null) return true; + return isNaN(value); +} + const Workspace = { defaultPrompt: "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.", + + // Internal fields that should not be configurable via API + systemManaged: ["slug", "vectorTag", "pfpFilename", "lastUpdatedAt"], + + // Fields that can be configured via API by users/developers + configurableFields: [ + "name", + "openAiTemp", + "openAiHistory", + "openAiPrompt", + "similarityThreshold", + "chatProvider", + "chatModel", + "topN", + "chatMode", + "agentProvider", + "agentModel", + "queryRefusalResponse", + ], + + // Used for generic updates so we can validate keys in request body writable: [ - // Used for generic updates so we can validate keys in request body "name", "slug", "vectorTag", @@ -28,6 +53,66 @@ const Workspace = { "agentModel", "queryRefusalResponse", ], + + validations: { + name: (value) => { + if (!value || typeof value !== "string") return null; + return String(value).slice(0, 255); + }, + openAiTemp: (value) => { + if (value === null || value === undefined) return null; + const temp = parseFloat(value); + if (isNullOrNaN(temp) || temp < 0 || temp > 1) return null; + return temp; + }, + openAiHistory: (value) => { + if (value === null || value === undefined) return 20; + const history = parseInt(value); + if (isNullOrNaN(history) || history < 0) return 20; + return history; + }, + similarityThreshold: (value) => { + if (value === null || value === undefined) return 0.25; + const threshold = parseFloat(value); + if (isNullOrNaN(threshold) || threshold < 0 || threshold > 1) return 0.25; + return threshold; + }, + topN: (value) => { + if (value === null || value === undefined) return 4; + const n = parseInt(value); + if (isNullOrNaN(n) || n < 1) return 4; + return n; + }, + chatMode: (value) => { + if (!value || !["chat", "query"].includes(value)) return "chat"; + return value; + }, + chatProvider: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + chatModel: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + agentProvider: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + agentModel: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + queryRefusalResponse: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + openAiPrompt: (value) => { + if (!value || typeof value !== "string") return null; + return String(value); + }, + }, + /** * The default Slugify module requires some additional mapping to prevent downstream issues * with some vector db providers and instead of building a normalization method for every provider @@ -55,7 +140,7 @@ const Workspace = { new: async function (name = null, creatorId = null, additionalFields = {}) { if (!name) return { workspace: null, message: "name cannot be null" }; - var slug = additionalFields.slug || this.slugify(name, { lower: true }); + var slug = this.slugify(name, { lower: true }); slug = slug || uuidv4(); const existingBySlug = await this.get({ slug }); @@ -64,22 +149,22 @@ const Workspace = { slug = this.slugify(`${name}-${slugSeed}`, { lower: true }); } - // Filter only valid fields - const validFields = Object.keys(additionalFields).filter((key) => - this.writable.includes(key) - ); + // Validate all configurable fields + const validatedFields = { + name: this.validations.name(name), + }; - const validAdditionalFields = {}; - validFields.forEach((key) => { - validAdditionalFields[key] = additionalFields[key]; + Object.keys(additionalFields).forEach((key) => { + if (this.configurableFields.includes(key) && this.validations[key]) { + validatedFields[key] = this.validations[key](additionalFields[key]); + } }); try { const workspace = await prisma.workspaces.create({ data: { - name, + ...validatedFields, slug, - ...validAdditionalFields, }, }); From 04d051a85012fb0a23794f145a10945b8fc85085 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Mon, 16 Dec 2024 13:26:21 -0800 Subject: [PATCH 11/14] update swagger comments --- server/endpoints/api/workspace/index.js | 1 - server/swagger/openapi.json | 1 - 2 files changed, 2 deletions(-) diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js index 67aa1e5d50c..d816fae51a8 100644 --- a/server/endpoints/api/workspace/index.js +++ b/server/endpoints/api/workspace/index.js @@ -29,7 +29,6 @@ function apiWorkspaceEndpoints(app) { "application/json": { example: { name: "My New Workspace", - slug: "custom-slug", similarityThreshold: 0.7, openAiTemp: 0.7, openAiHistory: 20, diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index a56e574052e..19d14766ce6 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -1480,7 +1480,6 @@ "application/json": { "example": { "name": "My New Workspace", - "slug": "custom-slug", "similarityThreshold": 0.7, "openAiTemp": 0.7, "openAiHistory": 20, From 1e3fe5f64f7a24274d889471254f13d93021151b Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Mon, 16 Dec 2024 14:58:57 -0800 Subject: [PATCH 12/14] simplify validations for workspace on frontend and API --- server/models/workspace.js | 103 ++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/server/models/workspace.js b/server/models/workspace.js index 13e322b62aa..5ab0f89488e 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -15,30 +15,12 @@ const Workspace = { defaultPrompt: "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.", - // Internal fields that should not be configurable via API - systemManaged: ["slug", "vectorTag", "pfpFilename", "lastUpdatedAt"], - - // Fields that can be configured via API by users/developers - configurableFields: [ - "name", - "openAiTemp", - "openAiHistory", - "openAiPrompt", - "similarityThreshold", - "chatProvider", - "chatModel", - "topN", - "chatMode", - "agentProvider", - "agentModel", - "queryRefusalResponse", - ], - // Used for generic updates so we can validate keys in request body + // commented fields are not writable, but are available on the db object writable: [ "name", - "slug", - "vectorTag", + // "slug", + // "vectorTag", "openAiTemp", "openAiHistory", "lastUpdatedAt", @@ -48,7 +30,7 @@ const Workspace = { "chatModel", "topN", "chatMode", - "pfpFilename", + // "pfpFilename", "agentProvider", "agentModel", "queryRefusalResponse", @@ -62,7 +44,7 @@ const Workspace = { openAiTemp: (value) => { if (value === null || value === undefined) return null; const temp = parseFloat(value); - if (isNullOrNaN(temp) || temp < 0 || temp > 1) return null; + if (isNullOrNaN(temp)) return null; return temp; }, openAiHistory: (value) => { @@ -138,6 +120,32 @@ const Workspace = { return slugifyModule(...args); }, + /** + * Validate the fields for a workspace update. + * @param {Object} updates - The updates to validate - should be writable fields + * @returns {Object} The validated updates. Only valid fields are returned. + */ + validateFields: function (updates = {}) { + const validatedFields = {}; + for (const [key, value] of Object.entries(updates)) { + if (!this.writable.includes(key)) continue; + if (this.validations[key]) { + validatedFields[key] = this.validations[key](value); + } else { + // If there is no validation for the field then we will just pass it through. + validatedFields[key] = value; + } + } + return validatedFields; + }, + + /** + * Create a new workspace. + * @param {string} name - The name of the workspace. + * @param {number} creatorId - The ID of the user creating the workspace. + * @param {Object} additionalFields - Additional fields to apply to the workspace - will be validated. + * @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the created workspace and an error message if applicable. + */ new: async function (name = null, creatorId = null, additionalFields = {}) { if (!name) return { workspace: null, message: "name cannot be null" }; var slug = this.slugify(name, { lower: true }); @@ -149,21 +157,11 @@ const Workspace = { slug = this.slugify(`${name}-${slugSeed}`, { lower: true }); } - // Validate all configurable fields - const validatedFields = { - name: this.validations.name(name), - }; - - Object.keys(additionalFields).forEach((key) => { - if (this.configurableFields.includes(key) && this.validations[key]) { - validatedFields[key] = this.validations[key](additionalFields[key]); - } - }); - try { const workspace = await prisma.workspaces.create({ data: { - ...validatedFields, + name: this.validations.name(name), + ...this.validateFields(additionalFields), slug, }, }); @@ -179,35 +177,36 @@ const Workspace = { } }, + /** + * Update the settings for a workspace. Applies validations to the updates provided. + * @param {number} id - The ID of the workspace to update. + * @param {Object} updates - The data to update. + * @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the updated workspace and an error message if applicable. + */ update: async function (id = null, updates = {}) { if (!id) throw new Error("No workspace id provided for update"); - const validFields = Object.keys(updates).filter((key) => - this.writable.includes(key) - ); - - Object.entries(updates).forEach(([key]) => { - if (validFields.includes(key)) return; - delete updates[key]; - }); - - if (Object.keys(updates).length === 0) + const validatedUpdates = this.validateFields(updates); + if (Object.keys(validatedUpdates).length === 0) return { workspace: { id }, message: "No valid fields to update!" }; // If the user unset the chatProvider we will need // to then clear the chatModel as well to prevent confusion during // LLM loading. - if (updates?.chatProvider === "default") { - updates.chatProvider = null; - updates.chatModel = null; + if (validatedUpdates?.chatProvider === "default") { + validatedUpdates.chatProvider = null; + validatedUpdates.chatModel = null; } - return this._update(id, updates); + return this._update(id, validatedUpdates); }, - // Explicit update of settings + key validations. - // Only use this method when directly setting a key value - // that takes no user input for the keys being modified. + /** + * Direct update of workspace settings without any validation. + * @param {number} id - The ID of the workspace to update. + * @param {Object} data - The data to update. + * @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the updated workspace and an error message if applicable. + */ _update: async function (id = null, data = {}) { if (!id) throw new Error("No workspace id provided for update"); From fac34a82ca902316e26ad3b4281c0bb88e0eff13 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Mon, 16 Dec 2024 15:11:26 -0800 Subject: [PATCH 13/14] cleanup validations --- server/models/workspace.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/server/models/workspace.js b/server/models/workspace.js index 5ab0f89488e..4471f67e7f5 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -38,31 +38,37 @@ const Workspace = { validations: { name: (value) => { - if (!value || typeof value !== "string") return null; + // 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. + if (!value || typeof value !== "string") return "My Workspace"; return String(value).slice(0, 255); }, openAiTemp: (value) => { if (value === null || value === undefined) return null; const temp = parseFloat(value); - if (isNullOrNaN(temp)) return null; + if (isNullOrNaN(temp) || temp < 0) return null; return temp; }, openAiHistory: (value) => { if (value === null || value === undefined) return 20; const history = parseInt(value); - if (isNullOrNaN(history) || history < 0) return 20; + if (isNullOrNaN(history)) return 20; + if (history < 0) return 0; return history; }, similarityThreshold: (value) => { if (value === null || value === undefined) return 0.25; const threshold = parseFloat(value); - if (isNullOrNaN(threshold) || threshold < 0 || threshold > 1) return 0.25; + if (isNullOrNaN(threshold)) return 0.25; + if (threshold < 0) return 0.0; + if (threshold > 1) return 1.0; return threshold; }, topN: (value) => { if (value === null || value === undefined) return 4; const n = parseInt(value); - if (isNullOrNaN(n) || n < 1) return 4; + if (isNullOrNaN(n)) return 4; + if (n < 1) return 1; return n; }, chatMode: (value) => { @@ -70,7 +76,7 @@ const Workspace = { return value; }, chatProvider: (value) => { - if (!value || typeof value !== "string") return null; + if (!value || typeof value !== "string" || value === "none") return null; return String(value); }, chatModel: (value) => { @@ -78,7 +84,7 @@ const Workspace = { return String(value); }, agentProvider: (value) => { - if (!value || typeof value !== "string") return null; + if (!value || typeof value !== "string" || value === "none") return null; return String(value); }, agentModel: (value) => { From 5f19e5bec6f823c47772e897573149c70f5af66e Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Mon, 16 Dec 2024 15:24:42 -0800 Subject: [PATCH 14/14] Enable default users to be able to add attachments, but not files to chats --- .../ChatContainer/DnDWrapper/index.jsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx index 32b58560fa9..32999e11c64 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx @@ -30,7 +30,6 @@ export function DnDFileUploaderProvider({ workspace, children }) { const { user } = useUser(); useEffect(() => { - if (!!user && user.role === "default") return; System.checkDocumentProcessorOnline().then((status) => setReady(status)); }, [user]); @@ -111,6 +110,8 @@ export function DnDFileUploaderProvider({ workspace, children }) { type: "attachment", }); } else { + // If the user is a default user, we do not want to allow them to upload files. + if (!!user && user.role === "default") continue; newAccepted.push({ uid: v4(), file, @@ -146,6 +147,8 @@ export function DnDFileUploaderProvider({ workspace, children }) { type: "attachment", }); } else { + // If the user is a default user, we do not want to allow them to upload files. + if (!!user && user.role === "default") continue; newAccepted.push({ uid: v4(), file, @@ -216,6 +219,8 @@ export default function DnDFileUploaderWrapper({ children }) { onDragEnter: () => setDragging(true), onDragLeave: () => setDragging(false), }); + const { user } = useUser(); + const canUploadAll = !user || user?.role !== "default"; return (
-

Add anything

+

+ Add {canUploadAll ? "anything" : "an image"} +

- Drop your file here to embed it into your
- workspace auto-magically. + {canUploadAll ? ( + <> + Drop your file here to embed it into your
+ workspace auto-magically. + + ) : ( + <> + Drop your image here to chat with it
+ auto-magically. + + )}