@opengeni/documents 0.2.31 → 0.2.37

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/dist/index.js CHANGED
@@ -2,13 +2,18 @@
2
2
  import { requireFile, withRlsContext, withWorkspaceRls } from "@opengeni/db";
3
3
  import * as schema from "@opengeni/db/schema";
4
4
  import { LiteParse } from "@llamaindex/liteparse";
5
- import { and, asc, desc, eq, inArray, sql } from "drizzle-orm";
5
+ import { and, asc, desc, eq, inArray, or, sql } from "drizzle-orm";
6
6
  import OpenAI from "openai";
7
7
  var DEFAULT_DOCUMENT_PARSER = "liteparse";
8
8
  var DEFAULT_DOCUMENT_EMBEDDING_MODEL = "text-embedding-3-large";
9
9
  var DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS = 3072;
10
10
  var DEFAULT_DOCUMENT_CHUNK_SIZE = 1200;
11
11
  var DEFAULT_DOCUMENT_CHUNK_OVERLAP = 160;
12
+ var DEFAULT_DOCUMENT_CURATION_MODEL = "gpt-4o-mini";
13
+ var DOCUMENT_CURATION_MAX_INPUT_CHARS = 24e3;
14
+ var DOCUMENT_CURATION_AUTO_FILE_CONFIDENCE = 0.75;
15
+ var DEFAULT_BASE_NAME = "Default";
16
+ var DEFAULT_BASE_DESCRIPTION = "Default base for dropped files and notes.";
12
17
  var LiteParseDocumentParser = class {
13
18
  name = DEFAULT_DOCUMENT_PARSER;
14
19
  parseQueue = Promise.resolve();
@@ -132,6 +137,123 @@ var DeterministicEmbeddingProvider = class {
132
137
  return deterministicEmbedding(text, this.dimensions);
133
138
  }
134
139
  };
140
+ function heuristicCuration(input, contentType = "application/octet-stream") {
141
+ const firstLine = input.text.split("\n").map((line) => line.replace(/^[#>\s*-]+/, "").trim()).find((line) => line.length >= 3);
142
+ const title = (firstLine ?? input.title).slice(0, 120).trim() || input.title;
143
+ const summaryWindow = input.text.replace(/\s+/g, " ").trim().slice(0, 360);
144
+ const summary = summaryWindow.length === 360 ? `${summaryWindow.slice(0, 357)}...` : summaryWindow;
145
+ return {
146
+ title,
147
+ summary: summary || null,
148
+ sourceKind: heuristicSourceKind(input.filename, contentType),
149
+ topics: [],
150
+ targetBaseId: null,
151
+ confidence: 0,
152
+ reason: null
153
+ };
154
+ }
155
+ function heuristicSourceKind(rawFilename, rawContentType) {
156
+ const filename = rawFilename.toLowerCase();
157
+ const contentType = rawContentType.toLowerCase();
158
+ if (filename.endsWith(".eml") || contentType === "message/rfc822") return "email";
159
+ if (filename.endsWith(".vtt") || filename.endsWith(".srt") || filename.includes("transcript")) {
160
+ return "meeting_transcript";
161
+ }
162
+ if (contentType === "text/html") return "web";
163
+ if (contentType === "application/pdf" || filename.endsWith(".docx") || filename.endsWith(".md")) {
164
+ return "document";
165
+ }
166
+ return "manual_upload";
167
+ }
168
+ var HeuristicCurationProvider = class {
169
+ model = "heuristic";
170
+ async curate(input) {
171
+ return heuristicCuration(input);
172
+ }
173
+ };
174
+ var CURATION_SYSTEM_PROMPT = [
175
+ "You organize a team knowledge base. Given the beginning of a dropped document",
176
+ "and the list of existing collections (bases), return STRICT JSON with keys:",
177
+ '"title" (concise, specific, <= 120 chars, no filename extensions),',
178
+ '"summary" (2-3 sentences, plain prose, what the document is and why it matters),',
179
+ '"sourceKind" (one of: manual_upload, meeting_transcript, repository, email, chat, document, web, other),',
180
+ '"topics" (3-6 short lowercase tags),',
181
+ '"targetBaseId" (the id of the best-fitting existing base, or null if none fits),',
182
+ '"confidence" (0..1 \u2014 how sure you are the document belongs in targetBaseId),',
183
+ '"reason" (one sentence explaining the filing choice).',
184
+ "Only pick a targetBaseId from the provided list. If the document fits no base",
185
+ "well, return targetBaseId null and confidence 0. Respond with JSON only."
186
+ ].join(" ");
187
+ var OpenAICurationProvider = class {
188
+ client = null;
189
+ apiKey;
190
+ baseURL;
191
+ defaultHeaders;
192
+ defaultQuery;
193
+ model;
194
+ constructor(args) {
195
+ this.apiKey = args.apiKey ?? process.env.OPENAI_API_KEY;
196
+ this.baseURL = args.baseURL;
197
+ this.defaultHeaders = args.defaultHeaders;
198
+ this.defaultQuery = args.defaultQuery;
199
+ this.model = args.model ?? DEFAULT_DOCUMENT_CURATION_MODEL;
200
+ }
201
+ async curate(input) {
202
+ const response = await this.openai().chat.completions.create({
203
+ model: this.model,
204
+ response_format: { type: "json_object" },
205
+ messages: [
206
+ { role: "system", content: CURATION_SYSTEM_PROMPT },
207
+ {
208
+ role: "user",
209
+ content: JSON.stringify({
210
+ filename: input.filename,
211
+ currentTitle: input.title,
212
+ bases: input.bases,
213
+ text: input.text
214
+ })
215
+ }
216
+ ]
217
+ });
218
+ const raw = response.choices[0]?.message?.content;
219
+ if (!raw) {
220
+ throw new Error("curation model returned no content");
221
+ }
222
+ return parseCurationOutcome(raw, input.bases);
223
+ }
224
+ openai() {
225
+ if (!this.apiKey) {
226
+ throw new Error("OpenAI document curation requires an API key");
227
+ }
228
+ this.client ??= new OpenAI({
229
+ apiKey: this.apiKey,
230
+ ...this.baseURL ? { baseURL: this.baseURL } : {},
231
+ ...this.defaultQuery ? { defaultQuery: this.defaultQuery } : {},
232
+ ...this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}
233
+ });
234
+ return this.client;
235
+ }
236
+ };
237
+ function parseCurationOutcome(raw, bases) {
238
+ const parsed = JSON.parse(raw);
239
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
240
+ throw new Error("curation model returned non-object JSON");
241
+ }
242
+ const record = parsed;
243
+ const knownBase = bases.find((base) => base.id === record.targetBaseId);
244
+ const confidence = typeof record.confidence === "number" && Number.isFinite(record.confidence) ? Math.min(1, Math.max(0, record.confidence)) : 0;
245
+ return {
246
+ title: cleanString(typeof record.title === "string" ? record.title.slice(0, 200) : null) ?? null,
247
+ summary: cleanString(typeof record.summary === "string" ? record.summary.slice(0, 2e3) : null) ?? null,
248
+ sourceKind: typeof record.sourceKind === "string" ? normalizeKnowledgeSourceKind(record.sourceKind) : null,
249
+ topics: cleanStringArray(
250
+ Array.isArray(record.topics) ? record.topics.filter((topic) => typeof topic === "string").slice(0, 8) : []
251
+ ).map((topic) => topic.toLowerCase().slice(0, 60)),
252
+ targetBaseId: knownBase?.id ?? null,
253
+ confidence: knownBase ? confidence : 0,
254
+ reason: cleanString(typeof record.reason === "string" ? record.reason.slice(0, 500) : null) ?? null
255
+ };
256
+ }
135
257
  function createDocumentServices(settings, overrides = {}) {
136
258
  const dimensions = settings?.documentEmbeddingDimensions ?? DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS;
137
259
  const openAIEmbeddingConfig = documentOpenAIEmbeddingConfig(settings);
@@ -145,9 +267,23 @@ function createDocumentServices(settings, overrides = {}) {
145
267
  ...openAIEmbeddingConfig,
146
268
  model: settings?.documentEmbeddingModel ?? DEFAULT_DOCUMENT_EMBEDDING_MODEL,
147
269
  dimensions
148
- }))
270
+ })),
271
+ curator: overrides.curator ?? createDocumentCurator(settings)
149
272
  };
150
273
  }
274
+ function createDocumentCurator(settings) {
275
+ const provider = settings?.documentCurationProvider ?? "openai";
276
+ if (provider === "none") return void 0;
277
+ if (provider === "heuristic") return new HeuristicCurationProvider();
278
+ const embeddingConfig = documentOpenAIEmbeddingConfig(settings);
279
+ return new OpenAICurationProvider({
280
+ apiKey: settings?.documentCurationApiKey ?? embeddingConfig.apiKey,
281
+ baseURL: settings?.documentCurationBaseUrl ?? embeddingConfig.baseURL,
282
+ defaultHeaders: embeddingConfig.defaultHeaders,
283
+ defaultQuery: embeddingConfig.defaultQuery,
284
+ model: settings?.documentCurationModel ?? DEFAULT_DOCUMENT_CURATION_MODEL
285
+ });
286
+ }
151
287
  function documentOpenAIEmbeddingConfig(settings) {
152
288
  if (!settings) return {};
153
289
  if (settings.documentEmbeddingApiKey || settings.documentEmbeddingBaseUrl) {
@@ -215,6 +351,27 @@ async function getDocumentBase(db, workspaceId, baseId) {
215
351
  return row ? mapDocumentBase(row) : null;
216
352
  });
217
353
  }
354
+ async function ensureDefaultBase(db, input) {
355
+ return await withRlsContext(
356
+ db,
357
+ { accountId: input.accountId, workspaceId: input.workspaceId },
358
+ async (scopedDb) => await scopedDb.transaction(async (tx) => {
359
+ const defaultName = sql`lower(btrim(${schema.documentBases.name})) = ${DEFAULT_BASE_NAME.toLowerCase()}`;
360
+ const [existing] = await tx.select().from(schema.documentBases).where(and(eq(schema.documentBases.workspaceId, input.workspaceId), defaultName)).limit(1);
361
+ if (existing) return mapDocumentBase(existing);
362
+ const [inserted] = await tx.insert(schema.documentBases).values({
363
+ accountId: input.accountId,
364
+ workspaceId: input.workspaceId,
365
+ name: DEFAULT_BASE_NAME,
366
+ description: DEFAULT_BASE_DESCRIPTION
367
+ }).onConflictDoNothing().returning();
368
+ if (inserted) return mapDocumentBase(inserted);
369
+ const [raced] = await tx.select().from(schema.documentBases).where(and(eq(schema.documentBases.workspaceId, input.workspaceId), defaultName)).limit(1);
370
+ if (raced) return mapDocumentBase(raced);
371
+ throw new Error("Failed to create Default document base");
372
+ })
373
+ );
374
+ }
218
375
  async function addDocumentToBase(db, input) {
219
376
  return await withRlsContext(
220
377
  db,
@@ -223,6 +380,9 @@ async function addDocumentToBase(db, input) {
223
380
  const base = await getDocumentBase(scopedDb, input.workspaceId, input.baseId);
224
381
  if (!base) throw new Error(`Document base not found: ${input.baseId}`);
225
382
  const file = await requireReadyFile(scopedDb, input.workspaceId, input.fileId);
383
+ if (input.visibility === "private" && !cleanString(input.createdBy ?? null)) {
384
+ throw new Error("private documents require a creating subject");
385
+ }
226
386
  const now = /* @__PURE__ */ new Date();
227
387
  const [existing] = await scopedDb.select().from(schema.documents).where(
228
388
  and(
@@ -232,6 +392,9 @@ async function addDocumentToBase(db, input) {
232
392
  )
233
393
  ).limit(1);
234
394
  if (existing) {
395
+ if (!documentMatchesAccess(existing, input.access)) {
396
+ throw new Error(`Document not found: ${existing.id}`);
397
+ }
235
398
  const [updated] = await scopedDb.update(schema.documents).set({
236
399
  title: cleanString(input.title) ?? cleanString(input.sourceTitle) ?? existing.title,
237
400
  ...input.sourceKind !== void 0 ? { sourceKind: input.sourceKind } : {},
@@ -247,7 +410,8 @@ async function addDocumentToBase(db, input) {
247
410
  }).where(
248
411
  and(
249
412
  eq(schema.documents.workspaceId, input.workspaceId),
250
- eq(schema.documents.id, existing.id)
413
+ eq(schema.documents.id, existing.id),
414
+ ...documentAccessConditions(input.access)
251
415
  )
252
416
  ).returning();
253
417
  return mapDocument(updated ?? existing);
@@ -269,6 +433,10 @@ async function addDocumentToBase(db, input) {
269
433
  sourceUpdatedAt: parseOptionalDate(input.sourceUpdatedAt),
270
434
  sourceVersion: cleanString(input.sourceVersion) ?? null,
271
435
  aclTags: cleanStringArray(input.aclTags),
436
+ visibility: input.visibility ?? "workspace",
437
+ agentAccess: input.agentAccess ?? true,
438
+ createdBy: input.createdBy ?? null,
439
+ curationStatus: input.curationStatus ?? "none",
272
440
  updatedAt: now
273
441
  }).returning();
274
442
  if (!row) throw new Error("Failed to create document");
@@ -276,6 +444,65 @@ async function addDocumentToBase(db, input) {
276
444
  }
277
445
  );
278
446
  }
447
+ async function moveDocumentToBase(db, input) {
448
+ return await withRlsContext(
449
+ db,
450
+ { accountId: input.accountId, workspaceId: input.workspaceId },
451
+ async (scopedDb) => {
452
+ const [row] = await scopedDb.select().from(schema.documents).where(
453
+ and(
454
+ eq(schema.documents.workspaceId, input.workspaceId),
455
+ eq(schema.documents.id, input.documentId),
456
+ ...documentAccessConditions(input.access)
457
+ )
458
+ ).limit(1);
459
+ if (!row) throw new Error(`Document not found: ${input.documentId}`);
460
+ const suggestion = row.curation?.suggestedBaseId;
461
+ const targetBaseId = input.targetBaseId ?? suggestion;
462
+ if (!targetBaseId) {
463
+ throw new Error("document has no suggested base; pass targetBaseId");
464
+ }
465
+ if (targetBaseId === row.baseId) return mapDocument(row);
466
+ const base = await getDocumentBase(scopedDb, input.workspaceId, targetBaseId);
467
+ if (!base) throw new Error(`Document base not found: ${targetBaseId}`);
468
+ const [conflict] = await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
469
+ and(
470
+ eq(schema.documents.workspaceId, input.workspaceId),
471
+ eq(schema.documents.baseId, targetBaseId),
472
+ eq(schema.documents.fileId, row.fileId)
473
+ )
474
+ ).limit(1);
475
+ if (conflict) {
476
+ throw new Error("a document for this file already exists in the target base");
477
+ }
478
+ const now = /* @__PURE__ */ new Date();
479
+ const moved = await scopedDb.transaction(async (tx) => {
480
+ const [updated] = await tx.update(schema.documents).set({
481
+ baseId: targetBaseId,
482
+ ...row.curationStatus === "suggested" || row.curationStatus === "pending" ? { curationStatus: "auto_filed" } : {},
483
+ updatedAt: now
484
+ }).where(
485
+ and(
486
+ eq(schema.documents.workspaceId, input.workspaceId),
487
+ eq(schema.documents.id, input.documentId),
488
+ ...documentAccessConditions(input.access)
489
+ )
490
+ ).returning();
491
+ if (updated) {
492
+ await tx.update(schema.documentChunks).set({ baseId: targetBaseId }).where(
493
+ and(
494
+ eq(schema.documentChunks.workspaceId, input.workspaceId),
495
+ eq(schema.documentChunks.documentId, input.documentId)
496
+ )
497
+ );
498
+ }
499
+ return updated;
500
+ });
501
+ if (!moved) throw new Error(`Document not found: ${input.documentId}`);
502
+ return mapDocument(moved);
503
+ }
504
+ );
505
+ }
279
506
  async function deleteDocumentFromBase(db, input) {
280
507
  await withRlsContext(
281
508
  db,
@@ -284,7 +511,8 @@ async function deleteDocumentFromBase(db, input) {
284
511
  const [document] = await scopedDb.select().from(schema.documents).where(
285
512
  and(
286
513
  eq(schema.documents.workspaceId, input.workspaceId),
287
- eq(schema.documents.id, input.documentId)
514
+ eq(schema.documents.id, input.documentId),
515
+ ...documentAccessConditions(input.access)
288
516
  )
289
517
  ).limit(1);
290
518
  if (!document) {
@@ -296,50 +524,64 @@ async function deleteDocumentFromBase(db, input) {
296
524
  await scopedDb.delete(schema.documents).where(
297
525
  and(
298
526
  eq(schema.documents.workspaceId, input.workspaceId),
299
- eq(schema.documents.id, input.documentId)
527
+ eq(schema.documents.id, input.documentId),
528
+ ...documentAccessConditions(input.access)
300
529
  )
301
530
  );
302
531
  }
303
532
  );
304
533
  }
305
- async function listDocuments(db, workspaceId, baseId) {
534
+ async function listDocuments(db, workspaceId, baseId, access) {
306
535
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
307
536
  const rows = await scopedDb.select().from(schema.documents).where(
308
- and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.baseId, baseId))
537
+ and(
538
+ eq(schema.documents.workspaceId, workspaceId),
539
+ eq(schema.documents.baseId, baseId),
540
+ ...documentAccessConditions(access)
541
+ )
309
542
  ).orderBy(asc(schema.documents.createdAt));
310
543
  return rows.map(mapDocument);
311
544
  });
312
545
  }
313
- async function getDocument(db, workspaceId, documentId) {
546
+ async function getDocument(db, workspaceId, documentId, access) {
314
547
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
315
548
  const [row] = await scopedDb.select().from(schema.documents).where(
316
- and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId))
549
+ and(
550
+ eq(schema.documents.workspaceId, workspaceId),
551
+ eq(schema.documents.id, documentId),
552
+ ...documentAccessConditions(access)
553
+ )
317
554
  ).limit(1);
318
555
  return row ? mapDocument(row) : null;
319
556
  });
320
557
  }
321
- async function queueDocumentForReindex(db, workspaceId, documentId) {
558
+ async function queueDocumentForReindex(db, workspaceId, documentId, access) {
322
559
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
323
560
  const [row] = await scopedDb.update(schema.documents).set({
324
561
  status: "queued",
325
562
  error: null,
326
563
  updatedAt: /* @__PURE__ */ new Date()
327
564
  }).where(
328
- and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId))
565
+ and(
566
+ eq(schema.documents.workspaceId, workspaceId),
567
+ eq(schema.documents.id, documentId),
568
+ ...documentAccessConditions(access)
569
+ )
329
570
  ).returning();
330
571
  if (!row) throw new Error(`Document not found: ${documentId}`);
331
572
  return mapDocument(row);
332
573
  });
333
574
  }
334
575
  async function indexDocumentNow(db, objectStorage, workspaceId, documentId, services = createDocumentServices(), hooks = {}) {
335
- const [document] = await withWorkspaceRls(
576
+ const [loadedDocument] = await withWorkspaceRls(
336
577
  db,
337
578
  workspaceId,
338
579
  async (scopedDb) => await scopedDb.select().from(schema.documents).where(
339
580
  and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId))
340
581
  ).limit(1)
341
582
  );
342
- if (!document) throw new Error(`Document not found: ${documentId}`);
583
+ if (!loadedDocument) throw new Error(`Document not found: ${documentId}`);
584
+ let document = loadedDocument;
343
585
  const file = await requireReadyFile(db, workspaceId, document.fileId);
344
586
  await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
345
587
  await scopedDb.update(schema.documents).set({
@@ -354,6 +596,9 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
354
596
  try {
355
597
  const bytes = await objectStorage.getFileBytes(file);
356
598
  const parsed = await services.parser.parse(bytes, file);
599
+ if (document.curationStatus === "pending") {
600
+ document = await curateDroppedDocument(db, services, document, parsed, file);
601
+ }
357
602
  const chunks = services.chunker.chunk(parsed, file);
358
603
  await hooks.beforeEmbed?.({
359
604
  accountId: document.accountId,
@@ -438,10 +683,104 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
438
683
  if (!failed) throw error;
439
684
  return mapDocument(failed);
440
685
  }
441
- const updated = await getDocument(db, workspaceId, documentId);
686
+ const updated = await getDocument(db, workspaceId, documentId, {
687
+ viewerSubjectId: document.createdBy
688
+ });
442
689
  if (!updated) throw new Error(`Document disappeared after indexing: ${documentId}`);
443
690
  return updated;
444
691
  }
692
+ async function curateDroppedDocument(db, services, document, parsed, file) {
693
+ if (!services.curator) {
694
+ const [updated2] = await withWorkspaceRls(
695
+ db,
696
+ document.workspaceId,
697
+ async (scopedDb) => await scopedDb.update(schema.documents).set({
698
+ curationStatus: "none",
699
+ summary: null,
700
+ topics: [],
701
+ curation: null,
702
+ updatedAt: /* @__PURE__ */ new Date()
703
+ }).where(
704
+ and(
705
+ eq(schema.documents.workspaceId, document.workspaceId),
706
+ eq(schema.documents.id, document.id)
707
+ )
708
+ ).returning()
709
+ );
710
+ return updated2 ?? document;
711
+ }
712
+ const bases = await listDocumentBases(db, document.workspaceId);
713
+ const candidates = bases.filter((base) => base.id !== document.baseId).map((base) => ({ id: base.id, name: base.name, description: base.description }));
714
+ const input = {
715
+ text: parsed.text.slice(0, DOCUMENT_CURATION_MAX_INPUT_CHARS),
716
+ filename: file.filename,
717
+ title: document.title,
718
+ bases: candidates
719
+ };
720
+ let outcome;
721
+ let model;
722
+ let failure = null;
723
+ try {
724
+ outcome = await services.curator.curate(input);
725
+ model = services.curator.model;
726
+ } catch (error) {
727
+ failure = error instanceof Error ? error.message : String(error);
728
+ console.warn("document curation failed; applying heuristic fallback", {
729
+ workspaceId: document.workspaceId,
730
+ documentId: document.id,
731
+ error: failure
732
+ });
733
+ outcome = heuristicCuration(input, file.contentType);
734
+ model = "heuristic";
735
+ }
736
+ const suggestedBase = candidates.find((base) => base.id === outcome.targetBaseId) ?? null;
737
+ let moveToBaseId = null;
738
+ if (suggestedBase && failure === null && outcome.confidence >= DOCUMENT_CURATION_AUTO_FILE_CONFIDENCE) {
739
+ const conflict = await withWorkspaceRls(
740
+ db,
741
+ document.workspaceId,
742
+ async (scopedDb) => await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
743
+ and(
744
+ eq(schema.documents.workspaceId, document.workspaceId),
745
+ eq(schema.documents.baseId, suggestedBase.id),
746
+ eq(schema.documents.fileId, document.fileId)
747
+ )
748
+ ).limit(1)
749
+ );
750
+ if (conflict.length === 0) {
751
+ moveToBaseId = suggestedBase.id;
752
+ }
753
+ }
754
+ const curation = {
755
+ suggestedBaseId: suggestedBase?.id ?? null,
756
+ suggestedBaseName: suggestedBase?.name ?? null,
757
+ confidence: outcome.confidence,
758
+ reason: failure ? `curation failed (${failure}); heuristic fallback applied` : outcome.reason,
759
+ originalTitle: document.title,
760
+ model
761
+ };
762
+ const curationStatus = failure ? "failed" : moveToBaseId ? "auto_filed" : "suggested";
763
+ const [updated] = await withWorkspaceRls(
764
+ db,
765
+ document.workspaceId,
766
+ async (scopedDb) => await scopedDb.update(schema.documents).set({
767
+ title: outcome.title ?? document.title,
768
+ summary: outcome.summary,
769
+ topics: outcome.topics,
770
+ ...outcome.sourceKind ? { sourceKind: outcome.sourceKind } : {},
771
+ ...moveToBaseId ? { baseId: moveToBaseId } : {},
772
+ curationStatus,
773
+ curation,
774
+ updatedAt: /* @__PURE__ */ new Date()
775
+ }).where(
776
+ and(
777
+ eq(schema.documents.workspaceId, document.workspaceId),
778
+ eq(schema.documents.id, document.id)
779
+ )
780
+ ).returning()
781
+ );
782
+ return updated ?? document;
783
+ }
445
784
  async function searchDocuments(db, input, services = createDocumentServices()) {
446
785
  const mode = input.mode ?? "hybrid";
447
786
  const limit = Math.min(Math.max(input.limit ?? 5, 1), 50);
@@ -539,7 +878,7 @@ async function keywordSearchDocuments(db, input, limit) {
539
878
  keywordScore: normalizeKeywordScore(Number(row.rank))
540
879
  }));
541
880
  }
542
- async function getDocumentChunk(db, workspaceId, chunkId) {
881
+ async function getDocumentChunk(db, workspaceId, chunkId, access) {
543
882
  const [row] = await withWorkspaceRls(
544
883
  db,
545
884
  workspaceId,
@@ -565,7 +904,8 @@ async function getDocumentChunk(db, workspaceId, chunkId) {
565
904
  and(
566
905
  eq(schema.documentChunks.workspaceId, workspaceId),
567
906
  eq(schema.documentChunks.id, chunkId),
568
- eq(schema.documents.status, "ready")
907
+ eq(schema.documents.status, "ready"),
908
+ ...documentAccessConditions(access)
569
909
  )
570
910
  ).limit(1)
571
911
  );
@@ -578,10 +918,36 @@ async function getDocumentChunk(db, workspaceId, chunkId) {
578
918
  keywordScore: null
579
919
  };
580
920
  }
921
+ function documentAccessConditions(access) {
922
+ if (access?.agentOnly) {
923
+ const viewer2 = access.viewerSubjectId;
924
+ const visibility = viewer2 ? or(eq(schema.documents.visibility, "workspace"), eq(schema.documents.createdBy, viewer2)) : eq(schema.documents.visibility, "workspace");
925
+ return [eq(schema.documents.agentAccess, true), ...visibility ? [visibility] : []];
926
+ }
927
+ const viewer = access?.viewerSubjectId;
928
+ if (viewer) {
929
+ const condition = or(
930
+ eq(schema.documents.visibility, "workspace"),
931
+ eq(schema.documents.createdBy, viewer)
932
+ );
933
+ return condition ? [condition] : [];
934
+ }
935
+ return [eq(schema.documents.visibility, "workspace")];
936
+ }
937
+ function documentMatchesAccess(document, access) {
938
+ if (access?.agentOnly) {
939
+ return document.agentAccess && (document.visibility !== "private" || !!access.viewerSubjectId && document.createdBy === access.viewerSubjectId);
940
+ }
941
+ return canViewDocument(document, access?.viewerSubjectId);
942
+ }
943
+ function canViewDocument(document, viewerSubjectId) {
944
+ return document.visibility !== "private" || !!viewerSubjectId && document.createdBy === viewerSubjectId;
945
+ }
581
946
  function documentSearchConditions(input, embeddingModel) {
582
947
  const conditions = [
583
948
  eq(schema.documents.status, "ready"),
584
- eq(schema.documentChunks.workspaceId, input.workspaceId)
949
+ eq(schema.documentChunks.workspaceId, input.workspaceId),
950
+ ...documentAccessConditions(input.access)
585
951
  ];
586
952
  if (embeddingModel) {
587
953
  conditions.push(eq(schema.documentChunks.embeddingModel, embeddingModel));
@@ -822,33 +1188,66 @@ function mapDocument(row) {
822
1188
  sourceUpdatedAt: row.sourceUpdatedAt?.toISOString() ?? null,
823
1189
  sourceVersion: row.sourceVersion,
824
1190
  aclTags: cleanStringArray(row.aclTags),
1191
+ visibility: normalizeDocumentVisibility(row.visibility),
1192
+ createdBy: row.createdBy,
1193
+ agentAccess: row.agentAccess,
1194
+ summary: row.summary,
1195
+ topics: cleanStringArray(row.topics),
1196
+ curationStatus: normalizeDocumentCurationStatus(row.curationStatus),
1197
+ curation: row.curation ?? null,
825
1198
  createdAt: row.createdAt.toISOString(),
826
1199
  updatedAt: row.updatedAt.toISOString()
827
1200
  };
828
1201
  }
1202
+ function normalizeDocumentVisibility(value) {
1203
+ return value === "private" ? "private" : "workspace";
1204
+ }
1205
+ function normalizeDocumentCurationStatus(value) {
1206
+ switch (value) {
1207
+ case "pending":
1208
+ case "suggested":
1209
+ case "auto_filed":
1210
+ case "failed":
1211
+ return value;
1212
+ default:
1213
+ return "none";
1214
+ }
1215
+ }
829
1216
  export {
1217
+ DEFAULT_BASE_DESCRIPTION,
1218
+ DEFAULT_BASE_NAME,
830
1219
  DEFAULT_DOCUMENT_CHUNK_OVERLAP,
831
1220
  DEFAULT_DOCUMENT_CHUNK_SIZE,
1221
+ DEFAULT_DOCUMENT_CURATION_MODEL,
832
1222
  DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS,
833
1223
  DEFAULT_DOCUMENT_EMBEDDING_MODEL,
834
1224
  DEFAULT_DOCUMENT_PARSER,
1225
+ DOCUMENT_CURATION_AUTO_FILE_CONFIDENCE,
1226
+ DOCUMENT_CURATION_MAX_INPUT_CHARS,
835
1227
  DeterministicEmbeddingProvider,
1228
+ HeuristicCurationProvider,
836
1229
  LiteParseDocumentParser,
1230
+ OpenAICurationProvider,
837
1231
  OpenAIEmbeddingProvider,
838
1232
  RecursiveTextChunker,
839
1233
  addDocumentToBase,
1234
+ canViewDocument,
840
1235
  chunkText,
841
1236
  createDocumentBase,
842
1237
  createDocumentServices,
843
1238
  deleteDocumentFromBase,
844
1239
  deterministicEmbedding,
845
1240
  documentOpenAIEmbeddingConfig,
1241
+ ensureDefaultBase,
846
1242
  getDocument,
847
1243
  getDocumentBase,
848
1244
  getDocumentChunk,
1245
+ heuristicCuration,
849
1246
  indexDocumentNow,
850
1247
  listDocumentBases,
851
1248
  listDocuments,
1249
+ moveDocumentToBase,
1250
+ parseCurationOutcome,
852
1251
  parseDocumentBytes,
853
1252
  queueDocumentForReindex,
854
1253
  searchDocuments