anything-llm/server/endpoints/admin.js
Timothy Carambat d1103e2b71
Add support for custom agent skills via plugins (#2202)
* Add support for custom agent skills via plugins
Update Admin.systemPreferences to updated endpoint (legacy has deprecation notice

* lint

* dev build

* patch safeJson
patch label loading

* allow plugins with no config options

* lint

* catch invalid setupArgs in frontend

* update link to docs page for agent skills

* remove unneeded files

---------

Co-authored-by: shatfield4 <seanhatfield5@gmail.com>
2024-09-10 17:06:02 -07:00

563 lines
18 KiB
JavaScript

const { ApiKey } = require("../models/apiKeys");
const { Document } = require("../models/documents");
const { EventLogs } = require("../models/eventLogs");
const { Invite } = require("../models/invite");
const { SystemSettings } = require("../models/systemSettings");
const { Telemetry } = require("../models/telemetry");
const { User } = require("../models/user");
const { DocumentVectors } = require("../models/vectors");
const { Workspace } = require("../models/workspace");
const { WorkspaceChats } = require("../models/workspaceChats");
const {
getVectorDbClass,
getEmbeddingEngineSelection,
} = require("../utils/helpers");
const {
validRoleSelection,
canModifyAdmin,
validCanModify,
} = require("../utils/helpers/admin");
const { reqBody, userFromSession, safeJsonParse } = require("../utils/http");
const {
strictMultiUserRoleValid,
flexUserRoleValid,
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const ImportedPlugin = require("../utils/agents/imported");
function adminEndpoints(app) {
if (!app) return;
app.get(
"/admin/users",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (_request, response) => {
try {
const users = await User.where();
response.status(200).json({ users });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.post(
"/admin/users/new",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const currUser = await userFromSession(request, response);
const newUserParams = reqBody(request);
const roleValidation = validRoleSelection(currUser, newUserParams);
if (!roleValidation.valid) {
response
.status(200)
.json({ user: null, error: roleValidation.error });
return;
}
const { user: newUser, error } = await User.create(newUserParams);
if (!!newUser) {
await EventLogs.logEvent(
"user_created",
{
userName: newUser.username,
createdBy: currUser.username,
},
currUser.id
);
}
response.status(200).json({ user: newUser, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.post(
"/admin/user/:id",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const currUser = await userFromSession(request, response);
const { id } = request.params;
const updates = reqBody(request);
const user = await User.get({ id: Number(id) });
const canModify = validCanModify(currUser, user);
if (!canModify.valid) {
response.status(200).json({ success: false, error: canModify.error });
return;
}
const roleValidation = validRoleSelection(currUser, updates);
if (!roleValidation.valid) {
response
.status(200)
.json({ success: false, error: roleValidation.error });
return;
}
const validAdminRoleModification = await canModifyAdmin(user, updates);
if (!validAdminRoleModification.valid) {
response
.status(200)
.json({ success: false, error: validAdminRoleModification.error });
return;
}
const { success, error } = await User.update(id, updates);
response.status(200).json({ success, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.delete(
"/admin/user/:id",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const currUser = await userFromSession(request, response);
const { id } = request.params;
const user = await User.get({ id: Number(id) });
const canModify = validCanModify(currUser, user);
if (!canModify.valid) {
response.status(200).json({ success: false, error: canModify.error });
return;
}
await User.delete({ id: Number(id) });
await EventLogs.logEvent(
"user_deleted",
{
userName: user.username,
deletedBy: currUser.username,
},
currUser.id
);
response.status(200).json({ success: true, error: null });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.get(
"/admin/invites",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (_request, response) => {
try {
const invites = await Invite.whereWithUsers();
response.status(200).json({ invites });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.post(
"/admin/invite/new",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const body = reqBody(request);
const { invite, error } = await Invite.create({
createdByUserId: user.id,
workspaceIds: body?.workspaceIds || [],
});
await EventLogs.logEvent(
"invite_created",
{
inviteCode: invite.code,
createdBy: response.locals?.user?.username,
},
response.locals?.user?.id
);
response.status(200).json({ invite, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.delete(
"/admin/invite/:id",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const { id } = request.params;
const { success, error } = await Invite.deactivate(id);
await EventLogs.logEvent(
"invite_deleted",
{ deletedBy: response.locals?.user?.username },
response.locals?.user?.id
);
response.status(200).json({ success, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.get(
"/admin/workspaces",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (_request, response) => {
try {
const workspaces = await Workspace.whereWithUsers();
response.status(200).json({ workspaces });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.get(
"/admin/workspaces/:workspaceId/users",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const { workspaceId } = request.params;
const users = await Workspace.workspaceUsers(workspaceId);
response.status(200).json({ users });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.post(
"/admin/workspaces/new",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const { name } = reqBody(request);
const { workspace, message: error } = await Workspace.new(
name,
user.id
);
response.status(200).json({ workspace, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.post(
"/admin/workspaces/:workspaceId/update-users",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const { workspaceId } = request.params;
const { userIds } = reqBody(request);
const { success, error } = await Workspace.updateUsers(
workspaceId,
userIds
);
response.status(200).json({ success, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.delete(
"/admin/workspaces/:id",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const { id } = request.params;
const VectorDb = getVectorDbClass();
const workspace = await Workspace.get({ id: Number(id) });
if (!workspace) {
response.sendStatus(404).end();
return;
}
await WorkspaceChats.delete({ workspaceId: Number(workspace.id) });
await DocumentVectors.deleteForWorkspace(Number(workspace.id));
await Document.delete({ workspaceId: Number(workspace.id) });
await Workspace.delete({ id: Number(workspace.id) });
try {
await VectorDb["delete-namespace"]({ namespace: workspace.slug });
} catch (e) {
console.error(e.message);
}
response.status(200).json({ success: true, error: null });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
// System preferences but only by array of labels
app.get(
"/admin/system-preferences-for",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const requestedSettings = {};
const labels = request.query.labels?.split(",") || [];
const needEmbedder = [
"text_splitter_chunk_size",
"max_embed_chunk_size",
];
const noRecord = [
"max_embed_chunk_size",
"agent_sql_connections",
"imported_agent_skills",
"feature_flags",
"meta_page_title",
"meta_page_favicon",
];
for (const label of labels) {
// Skip any settings that are not explicitly defined as public
if (!SystemSettings.publicFields.includes(label)) continue;
// Only get the embedder if the setting actually needs it
let embedder = needEmbedder.includes(label)
? getEmbeddingEngineSelection()
: null;
// Only get the record from db if the setting actually needs it
let setting = noRecord.includes(label)
? null
: await SystemSettings.get({ label });
switch (label) {
case "limit_user_messages":
requestedSettings[label] = setting?.value === "true";
break;
case "message_limit":
requestedSettings[label] = setting?.value
? Number(setting.value)
: 10;
break;
case "footer_data":
requestedSettings[label] = setting?.value ?? JSON.stringify([]);
break;
case "support_email":
requestedSettings[label] = setting?.value || null;
break;
case "text_splitter_chunk_size":
requestedSettings[label] =
setting?.value || embedder?.embeddingMaxChunkLength || null;
break;
case "text_splitter_chunk_overlap":
requestedSettings[label] = setting?.value || null;
break;
case "max_embed_chunk_size":
requestedSettings[label] =
embedder?.embeddingMaxChunkLength || 1000;
break;
case "agent_search_provider":
requestedSettings[label] = setting?.value || null;
break;
case "agent_sql_connections":
requestedSettings[label] =
await SystemSettings.brief.agent_sql_connections();
break;
case "default_agent_skills":
requestedSettings[label] = safeJsonParse(setting?.value, []);
break;
case "imported_agent_skills":
requestedSettings[label] = ImportedPlugin.listImportedPlugins();
break;
case "custom_app_name":
requestedSettings[label] = setting?.value || null;
break;
case "feature_flags":
requestedSettings[label] =
(await SystemSettings.getFeatureFlags()) || {};
break;
case "meta_page_title":
requestedSettings[label] =
await SystemSettings.getValueOrFallback({ label }, null);
break;
case "meta_page_favicon":
requestedSettings[label] =
await SystemSettings.getValueOrFallback({ label }, null);
break;
default:
break;
}
}
response.status(200).json({ settings: requestedSettings });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
// TODO: Delete this endpoint
// DEPRECATED - use /admin/system-preferences-for instead with ?labels=... comma separated string of labels
app.get(
"/admin/system-preferences",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (_, response) => {
try {
const embedder = getEmbeddingEngineSelection();
const settings = {
limit_user_messages:
(await SystemSettings.get({ label: "limit_user_messages" }))
?.value === "true",
message_limit:
Number(
(await SystemSettings.get({ label: "message_limit" }))?.value
) || 10,
footer_data:
(await SystemSettings.get({ label: "footer_data" }))?.value ||
JSON.stringify([]),
support_email:
(await SystemSettings.get({ label: "support_email" }))?.value ||
null,
text_splitter_chunk_size:
(await SystemSettings.get({ label: "text_splitter_chunk_size" }))
?.value ||
embedder?.embeddingMaxChunkLength ||
null,
text_splitter_chunk_overlap:
(await SystemSettings.get({ label: "text_splitter_chunk_overlap" }))
?.value || null,
max_embed_chunk_size: embedder?.embeddingMaxChunkLength || 1000,
agent_search_provider:
(await SystemSettings.get({ label: "agent_search_provider" }))
?.value || null,
agent_sql_connections:
await SystemSettings.brief.agent_sql_connections(),
default_agent_skills:
safeJsonParse(
(await SystemSettings.get({ label: "default_agent_skills" }))
?.value,
[]
) || [],
imported_agent_skills: ImportedPlugin.listImportedPlugins(),
custom_app_name:
(await SystemSettings.get({ label: "custom_app_name" }))?.value ||
null,
feature_flags: (await SystemSettings.getFeatureFlags()) || {},
meta_page_title: await SystemSettings.getValueOrFallback(
{ label: "meta_page_title" },
null
),
meta_page_favicon: await SystemSettings.getValueOrFallback(
{ label: "meta_page_favicon" },
null
),
};
response.status(200).json({ settings });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.post(
"/admin/system-preferences",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const updates = reqBody(request);
await SystemSettings.updateSettings(updates);
response.status(200).json({ success: true, error: null });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.get(
"/admin/api-keys",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
async (_request, response) => {
try {
const apiKeys = await ApiKey.whereWithUser({});
return response.status(200).json({
apiKeys,
error: null,
});
} catch (error) {
console.error(error);
response.status(500).json({
apiKey: null,
error: "Could not find an API Keys.",
});
}
}
);
app.post(
"/admin/generate-api-key",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const { apiKey, error } = await ApiKey.create(user.id);
await Telemetry.sendTelemetry("api_key_created");
await EventLogs.logEvent(
"api_key_created",
{ createdBy: user?.username },
user?.id
);
return response.status(200).json({
apiKey,
error,
});
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.delete(
"/admin/delete-api-key/:id",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
async (request, response) => {
try {
const { id } = request.params;
await ApiKey.delete({ id: Number(id) });
await EventLogs.logEvent(
"api_key_deleted",
{ deletedBy: response.locals?.user?.username },
response?.locals?.user?.id
);
return response.status(200).end();
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
}
module.exports = { adminEndpoints };