@opengeni/documents 0.2.0

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.
@@ -0,0 +1,124 @@
1
+ import { Settings } from '@opengeni/config';
2
+ import { FileAsset, AddDocumentRequest, Document, CreateDocumentBaseRequest, DocumentBase, DocumentSearchResult } from '@opengeni/contracts';
3
+ import { Database } from '@opengeni/db';
4
+ import { ObjectStorage } from '@opengeni/storage';
5
+
6
+ declare const DEFAULT_DOCUMENT_PARSER = "liteparse";
7
+ declare const DEFAULT_DOCUMENT_EMBEDDING_MODEL = "text-embedding-3-large";
8
+ declare const DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS = 3072;
9
+ declare const DEFAULT_DOCUMENT_CHUNK_SIZE = 1200;
10
+ declare const DEFAULT_DOCUMENT_CHUNK_OVERLAP = 160;
11
+ type ParsedDocument = {
12
+ text: string;
13
+ metadata?: Record<string, unknown>;
14
+ };
15
+ type DocumentChunk = {
16
+ text: string;
17
+ metadata: Record<string, unknown>;
18
+ };
19
+ type DocumentParser = {
20
+ name: string;
21
+ parse: (bytes: Uint8Array, file: FileAsset) => Promise<ParsedDocument>;
22
+ };
23
+ type DocumentChunker = {
24
+ chunk: (parsed: ParsedDocument, file: FileAsset) => DocumentChunk[];
25
+ };
26
+ type DocumentEmbedder = {
27
+ model: string;
28
+ dimensions: number;
29
+ embedMany: (texts: string[]) => Promise<number[][]>;
30
+ embedQuery: (text: string) => Promise<number[]>;
31
+ };
32
+ type DocumentServices = {
33
+ parser: DocumentParser;
34
+ chunker: DocumentChunker;
35
+ embedder: DocumentEmbedder;
36
+ };
37
+ type DocumentIndexHooks = {
38
+ beforeEmbed?: (input: {
39
+ accountId: string;
40
+ workspaceId: string;
41
+ documentId: string;
42
+ chunkCount: number;
43
+ }) => Promise<void>;
44
+ };
45
+ declare class LiteParseDocumentParser implements DocumentParser {
46
+ readonly name = "liteparse";
47
+ private parseQueue;
48
+ parse(bytes: Uint8Array, file: FileAsset): Promise<ParsedDocument>;
49
+ private parseWithLiteParse;
50
+ private enqueueParse;
51
+ }
52
+ declare class RecursiveTextChunker implements DocumentChunker {
53
+ private readonly maxChars;
54
+ private readonly overlapChars;
55
+ constructor(maxChars?: number, overlapChars?: number);
56
+ chunk(parsed: ParsedDocument, file: FileAsset): DocumentChunk[];
57
+ }
58
+ declare class OpenAIEmbeddingProvider implements DocumentEmbedder {
59
+ private client;
60
+ private readonly apiKey;
61
+ private readonly baseURL;
62
+ constructor(args: {
63
+ apiKey?: string | undefined;
64
+ baseURL?: string | undefined;
65
+ defaultHeaders?: Record<string, string> | undefined;
66
+ defaultQuery?: Record<string, string> | undefined;
67
+ model?: string | undefined;
68
+ dimensions?: number | undefined;
69
+ });
70
+ readonly model: string;
71
+ readonly dimensions: number;
72
+ private readonly defaultHeaders;
73
+ private readonly defaultQuery;
74
+ embedMany(texts: string[]): Promise<number[][]>;
75
+ embedQuery(text: string): Promise<number[]>;
76
+ private openai;
77
+ }
78
+ declare class DeterministicEmbeddingProvider implements DocumentEmbedder {
79
+ readonly dimensions: number;
80
+ readonly model: string;
81
+ constructor(dimensions?: number, model?: string);
82
+ embedMany(texts: string[]): Promise<number[][]>;
83
+ embedQuery(text: string): Promise<number[]>;
84
+ }
85
+ declare function createDocumentServices(settings?: Settings, overrides?: Partial<DocumentServices>): DocumentServices;
86
+ declare function documentOpenAIEmbeddingConfig(settings?: Settings): {
87
+ apiKey?: string | undefined;
88
+ baseURL?: string | undefined;
89
+ defaultHeaders?: Record<string, string> | undefined;
90
+ defaultQuery?: Record<string, string> | undefined;
91
+ };
92
+ declare function createDocumentBase(db: Database, input: CreateDocumentBaseRequest & {
93
+ accountId: string;
94
+ workspaceId: string;
95
+ }): Promise<DocumentBase>;
96
+ declare function listDocumentBases(db: Database, workspaceId: string): Promise<DocumentBase[]>;
97
+ declare function getDocumentBase(db: Database, workspaceId: string, baseId: string): Promise<DocumentBase | null>;
98
+ declare function addDocumentToBase(db: Database, input: AddDocumentRequest & {
99
+ accountId: string;
100
+ workspaceId: string;
101
+ baseId: string;
102
+ }): Promise<Document>;
103
+ declare function deleteDocumentFromBase(db: Database, input: {
104
+ accountId: string;
105
+ workspaceId: string;
106
+ baseId: string;
107
+ documentId: string;
108
+ }): Promise<void>;
109
+ declare function listDocuments(db: Database, workspaceId: string, baseId: string): Promise<Document[]>;
110
+ declare function getDocument(db: Database, workspaceId: string, documentId: string): Promise<Document | null>;
111
+ declare function queueDocumentForReindex(db: Database, workspaceId: string, documentId: string): Promise<Document>;
112
+ declare function indexDocumentNow(db: Database, objectStorage: ObjectStorage, workspaceId: string, documentId: string, services?: DocumentServices, hooks?: DocumentIndexHooks): Promise<Document>;
113
+ declare function searchDocuments(db: Database, input: {
114
+ workspaceId: string;
115
+ query: string;
116
+ baseIds?: string[];
117
+ limit?: number;
118
+ }, services?: Pick<DocumentServices, "embedder">): Promise<DocumentSearchResult[]>;
119
+ declare function getDocumentChunk(db: Database, workspaceId: string, chunkId: string): Promise<DocumentSearchResult | null>;
120
+ declare function parseDocumentBytes(bytes: Uint8Array, file: FileAsset, parser?: DocumentParser): Promise<ParsedDocument>;
121
+ declare function chunkText(text: string, maxChars?: number, overlapChars?: number): string[];
122
+ declare function deterministicEmbedding(text: string, dimensions?: number): number[];
123
+
124
+ export { DEFAULT_DOCUMENT_CHUNK_OVERLAP, DEFAULT_DOCUMENT_CHUNK_SIZE, DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS, DEFAULT_DOCUMENT_EMBEDDING_MODEL, DEFAULT_DOCUMENT_PARSER, DeterministicEmbeddingProvider, type DocumentChunk, type DocumentChunker, type DocumentEmbedder, type DocumentIndexHooks, type DocumentParser, type DocumentServices, LiteParseDocumentParser, OpenAIEmbeddingProvider, type ParsedDocument, RecursiveTextChunker, addDocumentToBase, chunkText, createDocumentBase, createDocumentServices, deleteDocumentFromBase, deterministicEmbedding, documentOpenAIEmbeddingConfig, getDocument, getDocumentBase, getDocumentChunk, indexDocumentNow, listDocumentBases, listDocuments, parseDocumentBytes, queueDocumentForReindex, searchDocuments };
package/dist/index.js ADDED
@@ -0,0 +1,557 @@
1
+ // src/index.ts
2
+ import { requireFile, withRlsContext, withWorkspaceRls } from "@opengeni/db";
3
+ import * as schema from "@opengeni/db/schema";
4
+ import { LiteParse } from "@llamaindex/liteparse";
5
+ import { and, asc, desc, eq, inArray, sql } from "drizzle-orm";
6
+ import OpenAI from "openai";
7
+ var DEFAULT_DOCUMENT_PARSER = "liteparse";
8
+ var DEFAULT_DOCUMENT_EMBEDDING_MODEL = "text-embedding-3-large";
9
+ var DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS = 3072;
10
+ var DEFAULT_DOCUMENT_CHUNK_SIZE = 1200;
11
+ var DEFAULT_DOCUMENT_CHUNK_OVERLAP = 160;
12
+ var LiteParseDocumentParser = class {
13
+ name = DEFAULT_DOCUMENT_PARSER;
14
+ parseQueue = Promise.resolve();
15
+ async parse(bytes, file) {
16
+ const text = isTextLike(file) ? Buffer.from(bytes).toString("utf8").replace(/\0/g, " ").trim() : await this.parseWithLiteParse(bytes);
17
+ if (!text.trim()) {
18
+ throw new Error(`Parsed document is empty: ${file.filename}`);
19
+ }
20
+ return {
21
+ text: text.trim(),
22
+ metadata: {
23
+ parser: this.name,
24
+ filename: file.filename,
25
+ contentType: file.contentType
26
+ }
27
+ };
28
+ }
29
+ async parseWithLiteParse(bytes) {
30
+ return await this.enqueueParse(async () => {
31
+ const parser = new LiteParse({ ocrEnabled: true, numWorkers: 1 });
32
+ const result = await parser.parse(Buffer.from(bytes), true);
33
+ const text = typeof result?.text === "string" ? result.text : "";
34
+ return text.replace(/\0/g, " ").trim();
35
+ });
36
+ }
37
+ async enqueueParse(task) {
38
+ const previous = this.parseQueue;
39
+ let release = () => void 0;
40
+ this.parseQueue = new Promise((resolve) => {
41
+ release = resolve;
42
+ });
43
+ await previous.catch(() => void 0);
44
+ try {
45
+ return await task();
46
+ } finally {
47
+ release();
48
+ }
49
+ }
50
+ };
51
+ var RecursiveTextChunker = class {
52
+ constructor(maxChars = DEFAULT_DOCUMENT_CHUNK_SIZE, overlapChars = DEFAULT_DOCUMENT_CHUNK_OVERLAP) {
53
+ this.maxChars = maxChars;
54
+ this.overlapChars = overlapChars;
55
+ if (overlapChars >= maxChars) {
56
+ throw new Error("document chunk overlap must be smaller than chunk size");
57
+ }
58
+ }
59
+ maxChars;
60
+ overlapChars;
61
+ chunk(parsed, file) {
62
+ return chunkText(parsed.text, this.maxChars, this.overlapChars).map((text, index) => ({
63
+ text,
64
+ metadata: {
65
+ ...parsed.metadata,
66
+ filename: file.filename,
67
+ contentType: file.contentType,
68
+ chunkIndex: index
69
+ }
70
+ }));
71
+ }
72
+ };
73
+ var OpenAIEmbeddingProvider = class {
74
+ client = null;
75
+ apiKey;
76
+ baseURL;
77
+ constructor(args) {
78
+ this.apiKey = args.apiKey ?? process.env.OPENAI_API_KEY;
79
+ this.baseURL = args.baseURL;
80
+ this.defaultHeaders = args.defaultHeaders;
81
+ this.defaultQuery = args.defaultQuery;
82
+ this.model = args.model ?? DEFAULT_DOCUMENT_EMBEDDING_MODEL;
83
+ this.dimensions = args.dimensions ?? DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS;
84
+ }
85
+ model;
86
+ dimensions;
87
+ defaultHeaders;
88
+ defaultQuery;
89
+ async embedMany(texts) {
90
+ if (texts.length === 0) return [];
91
+ const out = [];
92
+ for (let start = 0; start < texts.length; start += 64) {
93
+ const batch = texts.slice(start, start + 64);
94
+ const response = await this.openai().embeddings.create({
95
+ model: this.model,
96
+ input: batch,
97
+ dimensions: this.dimensions
98
+ });
99
+ for (const item of response.data) {
100
+ out.push(validateEmbedding(item.embedding, this.dimensions, this.model));
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ async embedQuery(text) {
106
+ const [embedding] = await this.embedMany([text]);
107
+ if (!embedding) {
108
+ throw new Error("Embedding provider returned no query embedding");
109
+ }
110
+ return embedding;
111
+ }
112
+ openai() {
113
+ if (!this.apiKey) {
114
+ throw new Error("OpenAI document embeddings require an API key");
115
+ }
116
+ this.client ??= new OpenAI({
117
+ apiKey: this.apiKey,
118
+ ...this.baseURL ? { baseURL: this.baseURL } : {},
119
+ ...this.defaultQuery ? { defaultQuery: this.defaultQuery } : {},
120
+ ...this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}
121
+ });
122
+ return this.client;
123
+ }
124
+ };
125
+ var DeterministicEmbeddingProvider = class {
126
+ constructor(dimensions = DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS, model = `deterministic-local-${dimensions}`) {
127
+ this.dimensions = dimensions;
128
+ this.model = model;
129
+ }
130
+ dimensions;
131
+ model;
132
+ async embedMany(texts) {
133
+ return texts.map((text) => deterministicEmbedding(text, this.dimensions));
134
+ }
135
+ async embedQuery(text) {
136
+ return deterministicEmbedding(text, this.dimensions);
137
+ }
138
+ };
139
+ function createDocumentServices(settings, overrides = {}) {
140
+ const dimensions = settings?.documentEmbeddingDimensions ?? DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS;
141
+ const openAIEmbeddingConfig = documentOpenAIEmbeddingConfig(settings);
142
+ return {
143
+ parser: overrides.parser ?? new LiteParseDocumentParser(),
144
+ chunker: overrides.chunker ?? new RecursiveTextChunker(
145
+ settings?.documentChunkSize ?? DEFAULT_DOCUMENT_CHUNK_SIZE,
146
+ settings?.documentChunkOverlap ?? DEFAULT_DOCUMENT_CHUNK_OVERLAP
147
+ ),
148
+ embedder: overrides.embedder ?? (settings?.documentEmbeddingProvider === "deterministic" ? new DeterministicEmbeddingProvider(dimensions, settings.documentEmbeddingModel) : new OpenAIEmbeddingProvider({
149
+ ...openAIEmbeddingConfig,
150
+ model: settings?.documentEmbeddingModel ?? DEFAULT_DOCUMENT_EMBEDDING_MODEL,
151
+ dimensions
152
+ }))
153
+ };
154
+ }
155
+ function documentOpenAIEmbeddingConfig(settings) {
156
+ if (!settings) return {};
157
+ if (settings.documentEmbeddingApiKey || settings.documentEmbeddingBaseUrl) {
158
+ return {
159
+ apiKey: settings.documentEmbeddingApiKey ?? settings.openaiApiKey ?? settings.azureOpenaiApiKey,
160
+ baseURL: settings.documentEmbeddingBaseUrl ?? settings.openaiBaseUrl ?? settings.azureOpenaiBaseUrl
161
+ };
162
+ }
163
+ if (settings.openaiProvider === "azure") {
164
+ const baseURL = settings.azureOpenaiBaseUrl ?? azureDeploymentBaseUrl(settings);
165
+ return {
166
+ apiKey: settings.azureOpenaiApiKey ?? settings.azureOpenaiAdToken ?? "azure-ad-token",
167
+ baseURL,
168
+ defaultQuery: azureOpenAIDefaultQuery(settings, baseURL),
169
+ defaultHeaders: settings.azureOpenaiAdToken && !settings.azureOpenaiApiKey ? { Authorization: `Bearer ${settings.azureOpenaiAdToken}` } : void 0
170
+ };
171
+ }
172
+ return {
173
+ apiKey: settings.openaiApiKey,
174
+ baseURL: settings.openaiBaseUrl
175
+ };
176
+ }
177
+ function azureDeploymentBaseUrl(settings) {
178
+ const endpoint = settings.azureOpenaiEndpoint?.replace(/\/+$/, "");
179
+ if (!endpoint || !settings.azureOpenaiDeployment) {
180
+ throw new Error("Azure OpenAI endpoint/deployment settings are incomplete");
181
+ }
182
+ return `${endpoint}/openai/deployments/${settings.azureOpenaiDeployment}`;
183
+ }
184
+ function azureOpenAIDefaultQuery(settings, baseURL) {
185
+ if (!settings.azureOpenaiApiVersion) return void 0;
186
+ const normalized = baseURL.replace(/\/+$/, "").toLowerCase();
187
+ if (normalized.endsWith("/openai/v1")) {
188
+ return void 0;
189
+ }
190
+ return { "api-version": settings.azureOpenaiApiVersion };
191
+ }
192
+ async function createDocumentBase(db, input) {
193
+ return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
194
+ const [row] = await scopedDb.insert(schema.documentBases).values({
195
+ accountId: input.accountId,
196
+ workspaceId: input.workspaceId,
197
+ name: input.name.trim(),
198
+ description: input.description?.trim() || null
199
+ }).returning();
200
+ if (!row) throw new Error("Failed to create document base");
201
+ return mapDocumentBase(row);
202
+ });
203
+ }
204
+ async function listDocumentBases(db, workspaceId) {
205
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
206
+ const rows = await scopedDb.select().from(schema.documentBases).where(eq(schema.documentBases.workspaceId, workspaceId)).orderBy(desc(schema.documentBases.createdAt));
207
+ return rows.map(mapDocumentBase);
208
+ });
209
+ }
210
+ async function getDocumentBase(db, workspaceId, baseId) {
211
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
212
+ const [row] = await scopedDb.select().from(schema.documentBases).where(and(eq(schema.documentBases.workspaceId, workspaceId), eq(schema.documentBases.id, baseId))).limit(1);
213
+ return row ? mapDocumentBase(row) : null;
214
+ });
215
+ }
216
+ async function addDocumentToBase(db, input) {
217
+ return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
218
+ const base = await getDocumentBase(scopedDb, input.workspaceId, input.baseId);
219
+ if (!base) throw new Error(`Document base not found: ${input.baseId}`);
220
+ const file = await requireReadyFile(scopedDb, input.workspaceId, input.fileId);
221
+ const now = /* @__PURE__ */ new Date();
222
+ const [existing] = await scopedDb.select().from(schema.documents).where(and(eq(schema.documents.workspaceId, input.workspaceId), eq(schema.documents.baseId, input.baseId), eq(schema.documents.fileId, input.fileId))).limit(1);
223
+ if (existing) {
224
+ return mapDocument(existing);
225
+ }
226
+ const [row] = await scopedDb.insert(schema.documents).values({
227
+ accountId: input.accountId,
228
+ workspaceId: input.workspaceId,
229
+ baseId: input.baseId,
230
+ fileId: input.fileId,
231
+ status: "queued",
232
+ title: file.filename,
233
+ parser: DEFAULT_DOCUMENT_PARSER,
234
+ updatedAt: now
235
+ }).returning();
236
+ if (!row) throw new Error("Failed to create document");
237
+ return mapDocument(row);
238
+ });
239
+ }
240
+ async function deleteDocumentFromBase(db, input) {
241
+ await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
242
+ const [document] = await scopedDb.select().from(schema.documents).where(and(eq(schema.documents.workspaceId, input.workspaceId), eq(schema.documents.id, input.documentId))).limit(1);
243
+ if (!document) {
244
+ throw new Error(`Document not found: ${input.documentId}`);
245
+ }
246
+ if (document.baseId !== input.baseId) {
247
+ throw new Error(`Document not found: ${input.documentId}`);
248
+ }
249
+ await scopedDb.delete(schema.documents).where(and(eq(schema.documents.workspaceId, input.workspaceId), eq(schema.documents.id, input.documentId)));
250
+ });
251
+ }
252
+ async function listDocuments(db, workspaceId, baseId) {
253
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
254
+ const rows = await scopedDb.select().from(schema.documents).where(and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.baseId, baseId))).orderBy(asc(schema.documents.createdAt));
255
+ return rows.map(mapDocument);
256
+ });
257
+ }
258
+ async function getDocument(db, workspaceId, documentId) {
259
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
260
+ const [row] = await scopedDb.select().from(schema.documents).where(and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId))).limit(1);
261
+ return row ? mapDocument(row) : null;
262
+ });
263
+ }
264
+ async function queueDocumentForReindex(db, workspaceId, documentId) {
265
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
266
+ const [row] = await scopedDb.update(schema.documents).set({
267
+ status: "queued",
268
+ error: null,
269
+ updatedAt: /* @__PURE__ */ new Date()
270
+ }).where(and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId))).returning();
271
+ if (!row) throw new Error(`Document not found: ${documentId}`);
272
+ return mapDocument(row);
273
+ });
274
+ }
275
+ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, services = createDocumentServices(), hooks = {}) {
276
+ const [document] = await withWorkspaceRls(
277
+ db,
278
+ workspaceId,
279
+ async (scopedDb) => await scopedDb.select().from(schema.documents).where(and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId))).limit(1)
280
+ );
281
+ if (!document) throw new Error(`Document not found: ${documentId}`);
282
+ const file = await requireReadyFile(db, workspaceId, document.fileId);
283
+ await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
284
+ await scopedDb.update(schema.documents).set({
285
+ status: "indexing",
286
+ parser: services.parser.name,
287
+ error: null,
288
+ updatedAt: /* @__PURE__ */ new Date()
289
+ }).where(and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId)));
290
+ });
291
+ try {
292
+ const bytes = await objectStorage.getFileBytes(file);
293
+ const parsed = await services.parser.parse(bytes, file);
294
+ const chunks = services.chunker.chunk(parsed, file);
295
+ await hooks.beforeEmbed?.({
296
+ accountId: document.accountId,
297
+ workspaceId: document.workspaceId,
298
+ documentId,
299
+ chunkCount: chunks.length
300
+ });
301
+ const embeddings = await services.embedder.embedMany(chunks.map((chunk) => chunk.text));
302
+ if (embeddings.length !== chunks.length) {
303
+ throw new Error(`Embedding provider returned ${embeddings.length} embeddings for ${chunks.length} chunks`);
304
+ }
305
+ await withWorkspaceRls(db, workspaceId, async (scopedDb) => await scopedDb.transaction(async (tx) => {
306
+ await tx.delete(schema.documentChunks).where(and(eq(schema.documentChunks.workspaceId, workspaceId), eq(schema.documentChunks.documentId, documentId)));
307
+ if (chunks.length > 0) {
308
+ await tx.insert(schema.documentChunks).values(chunks.map((chunk, index) => ({
309
+ accountId: document.accountId,
310
+ workspaceId: document.workspaceId,
311
+ documentId,
312
+ baseId: document.baseId,
313
+ fileId: file.id,
314
+ chunkIndex: index,
315
+ text: chunk.text,
316
+ metadata: chunk.metadata,
317
+ embedding: validateEmbedding(embeddings[index] ?? [], services.embedder.dimensions, services.embedder.model),
318
+ embeddingModel: services.embedder.model
319
+ })));
320
+ }
321
+ await tx.update(schema.documents).set({
322
+ status: "ready",
323
+ parser: services.parser.name,
324
+ chunkCount: chunks.length,
325
+ error: null,
326
+ updatedAt: /* @__PURE__ */ new Date()
327
+ }).where(and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId)));
328
+ }));
329
+ } catch (error) {
330
+ const [failed] = await withWorkspaceRls(
331
+ db,
332
+ workspaceId,
333
+ async (scopedDb) => await scopedDb.update(schema.documents).set({
334
+ status: "failed",
335
+ error: error instanceof Error ? error.message : String(error),
336
+ updatedAt: /* @__PURE__ */ new Date()
337
+ }).where(and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId))).returning()
338
+ );
339
+ if (!failed) throw error;
340
+ return mapDocument(failed);
341
+ }
342
+ const updated = await getDocument(db, workspaceId, documentId);
343
+ if (!updated) throw new Error(`Document disappeared after indexing: ${documentId}`);
344
+ return updated;
345
+ }
346
+ async function searchDocuments(db, input, services = createDocumentServices()) {
347
+ const limit = Math.min(Math.max(input.limit ?? 5, 1), 20);
348
+ const queryEmbedding = await services.embedder.embedQuery(input.query);
349
+ validateEmbedding(queryEmbedding, services.embedder.dimensions, services.embedder.model);
350
+ const distance = sql`${schema.documentChunks.embedding} <=> ${vectorLiteral(queryEmbedding)}::vector`;
351
+ const rows = await withWorkspaceRls(
352
+ db,
353
+ input.workspaceId,
354
+ async (scopedDb) => await scopedDb.select({
355
+ chunkId: schema.documentChunks.id,
356
+ documentId: schema.documentChunks.documentId,
357
+ baseId: schema.documentChunks.baseId,
358
+ fileId: schema.documentChunks.fileId,
359
+ title: schema.documents.title,
360
+ text: schema.documentChunks.text,
361
+ chunkIndex: schema.documentChunks.chunkIndex,
362
+ metadata: schema.documentChunks.metadata,
363
+ distance
364
+ }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(and(
365
+ eq(schema.documents.status, "ready"),
366
+ eq(schema.documentChunks.workspaceId, input.workspaceId),
367
+ eq(schema.documentChunks.embeddingModel, services.embedder.model),
368
+ input.baseIds && input.baseIds.length > 0 ? inArray(schema.documentChunks.baseId, input.baseIds) : void 0
369
+ )).orderBy(distance).limit(limit)
370
+ );
371
+ return rows.map((row) => ({
372
+ chunkId: row.chunkId,
373
+ workspaceId: input.workspaceId,
374
+ documentId: row.documentId,
375
+ baseId: row.baseId,
376
+ fileId: row.fileId,
377
+ title: row.title,
378
+ text: row.text,
379
+ score: 1 / (1 + Number(row.distance)),
380
+ chunkIndex: row.chunkIndex,
381
+ metadata: row.metadata
382
+ }));
383
+ }
384
+ async function getDocumentChunk(db, workspaceId, chunkId) {
385
+ const [row] = await withWorkspaceRls(
386
+ db,
387
+ workspaceId,
388
+ async (scopedDb) => await scopedDb.select({
389
+ chunkId: schema.documentChunks.id,
390
+ documentId: schema.documentChunks.documentId,
391
+ baseId: schema.documentChunks.baseId,
392
+ fileId: schema.documentChunks.fileId,
393
+ title: schema.documents.title,
394
+ text: schema.documentChunks.text,
395
+ chunkIndex: schema.documentChunks.chunkIndex,
396
+ metadata: schema.documentChunks.metadata
397
+ }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(and(eq(schema.documentChunks.workspaceId, workspaceId), eq(schema.documentChunks.id, chunkId), eq(schema.documents.status, "ready"))).limit(1)
398
+ );
399
+ if (!row) return null;
400
+ return {
401
+ chunkId: row.chunkId,
402
+ workspaceId,
403
+ documentId: row.documentId,
404
+ baseId: row.baseId,
405
+ fileId: row.fileId,
406
+ title: row.title,
407
+ text: row.text,
408
+ score: 1,
409
+ chunkIndex: row.chunkIndex,
410
+ metadata: row.metadata
411
+ };
412
+ }
413
+ async function parseDocumentBytes(bytes, file, parser = new LiteParseDocumentParser()) {
414
+ return await parser.parse(bytes, file);
415
+ }
416
+ function chunkText(text, maxChars = DEFAULT_DOCUMENT_CHUNK_SIZE, overlapChars = DEFAULT_DOCUMENT_CHUNK_OVERLAP) {
417
+ if (overlapChars >= maxChars) {
418
+ throw new Error("chunk overlap must be smaller than chunk size");
419
+ }
420
+ const normalized = text.replace(/\r\n/g, "\n").replace(/[ \t]+/g, " ").trim();
421
+ if (!normalized) return [];
422
+ const paragraphs = normalized.split(/\n{2,}/).map((part) => part.replace(/\s+/g, " ").trim()).filter(Boolean);
423
+ const chunks = [];
424
+ let current = "";
425
+ for (const paragraph of paragraphs.length > 0 ? paragraphs : [normalized.replace(/\s+/g, " ")]) {
426
+ for (const part of splitOversizedText(paragraph, maxChars)) {
427
+ if (!current) {
428
+ current = part;
429
+ } else if (current.length + 1 + part.length <= maxChars) {
430
+ current = `${current} ${part}`;
431
+ } else {
432
+ chunks.push(current);
433
+ current = withOverlap(current, overlapChars, part, maxChars);
434
+ }
435
+ }
436
+ }
437
+ if (current) chunks.push(current);
438
+ return chunks.map((chunk) => chunk.trim()).filter(Boolean);
439
+ }
440
+ function deterministicEmbedding(text, dimensions = DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS) {
441
+ const values = new Array(dimensions).fill(0);
442
+ const tokens = text.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? [];
443
+ for (const token of tokens) {
444
+ let hash = 2166136261;
445
+ for (const char of token) {
446
+ hash ^= char.codePointAt(0) ?? 0;
447
+ hash = Math.imul(hash, 16777619);
448
+ }
449
+ values[Math.abs(hash) % dimensions] += 1;
450
+ }
451
+ const norm = Math.hypot(...values) || 1;
452
+ return values.map((value) => Number((value / norm).toFixed(6)));
453
+ }
454
+ async function requireReadyFile(db, workspaceId, fileId) {
455
+ const file = await requireFile(db, workspaceId, fileId);
456
+ if (file.status !== "ready") {
457
+ throw new Error(`File ${fileId} is ${file.status}`);
458
+ }
459
+ return file;
460
+ }
461
+ function validateEmbedding(values, dimensions, model) {
462
+ if (values.length !== dimensions) {
463
+ throw new Error(`Embedding model ${model} returned ${values.length} dimensions; expected ${dimensions}`);
464
+ }
465
+ if (values.some((value) => !Number.isFinite(value))) {
466
+ throw new Error(`Embedding model ${model} returned non-finite values`);
467
+ }
468
+ return values;
469
+ }
470
+ function isTextLike(file) {
471
+ const contentType = file.contentType.toLowerCase();
472
+ const filename = file.filename.toLowerCase();
473
+ return contentType.startsWith("text/") || contentType === "application/json" || contentType === "application/xml" || contentType === "application/x-yaml" || filename.endsWith(".md") || filename.endsWith(".markdown") || filename.endsWith(".json") || filename.endsWith(".yaml") || filename.endsWith(".yml") || filename.endsWith(".csv") || filename.endsWith(".tsv") || filename.endsWith(".xml");
474
+ }
475
+ function splitOversizedText(text, maxChars) {
476
+ if (text.length <= maxChars) return [text];
477
+ const out = [];
478
+ let remaining = text;
479
+ while (remaining.length > maxChars) {
480
+ const window = remaining.slice(0, maxChars + 1);
481
+ const breakAt = Math.max(
482
+ window.lastIndexOf(". "),
483
+ window.lastIndexOf("? "),
484
+ window.lastIndexOf("! "),
485
+ window.lastIndexOf("; "),
486
+ window.lastIndexOf(", "),
487
+ window.lastIndexOf(" ")
488
+ );
489
+ const end = breakAt > Math.floor(maxChars * 0.5) ? breakAt + 1 : maxChars;
490
+ out.push(remaining.slice(0, end).trim());
491
+ remaining = remaining.slice(end).trim();
492
+ }
493
+ if (remaining) out.push(remaining);
494
+ return out;
495
+ }
496
+ function withOverlap(previous, overlapChars, next, maxChars) {
497
+ if (overlapChars <= 0) return next;
498
+ const overlap = previous.slice(Math.max(0, previous.length - overlapChars)).replace(/^\S+\s+/, "").trim();
499
+ const candidate = overlap ? `${overlap} ${next}` : next;
500
+ return candidate.length <= maxChars ? candidate : next;
501
+ }
502
+ function vectorLiteral(values) {
503
+ return `[${values.join(",")}]`;
504
+ }
505
+ function mapDocumentBase(row) {
506
+ return {
507
+ id: row.id,
508
+ workspaceId: row.workspaceId,
509
+ name: row.name,
510
+ description: row.description,
511
+ createdAt: row.createdAt.toISOString(),
512
+ updatedAt: row.updatedAt.toISOString()
513
+ };
514
+ }
515
+ function mapDocument(row) {
516
+ return {
517
+ id: row.id,
518
+ workspaceId: row.workspaceId,
519
+ baseId: row.baseId,
520
+ fileId: row.fileId,
521
+ status: row.status,
522
+ title: row.title,
523
+ parser: row.parser,
524
+ chunkCount: row.chunkCount,
525
+ error: row.error,
526
+ createdAt: row.createdAt.toISOString(),
527
+ updatedAt: row.updatedAt.toISOString()
528
+ };
529
+ }
530
+ export {
531
+ DEFAULT_DOCUMENT_CHUNK_OVERLAP,
532
+ DEFAULT_DOCUMENT_CHUNK_SIZE,
533
+ DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS,
534
+ DEFAULT_DOCUMENT_EMBEDDING_MODEL,
535
+ DEFAULT_DOCUMENT_PARSER,
536
+ DeterministicEmbeddingProvider,
537
+ LiteParseDocumentParser,
538
+ OpenAIEmbeddingProvider,
539
+ RecursiveTextChunker,
540
+ addDocumentToBase,
541
+ chunkText,
542
+ createDocumentBase,
543
+ createDocumentServices,
544
+ deleteDocumentFromBase,
545
+ deterministicEmbedding,
546
+ documentOpenAIEmbeddingConfig,
547
+ getDocument,
548
+ getDocumentBase,
549
+ getDocumentChunk,
550
+ indexDocumentNow,
551
+ listDocumentBases,
552
+ listDocuments,
553
+ parseDocumentBytes,
554
+ queueDocumentForReindex,
555
+ searchDocuments
556
+ };
557
+ //# sourceMappingURL=index.js.map