Skip to content

Commit

Permalink
Append writable fields to dev API new workspace endpoint (#2843)
Browse files Browse the repository at this point in the history
* add writible fields to dev api new workspace endpoint

* lint

* implement validations for workspace model

* update swagger comments

* simplify validations for workspace on frontend and API

* cleanup validations

---------

Co-authored-by: Timothy Carambat <[email protected]>
  • Loading branch information
shatfield4 and timothycarambat authored Dec 16, 2024
1 parent dd7c467 commit f8885a4
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 29 deletions.
23 changes: 20 additions & 3 deletions server/endpoints/api/workspace/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@ 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",
similarityThreshold: 0.7,
openAiTemp: 0.7,
openAiHistory: 20,
openAiPrompt: "Custom prompt for responses",
queryRefusalResponse: "Custom refusal message",
chatMode: "chat",
topN: 4
}
}
}
Expand Down Expand Up @@ -62,8 +69,18 @@ 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",
Expand Down
152 changes: 128 additions & 24 deletions server/models/workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ 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.",

// 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: [
// Used for generic updates so we can validate keys in request body
"name",
"slug",
"vectorTag",
// "slug",
// "vectorTag",
"openAiTemp",
"openAiHistory",
"lastUpdatedAt",
Expand All @@ -23,11 +30,77 @@ const Workspace = {
"chatModel",
"topN",
"chatMode",
"pfpFilename",
// "pfpFilename",
"agentProvider",
"agentModel",
"queryRefusalResponse",
],

validations: {
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.
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) || temp < 0) return null;
return temp;
},
openAiHistory: (value) => {
if (value === null || value === undefined) return 20;
const history = parseInt(value);
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)) 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)) return 4;
if (n < 1) return 1;
return n;
},
chatMode: (value) => {
if (!value || !["chat", "query"].includes(value)) return "chat";
return value;
},
chatProvider: (value) => {
if (!value || typeof value !== "string" || value === "none") return null;
return String(value);
},
chatModel: (value) => {
if (!value || typeof value !== "string") return null;
return String(value);
},
agentProvider: (value) => {
if (!value || typeof value !== "string" || value === "none") 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
Expand All @@ -53,8 +126,34 @@ const Workspace = {
return slugifyModule(...args);
},

new: async function (name = null, creatorId = null) {
if (!name) return { result: null, message: "name cannot be null" };
/**
* 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 });
slug = slug || uuidv4();

Expand All @@ -66,7 +165,11 @@ const Workspace = {

try {
const workspace = await prisma.workspaces.create({
data: { name, slug },
data: {
name: this.validations.name(name),
...this.validateFields(additionalFields),
slug,
},
});

// If created with a user then we need to create the relationship as well.
Expand All @@ -80,35 +183,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");

Expand Down
14 changes: 12 additions & 2 deletions server/swagger/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1451,6 +1451,9 @@
}
}
},
"400": {
"description": "Bad Request"
},
"403": {
"description": "Forbidden",
"content": {
Expand All @@ -1471,12 +1474,19 @@
}
},
"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",
"similarityThreshold": 0.7,
"openAiTemp": 0.7,
"openAiHistory": 20,
"openAiPrompt": "Custom prompt for responses",
"queryRefusalResponse": "Custom refusal message",
"chatMode": "chat",
"topN": 4
}
}
}
Expand Down

0 comments on commit f8885a4

Please sign in to comment.