Add knowledgebases

Knowledgebases give bots RAG over documents, scoped like extensions: to
particular bots, to a workspace, or to every bot, with their own button
beside Tools in the composer. Documents are chunked and embedded by the
file-ingest worker with the install's embedding model, which is now an
install-wide setting rather than a per-bot one.

- Word search runs on an FTS5 index ranked by BM25, with a scored LIKE
  fallback for scripts written without spaces.
- Changing the embedding model re-indexes documents automatically.
- Ingest jobs wait without spending attempts while every embedding model
  rests, fail at once on errors retrying can't fix, and keep passages
  searchable by words when the model refuses.
- Settings > Knowledgebases shows each knowledgebase's and document's
  index state: spinner, warning, grey check (words only) or green check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-26 16:41:01 +01:00
co-authored by Claude Opus 5.5
parent 2259b6895f
commit e7b23b7454
111 changed files with 4573 additions and 148 deletions
@@ -0,0 +1,52 @@
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
import grabKnowledgebasesOverview from "@/src/functions/backend/db/knowledgebases/grab-knowledgebases-overview";
import saveKnowledgebase from "@/src/functions/backend/db/knowledgebases/save-knowledgebase";
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
import { SaveKnowledgebaseRequestSchema } from "@/src/schemas/knowledgebase-schema";
import type { KnowledgebasesOverviewPayload } from "@/src/types";
import AppError from "@/src/utils/app-error";
import parseWithSchema from "@/src/utils/parse-with-schema";
import safeErrorMessage from "@/src/utils/safe-error-message";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
/**
* Super admin only: adds or edits a knowledgebase.
*/
export const handler: BunextAPIRouteHandler<
APIResponseObject<KnowledgebasesOverviewPayload>
> = async ({ req, body }) => {
try {
if (req.method != "POST") {
throw new AppError(`Method not allowed`);
}
const user = await grabSuperAdminUser({ req });
if (!user?.id) {
throw new AppError(`Only a super admin can do that.`);
}
const data = parseWithSchema({ schema: SaveKnowledgebaseRequestSchema, data: body });
const knowledgebase_id = await saveKnowledgebase({ user_id: user.id, data });
await logEvent({
title: data.knowledgebase_id ? "Knowledgebase changed" : "Knowledgebase added",
category: "system",
user_id: user.id,
plain_text: `User #${user.id} ${data.knowledgebase_id ? "changed" : "added"} the knowledgebase ${data.name} (#${knowledgebase_id}).`,
metadata: { knowledgebase_id },
ip_address: grabRequestIP(req),
});
return {
success: true,
msg: `Saved ${data.name}.`,
singleRes: await grabKnowledgebasesOverview({ user_id: user.id }),
};
} catch (error: unknown) {
return { success: false, msg: safeErrorMessage({ error }) };
}
};