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
+40
View File
@@ -28,6 +28,8 @@ import grabBotFileTools from "@/src/harness/tools/storage/grab-bot-file-tools";
import grabRunScopes from "@/src/functions/backend/db/bot-files/grab-run-scopes";
import openBotExtensions from "@/src/harness/tools/extensions/open-bot-extensions";
import openBotSkills from "@/src/harness/tools/skills/open-bot-skills";
import openBotKnowledgebases from "@/src/harness/tools/knowledgebases/open-bot-knowledgebases";
import grabKnowledgebaseContext from "@/src/harness/tools/knowledgebases/grab-knowledgebase-context";
import botMessagesToHarness from "./bot-messages-to-harness";
import composeComputersNote from "./compose-computers-note";
import grabSessionCarryOver from "./grab-session-carry-over";
@@ -262,6 +264,22 @@ export default async function runBotReply({
taken: [...tools, ...extensions.tools].map((tool) => tool.name),
});
/**
* The run's knowledgebases: a search tool and a reader, and the
* passages found for what the user just asked, so a plain question is
* answered from the documents.
*/
const knowledge = openBotKnowledgebases({
knowledgebases: extensions.knowledgebases,
taken: [...tools, ...extensions.tools, ...skills.tools].map((tool) => tool.name),
});
const knowledge_context = await grabKnowledgebaseContext({
knowledgebases: extensions.knowledgebases,
ids: knowledge.ids,
query: unansweredText({ messages: history, answered_id }),
signal,
});
/**
* delegate_task is only there while the coordinator, or a workspace's
* admin, has bots to hand work to. Anyone else in a workspace with an
@@ -298,7 +316,9 @@ export default async function runBotReply({
}),
crew_note,
skills.note,
knowledge.note,
extensions.note,
knowledge_context,
`Now: ${new Date().toUTCString()}.`,
]
.filter(Boolean)
@@ -332,6 +352,7 @@ export default async function runBotReply({
...file_tools,
...extensions.tools,
...skills.tools,
...knowledge.tools,
],
max_steps: await grabReplyMaxToolSteps(),
bot_id: bot.id,
@@ -365,3 +386,22 @@ function newestUserMessageId({ messages }: { messages: Pick<BotMessageWithAttach
...messages.filter((message) => message.author == "user").map((message) => Number(message.id)),
);
}
/**
* The text of the user messages the reply answers (those after the last
* answered one), else the newest user message: what the knowledgebases are
* searched for ahead of the reply.
*/
function unansweredText({
messages,
answered_id,
}: {
messages: Pick<BotMessageWithAttachments, "id" | "author" | "text">[];
answered_id?: number | string | null;
}) {
const from_user = messages.filter((message) => message.author == "user");
const unanswered = from_user.filter((message) => Number(message.id) > Number(answered_id || 0));
const picked = unanswered.length ? unanswered : from_user.slice(-1);
return picked.map((message) => message.text || "").join("\n");
}
+8 -1
View File
@@ -22,6 +22,7 @@ import runTextLoop from "@/src/harness/text/run-text-loop";
import openComputerTools from "@/src/harness/tools/computer/open-computer-tools";
import openBotExtensions, { type OpenBotExtensions } from "@/src/harness/tools/extensions/open-bot-extensions";
import openBotSkills, { type OpenBotSkills } from "@/src/harness/tools/skills/open-bot-skills";
import openBotKnowledgebases from "@/src/harness/tools/knowledgebases/open-bot-knowledgebases";
import grabBotTools from "@/src/harness/tools/bot/grab-bot-tools";
import grabBotSecretTools from "@/src/harness/tools/bot/secrets/grab-bot-secret-tools";
import createWorkspaceTool from "@/src/harness/tools/bot/create-workspace-tool";
@@ -242,6 +243,11 @@ export default async function runBotTask({
taken: [...tools, ...extensions.tools].map((tool) => tool.name),
});
const knowledge = openBotKnowledgebases({
knowledgebases: extensions.knowledgebases,
taken: [...tools, ...extensions.tools, ...skills.tools].map((tool) => tool.name),
});
const result = await runTextLoop({
system: [
prompt,
@@ -277,6 +283,7 @@ export default async function runBotTask({
.join("\n")}`
: "",
skills.note,
knowledge.note,
extensions.note,
]
.filter(Boolean)
@@ -291,7 +298,7 @@ export default async function runBotTask({
}),
},
],
tools: [...tools, ...extensions.tools, ...skills.tools],
tools: [...tools, ...extensions.tools, ...skills.tools, ...knowledge.tools],
max_steps: await grabHarnessMaxToolSteps(),
bot_id: bot.id,
workspace_id,