@opengeni/documents 0.8.28-canary.0 → 0.8.28-canary.2

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.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { knowledgeIndexChunks } from "./knowledge-index.js";
2
+ export type { KnowledgeIndexChunk } from "./knowledge-index.js";
1
3
  import type { Settings } from "@opengeni/config";
2
4
  import { type AddDocumentRequest, CreateDocumentBaseRequest, Document, DocumentAuthorityKind, DocumentBase, DocumentCurationStatus, DocumentSearchMode, DocumentSearchResult, DocumentStatus, DocumentVisibility, FileAsset, KnowledgeBrowseResponse, KnowledgeRecord, KnowledgeSearchResponse, KnowledgeSourceKind, ListIndexedDocumentsResponse, type ReclassifyDocumentAuthorityRequest, type RunDocumentDefaultCollectionBackfillRequest } from "@opengeni/contracts";
3
5
  import { type Database } from "@opengeni/db";
@@ -201,14 +203,6 @@ export type EffectiveKnowledgeBrowseInput = {
201
203
  limit?: number | undefined;
202
204
  agentAuthority?: AgentDocumentAuthorityContext | undefined;
203
205
  };
204
- export type DocumentIndexHooks = {
205
- beforeEmbed?: (input: {
206
- accountId: string;
207
- workspaceId: string;
208
- documentId: string;
209
- chunkCount: number;
210
- }) => Promise<void>;
211
- };
212
206
  export declare class LiteParseDocumentParser implements DocumentParser {
213
207
  readonly name = "liteparse";
214
208
  private parseQueue;
@@ -536,7 +530,7 @@ export declare function getDocumentOriginalFile(db: Database, input: {
536
530
  */
537
531
  export declare function getDocumentForIndexing(db: Database, workspaceId: string, documentId: string): Promise<Document | null>;
538
532
  export declare function queueDocumentForReindex(db: Database, workspaceId: string, documentId: string, access?: DocumentAccessFilter, organizationAuthorityGranted?: boolean): Promise<Document>;
539
- export declare function indexDocumentNow(db: Database, objectStorage: ObjectStorage, workspaceId: string, documentId: string, services?: DocumentServices, hooks?: DocumentIndexHooks, access?: DocumentAccessFilter): Promise<Document>;
533
+ export declare function indexDocumentNow(db: Database, objectStorage: ObjectStorage, workspaceId: string, documentId: string, services?: DocumentServices, access?: DocumentAccessFilter): Promise<Document>;
540
534
  export declare function searchDocuments(db: Database, input: DocumentSearchInput, services?: Pick<DocumentServices, "embedder">): Promise<DocumentSearchResult[]>;
541
535
  export type DocumentCandidateRelevanceFloor = {
542
536
  vectorScore: number;
package/dist/index.js CHANGED
@@ -1,3 +1,34 @@
1
+ // src/knowledge-index.ts
2
+ function* knowledgeIndexChunks(entry) {
3
+ const field = entry.content.length ? "content" : "title";
4
+ const text = field === "content" ? entry.content : entry.title;
5
+ const size = 1200;
6
+ const overlap = 160;
7
+ let start = 0;
8
+ let index = 0;
9
+ const scalarBoundary = (at) => {
10
+ const code = text.charCodeAt(at);
11
+ const previous = text.charCodeAt(at - 1);
12
+ return code >= 56320 && code <= 57343 && previous >= 55296 && previous <= 56319 ? at - 1 : at;
13
+ };
14
+ while (start < text.length) {
15
+ const end = scalarBoundary(Math.min(start + size, text.length));
16
+ const excerpt = text.slice(start, end);
17
+ yield {
18
+ index,
19
+ field,
20
+ start,
21
+ end,
22
+ text: excerpt,
23
+ embeddingInput: field === "content" ? `${entry.title}
24
+ ${excerpt}` : excerpt
25
+ };
26
+ if (end === text.length) return;
27
+ start = scalarBoundary(end - overlap);
28
+ index += 1;
29
+ }
30
+ }
31
+
1
32
  // src/index.ts
2
33
  import {
3
34
  KnowledgeProviderCitation,
@@ -13,6 +44,8 @@ import {
13
44
  OrganizationDocumentAuthorityReclassification
14
45
  } from "@opengeni/contracts";
15
46
  import {
47
+ claimKnowledgeDocumentPreparation,
48
+ completeKnowledgeDocumentPreparation,
16
49
  createPersonalDocumentAuthority,
17
50
  getFilesForSubject,
18
51
  resolveDocumentOriginalFileForSubject,
@@ -232,12 +265,12 @@ var LiteParseDocumentParser = class {
232
265
  name = DEFAULT_DOCUMENT_PARSER;
233
266
  parseQueue = Promise.resolve();
234
267
  async parse(bytes, file) {
235
- const text = isTextLike(file) ? Buffer.from(bytes).toString("utf8").replace(/\0/g, " ").trim() : await this.parseWithLiteParse(bytes);
268
+ const text = isTextLike(file) ? Buffer.from(bytes).toString("utf8") : await this.parseWithLiteParse(bytes);
236
269
  if (!text.trim()) {
237
270
  throw new Error(`Parsed document is empty: ${file.filename}`);
238
271
  }
239
272
  return {
240
- text: text.trim(),
273
+ text,
241
274
  metadata: {
242
275
  parser: this.name,
243
276
  filename: file.filename,
@@ -251,7 +284,7 @@ var LiteParseDocumentParser = class {
251
284
  const parser = new LiteParse({ ocrEnabled: true, numWorkers: 1, quiet: true });
252
285
  const result = await parser.parse(Buffer.from(bytes));
253
286
  const text = typeof result?.text === "string" ? result.text : "";
254
- return text.replace(/\0/g, " ").trim();
287
+ return text;
255
288
  });
256
289
  }
257
290
  async enqueueParse(task) {
@@ -1434,7 +1467,7 @@ function assertOrganizationDocumentAuthority(authorityKind, organizationAuthorit
1434
1467
  throw new Error("organization document mutations require exact account authority");
1435
1468
  }
1436
1469
  }
1437
- async function indexDocumentNow(db, objectStorage, workspaceId, documentId, services = createDocumentServices(), hooks = {}, access) {
1470
+ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, services = createDocumentServices(), access) {
1438
1471
  const [loadedDocument] = await withDocumentRls(
1439
1472
  db,
1440
1473
  workspaceId,
@@ -1444,13 +1477,7 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
1444
1477
  ).limit(1)
1445
1478
  );
1446
1479
  if (!loadedDocument) throw new Error(`Document not found: ${documentId}`);
1447
- let document = loadedDocument;
1448
- const file = await requireReadyFile(db, {
1449
- accountId: document.accountId,
1450
- workspaceId,
1451
- subjectId: cleanString(document.createdBy) ?? null,
1452
- fileId: document.fileId
1453
- });
1480
+ const document = loadedDocument;
1454
1481
  await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1455
1482
  await scopedDb.update(schema.documents).set({
1456
1483
  status: "indexing",
@@ -1462,88 +1489,46 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
1462
1489
  );
1463
1490
  });
1464
1491
  try {
1465
- const object = await retryWhileMissing(
1466
- async () => objectStorage.getObjectBytes(file.objectKey)
1467
- );
1468
- if (!object) throw new Error("document source object is missing");
1469
- const bytes = object.bytes;
1470
- const parsed = await services.parser.parse(bytes, file);
1471
- if (document.curationStatus === "pending") {
1472
- document = await curateDroppedDocument(db, services, document, parsed, file);
1473
- }
1474
- const chunks = services.chunker.chunk(parsed, file);
1475
- await hooks.beforeEmbed?.({
1492
+ const identity = {
1476
1493
  accountId: document.accountId,
1477
- workspaceId: document.workspaceId,
1494
+ workspaceId,
1478
1495
  documentId,
1479
- chunkCount: chunks.length
1480
- });
1481
- const embeddings = await services.embedder.embedMany(chunks.map((chunk) => chunk.text));
1482
- if (embeddings.length !== chunks.length) {
1483
- throw new Error(
1484
- `Embedding provider returned ${embeddings.length} embeddings for ${chunks.length} chunks`
1485
- );
1496
+ fileId: document.fileId
1497
+ };
1498
+ const preparation = await claimKnowledgeDocumentPreparation(db, identity);
1499
+ let chunkCount = 0;
1500
+ if (preparation.status === "prepare") {
1501
+ const file = preparation.file;
1502
+ const object = await retryWhileMissing(() => objectStorage.getObjectBytes(file.objectKey));
1503
+ if (!object) throw new Error("document source object is missing");
1504
+ const bytes = object.bytes;
1505
+ if (bytes.length !== file.sizeBytes) throw new Error("document source size changed");
1506
+ const sourceVersion = createHash("sha256").update(bytes).digest("hex");
1507
+ if (file.sha256 && file.sha256 !== sourceVersion)
1508
+ throw new Error("document source hash changed");
1509
+ const parsed = await services.parser.parse(bytes, file);
1510
+ await completeKnowledgeDocumentPreparation(db, {
1511
+ ...identity,
1512
+ leaseId: preparation.leaseId,
1513
+ title: document.title,
1514
+ content: parsed.text,
1515
+ sourceVersion
1516
+ });
1517
+ for (const _chunk of knowledgeIndexChunks({ title: document.title, content: parsed.text }))
1518
+ chunkCount++;
1486
1519
  }
1487
- await withDocumentRls(
1488
- db,
1489
- workspaceId,
1490
- access,
1491
- async (scopedDb) => await scopedDb.transaction(async (tx) => {
1492
- await tx.delete(schema.documentChunks).where(
1493
- and(
1494
- eq(schema.documentChunks.workspaceId, workspaceId),
1495
- eq(schema.documentChunks.documentId, documentId)
1496
- )
1497
- );
1498
- if (chunks.length > 0) {
1499
- await tx.insert(schema.documentChunks).values(
1500
- chunks.map((chunk, index) => ({
1501
- accountId: document.accountId,
1502
- workspaceId: document.workspaceId,
1503
- documentId,
1504
- baseId: document.baseId,
1505
- fileId: file.id,
1506
- authorityKind: document.authorityKind,
1507
- authorityWorkspaceId: document.authorityWorkspaceId,
1508
- authoritySubjectId: document.authoritySubjectId,
1509
- chunkIndex: index,
1510
- text: chunk.text,
1511
- metadata: {
1512
- ...chunk.metadata,
1513
- documentTitle: document.title,
1514
- sourceKind: document.sourceKind,
1515
- sourceUri: document.sourceUri,
1516
- sourceExternalId: document.sourceExternalId,
1517
- sourceTitle: document.sourceTitle,
1518
- sourceAuthor: document.sourceAuthor,
1519
- sourceCreatedAt: document.sourceCreatedAt?.toISOString() ?? null,
1520
- sourceUpdatedAt: document.sourceUpdatedAt?.toISOString() ?? null,
1521
- sourceVersion: document.sourceVersion,
1522
- aclTags: document.aclTags
1523
- },
1524
- embedding: validateEmbedding(
1525
- embeddings[index] ?? [],
1526
- services.embedder.dimensions,
1527
- services.embedder.model
1528
- ),
1529
- embeddingModel: services.embedder.model
1530
- }))
1531
- );
1532
- }
1533
- await tx.update(schema.documents).set({
1534
- status: "ready",
1535
- parser: services.parser.name,
1536
- chunkCount: chunks.length,
1537
- error: null,
1538
- updatedAt: /* @__PURE__ */ new Date()
1539
- }).where(
1540
- and(
1541
- eq(schema.documents.workspaceId, workspaceId),
1542
- eq(schema.documents.id, documentId)
1543
- )
1544
- );
1545
- })
1546
- );
1520
+ await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1521
+ await scopedDb.update(schema.documents).set({
1522
+ status: "ready",
1523
+ parser: services.parser.name,
1524
+ chunkCount,
1525
+ error: null,
1526
+ ...document.curationStatus === "pending" ? { curationStatus: "none" } : {},
1527
+ updatedAt: /* @__PURE__ */ new Date()
1528
+ }).where(
1529
+ and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId))
1530
+ );
1531
+ });
1547
1532
  } catch (error) {
1548
1533
  const [failed] = await withDocumentRls(
1549
1534
  db,
@@ -1564,105 +1549,6 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
1564
1549
  if (!updated) throw new Error(`Document disappeared after indexing: ${documentId}`);
1565
1550
  return updated;
1566
1551
  }
1567
- async function curateDroppedDocument(db, services, document, parsed, file) {
1568
- if (!services.curator) {
1569
- const [updated2] = await withDocumentRls(
1570
- db,
1571
- document.workspaceId,
1572
- { viewerSubjectId: document.authoritySubjectId },
1573
- async (scopedDb) => await scopedDb.update(schema.documents).set({
1574
- curationStatus: "none",
1575
- summary: null,
1576
- topics: [],
1577
- curation: null,
1578
- updatedAt: /* @__PURE__ */ new Date()
1579
- }).where(
1580
- and(
1581
- eq(schema.documents.workspaceId, document.workspaceId),
1582
- eq(schema.documents.id, document.id)
1583
- )
1584
- ).returning()
1585
- );
1586
- return updated2 ?? document;
1587
- }
1588
- const bases = await listDocumentBases(db, document.workspaceId);
1589
- const candidates = bases.filter((base) => base.id !== document.baseId).map((base) => ({
1590
- id: base.id,
1591
- name: base.name,
1592
- description: base.description
1593
- }));
1594
- const input = {
1595
- text: parsed.text.slice(0, DOCUMENT_CURATION_MAX_INPUT_CHARS),
1596
- filename: file.filename,
1597
- title: document.title,
1598
- bases: candidates
1599
- };
1600
- let outcome;
1601
- let model;
1602
- let failure = null;
1603
- try {
1604
- outcome = await services.curator.curate(input);
1605
- model = services.curator.model;
1606
- } catch (error) {
1607
- failure = error instanceof Error ? error.message : String(error);
1608
- console.warn("document curation failed; applying heuristic fallback", {
1609
- workspaceId: document.workspaceId,
1610
- documentId: document.id,
1611
- error: failure
1612
- });
1613
- outcome = heuristicCuration(input, file.contentType);
1614
- model = "heuristic";
1615
- }
1616
- const suggestedBase = candidates.find((base) => base.id === outcome.targetBaseId) ?? null;
1617
- let moveToBaseId = null;
1618
- if (suggestedBase && failure === null && outcome.confidence >= DOCUMENT_CURATION_AUTO_FILE_CONFIDENCE) {
1619
- const conflict = await withDocumentRls(
1620
- db,
1621
- document.workspaceId,
1622
- { viewerSubjectId: document.authoritySubjectId },
1623
- async (scopedDb) => await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
1624
- and(
1625
- eq(schema.documents.workspaceId, document.workspaceId),
1626
- eq(schema.documents.baseId, suggestedBase.id),
1627
- eq(schema.documents.fileId, document.fileId)
1628
- )
1629
- ).limit(1)
1630
- );
1631
- if (conflict.length === 0) {
1632
- moveToBaseId = suggestedBase.id;
1633
- }
1634
- }
1635
- const curation = {
1636
- suggestedBaseId: suggestedBase?.id ?? null,
1637
- suggestedBaseName: suggestedBase?.name ?? null,
1638
- confidence: outcome.confidence,
1639
- reason: failure ? `curation failed (${failure}); heuristic fallback applied` : outcome.reason,
1640
- originalTitle: document.title,
1641
- model
1642
- };
1643
- const curationStatus = failure ? "failed" : moveToBaseId ? "auto_filed" : "suggested";
1644
- const [updated] = await withDocumentRls(
1645
- db,
1646
- document.workspaceId,
1647
- { viewerSubjectId: document.authoritySubjectId },
1648
- async (scopedDb) => await scopedDb.update(schema.documents).set({
1649
- title: outcome.title ?? document.title,
1650
- summary: outcome.summary,
1651
- topics: outcome.topics,
1652
- ...outcome.sourceKind ? { sourceKind: outcome.sourceKind } : {},
1653
- ...moveToBaseId ? { baseId: moveToBaseId } : {},
1654
- curationStatus,
1655
- curation,
1656
- updatedAt: /* @__PURE__ */ new Date()
1657
- }).where(
1658
- and(
1659
- eq(schema.documents.workspaceId, document.workspaceId),
1660
- eq(schema.documents.id, document.id)
1661
- )
1662
- ).returning()
1663
- );
1664
- return updated ?? document;
1665
- }
1666
1552
  async function searchDocuments(db, input, services = createDocumentServices()) {
1667
1553
  return (await searchDocumentCandidates(db, input, services)).results;
1668
1554
  }
@@ -3145,6 +3031,7 @@ export {
3145
3031
  getEffectiveKnowledgeRecord,
3146
3032
  heuristicCuration,
3147
3033
  indexDocumentNow,
3034
+ knowledgeIndexChunks,
3148
3035
  listAccessibleDocuments,
3149
3036
  listDocumentAuthorityReclassifications,
3150
3037
  listDocumentBases,