@opengeni/documents 0.5.18 → 0.5.32

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
@@ -8,9 +8,8 @@ import {
8
8
  withWorkspaceSubjectRls
9
9
  } from "@opengeni/db";
10
10
  import * as schema from "@opengeni/db/schema";
11
- import { LiteParse } from "@llamaindex/liteparse";
12
- import { and, asc, desc, eq, inArray, or, sql } from "drizzle-orm";
13
- import OpenAI from "openai";
11
+ import { createHash } from "crypto";
12
+ import { and, asc, desc, eq, gt, inArray, isNotNull, or, sql } from "drizzle-orm";
14
13
  var DEFAULT_DOCUMENT_PARSER = "liteparse";
15
14
  var DEFAULT_DOCUMENT_EMBEDDING_MODEL = "text-embedding-3-large";
16
15
  var DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS = 3072;
@@ -19,6 +18,7 @@ var DEFAULT_DOCUMENT_CHUNK_OVERLAP = 160;
19
18
  var DEFAULT_DOCUMENT_CURATION_MODEL = "gpt-4o-mini";
20
19
  var DOCUMENT_CURATION_MAX_INPUT_CHARS = 24e3;
21
20
  var DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES = 1024;
21
+ var DOCUMENT_INDEX_CHECKPOINT_MAX_CHARS = 1024;
22
22
  var DOCUMENT_CURATION_AUTO_FILE_CONFIDENCE = 0.75;
23
23
  var DEFAULT_BASE_NAME = "Default";
24
24
  var DEFAULT_BASE_DESCRIPTION = "Default base for dropped files and notes.";
@@ -44,6 +44,7 @@ var LiteParseDocumentParser = class {
44
44
  }
45
45
  async parseWithLiteParse(bytes) {
46
46
  return await this.enqueueParse(async () => {
47
+ const { LiteParse } = await import("@llamaindex/liteparse");
47
48
  const parser = new LiteParse({ ocrEnabled: true, numWorkers: 1 });
48
49
  const result = await parser.parse(Buffer.from(bytes), true);
49
50
  const text = typeof result?.text === "string" ? result.text : "";
@@ -85,7 +86,7 @@ var RecursiveTextChunker = class {
85
86
  }
86
87
  };
87
88
  var OpenAIEmbeddingProvider = class {
88
- client = null;
89
+ clientPromise = null;
89
90
  apiKey;
90
91
  baseURL;
91
92
  constructor(args) {
@@ -105,7 +106,7 @@ var OpenAIEmbeddingProvider = class {
105
106
  const out = [];
106
107
  for (let start = 0; start < texts.length; start += 64) {
107
108
  const batch = texts.slice(start, start + 64);
108
- const response = await this.openai().embeddings.create({
109
+ const response = await (await this.openai()).embeddings.create({
109
110
  model: this.model,
110
111
  input: batch,
111
112
  dimensions: this.dimensions
@@ -123,17 +124,19 @@ var OpenAIEmbeddingProvider = class {
123
124
  }
124
125
  return embedding;
125
126
  }
126
- openai() {
127
+ async openai() {
127
128
  if (!this.apiKey) {
128
129
  throw new Error("OpenAI document embeddings require an API key");
129
130
  }
130
- this.client ??= new OpenAI({
131
- apiKey: this.apiKey,
132
- ...this.baseURL ? { baseURL: this.baseURL } : {},
133
- ...this.defaultQuery ? { defaultQuery: this.defaultQuery } : {},
134
- ...this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}
135
- });
136
- return this.client;
131
+ this.clientPromise ??= import("openai").then(
132
+ ({ default: OpenAIClient }) => new OpenAIClient({
133
+ apiKey: this.apiKey,
134
+ ...this.baseURL ? { baseURL: this.baseURL } : {},
135
+ ...this.defaultQuery ? { defaultQuery: this.defaultQuery } : {},
136
+ ...this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}
137
+ })
138
+ );
139
+ return await this.clientPromise;
137
140
  }
138
141
  };
139
142
  var DeterministicEmbeddingProvider = class {
@@ -196,7 +199,7 @@ var CURATION_SYSTEM_PROMPT = [
196
199
  "well, return targetBaseId null and confidence 0. Respond with JSON only."
197
200
  ].join(" ");
198
201
  var OpenAICurationProvider = class {
199
- client = null;
202
+ clientPromise = null;
200
203
  apiKey;
201
204
  baseURL;
202
205
  defaultHeaders;
@@ -210,7 +213,7 @@ var OpenAICurationProvider = class {
210
213
  this.model = args.model ?? DEFAULT_DOCUMENT_CURATION_MODEL;
211
214
  }
212
215
  async curate(input) {
213
- const response = await this.openai().chat.completions.create({
216
+ const response = await (await this.openai()).chat.completions.create({
214
217
  model: this.model,
215
218
  response_format: { type: "json_object" },
216
219
  messages: [
@@ -232,17 +235,19 @@ var OpenAICurationProvider = class {
232
235
  }
233
236
  return parseCurationOutcome(raw, input.bases);
234
237
  }
235
- openai() {
238
+ async openai() {
236
239
  if (!this.apiKey) {
237
240
  throw new Error("OpenAI document curation requires an API key");
238
241
  }
239
- this.client ??= new OpenAI({
240
- apiKey: this.apiKey,
241
- ...this.baseURL ? { baseURL: this.baseURL } : {},
242
- ...this.defaultQuery ? { defaultQuery: this.defaultQuery } : {},
243
- ...this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}
244
- });
245
- return this.client;
242
+ this.clientPromise ??= import("openai").then(
243
+ ({ default: OpenAIClient }) => new OpenAIClient({
244
+ apiKey: this.apiKey,
245
+ ...this.baseURL ? { baseURL: this.baseURL } : {},
246
+ ...this.defaultQuery ? { defaultQuery: this.defaultQuery } : {},
247
+ ...this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}
248
+ })
249
+ );
250
+ return await this.clientPromise;
246
251
  }
247
252
  };
248
253
  function parseCurationOutcome(raw, bases) {
@@ -534,15 +539,24 @@ async function addDocumentToBase(db, input) {
534
539
  const base = await getDocumentBase(scopedDb, input.workspaceId, input.baseId);
535
540
  if (!base) throw new Error(`Document base not found: ${input.baseId}`);
536
541
  const file = await requireReadyFile(scopedDb, input.workspaceId, input.fileId);
542
+ const knowledgeSourceIdentity = cleanString(input.knowledgeSourceIdentity ?? null);
543
+ if (knowledgeSourceIdentity && knowledgeSourceIdentity.length > 512) {
544
+ throw new Error("knowledge source document identity exceeds 512 characters");
545
+ }
537
546
  const now = /* @__PURE__ */ new Date();
538
547
  const [existing] = await scopedDb.select().from(schema.documents).where(
539
548
  and(
540
549
  eq(schema.documents.workspaceId, input.workspaceId),
541
- eq(schema.documents.baseId, input.baseId),
542
- eq(schema.documents.fileId, input.fileId)
550
+ ...knowledgeSourceIdentity ? [eq(schema.documents.knowledgeSourceIdentity, knowledgeSourceIdentity)] : [
551
+ eq(schema.documents.baseId, input.baseId),
552
+ eq(schema.documents.fileId, input.fileId)
553
+ ]
543
554
  )
544
555
  ).limit(1);
545
556
  if (existing) {
557
+ if (knowledgeSourceIdentity && existing.fileId !== input.fileId) {
558
+ throw new Error("knowledge source document identity is bound to different content");
559
+ }
546
560
  if (!documentMatchesAccess(existing, input.workspaceId, input.access)) {
547
561
  throw new Error(`Document not found: ${existing.id}`);
548
562
  }
@@ -587,6 +601,7 @@ async function addDocumentToBase(db, input) {
587
601
  sourceCreatedAt: parseOptionalDate(input.sourceCreatedAt),
588
602
  sourceUpdatedAt: parseOptionalDate(input.sourceUpdatedAt),
589
603
  sourceVersion: cleanString(input.sourceVersion) ?? null,
604
+ knowledgeSourceIdentity,
590
605
  aclTags: cleanStringArray(input.aclTags),
591
606
  authorityKind: authority.kind,
592
607
  authorityWorkspaceId: authority.workspaceId,
@@ -630,7 +645,7 @@ async function moveDocumentToBase(db, input) {
630
645
  and(
631
646
  eq(schema.documents.workspaceId, input.workspaceId),
632
647
  eq(schema.documents.baseId, targetBaseId),
633
- eq(schema.documents.fileId, row.fileId)
648
+ ...row.knowledgeSourceIdentity ? [eq(schema.documents.knowledgeSourceIdentity, row.knowledgeSourceIdentity)] : [eq(schema.documents.fileId, row.fileId)]
634
649
  )
635
650
  ).limit(1);
636
651
  if (conflict) {
@@ -710,6 +725,101 @@ async function listDocuments(db, workspaceId, baseId, access) {
710
725
  return rows.map(mapDocument);
711
726
  });
712
727
  }
728
+ async function listEffectiveIndexedDocuments(db, input) {
729
+ const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
730
+ const limit = input.limit ?? 50;
731
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
732
+ throw new Error("indexed document list limit must be between 1 and 100");
733
+ }
734
+ const afterSequence = input.checkpoint ? decodeDocumentIndexCheckpoint(input.checkpoint, {
735
+ accountId: input.accountId,
736
+ workspaceId: input.workspaceId,
737
+ initiatingSubjectId
738
+ }) : 0n;
739
+ const access = {
740
+ agentOnly: true,
741
+ viewerSubjectId: initiatingSubjectId
742
+ };
743
+ const rows = await withDocumentAccountRls(
744
+ db,
745
+ input.accountId,
746
+ input.workspaceId,
747
+ access,
748
+ async (scopedDb) => await scopedDb.select().from(schema.documents).where(
749
+ and(
750
+ eq(schema.documents.accountId, input.accountId),
751
+ eq(schema.documents.status, "ready"),
752
+ isNotNull(schema.documents.indexSequence),
753
+ gt(schema.documents.indexSequence, afterSequence),
754
+ ...documentAccessConditions(input.workspaceId, access)
755
+ )
756
+ ).orderBy(asc(schema.documents.indexSequence)).limit(limit + 1)
757
+ );
758
+ const hasMore = rows.length > limit;
759
+ const pageRows = rows.slice(0, limit);
760
+ const nextSequence = pageRows.at(-1)?.indexSequence ?? afterSequence;
761
+ if (nextSequence === null) {
762
+ throw new Error("ready document is missing its index sequence");
763
+ }
764
+ return {
765
+ documents: pageRows.map(mapIndexedDocumentSummary),
766
+ nextCheckpoint: encodeDocumentIndexCheckpoint({
767
+ accountId: input.accountId,
768
+ workspaceId: input.workspaceId,
769
+ initiatingSubjectId,
770
+ sequence: nextSequence
771
+ }),
772
+ hasMore
773
+ };
774
+ }
775
+ function encodeDocumentIndexCheckpoint(input) {
776
+ if (input.sequence < 0n) throw new Error("document index checkpoint sequence is invalid");
777
+ return Buffer.from(
778
+ JSON.stringify({
779
+ v: 1,
780
+ s: documentIndexCheckpointScope(input),
781
+ q: input.sequence.toString()
782
+ }),
783
+ "utf8"
784
+ ).toString("base64url");
785
+ }
786
+ function decodeDocumentIndexCheckpoint(value, scope) {
787
+ try {
788
+ if (!value || value.length > DOCUMENT_INDEX_CHECKPOINT_MAX_CHARS) {
789
+ throw new Error("checkpoint length");
790
+ }
791
+ const bytes = Buffer.from(value, "base64url");
792
+ if (bytes.toString("base64url") !== value) throw new Error("checkpoint encoding");
793
+ const parsed = JSON.parse(bytes.toString("utf8"));
794
+ if (Object.keys(parsed).sort().join(",") !== "q,s,v" || parsed.v !== 1 || typeof parsed.s !== "string" || typeof parsed.q !== "string" || !/^(0|[1-9][0-9]*)$/.test(parsed.q)) {
795
+ throw new Error("checkpoint payload");
796
+ }
797
+ if (parsed.s !== documentIndexCheckpointScope(scope)) {
798
+ throw new Error("document index checkpoint belongs to a different workspace or subject");
799
+ }
800
+ return BigInt(parsed.q);
801
+ } catch (error) {
802
+ if (error instanceof Error && error.message === "document index checkpoint belongs to a different workspace or subject") {
803
+ throw error;
804
+ }
805
+ throw new Error("invalid document index checkpoint", { cause: error });
806
+ }
807
+ }
808
+ function documentIndexCheckpointScope(input) {
809
+ return createHash("sha256").update("opengeni:document-index-checkpoint:v1\0").update(input.accountId).update("\0").update(input.workspaceId).update("\0").update(canonicalEffectiveDocumentSubject(input.initiatingSubjectId)).digest("hex");
810
+ }
811
+ function canonicalEffectiveDocumentSubject(value) {
812
+ const subjectId = cleanString(value);
813
+ if (!subjectId || subjectId !== value) {
814
+ throw new Error("effective document retrieval requires an initiating subject");
815
+ }
816
+ if (new TextEncoder().encode(subjectId).byteLength > DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES) {
817
+ throw new Error(
818
+ `effective document initiating subject exceeds ${DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES} UTF-8 bytes`
819
+ );
820
+ }
821
+ return subjectId;
822
+ }
713
823
  async function getDocument(db, workspaceId, documentId, access) {
714
824
  return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
715
825
  const [row] = await scopedDb.select().from(schema.documents).where(
@@ -1508,6 +1618,40 @@ function mapDocument(row) {
1508
1618
  updatedAt: row.updatedAt.toISOString()
1509
1619
  };
1510
1620
  }
1621
+ function mapIndexedDocumentSummary(row) {
1622
+ if (row.indexSequence === null || row.indexedAt === null) {
1623
+ throw new Error(`Ready document is missing index completion metadata: ${row.id}`);
1624
+ }
1625
+ return {
1626
+ id: row.id,
1627
+ title: row.title,
1628
+ parser: row.parser,
1629
+ chunkCount: row.chunkCount,
1630
+ indexedAt: row.indexedAt.toISOString(),
1631
+ summary: row.summary,
1632
+ topics: cleanStringArray(row.topics),
1633
+ source: {
1634
+ kind: normalizeKnowledgeSourceKind(row.sourceKind),
1635
+ uri: row.sourceUri,
1636
+ externalId: row.sourceExternalId,
1637
+ title: row.sourceTitle,
1638
+ author: row.sourceAuthor,
1639
+ createdAt: row.sourceCreatedAt?.toISOString() ?? null,
1640
+ updatedAt: row.sourceUpdatedAt?.toISOString() ?? null,
1641
+ version: row.sourceVersion
1642
+ },
1643
+ provenance: {
1644
+ ingestionWorkspaceId: row.workspaceId,
1645
+ baseId: row.baseId,
1646
+ fileId: row.fileId,
1647
+ authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
1648
+ authorityWorkspaceId: row.authorityWorkspaceId,
1649
+ authoritySubjectId: row.authoritySubjectId,
1650
+ createdBy: row.createdBy,
1651
+ createdAt: row.createdAt.toISOString()
1652
+ }
1653
+ };
1654
+ }
1511
1655
  function normalizeDocumentAuthorityKind(value) {
1512
1656
  switch (value) {
1513
1657
  case "organization":
@@ -1543,6 +1687,7 @@ export {
1543
1687
  DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES,
1544
1688
  DOCUMENT_CURATION_AUTO_FILE_CONFIDENCE,
1545
1689
  DOCUMENT_CURATION_MAX_INPUT_CHARS,
1690
+ DOCUMENT_INDEX_CHECKPOINT_MAX_CHARS,
1546
1691
  DeterministicEmbeddingProvider,
1547
1692
  HeuristicCurationProvider,
1548
1693
  LiteParseDocumentParser,
@@ -1554,9 +1699,11 @@ export {
1554
1699
  chunkText,
1555
1700
  createDocumentBase,
1556
1701
  createDocumentServices,
1702
+ decodeDocumentIndexCheckpoint,
1557
1703
  deleteDocumentFromBase,
1558
1704
  deterministicEmbedding,
1559
1705
  documentOpenAIEmbeddingConfig,
1706
+ encodeDocumentIndexCheckpoint,
1560
1707
  ensureDefaultBase,
1561
1708
  getDocument,
1562
1709
  getDocumentBase,
@@ -1567,6 +1714,7 @@ export {
1567
1714
  listDocumentBases,
1568
1715
  listDocumentBasesEnsuringDefault,
1569
1716
  listDocuments,
1717
+ listEffectiveIndexedDocuments,
1570
1718
  moveDocumentToBase,
1571
1719
  parseCurationOutcome,
1572
1720
  parseDocumentBytes,