wizards 0.2.17 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/runtime/dist/agents/project-assistant.js +20 -2
- package/apps/runtime/dist/authoring/guide.js +6 -3
- package/apps/runtime/dist/db/migrations/tenant/0009_knowledge.sql +104 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0009_snapshot.json +2837 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
- package/apps/runtime/dist/db/schema.js +160 -8
- package/apps/runtime/dist/engine/steps.js +3 -11
- package/apps/runtime/dist/env.js +2 -0
- package/apps/runtime/dist/files/storage.js +15 -0
- package/apps/runtime/dist/models.js +38 -0
- package/apps/runtime/dist/plugins/loader.js +2 -0
- package/apps/runtime/dist/routes/studio.js +147 -6
- package/apps/runtime/dist/services/cloud.js +59 -1
- package/apps/runtime/dist/services/knowledge-model.js +558 -0
- package/apps/runtime/dist/services/knowledge.js +721 -0
- package/apps/runtime/dist/services/project-files.js +114 -6
- package/apps/runtime/dist/services/project-index.js +632 -94
- package/apps/runtime/dist/services/relevance.js +90 -0
- package/apps/runtime/dist/services/space-categories.js +882 -0
- package/apps/runtime/dist/services/space-data.js +466 -38
- package/apps/runtime/dist/services/space-origin.js +164 -0
- package/apps/runtime/dist/services/spaces.js +155 -1
- package/apps/runtime/dist/tools/pages.js +27 -22
- package/apps/runtime/dist/tools/project.js +130 -40
- package/apps/web/dist/assets/{Account-LVXhagaS.js → Account-Cp1cgqjV.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-DM0KAoy6.js → ChangelogDialog-Chke1qUe.js} +1 -1
- package/apps/web/dist/assets/{ChatPanel-CxOQmtv-.js → ChatPanel-DWrwewr6.js} +1 -1
- package/apps/web/dist/assets/{CreditsDialog-DkW3YT44.js → CreditsDialog-Ckema8Ss.js} +1 -1
- package/apps/web/dist/assets/{EditorPage-hbisE1Wf.js → EditorPage-XX6pfHk5.js} +1 -1
- package/apps/web/dist/assets/{FlowPreview-v7or29ku.js → FlowPreview-DS545n78.js} +1 -1
- package/apps/web/dist/assets/{Harness-RZF-bBmb.js → Harness-H3yXP-2L.js} +1 -1
- package/apps/web/dist/assets/{HomePage-mxG-a_KU.js → HomePage-Bds84ShK.js} +1 -1
- package/apps/web/dist/assets/{ImportWizard-CiPyUt3E.js → ImportWizard-0_NkA8aS.js} +1 -1
- package/apps/web/dist/assets/{LocalRuntime-Dqp9wkN3.js → LocalRuntime-DnABzTOe.js} +1 -1
- package/apps/web/dist/assets/{NewWizardPage-DHwRsi0_.js → NewWizardPage-BqP75V9K.js} +2 -2
- package/apps/web/dist/assets/ProjectPage-DbOvwOdZ.js +5 -0
- package/apps/web/dist/assets/{PublicRunner-TGkOwbAP.js → PublicRunner-CtZ7vxl_.js} +1 -1
- package/apps/web/dist/assets/{RunnerView-DzHGshZj.js → RunnerView-xV5bWclu.js} +1 -1
- package/apps/web/dist/assets/{SettingsPage-CG3Xtfej.js → SettingsPage-DKt55zFh.js} +1 -1
- package/apps/web/dist/assets/{SetupPage-O1GfPlqX.js → SetupPage-c6mMD9zN.js} +1 -1
- package/apps/web/dist/assets/{SharePage-CCNUhtry.js → SharePage-DTKIbMj-.js} +1 -1
- package/apps/web/dist/assets/{api-DB22_B7G.js → api-CVG-xDEn.js} +1 -1
- package/apps/web/dist/assets/changelog-CXXT0zsp.js +1 -0
- package/apps/web/dist/assets/{esm-3TT9i_rv.js → esm-DLM3jdbm.js} +1 -1
- package/apps/web/dist/assets/{index-CLe9Wrpu.js → index-1gmaxDSk.js} +3 -3
- package/apps/web/dist/assets/index-Bwyehu0k.css +1 -0
- package/apps/web/dist/assets/{meta-CEsBCPfA.js → meta-m947x6uG.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-pOcjznGJ.js → settings-menu-DUY6__C3.js} +1 -1
- package/apps/web/dist/assets/ui-BPY2pIh0.js +1 -0
- package/apps/web/dist/index.html +4 -4
- package/apps/web/dist/mcp/flow.html +2 -2
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +164 -0
- package/node_modules/@engenty-wizards/shared/dist/knowledge.d.ts +199 -0
- package/node_modules/@engenty-wizards/shared/dist/knowledge.js +68 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +1 -16
- package/node_modules/@engenty-wizards/shared/dist/projects.js +1 -1
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +8 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +5 -0
- package/node_modules/@engenty-wizards/shared/dist/space-data.d.ts +20 -0
- package/node_modules/@engenty-wizards/shared/dist/space-data.js +2 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/ProjectPage-Bb7jN_6n.js +0 -4
- package/apps/web/dist/assets/changelog-BkIJdZ7M.js +0 -1
- package/apps/web/dist/assets/index-BPpKz65x.css +0 -1
- package/apps/web/dist/assets/ui-I7QYMX0F.js +0 -1
|
@@ -1,21 +1,25 @@
|
|
|
1
|
+
import { formatTableCell } from "@engenty-wizards/shared/engenty/data-tables";
|
|
1
2
|
import { embedMany } from "ai";
|
|
2
|
-
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
3
|
-
import { db, schema } from "../db/client.js";
|
|
4
|
-
import { parseDocument } from "../documents/parse.js";
|
|
3
|
+
import { and, eq, inArray, isNotNull, isNull, sql } from "drizzle-orm";
|
|
4
|
+
import { db, schema, withTenant } from "../db/client.js";
|
|
5
5
|
import { createSearchChunks } from "../engenty/search-index/chunks.js";
|
|
6
6
|
import { getBlob, putBlob } from "../files/blobs.js";
|
|
7
7
|
import { embeddingModel } from "../models.js";
|
|
8
8
|
import { pluginIdsOf } from "../plugins/registry.js";
|
|
9
9
|
import { currentTenant } from "../tenants/tenant.js";
|
|
10
10
|
import { ServiceError } from "./errors.js";
|
|
11
|
+
import { filePaths, pageTree, tablePaths } from "./knowledge.js";
|
|
12
|
+
import { judgeRelevance, MIN_RELEVANCE } from "./relevance.js";
|
|
13
|
+
import { categoryLines, categoryRows, filterItems, itemCategoriesOf, } from "./space-categories.js";
|
|
11
14
|
const CHUNK = { mode: "paragraph", max_chunk_length: 1200, overlap: 120 };
|
|
12
|
-
/**
|
|
13
|
-
const
|
|
15
|
+
/** Units of one item; a longer one keeps its start. */
|
|
16
|
+
const MAX_UNITS = 2000;
|
|
17
|
+
/** A section longer than this is cut at paragraphs into parts about PART_CHARS long. */
|
|
18
|
+
const SECTION_CHARS = 2400;
|
|
19
|
+
const PART_CHARS = 1800;
|
|
20
|
+
/** What the keywords and the vectors hand on to the classifier. */
|
|
21
|
+
const CANDIDATES = 24;
|
|
14
22
|
const vectorBytes = (v) => Buffer.from(new Float32Array(v).buffer);
|
|
15
|
-
/** The page marks of the document converter, as a line a reader understands. */
|
|
16
|
-
function withPageLines(markdown) {
|
|
17
|
-
return markdown.replace(/<page-break number="(\d+)"[^>]*><\/page-break>/g, (_m, n) => `\n\n[Seite ${n}]\n\n`);
|
|
18
|
-
}
|
|
19
23
|
async function embed(texts) {
|
|
20
24
|
const resolved = await embeddingModel();
|
|
21
25
|
if (!resolved || !texts.length) {
|
|
@@ -27,35 +31,339 @@ async function embed(texts) {
|
|
|
27
31
|
export async function dropIndex(fileId) {
|
|
28
32
|
await db.delete(schema.projectChunk).where(eq(schema.projectChunk.fileId, fileId));
|
|
29
33
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
export async function documentText(file) {
|
|
35
|
+
return file.textHash ? (await getBlob(file.textHash)).toString("utf8") : "";
|
|
36
|
+
}
|
|
37
|
+
/** Keeps a document's text, for a step that reads the file itself. */
|
|
38
|
+
export async function keepText(text) {
|
|
39
|
+
return putBlob(Buffer.from(text, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Replaces the units `scope` held with `units`. A unit whose text has not changed keeps its
|
|
43
|
+
* vector: editing one row of a long table embeds one row.
|
|
44
|
+
*/
|
|
45
|
+
async function writeUnits(scope, units) {
|
|
46
|
+
const before = await db
|
|
47
|
+
.select({
|
|
48
|
+
text: schema.projectChunk.text,
|
|
49
|
+
embedding: schema.projectChunk.embedding,
|
|
50
|
+
model: schema.projectChunk.model,
|
|
51
|
+
})
|
|
52
|
+
.from(schema.projectChunk)
|
|
53
|
+
.where(scope);
|
|
54
|
+
const resolved = await embeddingModel().catch(() => null);
|
|
55
|
+
const kept = new Map(before
|
|
56
|
+
.filter((b) => b.embedding && b.model && b.model === resolved?.ref)
|
|
57
|
+
.map((b) => [b.text, b.embedding]));
|
|
58
|
+
const missing = [...new Set(units.map((u) => u.text).filter((t) => !kept.has(t)))];
|
|
59
|
+
// Without an embedding model, or when it fails, the keywords still find the unit.
|
|
60
|
+
const embedded = await embed(missing).catch((err) => {
|
|
37
61
|
console.error("[project-index] embedding failed", err);
|
|
38
62
|
return null;
|
|
39
63
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
if (embedded) {
|
|
65
|
+
for (const [i, text] of missing.entries()) {
|
|
66
|
+
kept.set(text, vectorBytes(embedded.vectors[i]));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const model = embedded?.model ?? (kept.size ? (resolved?.ref ?? null) : null);
|
|
70
|
+
await db.delete(schema.projectChunk).where(scope);
|
|
71
|
+
for (let i = 0; i < units.length; i += 100) {
|
|
72
|
+
await db.insert(schema.projectChunk).values(units.slice(i, i + 100).map((u) => {
|
|
73
|
+
const vector = kept.get(u.text) ?? null;
|
|
74
|
+
return { ...u, embedding: vector, model: vector ? model : null };
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const SHEET = /^\s*<!--\s*S\.\s*(\d+)\s*-->\s*$/;
|
|
79
|
+
/** A page's Markdown cut at its headings; the page marks of its original say where each starts. */
|
|
80
|
+
export function sectionsOf(markdown) {
|
|
81
|
+
const out = [];
|
|
82
|
+
const stack = [];
|
|
83
|
+
let sheet = null;
|
|
84
|
+
let current = { heading: [], sheet: null, body: "", lines: [] };
|
|
85
|
+
let fence = false;
|
|
86
|
+
const close = () => {
|
|
87
|
+
const body = current.lines.join("\n").trim();
|
|
88
|
+
if (body) {
|
|
89
|
+
out.push({ heading: current.heading, sheet: current.sheet, body });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
for (const line of markdown.split("\n")) {
|
|
93
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
94
|
+
fence = !fence;
|
|
95
|
+
}
|
|
96
|
+
const mark = fence ? null : SHEET.exec(line);
|
|
97
|
+
if (mark) {
|
|
98
|
+
sheet = Number(mark[1]);
|
|
99
|
+
if (current.lines.join("").trim()) {
|
|
100
|
+
current.lines.push(`[S. ${sheet}]`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
current.sheet = sheet;
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const h = fence ? null : /^(#{1,4})\s+(.+?)\s*#*\s*$/.exec(line);
|
|
108
|
+
if (h) {
|
|
109
|
+
close();
|
|
110
|
+
const level = h[1].length;
|
|
111
|
+
while (stack.length && stack.at(-1).level >= level) {
|
|
112
|
+
stack.pop();
|
|
113
|
+
}
|
|
114
|
+
stack.push({ level, text: h[2].trim() });
|
|
115
|
+
current = { heading: stack.map((s) => s.text), sheet, body: "", lines: [] };
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
current.lines.push(line);
|
|
119
|
+
}
|
|
120
|
+
close();
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
/** A long section in parts at its paragraphs; a paragraph longer than a part at its sentences. */
|
|
124
|
+
function partsOf(body) {
|
|
125
|
+
if (body.length <= SECTION_CHARS) {
|
|
126
|
+
return [body];
|
|
127
|
+
}
|
|
128
|
+
const pieces = body
|
|
129
|
+
.split(/\n\s*\n/)
|
|
130
|
+
.flatMap((p) => p.length <= PART_CHARS ? [p] : (p.match(/[^.!?]+[.!?]+["»“)]?\s*|[^.!?]+$/g) ?? [p]));
|
|
131
|
+
const parts = [];
|
|
132
|
+
let now = "";
|
|
133
|
+
for (const piece of pieces) {
|
|
134
|
+
if (now && now.length + piece.length > PART_CHARS) {
|
|
135
|
+
parts.push(now.trim());
|
|
136
|
+
now = "";
|
|
137
|
+
}
|
|
138
|
+
now += (now ? "\n\n" : "") + piece;
|
|
139
|
+
}
|
|
140
|
+
if (now.trim()) {
|
|
141
|
+
parts.push(now.trim());
|
|
142
|
+
}
|
|
143
|
+
return parts;
|
|
144
|
+
}
|
|
145
|
+
const head = (titles, lines) => [titles.join(" › "), lines.length ? lines.join(" · ") : ""].filter(Boolean).join("\n");
|
|
146
|
+
async function indexPage(pageId) {
|
|
147
|
+
const scope = eq(schema.projectChunk.pageId, pageId);
|
|
148
|
+
const page = await db.query.spacePage.findFirst({ where: eq(schema.spacePage.id, pageId) });
|
|
149
|
+
if (!page || page.wizardId) {
|
|
150
|
+
await db.delete(schema.projectChunk).where(scope);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const [tree, categories] = await Promise.all([
|
|
154
|
+
pageTree(page.projectId),
|
|
155
|
+
categoryRows(page.projectId),
|
|
156
|
+
]);
|
|
157
|
+
const node = tree.get(page.id);
|
|
158
|
+
const lines = categoryLines(categories, (await itemCategoriesOf([{ pageId: page.id }])).get(`p:${page.id}`));
|
|
159
|
+
const prefix = head(node?.titles ?? [page.title], lines);
|
|
160
|
+
const sections = sectionsOf(page.markdown);
|
|
161
|
+
const units = [];
|
|
162
|
+
// A page without text is still found by its title and Kategorien.
|
|
163
|
+
for (const section of sections.length ? sections : [{ heading: [], sheet: null, body: "" }]) {
|
|
164
|
+
// A page's own first heading is its title: the path says it already.
|
|
165
|
+
const path = section.heading[0]?.toLowerCase() === page.title.toLowerCase()
|
|
166
|
+
? section.heading.slice(1)
|
|
167
|
+
: section.heading;
|
|
168
|
+
const heading = path.join(" › ") || null;
|
|
169
|
+
for (const part of partsOf(section.body)) {
|
|
170
|
+
units.push({
|
|
171
|
+
projectId: page.projectId,
|
|
172
|
+
pageId: page.id,
|
|
173
|
+
heading,
|
|
174
|
+
sheet: section.sheet,
|
|
175
|
+
idx: units.length,
|
|
176
|
+
text: [prefix, heading ? `## ${heading}` : "", part].filter(Boolean).join("\n\n"),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
await writeUnits(scope, units.slice(0, MAX_UNITS));
|
|
181
|
+
}
|
|
182
|
+
async function indexRows(tableId, rowIds) {
|
|
183
|
+
const table = await db.query.spaceTable.findFirst({ where: eq(schema.spaceTable.id, tableId) });
|
|
184
|
+
const scope = rowIds
|
|
185
|
+
? inArray(schema.projectChunk.rowId, rowIds)
|
|
186
|
+
: eq(schema.projectChunk.tableId, tableId);
|
|
187
|
+
if (!table || table.wizardId) {
|
|
188
|
+
await db.delete(schema.projectChunk).where(scope);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const rows = await db.query.spaceTableRow.findMany({
|
|
192
|
+
where: rowIds
|
|
193
|
+
? and(eq(schema.spaceTableRow.tableId, table.id), inArray(schema.spaceTableRow.id, rowIds))
|
|
194
|
+
: eq(schema.spaceTableRow.tableId, table.id),
|
|
195
|
+
});
|
|
196
|
+
const categories = await categoryRows(table.projectId);
|
|
197
|
+
const cats = await itemCategoriesOf([
|
|
198
|
+
{ tableId: table.id },
|
|
199
|
+
...rows.map((r) => ({ rowId: r.id })),
|
|
200
|
+
]);
|
|
201
|
+
const tableLines = categoryLines(categories, cats.get(`t:${table.id}`));
|
|
202
|
+
const units = rows.map((row, i) => {
|
|
203
|
+
const own = categoryLines(categories, cats.get(`r:${row.id}`)).filter((l) => !tableLines.includes(l));
|
|
204
|
+
const cells = table.columns
|
|
205
|
+
.map((c) => [c.name, formatTableCell(c, row.cells[c.id], "de")])
|
|
206
|
+
.filter(([, v]) => v);
|
|
207
|
+
const body = table.format === "faq" && cells.length >= 2
|
|
208
|
+
? `${cells[0][1]}\n\n${cells
|
|
209
|
+
.slice(1)
|
|
210
|
+
.map(([, v]) => v)
|
|
211
|
+
.join("\n\n")}`
|
|
212
|
+
: cells.map(([name, v]) => `${name}: ${v}`).join("\n");
|
|
213
|
+
return {
|
|
214
|
+
projectId: table.projectId,
|
|
215
|
+
tableId: table.id,
|
|
216
|
+
rowId: row.id,
|
|
217
|
+
heading: table.format === "faq" ? (cells[0]?.[1] ?? null) : null,
|
|
218
|
+
idx: i,
|
|
219
|
+
text: `${head([table.title], [...tableLines, ...own])}\n\n${body}`,
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
await writeUnits(scope, units);
|
|
223
|
+
}
|
|
224
|
+
async function indexFile(fileId) {
|
|
225
|
+
const file = await db.query.projectFile.findFirst({ where: eq(schema.projectFile.id, fileId) });
|
|
226
|
+
if (file?.kind !== "document" || file.status !== "ready") {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const converted = (await db.query.spacePage.findFirst({
|
|
230
|
+
where: eq(schema.spacePage.fileId, file.id),
|
|
231
|
+
columns: { id: true },
|
|
232
|
+
})) ??
|
|
233
|
+
(await db.query.spaceTable.findFirst({
|
|
234
|
+
where: eq(schema.spaceTable.fileId, file.id),
|
|
235
|
+
columns: { id: true },
|
|
236
|
+
}));
|
|
237
|
+
const scope = eq(schema.projectChunk.fileId, file.id);
|
|
238
|
+
if (converted) {
|
|
239
|
+
// Its text is the page's now; the file stands behind it as the original.
|
|
240
|
+
await db.delete(schema.projectChunk).where(scope);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const passages = await db
|
|
244
|
+
.select({ heading: schema.projectChunk.heading, idx: schema.projectChunk.idx })
|
|
245
|
+
.from(schema.projectChunk)
|
|
246
|
+
.where(scope);
|
|
247
|
+
// Passages of a document read before Wissen had pages stay until it is read again.
|
|
248
|
+
if (passages.some((p) => p.heading === null)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const categories = await categoryRows(file.projectId);
|
|
252
|
+
const lines = categoryLines(categories, (await itemCategoriesOf([{ fileId: file.id }])).get(`f:${file.id}`));
|
|
253
|
+
await writeUnits(scope, [
|
|
254
|
+
{
|
|
43
255
|
projectId: file.projectId,
|
|
44
256
|
fileId: file.id,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
257
|
+
heading: file.name,
|
|
258
|
+
idx: 0,
|
|
259
|
+
text: [head([file.name], lines), file.description].filter(Boolean).join("\n\n"),
|
|
260
|
+
},
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
async function indexValue(valueId) {
|
|
264
|
+
const scope = eq(schema.projectChunk.valueId, valueId);
|
|
265
|
+
const value = await db.query.spaceCategoryValue.findFirst({
|
|
266
|
+
where: eq(schema.spaceCategoryValue.id, valueId),
|
|
267
|
+
});
|
|
268
|
+
const category = value
|
|
269
|
+
? await db.query.spaceCategory.findFirst({
|
|
270
|
+
where: eq(schema.spaceCategory.id, value.categoryId),
|
|
271
|
+
})
|
|
272
|
+
: undefined;
|
|
273
|
+
if (!(value?.summary && category)) {
|
|
274
|
+
await db.delete(schema.projectChunk).where(scope);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const units = partsOf(value.summary).map((part, i) => ({
|
|
278
|
+
projectId: category.projectId,
|
|
279
|
+
valueId: value.id,
|
|
280
|
+
heading: `${category.name}: ${value.value}`,
|
|
281
|
+
idx: i,
|
|
282
|
+
text: `${category.name}: ${value.value} — Übersicht\n\n${part}`,
|
|
283
|
+
}));
|
|
284
|
+
await writeUnits(scope, units);
|
|
285
|
+
}
|
|
286
|
+
/** Every page under a page. */
|
|
287
|
+
async function subPages(pageId) {
|
|
288
|
+
const rows = await db.all(sql `
|
|
289
|
+
WITH RECURSIVE tree(id) AS (
|
|
290
|
+
SELECT id FROM space_page WHERE parent_id = ${pageId}
|
|
291
|
+
UNION SELECT p.id FROM space_page p JOIN tree ON p.parent_id = tree.id
|
|
292
|
+
) SELECT id FROM tree`);
|
|
293
|
+
return rows.map((r) => r.id);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Brings one item's units up to date. `p:` a page, `P:` a page and its sub-pages (its title or
|
|
297
|
+
* Kategorien changed: they stand in theirs), `t:` a table's rows, `r:` one row, `f:` a file, `v:`
|
|
298
|
+
* a value's Übersicht.
|
|
299
|
+
*/
|
|
300
|
+
export async function indexItem(key) {
|
|
301
|
+
const id = key.slice(2);
|
|
302
|
+
switch (key[0]) {
|
|
303
|
+
case "p":
|
|
304
|
+
return indexPage(id);
|
|
305
|
+
case "P":
|
|
306
|
+
await indexPage(id);
|
|
307
|
+
for (const sub of await subPages(id)) {
|
|
308
|
+
await indexPage(sub);
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
case "t":
|
|
312
|
+
return indexRows(id);
|
|
313
|
+
case "r": {
|
|
314
|
+
const row = await db.query.spaceTableRow.findFirst({
|
|
315
|
+
where: eq(schema.spaceTableRow.id, id),
|
|
316
|
+
columns: { tableId: true },
|
|
317
|
+
});
|
|
318
|
+
return row ? indexRows(row.tableId, [id]) : undefined;
|
|
319
|
+
}
|
|
320
|
+
case "f":
|
|
321
|
+
return indexFile(id);
|
|
322
|
+
case "v":
|
|
323
|
+
return indexValue(id);
|
|
324
|
+
default:
|
|
325
|
+
return;
|
|
50
326
|
}
|
|
51
|
-
return {
|
|
52
|
-
textHash: await putBlob(Buffer.from(text, "utf8")),
|
|
53
|
-
pages: parsed.pages,
|
|
54
|
-
chars: text.length,
|
|
55
|
-
indexed: chunks.length ? (embedded ? "embeddings" : "keywords") : null,
|
|
56
|
-
text,
|
|
57
|
-
};
|
|
58
327
|
}
|
|
328
|
+
// --- the queue ------------------------------------------------------------------------
|
|
329
|
+
// A page saves a moment after the person stops typing; the index follows a moment later, once
|
|
330
|
+
// per item however often it changed, one item after the other.
|
|
331
|
+
const INDEX_DELAY_MS = 1500;
|
|
332
|
+
const waiting = new Map();
|
|
333
|
+
let working = Promise.resolve();
|
|
334
|
+
function run(tenant, key) {
|
|
335
|
+
working = working
|
|
336
|
+
.then(() => withTenant(tenant, () => indexItem(key)))
|
|
337
|
+
.catch((err) => console.error(`[project-index] ${key}`, err));
|
|
338
|
+
}
|
|
339
|
+
/** Indexes an item a moment from now; a change before then waits again. */
|
|
340
|
+
export function queueIndex(key) {
|
|
341
|
+
const tenant = currentTenant();
|
|
342
|
+
const id = `${tenant}|${key}`;
|
|
343
|
+
const known = waiting.get(id);
|
|
344
|
+
if (known) {
|
|
345
|
+
clearTimeout(known.timer);
|
|
346
|
+
}
|
|
347
|
+
const timer = setTimeout(() => {
|
|
348
|
+
waiting.delete(id);
|
|
349
|
+
run(tenant, key);
|
|
350
|
+
}, INDEX_DELAY_MS);
|
|
351
|
+
timer.unref?.();
|
|
352
|
+
waiting.set(id, { tenant, key, timer });
|
|
353
|
+
}
|
|
354
|
+
/** Indexes what waits now, and resolves when the index has caught up. */
|
|
355
|
+
export async function flushIndex() {
|
|
356
|
+
while (waiting.size) {
|
|
357
|
+
for (const [id, entry] of [...waiting]) {
|
|
358
|
+
clearTimeout(entry.timer);
|
|
359
|
+
waiting.delete(id);
|
|
360
|
+
run(entry.tenant, entry.key);
|
|
361
|
+
}
|
|
362
|
+
await working;
|
|
363
|
+
}
|
|
364
|
+
await working;
|
|
365
|
+
}
|
|
366
|
+
// --- plugins' texts ---------------------------------------------------------------------
|
|
59
367
|
/** Puts a plugin's text into the space's index, in the place of what its key held. */
|
|
60
368
|
export async function putIndexEntry(plugin, entry) {
|
|
61
369
|
const space = await db.query.project.findFirst({ where: eq(schema.project.id, entry.space) });
|
|
@@ -63,25 +371,16 @@ export async function putIndexEntry(plugin, entry) {
|
|
|
63
371
|
throw new ServiceError("not_found", `There is no space "${entry.space}".`);
|
|
64
372
|
}
|
|
65
373
|
const text = `${entry.title.trim()}\n\n${entry.text.trim()}`;
|
|
66
|
-
const chunks = createSearchChunks({ doc_id: `${plugin}:${entry.key}`, text, ...CHUNK }).slice(0,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
ref: entry.key,
|
|
77
|
-
title: entry.title.trim().slice(0, 300),
|
|
78
|
-
link: entry.link ?? null,
|
|
79
|
-
idx: c.chunk_index,
|
|
80
|
-
text: c.text,
|
|
81
|
-
embedding: embedded ? vectorBytes(embedded.vectors[i + j]) : null,
|
|
82
|
-
model: embedded?.model ?? null,
|
|
83
|
-
})));
|
|
84
|
-
}
|
|
374
|
+
const chunks = createSearchChunks({ doc_id: `${plugin}:${entry.key}`, text, ...CHUNK }).slice(0, MAX_UNITS);
|
|
375
|
+
await writeUnits(and(eq(schema.projectChunk.projectId, entry.space), eq(schema.projectChunk.plugin, plugin), eq(schema.projectChunk.ref, entry.key)), chunks.map((c) => ({
|
|
376
|
+
projectId: entry.space,
|
|
377
|
+
plugin,
|
|
378
|
+
ref: entry.key,
|
|
379
|
+
title: entry.title.trim().slice(0, 300),
|
|
380
|
+
link: entry.link ?? null,
|
|
381
|
+
idx: c.chunk_index,
|
|
382
|
+
text: c.text,
|
|
383
|
+
})));
|
|
85
384
|
}
|
|
86
385
|
/** Takes a plugin's text out of the space's index; without a key, all of the plugin's texts. */
|
|
87
386
|
export async function removeIndexEntries(plugin, space, key) {
|
|
@@ -101,69 +400,308 @@ export async function hasIndexEntries(projectId) {
|
|
|
101
400
|
});
|
|
102
401
|
return Boolean(found);
|
|
103
402
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
403
|
+
// --- search -------------------------------------------------------------------------------
|
|
404
|
+
const STOP = new Set("der die das den dem des ein eine einen einem einer und oder aber ist sind war wie was wer wo für von mit auf aus bei zu zum zur im in an am es ich du wir ihr sie nicht auch gibt gibt's welche welcher welches the a an of to and or is are was for with on at by it this that what which".split(" "));
|
|
405
|
+
const wordsOf = (query) => [...new Set(query.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [])]
|
|
406
|
+
.filter((w) => !STOP.has(w))
|
|
407
|
+
.slice(0, 16);
|
|
108
408
|
/** The question's words as an FTS5 query: any of them, also as the start of a longer word. */
|
|
109
409
|
function keywordQuery(query) {
|
|
110
|
-
const words =
|
|
111
|
-
.filter((w) => !STOP.has(w))
|
|
112
|
-
.slice(0, 16);
|
|
410
|
+
const words = wordsOf(query);
|
|
113
411
|
return words.length ? words.map((w) => `"${w}"*`).join(" OR ") : null;
|
|
114
412
|
}
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
|
|
413
|
+
/**
|
|
414
|
+
* The question as a trigram query: each word of three letters or more anywhere in a unit, so
|
|
415
|
+
* "Förderhöhe" finds "Höchstförderhöhe"; a code with digits also as it was written ("WIN 6").
|
|
416
|
+
*/
|
|
417
|
+
function trigramQuery(query) {
|
|
418
|
+
const words = wordsOf(query).filter((w) => w.length >= 3);
|
|
419
|
+
const codes = (query.match(/[\p{L}\p{N}][\p{L}\p{N}\-./ ]*\d[\p{L}\p{N}\-./]*/gu) ?? [])
|
|
420
|
+
.map((c) => c.trim())
|
|
421
|
+
.filter((c) => c.length >= 3 && c.includes(" "));
|
|
422
|
+
const terms = [...new Set([...codes, ...words])].map((t) => `"${t.replace(/"/g, "")}"`);
|
|
423
|
+
return terms.length ? terms.join(" OR ") : null;
|
|
424
|
+
}
|
|
425
|
+
const COLUMNS = sql.raw("c.id, c.file_id, c.page_id, c.row_id, c.table_id, c.value_id, c.heading, c.sheet, c.plugin, c.ref, c.title, c.link, c.idx, c.text");
|
|
426
|
+
/** A unit of the space's own Wissen, or of a plugin's text while the tenant has that plugin. */
|
|
427
|
+
function searchable(plugins) {
|
|
118
428
|
return plugins.length
|
|
119
|
-
? sql `(
|
|
120
|
-
: sql
|
|
429
|
+
? sql `(c.plugin IS NULL OR c.plugin IN (${sql.join(plugins.map((p) => sql `${p}`), sql `, `)}))`
|
|
430
|
+
: sql `c.plugin IS NULL`;
|
|
121
431
|
}
|
|
122
|
-
|
|
123
|
-
|
|
432
|
+
const IN_LIMIT = 2000;
|
|
433
|
+
/** The units a filter leaves, as SQL while the lists are short; else null and checked after. */
|
|
434
|
+
function filtered(f) {
|
|
435
|
+
const lists = [
|
|
436
|
+
["c.page_id", f.pages],
|
|
437
|
+
["c.row_id", f.rows],
|
|
438
|
+
["c.file_id", f.files],
|
|
439
|
+
];
|
|
440
|
+
if (lists.reduce((n, [, s]) => n + s.size, 0) > IN_LIMIT) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
const parts = lists
|
|
444
|
+
.filter(([, s]) => s.size)
|
|
445
|
+
.map(([column, s]) => sql `${sql.raw(column)} IN (${sql.join([...s].map((id) => sql `${id}`), sql `, `)})`);
|
|
446
|
+
return parts.length ? sql `(${sql.join(parts, sql ` OR `)})` : sql `0`;
|
|
447
|
+
}
|
|
448
|
+
const leaves = (f, u) => (u.page_id !== null && f.pages.has(u.page_id)) ||
|
|
449
|
+
(u.row_id !== null && f.rows.has(u.row_id)) ||
|
|
450
|
+
(u.file_id !== null && f.files.has(u.file_id));
|
|
451
|
+
/** The key of the item a unit belongs to: hits are grouped by it. */
|
|
452
|
+
function itemOf(u) {
|
|
453
|
+
if (u.page_id) {
|
|
454
|
+
return `p:${u.page_id}`;
|
|
455
|
+
}
|
|
456
|
+
if (u.row_id) {
|
|
457
|
+
return `r:${u.row_id}`;
|
|
458
|
+
}
|
|
459
|
+
if (u.value_id) {
|
|
460
|
+
return `v:${u.value_id}`;
|
|
461
|
+
}
|
|
462
|
+
if (u.file_id) {
|
|
463
|
+
return `f:${u.file_id}`;
|
|
464
|
+
}
|
|
465
|
+
return `x:${u.plugin}:${u.ref}`;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Searches Wissen: items whose Kategorien pass `where`, then the units the keywords, the
|
|
469
|
+
* trigrams and the vectors rank best, merged by rank (RRF), then the classifier's judgement of
|
|
470
|
+
* each of the CANDIDATES best. Gives up to `limit` items with their best section. `check: false`
|
|
471
|
+
* leaves the classifier out.
|
|
472
|
+
*/
|
|
473
|
+
export async function searchKnowledge(projectId, query, opts = {}) {
|
|
474
|
+
const limit = Math.min(Math.max(1, opts.limit ?? 6), 12);
|
|
475
|
+
const filter = await filterItems(projectId, opts.where);
|
|
476
|
+
if (filter && !filter.size) {
|
|
477
|
+
return { hits: [], candidates: 0, checked: false, filtered: 0 };
|
|
478
|
+
}
|
|
124
479
|
const plugins = [...(await pluginIdsOf(currentTenant()))];
|
|
480
|
+
const inFilter = filter ? filtered(filter) : null;
|
|
481
|
+
// A filter keeps plugins' texts and Übersichten out: they have no Kategorien.
|
|
482
|
+
const where = and(sql `c.project_id = ${projectId}`, searchable(plugins), filter
|
|
483
|
+
? (inFilter ?? sql `(c.page_id IS NOT NULL OR c.row_id IS NOT NULL OR c.file_id IS NOT NULL)`)
|
|
484
|
+
: undefined);
|
|
485
|
+
const take = filter && !inFilter ? 300 : CANDIDATES;
|
|
125
486
|
const lists = [];
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
487
|
+
const words = keywordQuery(query);
|
|
488
|
+
if (words) {
|
|
128
489
|
lists.push(await db.all(sql `
|
|
129
|
-
SELECT
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
490
|
+
SELECT ${COLUMNS} FROM project_chunk_fts f JOIN project_chunk c ON c.id = f.rowid
|
|
491
|
+
WHERE project_chunk_fts MATCH ${words} AND ${where}
|
|
492
|
+
ORDER BY bm25(project_chunk_fts) LIMIT ${take}`));
|
|
493
|
+
}
|
|
494
|
+
const grams = trigramQuery(query);
|
|
495
|
+
if (grams) {
|
|
496
|
+
lists.push(await db
|
|
497
|
+
.all(sql `
|
|
498
|
+
SELECT ${COLUMNS} FROM project_chunk_tri f JOIN project_chunk c ON c.id = f.rowid
|
|
499
|
+
WHERE project_chunk_tri MATCH ${grams} AND ${where}
|
|
500
|
+
ORDER BY bm25(project_chunk_tri) LIMIT ${take}`)
|
|
501
|
+
.catch(() => []));
|
|
134
502
|
}
|
|
135
503
|
const embedded = await embed([query]).catch(() => null);
|
|
136
504
|
if (embedded) {
|
|
137
505
|
lists.push(await db.all(sql `
|
|
138
|
-
SELECT
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
ORDER BY vector_distance_cos(embedding, ${vectorBytes(embedded.vectors[0])}) LIMIT 24`));
|
|
506
|
+
SELECT ${COLUMNS} FROM project_chunk c
|
|
507
|
+
WHERE ${where} AND c.model = ${embedded.model}
|
|
508
|
+
ORDER BY vector_distance_cos(c.embedding, ${vectorBytes(embedded.vectors[0])}) LIMIT ${take}`));
|
|
142
509
|
}
|
|
143
|
-
// Reciprocal rank fusion: a
|
|
510
|
+
// Reciprocal rank fusion: a unit the lists rank high comes first.
|
|
144
511
|
const scored = new Map();
|
|
145
512
|
for (const list of lists) {
|
|
146
|
-
list
|
|
147
|
-
|
|
513
|
+
list
|
|
514
|
+
.filter((u) => !filter || inFilter || leaves(filter, u))
|
|
515
|
+
.slice(0, CANDIDATES)
|
|
516
|
+
.forEach((unit, rank) => {
|
|
517
|
+
const entry = scored.get(unit.id) ?? { unit, score: 0 };
|
|
148
518
|
entry.score += 1 / (60 + rank);
|
|
149
|
-
scored.set(
|
|
519
|
+
scored.set(unit.id, entry);
|
|
150
520
|
});
|
|
151
521
|
}
|
|
152
|
-
const
|
|
153
|
-
if (!
|
|
154
|
-
return [];
|
|
522
|
+
const candidates = [...scored.values()].sort((a, b) => b.score - a.score).slice(0, CANDIDATES);
|
|
523
|
+
if (!candidates.length) {
|
|
524
|
+
return { hits: [], candidates: 0, checked: false, filtered: filter?.size ?? null };
|
|
155
525
|
}
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
?
|
|
526
|
+
const described = await describe(projectId, candidates.map((c) => c.unit));
|
|
527
|
+
const judged = opts.check === false
|
|
528
|
+
? null
|
|
529
|
+
: await judgeRelevance(query, described.map((d) => ({
|
|
530
|
+
title: d.title,
|
|
531
|
+
path: d.path,
|
|
532
|
+
categories: d.categories,
|
|
533
|
+
heading: d.heading,
|
|
534
|
+
text: d.text,
|
|
535
|
+
})), opts.signal);
|
|
536
|
+
const ranked = described
|
|
537
|
+
.map((hit, i) => ({ hit: { ...hit, relevance: judged?.[i] ?? null }, rank: i }))
|
|
538
|
+
.filter(({ hit }) => hit.relevance === null || hit.relevance >= MIN_RELEVANCE)
|
|
539
|
+
.sort((a, b) => judged
|
|
540
|
+
? (b.hit.relevance ?? MIN_RELEVANCE) - (a.hit.relevance ?? MIN_RELEVANCE) || a.rank - b.rank
|
|
541
|
+
: a.rank - b.rank);
|
|
542
|
+
// One hit per item: its best section.
|
|
543
|
+
const seen = new Set();
|
|
544
|
+
const hits = [];
|
|
545
|
+
for (const { hit } of ranked) {
|
|
546
|
+
if (seen.has(hit.key)) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
seen.add(hit.key);
|
|
550
|
+
hits.push(hit);
|
|
551
|
+
if (hits.length >= limit) {
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
hits,
|
|
557
|
+
candidates: candidates.length,
|
|
558
|
+
checked: judged !== null,
|
|
559
|
+
filtered: filter?.size ?? null,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/** What a step and the studio see of each unit: the item's path, title and Kategorien. */
|
|
563
|
+
async function describe(projectId, units) {
|
|
564
|
+
const pageIds = [...new Set(units.flatMap((u) => (u.page_id ? [u.page_id] : [])))];
|
|
565
|
+
const rowIds = [...new Set(units.flatMap((u) => (u.row_id ? [u.row_id] : [])))];
|
|
566
|
+
const fileIds = [...new Set(units.flatMap((u) => (u.file_id ? [u.file_id] : [])))];
|
|
567
|
+
const valueIds = [...new Set(units.flatMap((u) => (u.value_id ? [u.value_id] : [])))];
|
|
568
|
+
const [tree, tPaths, fPaths, categories, rows, files, values] = await Promise.all([
|
|
569
|
+
pageIds.length ? pageTree(projectId) : new Map(),
|
|
570
|
+
rowIds.length ? tablePaths(projectId) : new Map(),
|
|
571
|
+
fileIds.length ? filePaths(projectId) : new Map(),
|
|
572
|
+
categoryRows(projectId),
|
|
573
|
+
rowIds.length
|
|
574
|
+
? db.query.spaceTableRow.findMany({
|
|
575
|
+
where: inArray(schema.spaceTableRow.id, rowIds),
|
|
576
|
+
columns: { id: true, tableId: true },
|
|
577
|
+
})
|
|
578
|
+
: [],
|
|
579
|
+
fileIds.length
|
|
580
|
+
? db.query.projectFile.findMany({
|
|
581
|
+
where: inArray(schema.projectFile.id, fileIds),
|
|
582
|
+
columns: { id: true, name: true },
|
|
583
|
+
})
|
|
584
|
+
: [],
|
|
585
|
+
valueIds.length
|
|
586
|
+
? db.query.spaceCategoryValue.findMany({
|
|
587
|
+
where: inArray(schema.spaceCategoryValue.id, valueIds),
|
|
588
|
+
})
|
|
589
|
+
: [],
|
|
590
|
+
]);
|
|
591
|
+
const tableIds = [...new Set(rows.map((r) => r.tableId))];
|
|
592
|
+
const tables = tableIds.length
|
|
593
|
+
? await db.query.spaceTable.findMany({
|
|
594
|
+
where: inArray(schema.spaceTable.id, tableIds),
|
|
595
|
+
columns: { id: true, title: true },
|
|
596
|
+
})
|
|
159
597
|
: [];
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
598
|
+
const cats = await itemCategoriesOf([
|
|
599
|
+
...pageIds.map((pageId) => ({ pageId })),
|
|
600
|
+
...rowIds.map((rowId) => ({ rowId })),
|
|
601
|
+
...tableIds.map((tableId) => ({ tableId })),
|
|
602
|
+
...fileIds.map((fileId) => ({ fileId })),
|
|
603
|
+
]);
|
|
604
|
+
// The unit's text without the head it was indexed with: the step gets that as fields.
|
|
605
|
+
const bodyOf = (u) => {
|
|
606
|
+
const parts = u.text.split("\n\n");
|
|
607
|
+
if (!(u.page_id || u.row_id || u.value_id) || parts.length < 2) {
|
|
608
|
+
return u.text;
|
|
609
|
+
}
|
|
610
|
+
const rest = parts.slice(1);
|
|
611
|
+
return (rest[0].startsWith("## ") ? rest.slice(1) : rest).join("\n\n");
|
|
612
|
+
};
|
|
613
|
+
return units.map((u) => {
|
|
614
|
+
const base = {
|
|
615
|
+
key: itemOf(u),
|
|
616
|
+
heading: u.heading,
|
|
617
|
+
sheet: u.sheet,
|
|
618
|
+
rowId: u.row_id,
|
|
619
|
+
text: bodyOf(u),
|
|
620
|
+
relevance: null,
|
|
621
|
+
link: u.link,
|
|
622
|
+
plugin: u.plugin,
|
|
623
|
+
};
|
|
624
|
+
if (u.page_id) {
|
|
625
|
+
const node = tree.get(u.page_id);
|
|
626
|
+
return {
|
|
627
|
+
...base,
|
|
628
|
+
kind: "page",
|
|
629
|
+
open: `p:${u.page_id}`,
|
|
630
|
+
path: node?.path ?? "",
|
|
631
|
+
title: node?.titles.join(" › ") ?? "",
|
|
632
|
+
categories: categoryLines(categories, cats.get(`p:${u.page_id}`)),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (u.row_id) {
|
|
636
|
+
const tableId = rows.find((r) => r.id === u.row_id)?.tableId ?? "";
|
|
637
|
+
return {
|
|
638
|
+
...base,
|
|
639
|
+
kind: "table",
|
|
640
|
+
open: `t:${tableId}`,
|
|
641
|
+
path: tPaths.get(tableId) ?? "",
|
|
642
|
+
title: tables.find((t) => t.id === tableId)?.title ?? "",
|
|
643
|
+
categories: [
|
|
644
|
+
...categoryLines(categories, cats.get(`t:${tableId}`)),
|
|
645
|
+
...categoryLines(categories, cats.get(`r:${u.row_id}`)),
|
|
646
|
+
].filter((l, i, all) => all.indexOf(l) === i),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (u.value_id) {
|
|
650
|
+
const value = values.find((v) => v.id === u.value_id);
|
|
651
|
+
const category = categories.find((c) => c.id === value?.categoryId);
|
|
652
|
+
return {
|
|
653
|
+
...base,
|
|
654
|
+
kind: "summary",
|
|
655
|
+
open: `v:${u.value_id}`,
|
|
656
|
+
path: "",
|
|
657
|
+
title: `${category?.name}: ${value?.value} — Übersicht`,
|
|
658
|
+
categories: [],
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (u.file_id) {
|
|
662
|
+
return {
|
|
663
|
+
...base,
|
|
664
|
+
kind: "file",
|
|
665
|
+
open: `f:${u.file_id}`,
|
|
666
|
+
path: fPaths.get(u.file_id) ?? "",
|
|
667
|
+
title: files.find((f) => f.id === u.file_id)?.name ?? "",
|
|
668
|
+
categories: categoryLines(categories, cats.get(`f:${u.file_id}`)),
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
return { ...base, kind: "plugin", open: null, path: "", title: u.title ?? "", categories: [] };
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
/** Puts every item of a space into the index again: after a sync, or a change of embedding model. */
|
|
675
|
+
export async function reindexSpace(projectId) {
|
|
676
|
+
const [pages, tables, files, values] = await Promise.all([
|
|
677
|
+
db.query.spacePage.findMany({
|
|
678
|
+
where: and(eq(schema.spacePage.projectId, projectId), isNull(schema.spacePage.wizardId)),
|
|
679
|
+
columns: { id: true },
|
|
680
|
+
}),
|
|
681
|
+
db.query.spaceTable.findMany({
|
|
682
|
+
where: and(eq(schema.spaceTable.projectId, projectId), isNull(schema.spaceTable.wizardId)),
|
|
683
|
+
columns: { id: true },
|
|
684
|
+
}),
|
|
685
|
+
db.query.projectFile.findMany({
|
|
686
|
+
where: and(eq(schema.projectFile.projectId, projectId), eq(schema.projectFile.kind, "document")),
|
|
687
|
+
columns: { id: true },
|
|
688
|
+
}),
|
|
689
|
+
db
|
|
690
|
+
.select({ id: schema.spaceCategoryValue.id })
|
|
691
|
+
.from(schema.spaceCategoryValue)
|
|
692
|
+
.innerJoin(schema.spaceCategory, eq(schema.spaceCategory.id, schema.spaceCategoryValue.categoryId))
|
|
693
|
+
.where(and(eq(schema.spaceCategory.projectId, projectId), isNotNull(schema.spaceCategoryValue.summary))),
|
|
694
|
+
]);
|
|
695
|
+
for (const p of pages) {
|
|
696
|
+
queueIndex(`p:${p.id}`);
|
|
697
|
+
}
|
|
698
|
+
for (const t of tables) {
|
|
699
|
+
queueIndex(`t:${t.id}`);
|
|
700
|
+
}
|
|
701
|
+
for (const f of files) {
|
|
702
|
+
queueIndex(`f:${f.id}`);
|
|
703
|
+
}
|
|
704
|
+
for (const v of values) {
|
|
705
|
+
queueIndex(`v:${v.id}`);
|
|
706
|
+
}
|
|
169
707
|
}
|