@opengeni/documents 0.5.39 → 0.6.9-canary.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.
- package/dist/index.d.ts +177 -5
- package/dist/index.js +881 -137
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +1227 -150
package/dist/index.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
KnowledgeProviderCitation,
|
|
4
|
+
DocumentAuthorityReclassification,
|
|
5
|
+
ListDocumentAuthorityReclassificationsResponse,
|
|
6
|
+
DocumentDefaultCollectionBackfill
|
|
7
|
+
} from "@opengeni/contracts";
|
|
8
|
+
import {
|
|
9
|
+
createPersonalDocumentAuthority,
|
|
10
|
+
getFilesForSubject,
|
|
11
|
+
resolveDocumentOriginalFileForSubject,
|
|
4
12
|
rlsContextForWorkspace,
|
|
5
13
|
setSubjectRlsContext,
|
|
6
14
|
withRlsContext,
|
|
@@ -8,9 +16,20 @@ import {
|
|
|
8
16
|
withWorkspaceSubjectRls
|
|
9
17
|
} from "@opengeni/db";
|
|
10
18
|
import * as schema from "@opengeni/db/schema";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
19
|
+
import { retryWhileMissing } from "@opengeni/storage";
|
|
20
|
+
import { createHash, randomUUID } from "crypto";
|
|
21
|
+
import { and, asc, desc, eq, gt, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
|
|
22
|
+
import {
|
|
23
|
+
KNOWLEDGE_BROWSE_CURSOR_MAX_CHARS,
|
|
24
|
+
KNOWLEDGE_BROWSE_MAX_LIMIT,
|
|
25
|
+
KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES,
|
|
26
|
+
KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES,
|
|
27
|
+
KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS,
|
|
28
|
+
KNOWLEDGE_SEARCH_MAX_RESULTS,
|
|
29
|
+
KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE,
|
|
30
|
+
KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
|
|
31
|
+
KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
|
|
32
|
+
} from "@opengeni/contracts";
|
|
14
33
|
|
|
15
34
|
// src/knowledge-projection.ts
|
|
16
35
|
import {
|
|
@@ -694,6 +713,135 @@ async function listDocumentBasesEnsuringDefault(db, input) {
|
|
|
694
713
|
await ensureDefaultBase(db, input);
|
|
695
714
|
return await listDocumentBases(db, input.workspaceId);
|
|
696
715
|
}
|
|
716
|
+
async function runDocumentDefaultCollectionBackfill(db, input) {
|
|
717
|
+
return await withDocumentAccountRls(
|
|
718
|
+
db,
|
|
719
|
+
input.accountId,
|
|
720
|
+
input.workspaceId,
|
|
721
|
+
{ viewerSubjectId: input.actorSubjectId },
|
|
722
|
+
async (scopedDb) => {
|
|
723
|
+
const command = {
|
|
724
|
+
accountId: input.accountId,
|
|
725
|
+
workspaceId: input.workspaceId,
|
|
726
|
+
actorSubjectId: input.actorSubjectId,
|
|
727
|
+
runId: input.runId,
|
|
728
|
+
operationId: input.operationId,
|
|
729
|
+
batchSize: input.batchSize,
|
|
730
|
+
accountAdminAuthorization: input.accountAdminAuthorization
|
|
731
|
+
};
|
|
732
|
+
const rows = await scopedDb.execute(sql`
|
|
733
|
+
SELECT run_document_default_collection_backfill(
|
|
734
|
+
${JSON.stringify(command)}::jsonb
|
|
735
|
+
) AS result
|
|
736
|
+
`);
|
|
737
|
+
const row = rows[0];
|
|
738
|
+
if (!row) throw new Error("Document Default collection backfill returned no result");
|
|
739
|
+
return DocumentDefaultCollectionBackfill.parse(row.result);
|
|
740
|
+
}
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
async function reclassifyDocumentAuthority(db, input) {
|
|
744
|
+
return await withDocumentAccountRls(
|
|
745
|
+
db,
|
|
746
|
+
input.accountId,
|
|
747
|
+
input.workspaceId,
|
|
748
|
+
{ viewerSubjectId: input.actorSubjectId },
|
|
749
|
+
async (scopedDb) => {
|
|
750
|
+
const command = {
|
|
751
|
+
accountId: input.accountId,
|
|
752
|
+
workspaceId: input.workspaceId,
|
|
753
|
+
documentId: input.documentId,
|
|
754
|
+
operationId: input.operationId,
|
|
755
|
+
actorSubjectId: input.actorSubjectId,
|
|
756
|
+
expectedAuthority: input.expectedAuthority,
|
|
757
|
+
targetAuthorityKind: input.targetAuthorityKind,
|
|
758
|
+
accountAdminAuthorization: input.accountAdminAuthorization
|
|
759
|
+
};
|
|
760
|
+
const rows = await scopedDb.execute(sql`
|
|
761
|
+
SELECT reclassify_document_authority(${JSON.stringify(command)}::jsonb) AS result
|
|
762
|
+
`);
|
|
763
|
+
const row = rows[0];
|
|
764
|
+
if (!row) throw new Error("Document authority reclassification returned no result");
|
|
765
|
+
return DocumentAuthorityReclassification.parse(row.result);
|
|
766
|
+
}
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
async function listDocumentAuthorityReclassifications(db, input) {
|
|
770
|
+
const limit = input.limit ?? 50;
|
|
771
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
772
|
+
throw new Error("document authority receipt limit must be between 1 and 100");
|
|
773
|
+
}
|
|
774
|
+
const cursor = input.cursor ? decodeDocumentAuthorityReclassificationCursor(input.cursor, input) : null;
|
|
775
|
+
return await withDocumentAccountRls(
|
|
776
|
+
db,
|
|
777
|
+
input.accountId,
|
|
778
|
+
input.workspaceId,
|
|
779
|
+
{ viewerSubjectId: input.actorSubjectId },
|
|
780
|
+
async (scopedDb) => {
|
|
781
|
+
const rows = await scopedDb.execute(sql`
|
|
782
|
+
SELECT list_document_authority_reclassifications(
|
|
783
|
+
${input.accountId}::uuid,
|
|
784
|
+
${input.workspaceId}::uuid,
|
|
785
|
+
${input.actorSubjectId},
|
|
786
|
+
${input.documentId}::uuid,
|
|
787
|
+
${limit + 1}::integer,
|
|
788
|
+
${cursor?.createdAt ?? null}::timestamptz,
|
|
789
|
+
${cursor?.operationId ?? null}::uuid
|
|
790
|
+
) AS result
|
|
791
|
+
`);
|
|
792
|
+
const parsed = rows.map((row) => DocumentAuthorityReclassification.parse(row.result));
|
|
793
|
+
const hasMore = parsed.length > limit;
|
|
794
|
+
const receipts = parsed.slice(0, limit);
|
|
795
|
+
const tail = hasMore ? receipts.at(-1) : null;
|
|
796
|
+
return ListDocumentAuthorityReclassificationsResponse.parse({
|
|
797
|
+
receipts,
|
|
798
|
+
hasMore,
|
|
799
|
+
nextCursor: tail ? encodeDocumentAuthorityReclassificationCursor(input, {
|
|
800
|
+
createdAt: tail.createdAt,
|
|
801
|
+
operationId: tail.operationId
|
|
802
|
+
}) : null
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
function documentAuthorityReclassificationCursorScope(input) {
|
|
808
|
+
return createHash("sha256").update(
|
|
809
|
+
JSON.stringify([
|
|
810
|
+
"document_authority_reclassification_cursor",
|
|
811
|
+
1,
|
|
812
|
+
input.accountId,
|
|
813
|
+
input.workspaceId,
|
|
814
|
+
input.documentId,
|
|
815
|
+
input.actorSubjectId
|
|
816
|
+
]),
|
|
817
|
+
"utf8"
|
|
818
|
+
).digest("hex").slice(0, 32);
|
|
819
|
+
}
|
|
820
|
+
function encodeDocumentAuthorityReclassificationCursor(scope, cursor) {
|
|
821
|
+
return Buffer.from(
|
|
822
|
+
JSON.stringify({
|
|
823
|
+
v: 1,
|
|
824
|
+
s: documentAuthorityReclassificationCursorScope(scope),
|
|
825
|
+
t: cursor.createdAt,
|
|
826
|
+
i: cursor.operationId
|
|
827
|
+
}),
|
|
828
|
+
"utf8"
|
|
829
|
+
).toString("base64url");
|
|
830
|
+
}
|
|
831
|
+
function decodeDocumentAuthorityReclassificationCursor(value, scope) {
|
|
832
|
+
try {
|
|
833
|
+
if (!value || value.length > 1024) throw new Error("cursor length");
|
|
834
|
+
const bytes = Buffer.from(value, "base64url");
|
|
835
|
+
if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
|
|
836
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
837
|
+
if (Object.keys(parsed).sort().join(",") !== "i,s,t,v" || parsed.v !== 1 || parsed.s !== documentAuthorityReclassificationCursorScope(scope) || typeof parsed.t !== "string" || !Number.isFinite(new Date(parsed.t).getTime()) || typeof parsed.i !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(parsed.i)) {
|
|
838
|
+
throw new Error("cursor payload");
|
|
839
|
+
}
|
|
840
|
+
return { createdAt: parsed.t, operationId: parsed.i };
|
|
841
|
+
} catch (error) {
|
|
842
|
+
throw new Error("invalid document authority receipt cursor", { cause: error });
|
|
843
|
+
}
|
|
844
|
+
}
|
|
697
845
|
async function addDocumentToBase(db, input) {
|
|
698
846
|
return await withRlsContext(
|
|
699
847
|
db,
|
|
@@ -715,7 +863,18 @@ async function addDocumentToBase(db, input) {
|
|
|
715
863
|
if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
|
|
716
864
|
const base = await getDocumentBase(scopedDb, input.workspaceId, input.baseId);
|
|
717
865
|
if (!base) throw new Error(`Document base not found: ${input.baseId}`);
|
|
718
|
-
const
|
|
866
|
+
const initiatingSubjectId = cleanString(input.initiatingSubjectId ?? null);
|
|
867
|
+
const createdBy = cleanString(input.createdBy ?? null);
|
|
868
|
+
if (initiatingSubjectId && createdBy && initiatingSubjectId !== createdBy) {
|
|
869
|
+
throw new Error("document file authority must match the exact initiating subject");
|
|
870
|
+
}
|
|
871
|
+
const fileAuthoritySubjectId = initiatingSubjectId ?? createdBy ?? null;
|
|
872
|
+
const file = await requireReadyFile(scopedDb, {
|
|
873
|
+
accountId: input.accountId,
|
|
874
|
+
workspaceId: input.workspaceId,
|
|
875
|
+
subjectId: fileAuthoritySubjectId,
|
|
876
|
+
fileId: input.fileId
|
|
877
|
+
});
|
|
719
878
|
const knowledgeSourceIdentity = cleanString(input.knowledgeSourceIdentity ?? null);
|
|
720
879
|
if (knowledgeSourceIdentity && knowledgeSourceIdentity.length > 512) {
|
|
721
880
|
throw new Error("knowledge source document identity exceeds 512 characters");
|
|
@@ -762,33 +921,47 @@ async function addDocumentToBase(db, input) {
|
|
|
762
921
|
).returning();
|
|
763
922
|
return mapDocument(updated ?? existing);
|
|
764
923
|
}
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
924
|
+
const documentId = randomUUID();
|
|
925
|
+
const row = await scopedDb.transaction(async (tx) => {
|
|
926
|
+
const userAuthority = authority.kind === "personal" ? await createPersonalDocumentAuthority(tx, {
|
|
927
|
+
accountId: input.accountId,
|
|
928
|
+
workspaceId: input.workspaceId,
|
|
929
|
+
subjectId: authority.subjectId,
|
|
930
|
+
documentId
|
|
931
|
+
}) : null;
|
|
932
|
+
const [inserted] = await tx.insert(schema.documents).values({
|
|
933
|
+
id: documentId,
|
|
934
|
+
accountId: input.accountId,
|
|
935
|
+
workspaceId: input.workspaceId,
|
|
936
|
+
baseId: input.baseId,
|
|
937
|
+
fileId: input.fileId,
|
|
938
|
+
status: "queued",
|
|
939
|
+
title: cleanString(input.title) ?? cleanString(input.sourceTitle) ?? file.filename,
|
|
940
|
+
parser: DEFAULT_DOCUMENT_PARSER,
|
|
941
|
+
sourceKind: input.sourceKind ?? "manual_upload",
|
|
942
|
+
sourceUri: cleanString(input.sourceUri) ?? null,
|
|
943
|
+
sourceExternalId: cleanString(input.sourceExternalId) ?? null,
|
|
944
|
+
sourceTitle: cleanString(input.sourceTitle) ?? null,
|
|
945
|
+
sourceAuthor: cleanString(input.sourceAuthor) ?? null,
|
|
946
|
+
sourceCreatedAt: parseOptionalDate(input.sourceCreatedAt),
|
|
947
|
+
sourceUpdatedAt: parseOptionalDate(input.sourceUpdatedAt),
|
|
948
|
+
sourceVersion: cleanString(input.sourceVersion) ?? null,
|
|
949
|
+
knowledgeSourceIdentity,
|
|
950
|
+
aclTags: cleanStringArray(input.aclTags),
|
|
951
|
+
authorityKind: authority.kind,
|
|
952
|
+
authorityWorkspaceId: userAuthority ? null : authority.workspaceId,
|
|
953
|
+
authoritySubjectId: authority.subjectId,
|
|
954
|
+
authorityId: userAuthority?.authorityId ?? null,
|
|
955
|
+
ownerOrganizationMembershipId: userAuthority?.ownerOrganizationMembershipId ?? null,
|
|
956
|
+
originWorkspaceId: input.workspaceId,
|
|
957
|
+
visibility: authority.kind === "personal" ? "private" : "workspace",
|
|
958
|
+
agentAccess: input.agentAccess ?? true,
|
|
959
|
+
createdBy: fileAuthoritySubjectId,
|
|
960
|
+
curationStatus: input.curationStatus ?? "none",
|
|
961
|
+
updatedAt: now
|
|
962
|
+
}).returning();
|
|
963
|
+
return inserted;
|
|
964
|
+
});
|
|
792
965
|
if (!row) throw new Error("Failed to create document");
|
|
793
966
|
return mapDocument(row);
|
|
794
967
|
}
|
|
@@ -803,7 +976,7 @@ async function moveDocumentToBase(db, input) {
|
|
|
803
976
|
if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
|
|
804
977
|
const [row] = await scopedDb.select().from(schema.documents).where(
|
|
805
978
|
and(
|
|
806
|
-
eq(schema.documents.
|
|
979
|
+
eq(schema.documents.accountId, input.accountId),
|
|
807
980
|
eq(schema.documents.id, input.documentId),
|
|
808
981
|
...documentAccessConditions(input.workspaceId, input.access)
|
|
809
982
|
)
|
|
@@ -816,11 +989,11 @@ async function moveDocumentToBase(db, input) {
|
|
|
816
989
|
throw new Error("document has no suggested base; pass targetBaseId");
|
|
817
990
|
}
|
|
818
991
|
if (targetBaseId === row.baseId) return mapDocument(row);
|
|
819
|
-
const base = await getDocumentBase(scopedDb,
|
|
992
|
+
const base = await getDocumentBase(scopedDb, row.workspaceId, targetBaseId);
|
|
820
993
|
if (!base) throw new Error(`Document base not found: ${targetBaseId}`);
|
|
821
994
|
const [conflict] = await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
|
|
822
995
|
and(
|
|
823
|
-
eq(schema.documents.workspaceId,
|
|
996
|
+
eq(schema.documents.workspaceId, row.workspaceId),
|
|
824
997
|
eq(schema.documents.baseId, targetBaseId),
|
|
825
998
|
...row.knowledgeSourceIdentity ? [eq(schema.documents.knowledgeSourceIdentity, row.knowledgeSourceIdentity)] : [eq(schema.documents.fileId, row.fileId)]
|
|
826
999
|
)
|
|
@@ -836,7 +1009,7 @@ async function moveDocumentToBase(db, input) {
|
|
|
836
1009
|
updatedAt: now
|
|
837
1010
|
}).where(
|
|
838
1011
|
and(
|
|
839
|
-
eq(schema.documents.workspaceId,
|
|
1012
|
+
eq(schema.documents.workspaceId, row.workspaceId),
|
|
840
1013
|
eq(schema.documents.id, input.documentId),
|
|
841
1014
|
...documentAccessConditions(input.workspaceId, input.access)
|
|
842
1015
|
)
|
|
@@ -844,7 +1017,7 @@ async function moveDocumentToBase(db, input) {
|
|
|
844
1017
|
if (updated) {
|
|
845
1018
|
await tx.update(schema.documentChunks).set({ baseId: targetBaseId }).where(
|
|
846
1019
|
and(
|
|
847
|
-
eq(schema.documentChunks.workspaceId,
|
|
1020
|
+
eq(schema.documentChunks.workspaceId, row.workspaceId),
|
|
848
1021
|
eq(schema.documentChunks.documentId, input.documentId)
|
|
849
1022
|
)
|
|
850
1023
|
);
|
|
@@ -865,7 +1038,7 @@ async function deleteDocumentFromBase(db, input) {
|
|
|
865
1038
|
if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
|
|
866
1039
|
const [document] = await scopedDb.select().from(schema.documents).where(
|
|
867
1040
|
and(
|
|
868
|
-
eq(schema.documents.
|
|
1041
|
+
eq(schema.documents.accountId, input.accountId),
|
|
869
1042
|
eq(schema.documents.id, input.documentId),
|
|
870
1043
|
...documentAccessConditions(input.workspaceId, input.access)
|
|
871
1044
|
)
|
|
@@ -882,7 +1055,7 @@ async function deleteDocumentFromBase(db, input) {
|
|
|
882
1055
|
}
|
|
883
1056
|
await scopedDb.delete(schema.documents).where(
|
|
884
1057
|
and(
|
|
885
|
-
eq(schema.documents.
|
|
1058
|
+
eq(schema.documents.accountId, input.accountId),
|
|
886
1059
|
eq(schema.documents.id, input.documentId),
|
|
887
1060
|
...documentAccessConditions(input.workspaceId, input.access)
|
|
888
1061
|
)
|
|
@@ -902,6 +1075,12 @@ async function listDocuments(db, workspaceId, baseId, access) {
|
|
|
902
1075
|
return rows.map(mapDocument);
|
|
903
1076
|
});
|
|
904
1077
|
}
|
|
1078
|
+
async function listAccessibleDocuments(db, workspaceId, access) {
|
|
1079
|
+
return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
|
|
1080
|
+
const rows = await scopedDb.select().from(schema.documents).where(and(...documentAccessConditions(workspaceId, access))).orderBy(desc(schema.documents.updatedAt), asc(schema.documents.createdAt));
|
|
1081
|
+
return rows.map(mapDocument);
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
905
1084
|
async function listEffectiveIndexedDocuments(db, input) {
|
|
906
1085
|
const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
|
|
907
1086
|
const limit = input.limit ?? 50;
|
|
@@ -913,10 +1092,13 @@ async function listEffectiveIndexedDocuments(db, input) {
|
|
|
913
1092
|
workspaceId: input.workspaceId,
|
|
914
1093
|
initiatingSubjectId
|
|
915
1094
|
}) : 0n;
|
|
916
|
-
const access = {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1095
|
+
const access = await resolveEffectiveDocumentAccess(db, {
|
|
1096
|
+
accountId: input.accountId,
|
|
1097
|
+
workspaceId: input.workspaceId,
|
|
1098
|
+
initiatingSubjectId,
|
|
1099
|
+
surface: "agent",
|
|
1100
|
+
agentAuthority: input.agentAuthority
|
|
1101
|
+
});
|
|
920
1102
|
const rows = await withDocumentAccountRls(
|
|
921
1103
|
db,
|
|
922
1104
|
input.accountId,
|
|
@@ -1000,23 +1182,33 @@ function canonicalEffectiveDocumentSubject(value) {
|
|
|
1000
1182
|
async function getDocument(db, workspaceId, documentId, access) {
|
|
1001
1183
|
return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
|
|
1002
1184
|
const [row] = await scopedDb.select().from(schema.documents).where(
|
|
1003
|
-
and(
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1185
|
+
and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
|
|
1186
|
+
).limit(1);
|
|
1187
|
+
return row ? mapDocument(row) : null;
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
async function getDocumentOriginalFile(db, input) {
|
|
1191
|
+
const subjectId = cleanString(input.access.viewerSubjectId ?? null);
|
|
1192
|
+
if (!subjectId || input.access.agentOnly) return null;
|
|
1193
|
+
return await resolveDocumentOriginalFileForSubject(db, {
|
|
1194
|
+
accountId: input.accountId,
|
|
1195
|
+
workspaceId: input.workspaceId,
|
|
1196
|
+
subjectId,
|
|
1197
|
+
documentId: input.documentId
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
async function getDocumentForIndexing(db, workspaceId, documentId) {
|
|
1201
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
1202
|
+
const [row] = await scopedDb.select().from(schema.documents).where(
|
|
1203
|
+
and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId))
|
|
1008
1204
|
).limit(1);
|
|
1009
1205
|
return row ? mapDocument(row) : null;
|
|
1010
1206
|
});
|
|
1011
1207
|
}
|
|
1012
1208
|
async function queueDocumentForReindex(db, workspaceId, documentId, access, organizationAuthorityGranted) {
|
|
1013
1209
|
return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
|
|
1014
|
-
const [document] = await scopedDb.select(
|
|
1015
|
-
and(
|
|
1016
|
-
eq(schema.documents.workspaceId, workspaceId),
|
|
1017
|
-
eq(schema.documents.id, documentId),
|
|
1018
|
-
...documentAccessConditions(workspaceId, access)
|
|
1019
|
-
)
|
|
1210
|
+
const [document] = await scopedDb.select().from(schema.documents).where(
|
|
1211
|
+
and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
|
|
1020
1212
|
).limit(1);
|
|
1021
1213
|
if (!document) throw new Error(`Document not found: ${documentId}`);
|
|
1022
1214
|
assertOrganizationDocumentAuthority(document.authorityKind, organizationAuthorityGranted);
|
|
@@ -1025,11 +1217,7 @@ async function queueDocumentForReindex(db, workspaceId, documentId, access, orga
|
|
|
1025
1217
|
error: null,
|
|
1026
1218
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1027
1219
|
}).where(
|
|
1028
|
-
and(
|
|
1029
|
-
eq(schema.documents.workspaceId, workspaceId),
|
|
1030
|
-
eq(schema.documents.id, documentId),
|
|
1031
|
-
...documentAccessConditions(workspaceId, access)
|
|
1032
|
-
)
|
|
1220
|
+
and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
|
|
1033
1221
|
).returning();
|
|
1034
1222
|
if (!row) throw new Error(`Document not found: ${documentId}`);
|
|
1035
1223
|
return mapDocument(row);
|
|
@@ -1051,7 +1239,12 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
|
|
|
1051
1239
|
);
|
|
1052
1240
|
if (!loadedDocument) throw new Error(`Document not found: ${documentId}`);
|
|
1053
1241
|
let document = loadedDocument;
|
|
1054
|
-
const file = await requireReadyFile(db,
|
|
1242
|
+
const file = await requireReadyFile(db, {
|
|
1243
|
+
accountId: document.accountId,
|
|
1244
|
+
workspaceId,
|
|
1245
|
+
subjectId: cleanString(document.createdBy) ?? null,
|
|
1246
|
+
fileId: document.fileId
|
|
1247
|
+
});
|
|
1055
1248
|
await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
|
|
1056
1249
|
await scopedDb.update(schema.documents).set({
|
|
1057
1250
|
status: "indexing",
|
|
@@ -1063,7 +1256,11 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
|
|
|
1063
1256
|
);
|
|
1064
1257
|
});
|
|
1065
1258
|
try {
|
|
1066
|
-
const
|
|
1259
|
+
const object = await retryWhileMissing(
|
|
1260
|
+
async () => objectStorage.getObjectBytes(file.objectKey)
|
|
1261
|
+
);
|
|
1262
|
+
if (!object) throw new Error("document source object is missing");
|
|
1263
|
+
const bytes = object.bytes;
|
|
1067
1264
|
const parsed = await services.parser.parse(bytes, file);
|
|
1068
1265
|
if (document.curationStatus === "pending") {
|
|
1069
1266
|
document = await curateDroppedDocument(db, services, document, parsed, file);
|
|
@@ -1157,9 +1354,7 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
|
|
|
1157
1354
|
if (!failed) throw error;
|
|
1158
1355
|
return mapDocument(failed);
|
|
1159
1356
|
}
|
|
1160
|
-
const updated = await
|
|
1161
|
-
viewerSubjectId: document.authoritySubjectId
|
|
1162
|
-
});
|
|
1357
|
+
const updated = await getDocumentForIndexing(db, workspaceId, documentId);
|
|
1163
1358
|
if (!updated) throw new Error(`Document disappeared after indexing: ${documentId}`);
|
|
1164
1359
|
return updated;
|
|
1165
1360
|
}
|
|
@@ -1259,6 +1454,9 @@ async function curateDroppedDocument(db, services, document, parsed, file) {
|
|
|
1259
1454
|
return updated ?? document;
|
|
1260
1455
|
}
|
|
1261
1456
|
async function searchDocuments(db, input, services = createDocumentServices()) {
|
|
1457
|
+
return (await searchDocumentCandidates(db, input, services)).results;
|
|
1458
|
+
}
|
|
1459
|
+
async function searchDocumentCandidates(db, input, services, relevanceFloor) {
|
|
1262
1460
|
await assertDocumentAccountWorkspace(db, input.accountId, input.workspaceId);
|
|
1263
1461
|
const mode = input.mode ?? "hybrid";
|
|
1264
1462
|
const limit = Math.min(Math.max(input.limit ?? 5, 1), 50);
|
|
@@ -1283,13 +1481,34 @@ async function searchDocuments(db, input, services = createDocumentServices()) {
|
|
|
1283
1481
|
if (mode === "keyword" || mode === "hybrid") {
|
|
1284
1482
|
rows.push(...await keywordSearchDocuments(db, input, candidateLimit));
|
|
1285
1483
|
}
|
|
1286
|
-
|
|
1484
|
+
const merged = mergeDocumentSearchRows(rows, mode);
|
|
1485
|
+
return selectDocumentSearchCandidateWindow(merged, limit, relevanceFloor);
|
|
1486
|
+
}
|
|
1487
|
+
function selectDocumentSearchCandidateWindow(merged, limit, relevanceFloor) {
|
|
1488
|
+
const boundedLimit = Math.min(Math.max(Math.trunc(limit), 1), 50);
|
|
1489
|
+
if (!relevanceFloor) {
|
|
1490
|
+
return { results: merged.slice(0, boundedLimit), belowRelevanceFloor: 0 };
|
|
1491
|
+
}
|
|
1492
|
+
let belowRelevanceFloor = 0;
|
|
1493
|
+
const relevant = merged.filter((result) => {
|
|
1494
|
+
const included = result.vectorScore !== null && result.vectorScore >= relevanceFloor.vectorScore || result.keywordScore !== null && result.keywordScore >= relevanceFloor.keywordScore;
|
|
1495
|
+
if (!included) belowRelevanceFloor += 1;
|
|
1496
|
+
return included;
|
|
1497
|
+
});
|
|
1498
|
+
return { results: relevant.slice(0, boundedLimit), belowRelevanceFloor };
|
|
1287
1499
|
}
|
|
1288
1500
|
async function searchEffectiveDocuments(db, input, services = createDocumentServices()) {
|
|
1289
1501
|
const initiatingSubjectId = cleanString(input.initiatingSubjectId);
|
|
1290
1502
|
if (!initiatingSubjectId) {
|
|
1291
1503
|
throw new Error("effective document retrieval requires an initiating subject");
|
|
1292
1504
|
}
|
|
1505
|
+
const access = await resolveEffectiveDocumentAccess(db, {
|
|
1506
|
+
accountId: input.accountId,
|
|
1507
|
+
workspaceId: input.workspaceId,
|
|
1508
|
+
initiatingSubjectId,
|
|
1509
|
+
surface: input.surface,
|
|
1510
|
+
agentAuthority: input.agentAuthority
|
|
1511
|
+
});
|
|
1293
1512
|
return await searchDocuments(
|
|
1294
1513
|
db,
|
|
1295
1514
|
{
|
|
@@ -1303,29 +1522,62 @@ async function searchEffectiveDocuments(db, input, services = createDocumentServ
|
|
|
1303
1522
|
aclTags: input.aclTags,
|
|
1304
1523
|
// Construct the lower-level access filter here instead of spreading the
|
|
1305
1524
|
// caller input, so an untyped/legacy access override is always ignored.
|
|
1306
|
-
access
|
|
1307
|
-
viewerSubjectId: initiatingSubjectId,
|
|
1308
|
-
...input.surface === "agent" ? { agentOnly: true } : {}
|
|
1309
|
-
}
|
|
1525
|
+
access
|
|
1310
1526
|
},
|
|
1311
1527
|
services
|
|
1312
1528
|
);
|
|
1313
1529
|
}
|
|
1314
1530
|
async function searchEffectiveKnowledge(db, input, services = createDocumentServices()) {
|
|
1315
1531
|
const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
|
|
1316
|
-
const
|
|
1532
|
+
const requestedLimit = Math.min(Math.max(input.limit ?? 5, 1), KNOWLEDGE_SEARCH_MAX_RESULTS);
|
|
1533
|
+
const access = await resolveEffectiveDocumentAccess(db, {
|
|
1534
|
+
accountId: input.accountId,
|
|
1535
|
+
workspaceId: input.workspaceId,
|
|
1536
|
+
initiatingSubjectId,
|
|
1537
|
+
surface: input.surface,
|
|
1538
|
+
agentAuthority: input.agentAuthority
|
|
1539
|
+
});
|
|
1540
|
+
const candidateLimit = Math.min(requestedLimit * 4, KNOWLEDGE_SEARCH_MAX_RESULTS);
|
|
1541
|
+
const rankedSelection = await searchDocumentCandidates(
|
|
1317
1542
|
db,
|
|
1318
|
-
{
|
|
1319
|
-
|
|
1543
|
+
{
|
|
1544
|
+
accountId: input.accountId,
|
|
1545
|
+
workspaceId: input.workspaceId,
|
|
1546
|
+
query: input.query,
|
|
1547
|
+
limit: candidateLimit,
|
|
1548
|
+
...input.baseIds ? { baseIds: input.baseIds } : {},
|
|
1549
|
+
...input.mode ? { mode: input.mode } : {},
|
|
1550
|
+
...input.sourceKinds ? { sourceKinds: input.sourceKinds } : {},
|
|
1551
|
+
...input.aclTags ? { aclTags: input.aclTags } : {},
|
|
1552
|
+
access
|
|
1553
|
+
},
|
|
1554
|
+
services,
|
|
1555
|
+
{
|
|
1556
|
+
vectorScore: KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
|
|
1557
|
+
keywordScore: KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE
|
|
1558
|
+
}
|
|
1320
1559
|
);
|
|
1321
|
-
|
|
1322
|
-
|
|
1560
|
+
const ranked = rankedSelection.results;
|
|
1561
|
+
if (ranked.length === 0) {
|
|
1562
|
+
return selectKnowledgeSearchResults({
|
|
1563
|
+
candidates: [],
|
|
1564
|
+
rankedCandidateCount: 0,
|
|
1565
|
+
requestedLimit,
|
|
1566
|
+
alreadyBelowRelevanceFloor: rankedSelection.belowRelevanceFloor
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1323
1569
|
const current = await withDocumentAccountRls(
|
|
1324
1570
|
db,
|
|
1325
1571
|
input.accountId,
|
|
1326
1572
|
input.workspaceId,
|
|
1327
1573
|
access,
|
|
1328
|
-
async (scopedDb) => await scopedDb.select({
|
|
1574
|
+
async (scopedDb) => await scopedDb.select({
|
|
1575
|
+
chunk: schema.documentChunks,
|
|
1576
|
+
document: schema.documents,
|
|
1577
|
+
citation: googleDriveCitationProjection(input.workspaceId, access),
|
|
1578
|
+
previousChunkId: knowledgePreviousChunkIdProjection(),
|
|
1579
|
+
nextChunkId: knowledgeNextChunkIdProjection()
|
|
1580
|
+
}).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
|
|
1329
1581
|
and(
|
|
1330
1582
|
eq(schema.documents.accountId, input.accountId),
|
|
1331
1583
|
eq(schema.documentChunks.accountId, input.accountId),
|
|
@@ -1339,28 +1591,310 @@ async function searchEffectiveKnowledge(db, input, services = createDocumentServ
|
|
|
1339
1591
|
)
|
|
1340
1592
|
);
|
|
1341
1593
|
const currentByChunkId = new Map(current.map((row) => [row.chunk.id, row]));
|
|
1342
|
-
return {
|
|
1343
|
-
|
|
1594
|
+
return selectKnowledgeSearchResults({
|
|
1595
|
+
rankedCandidateCount: ranked.length,
|
|
1596
|
+
requestedLimit,
|
|
1597
|
+
alreadyBelowRelevanceFloor: rankedSelection.belowRelevanceFloor,
|
|
1598
|
+
candidates: ranked.flatMap((rankedResult) => {
|
|
1344
1599
|
const row = currentByChunkId.get(rankedResult.chunkId);
|
|
1345
1600
|
if (!row) return [];
|
|
1346
1601
|
return [
|
|
1347
1602
|
{
|
|
1348
|
-
record: knowledgeChunkRecord(row.document, row.chunk
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1603
|
+
record: knowledgeChunkRecord(row.document, row.chunk, row.citation, {
|
|
1604
|
+
previousChunkId: row.previousChunkId,
|
|
1605
|
+
nextChunkId: row.nextChunkId
|
|
1606
|
+
}),
|
|
1607
|
+
semanticScore: rankedResult.score,
|
|
1608
|
+
matchType: rankedResult.matchType,
|
|
1609
|
+
vectorScore: rankedResult.vectorScore,
|
|
1610
|
+
keywordScore: rankedResult.keywordScore
|
|
1355
1611
|
}
|
|
1356
1612
|
];
|
|
1357
1613
|
})
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
function selectKnowledgeSearchResults(input) {
|
|
1617
|
+
const requestedLimit = Math.min(
|
|
1618
|
+
Math.max(Math.trunc(input.requestedLimit), 1),
|
|
1619
|
+
KNOWLEDGE_SEARCH_MAX_RESULTS
|
|
1620
|
+
);
|
|
1621
|
+
const nowMs = (input.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
1622
|
+
let belowRelevanceFloor = Math.min(
|
|
1623
|
+
Math.max(0, Math.trunc(input.alreadyBelowRelevanceFloor ?? 0)),
|
|
1624
|
+
KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS
|
|
1625
|
+
);
|
|
1626
|
+
const relevant = [];
|
|
1627
|
+
for (const candidate of input.candidates) {
|
|
1628
|
+
const relevanceSignals = [];
|
|
1629
|
+
if (candidate.vectorScore !== null && candidate.vectorScore >= KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE) {
|
|
1630
|
+
relevanceSignals.push("vector");
|
|
1631
|
+
}
|
|
1632
|
+
if (candidate.keywordScore !== null && candidate.keywordScore >= KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE) {
|
|
1633
|
+
relevanceSignals.push("keyword");
|
|
1634
|
+
}
|
|
1635
|
+
if (relevanceSignals.length === 0) {
|
|
1636
|
+
belowRelevanceFloor = Math.min(belowRelevanceFloor + 1, KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS);
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
const freshness = knowledgeFreshness(candidate.record.quality.freshnessAt, nowMs);
|
|
1640
|
+
const qualityAdjustment = freshness === "current" ? 0.02 : freshness === "aging" ? 0.01 : 0;
|
|
1641
|
+
relevant.push({
|
|
1642
|
+
record: candidate.record,
|
|
1643
|
+
retrieval: {
|
|
1644
|
+
score: roundScore(Math.min(1, candidate.semanticScore + qualityAdjustment)),
|
|
1645
|
+
semanticScore: roundScore(candidate.semanticScore),
|
|
1646
|
+
matchType: candidate.matchType,
|
|
1647
|
+
vectorScore: candidate.vectorScore === null ? null : roundScore(candidate.vectorScore),
|
|
1648
|
+
keywordScore: candidate.keywordScore === null ? null : roundScore(candidate.keywordScore),
|
|
1649
|
+
relevanceSignals,
|
|
1650
|
+
freshness,
|
|
1651
|
+
qualityAdjustment,
|
|
1652
|
+
duplicateCount: 0
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
relevant.sort(compareKnowledgeSearchResults);
|
|
1657
|
+
const deduped = [];
|
|
1658
|
+
const byContent = /* @__PURE__ */ new Map();
|
|
1659
|
+
let asDuplicate = 0;
|
|
1660
|
+
for (const result of relevant) {
|
|
1661
|
+
const key = knowledgeTextualContentKey(result.record);
|
|
1662
|
+
const retainedIndex = byContent.get(key);
|
|
1663
|
+
if (retainedIndex === void 0) {
|
|
1664
|
+
byContent.set(key, deduped.length);
|
|
1665
|
+
deduped.push(result);
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
asDuplicate += 1;
|
|
1669
|
+
const retained = deduped[retainedIndex];
|
|
1670
|
+
retained.retrieval.duplicateCount += 1;
|
|
1671
|
+
}
|
|
1672
|
+
const forLimit = Math.max(0, deduped.length - requestedLimit);
|
|
1673
|
+
const bounded = deduped.slice(0, requestedLimit);
|
|
1674
|
+
let forResponseBudget = 0;
|
|
1675
|
+
let response = knowledgeSearchResponse({
|
|
1676
|
+
results: bounded,
|
|
1677
|
+
rankedCandidateCount: input.rankedCandidateCount,
|
|
1678
|
+
recheckedCandidateCount: input.candidates.length,
|
|
1679
|
+
belowRelevanceFloor,
|
|
1680
|
+
asDuplicate,
|
|
1681
|
+
forLimit,
|
|
1682
|
+
forResponseBudget
|
|
1683
|
+
});
|
|
1684
|
+
while (knowledgeResponseBytes(response) > KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES) {
|
|
1685
|
+
if (bounded.length === 0) {
|
|
1686
|
+
throw new Error("knowledge search selection facts exceed the response budget");
|
|
1687
|
+
}
|
|
1688
|
+
bounded.pop();
|
|
1689
|
+
forResponseBudget += 1;
|
|
1690
|
+
response = knowledgeSearchResponse({
|
|
1691
|
+
results: bounded,
|
|
1692
|
+
rankedCandidateCount: input.rankedCandidateCount,
|
|
1693
|
+
recheckedCandidateCount: input.candidates.length,
|
|
1694
|
+
belowRelevanceFloor,
|
|
1695
|
+
asDuplicate,
|
|
1696
|
+
forLimit,
|
|
1697
|
+
forResponseBudget
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
return response;
|
|
1701
|
+
}
|
|
1702
|
+
function compareKnowledgeSearchResults(left, right) {
|
|
1703
|
+
return right.retrieval.score - left.retrieval.score || right.retrieval.semanticScore - left.retrieval.semanticScore || (right.retrieval.vectorScore ?? 0) - (left.retrieval.vectorScore ?? 0) || (right.retrieval.keywordScore ?? 0) - (left.retrieval.keywordScore ?? 0) || (left.record.id === right.record.id ? 0 : left.record.id < right.record.id ? -1 : 1);
|
|
1704
|
+
}
|
|
1705
|
+
function knowledgeFreshness(value, nowMs) {
|
|
1706
|
+
const freshnessMs = Date.parse(value);
|
|
1707
|
+
if (!Number.isFinite(freshnessMs)) return "stale";
|
|
1708
|
+
const ageDays = Math.max(0, (nowMs - freshnessMs) / 864e5);
|
|
1709
|
+
if (ageDays <= 90) return "current";
|
|
1710
|
+
return ageDays <= 365 ? "aging" : "stale";
|
|
1711
|
+
}
|
|
1712
|
+
function knowledgeTextualContentKey(record) {
|
|
1713
|
+
return createHash("sha256").update("opengeni:knowledge-search-content:v1\0").update(record.title).update("\0").update(
|
|
1714
|
+
JSON.stringify({
|
|
1715
|
+
body: record.content.body,
|
|
1716
|
+
summary: record.content.summary,
|
|
1717
|
+
topics: record.content.topics
|
|
1718
|
+
})
|
|
1719
|
+
).digest("hex");
|
|
1720
|
+
}
|
|
1721
|
+
function knowledgeSearchResponse(input) {
|
|
1722
|
+
const selection = {
|
|
1723
|
+
relevanceFloor: {
|
|
1724
|
+
policy: "any_signal",
|
|
1725
|
+
vectorScore: KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
|
|
1726
|
+
keywordScore: KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE
|
|
1727
|
+
},
|
|
1728
|
+
dedupe: { policy: "exact_textual_content" },
|
|
1729
|
+
candidates: {
|
|
1730
|
+
ranked: input.rankedCandidateCount,
|
|
1731
|
+
rechecked: input.recheckedCandidateCount,
|
|
1732
|
+
omittedOnRecheck: Math.max(0, input.rankedCandidateCount - input.recheckedCandidateCount)
|
|
1733
|
+
},
|
|
1734
|
+
omitted: {
|
|
1735
|
+
belowRelevanceFloor: input.belowRelevanceFloor,
|
|
1736
|
+
asDuplicate: input.asDuplicate,
|
|
1737
|
+
forLimit: input.forLimit,
|
|
1738
|
+
forResponseBudget: input.forResponseBudget
|
|
1739
|
+
},
|
|
1740
|
+
budget: {
|
|
1741
|
+
maxResults: KNOWLEDGE_SEARCH_MAX_RESULTS,
|
|
1742
|
+
maxResponseBytes: KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES,
|
|
1743
|
+
responseBytes: 0,
|
|
1744
|
+
tokenEstimateBytesPerToken: KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN,
|
|
1745
|
+
estimatedTokens: 0,
|
|
1746
|
+
maxEstimatedTokens: Math.ceil(
|
|
1747
|
+
KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
|
|
1748
|
+
)
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
const response = {
|
|
1752
|
+
results: [...input.results],
|
|
1753
|
+
selection
|
|
1754
|
+
};
|
|
1755
|
+
for (let index = 0; index < 8; index += 1) {
|
|
1756
|
+
const responseBytes = knowledgeResponseBytes(response);
|
|
1757
|
+
const estimatedTokens = Math.ceil(
|
|
1758
|
+
responseBytes / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
|
|
1759
|
+
);
|
|
1760
|
+
if (response.selection.budget.responseBytes === responseBytes && response.selection.budget.estimatedTokens === estimatedTokens) {
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
response.selection.budget.responseBytes = responseBytes;
|
|
1764
|
+
response.selection.budget.estimatedTokens = estimatedTokens;
|
|
1765
|
+
}
|
|
1766
|
+
return response;
|
|
1767
|
+
}
|
|
1768
|
+
function knowledgeResponseBytes(response) {
|
|
1769
|
+
return Buffer.byteLength(JSON.stringify(response), "utf8");
|
|
1770
|
+
}
|
|
1771
|
+
function selectKnowledgeBrowseRecords(input) {
|
|
1772
|
+
const entries = input.entries.slice(0, KNOWLEDGE_BROWSE_MAX_LIMIT);
|
|
1773
|
+
const selected = entries.map((entry) => entry.record);
|
|
1774
|
+
let omittedForResponseBudget = 0;
|
|
1775
|
+
let compactedRecordCount = 0;
|
|
1776
|
+
let response = knowledgeBrowseResponse({
|
|
1777
|
+
records: selected,
|
|
1778
|
+
nextCursor: input.hasMoreAfterEntries ? entries.at(-1)?.cursorAfter ?? null : null,
|
|
1779
|
+
hasMore: input.hasMoreAfterEntries,
|
|
1780
|
+
omittedForResponseBudget,
|
|
1781
|
+
compactedRecordCount
|
|
1782
|
+
});
|
|
1783
|
+
while (selected.length > 1 && knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
|
|
1784
|
+
selected.pop();
|
|
1785
|
+
omittedForResponseBudget += 1;
|
|
1786
|
+
response = knowledgeBrowseResponse({
|
|
1787
|
+
records: selected,
|
|
1788
|
+
nextCursor: entries[selected.length - 1]?.cursorAfter ?? null,
|
|
1789
|
+
hasMore: true,
|
|
1790
|
+
omittedForResponseBudget,
|
|
1791
|
+
compactedRecordCount
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
if (selected.length === 1 && knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
|
|
1795
|
+
selected[0] = compactKnowledgeBrowseRecord(selected[0]);
|
|
1796
|
+
compactedRecordCount = 1;
|
|
1797
|
+
response = knowledgeBrowseResponse({
|
|
1798
|
+
records: selected,
|
|
1799
|
+
nextCursor: omittedForResponseBudget > 0 || input.hasMoreAfterEntries ? entries[0]?.cursorAfter ?? null : null,
|
|
1800
|
+
hasMore: omittedForResponseBudget > 0 || input.hasMoreAfterEntries,
|
|
1801
|
+
omittedForResponseBudget,
|
|
1802
|
+
compactedRecordCount
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
if (knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
|
|
1806
|
+
throw new Error("knowledge browse discovery projection exceeds the response budget");
|
|
1807
|
+
}
|
|
1808
|
+
return response;
|
|
1809
|
+
}
|
|
1810
|
+
function compactKnowledgeBrowseRecord(record) {
|
|
1811
|
+
const fields = /* @__PURE__ */ new Set([
|
|
1812
|
+
...record.projection.fields,
|
|
1813
|
+
"content.body",
|
|
1814
|
+
"content.summary",
|
|
1815
|
+
"content.topics",
|
|
1816
|
+
"content.metadata",
|
|
1817
|
+
"provenance.source.uri",
|
|
1818
|
+
"provenance.source.externalId",
|
|
1819
|
+
"provenance.source.title",
|
|
1820
|
+
"provenance.source.author",
|
|
1821
|
+
"provenance.source.version",
|
|
1822
|
+
"provenance.citation"
|
|
1823
|
+
]);
|
|
1824
|
+
return {
|
|
1825
|
+
...record,
|
|
1826
|
+
content: {
|
|
1827
|
+
format: "markdown",
|
|
1828
|
+
body: null,
|
|
1829
|
+
summary: null,
|
|
1830
|
+
topics: [],
|
|
1831
|
+
metadata: {}
|
|
1832
|
+
},
|
|
1833
|
+
provenance: {
|
|
1834
|
+
...record.provenance,
|
|
1835
|
+
source: {
|
|
1836
|
+
...record.provenance.source,
|
|
1837
|
+
uri: null,
|
|
1838
|
+
externalId: null,
|
|
1839
|
+
title: null,
|
|
1840
|
+
author: null,
|
|
1841
|
+
version: null
|
|
1842
|
+
},
|
|
1843
|
+
citation: null
|
|
1844
|
+
},
|
|
1845
|
+
links: record.links.filter((link) => link.target.kind === "knowledge"),
|
|
1846
|
+
projection: {
|
|
1847
|
+
truncated: true,
|
|
1848
|
+
fields: [...fields].sort()
|
|
1849
|
+
}
|
|
1358
1850
|
};
|
|
1359
1851
|
}
|
|
1852
|
+
function knowledgeBrowseResponse(input) {
|
|
1853
|
+
const response = {
|
|
1854
|
+
records: [...input.records],
|
|
1855
|
+
nextCursor: input.nextCursor,
|
|
1856
|
+
hasMore: input.hasMore,
|
|
1857
|
+
selection: {
|
|
1858
|
+
omitted: { forResponseBudget: input.omittedForResponseBudget },
|
|
1859
|
+
compactedRecordCount: input.compactedRecordCount,
|
|
1860
|
+
budget: {
|
|
1861
|
+
maxResults: KNOWLEDGE_BROWSE_MAX_LIMIT,
|
|
1862
|
+
maxResponseBytes: KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES,
|
|
1863
|
+
responseBytes: 0,
|
|
1864
|
+
tokenEstimateBytesPerToken: KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN,
|
|
1865
|
+
estimatedTokens: 0,
|
|
1866
|
+
maxEstimatedTokens: Math.ceil(
|
|
1867
|
+
KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
|
|
1868
|
+
)
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
for (let index = 0; index < 8; index += 1) {
|
|
1873
|
+
const responseBytes = knowledgeBrowseResponseBytes(response);
|
|
1874
|
+
const estimatedTokens = Math.ceil(
|
|
1875
|
+
responseBytes / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
|
|
1876
|
+
);
|
|
1877
|
+
if (response.selection.budget.responseBytes === responseBytes && response.selection.budget.estimatedTokens === estimatedTokens) {
|
|
1878
|
+
break;
|
|
1879
|
+
}
|
|
1880
|
+
response.selection.budget.responseBytes = responseBytes;
|
|
1881
|
+
response.selection.budget.estimatedTokens = estimatedTokens;
|
|
1882
|
+
}
|
|
1883
|
+
return response;
|
|
1884
|
+
}
|
|
1885
|
+
function knowledgeBrowseResponseBytes(response) {
|
|
1886
|
+
return Buffer.byteLength(JSON.stringify(response), "utf8");
|
|
1887
|
+
}
|
|
1360
1888
|
async function getEffectiveKnowledgeRecord(db, input) {
|
|
1361
1889
|
const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
|
|
1362
1890
|
const target = parseKnowledgeRecordId(input.id);
|
|
1363
|
-
const access =
|
|
1891
|
+
const access = await resolveEffectiveDocumentAccess(db, {
|
|
1892
|
+
accountId: input.accountId,
|
|
1893
|
+
workspaceId: input.workspaceId,
|
|
1894
|
+
initiatingSubjectId,
|
|
1895
|
+
surface: input.surface ?? "agent",
|
|
1896
|
+
agentAuthority: input.agentAuthority
|
|
1897
|
+
});
|
|
1364
1898
|
return await withDocumentAccountRls(
|
|
1365
1899
|
db,
|
|
1366
1900
|
input.accountId,
|
|
@@ -1368,7 +1902,18 @@ async function getEffectiveKnowledgeRecord(db, input) {
|
|
|
1368
1902
|
access,
|
|
1369
1903
|
async (scopedDb) => {
|
|
1370
1904
|
if (target.kind === "document") {
|
|
1371
|
-
const [
|
|
1905
|
+
const [row2] = await scopedDb.select({
|
|
1906
|
+
document: schema.documents,
|
|
1907
|
+
citation: googleDriveCitationProjection(input.workspaceId, access),
|
|
1908
|
+
firstChunkId: schema.documentChunks.id
|
|
1909
|
+
}).from(schema.documents).leftJoin(
|
|
1910
|
+
schema.documentChunks,
|
|
1911
|
+
and(
|
|
1912
|
+
eq(schema.documentChunks.accountId, schema.documents.accountId),
|
|
1913
|
+
eq(schema.documentChunks.documentId, schema.documents.id),
|
|
1914
|
+
eq(schema.documentChunks.chunkIndex, 0)
|
|
1915
|
+
)
|
|
1916
|
+
).where(
|
|
1372
1917
|
and(
|
|
1373
1918
|
eq(schema.documents.accountId, input.accountId),
|
|
1374
1919
|
eq(schema.documents.id, target.id),
|
|
@@ -1376,9 +1921,15 @@ async function getEffectiveKnowledgeRecord(db, input) {
|
|
|
1376
1921
|
...documentAccessConditions(input.workspaceId, access)
|
|
1377
1922
|
)
|
|
1378
1923
|
).limit(1);
|
|
1379
|
-
return
|
|
1924
|
+
return row2 ? knowledgeDocumentRecord(row2.document, row2.citation, row2.firstChunkId) : null;
|
|
1380
1925
|
}
|
|
1381
|
-
const [row] = await scopedDb.select({
|
|
1926
|
+
const [row] = await scopedDb.select({
|
|
1927
|
+
chunk: schema.documentChunks,
|
|
1928
|
+
document: schema.documents,
|
|
1929
|
+
citation: googleDriveCitationProjection(input.workspaceId, access),
|
|
1930
|
+
previousChunkId: knowledgePreviousChunkIdProjection(),
|
|
1931
|
+
nextChunkId: knowledgeNextChunkIdProjection()
|
|
1932
|
+
}).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
|
|
1382
1933
|
and(
|
|
1383
1934
|
eq(schema.documents.accountId, input.accountId),
|
|
1384
1935
|
eq(schema.documentChunks.accountId, input.accountId),
|
|
@@ -1387,7 +1938,10 @@ async function getEffectiveKnowledgeRecord(db, input) {
|
|
|
1387
1938
|
...documentAccessConditions(input.workspaceId, access)
|
|
1388
1939
|
)
|
|
1389
1940
|
).limit(1);
|
|
1390
|
-
return row ? knowledgeChunkRecord(row.document, row.chunk
|
|
1941
|
+
return row ? knowledgeChunkRecord(row.document, row.chunk, row.citation, {
|
|
1942
|
+
previousChunkId: row.previousChunkId,
|
|
1943
|
+
nextChunkId: row.nextChunkId
|
|
1944
|
+
}) : null;
|
|
1391
1945
|
}
|
|
1392
1946
|
);
|
|
1393
1947
|
}
|
|
@@ -1417,11 +1971,14 @@ async function browseEffectiveKnowledge(db, input) {
|
|
|
1417
1971
|
topic,
|
|
1418
1972
|
sourceKinds
|
|
1419
1973
|
};
|
|
1420
|
-
const
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1974
|
+
const topLevelAfter = !parent && input.cursor ? decodeKnowledgeBrowseCursor(input.cursor, cursorScope) : 0n;
|
|
1975
|
+
const access = await resolveEffectiveDocumentAccess(db, {
|
|
1976
|
+
accountId: input.accountId,
|
|
1977
|
+
workspaceId: input.workspaceId,
|
|
1978
|
+
initiatingSubjectId,
|
|
1979
|
+
surface: input.surface ?? "agent",
|
|
1980
|
+
agentAuthority: input.agentAuthority
|
|
1981
|
+
});
|
|
1425
1982
|
return await withDocumentAccountRls(
|
|
1426
1983
|
db,
|
|
1427
1984
|
input.accountId,
|
|
@@ -1429,7 +1986,7 @@ async function browseEffectiveKnowledge(db, input) {
|
|
|
1429
1986
|
access,
|
|
1430
1987
|
async (scopedDb) => {
|
|
1431
1988
|
if (parent) {
|
|
1432
|
-
const [authorizedParent] = await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
|
|
1989
|
+
const [authorizedParent] = await scopedDb.select({ id: schema.documents.id, indexSequence: schema.documents.indexSequence }).from(schema.documents).where(
|
|
1433
1990
|
and(
|
|
1434
1991
|
eq(schema.documents.accountId, input.accountId),
|
|
1435
1992
|
eq(schema.documents.id, parent.id),
|
|
@@ -1437,51 +1994,96 @@ async function browseEffectiveKnowledge(db, input) {
|
|
|
1437
1994
|
...documentAccessConditions(input.workspaceId, access)
|
|
1438
1995
|
)
|
|
1439
1996
|
).limit(1);
|
|
1440
|
-
if (!authorizedParent)
|
|
1441
|
-
|
|
1997
|
+
if (!authorizedParent) {
|
|
1998
|
+
return selectKnowledgeBrowseRecords({ entries: [], hasMoreAfterEntries: false });
|
|
1999
|
+
}
|
|
2000
|
+
if (authorizedParent.indexSequence === null) {
|
|
2001
|
+
throw new Error("ready knowledge document is missing its index revision");
|
|
2002
|
+
}
|
|
2003
|
+
const parentCursorScope = {
|
|
2004
|
+
...cursorScope,
|
|
2005
|
+
parentRevision: authorizedParent.indexSequence.toString()
|
|
2006
|
+
};
|
|
2007
|
+
const after = input.cursor ? decodeKnowledgeBrowseCursor(input.cursor, parentCursorScope) : 0n;
|
|
2008
|
+
if (after > 2147483648n) {
|
|
2009
|
+
throw new Error("invalid knowledge browse cursor");
|
|
2010
|
+
}
|
|
2011
|
+
const rows2 = await scopedDb.select({
|
|
2012
|
+
chunk: schema.documentChunks,
|
|
2013
|
+
document: schema.documents,
|
|
2014
|
+
citation: googleDriveCitationProjection(input.workspaceId, access),
|
|
2015
|
+
previousChunkId: knowledgePreviousChunkIdProjection(),
|
|
2016
|
+
nextChunkId: knowledgeNextChunkIdProjection()
|
|
2017
|
+
}).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
|
|
1442
2018
|
and(
|
|
1443
2019
|
eq(schema.documentChunks.accountId, input.accountId),
|
|
1444
2020
|
eq(schema.documentChunks.documentId, parent.id),
|
|
1445
2021
|
gt(schema.documentChunks.chunkIndex, Number(after) - 1),
|
|
1446
2022
|
eq(schema.documents.status, "ready"),
|
|
2023
|
+
eq(schema.documents.indexSequence, authorizedParent.indexSequence),
|
|
1447
2024
|
...documentAccessConditions(input.workspaceId, access)
|
|
1448
2025
|
)
|
|
1449
2026
|
).orderBy(asc(schema.documentChunks.chunkIndex)).limit(limit + 1);
|
|
1450
2027
|
const hasMore2 = rows2.length > limit;
|
|
1451
2028
|
const page2 = rows2.slice(0, limit);
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
2029
|
+
return selectKnowledgeBrowseRecords({
|
|
2030
|
+
entries: page2.map((row) => ({
|
|
2031
|
+
record: knowledgeChunkRecord(row.document, row.chunk, row.citation, {
|
|
2032
|
+
previousChunkId: row.previousChunkId,
|
|
2033
|
+
nextChunkId: row.nextChunkId
|
|
2034
|
+
}),
|
|
2035
|
+
cursorAfter: encodeKnowledgeBrowseCursor(
|
|
2036
|
+
parentCursorScope,
|
|
2037
|
+
BigInt(row.chunk.chunkIndex + 1)
|
|
2038
|
+
)
|
|
2039
|
+
})),
|
|
2040
|
+
hasMoreAfterEntries: hasMore2
|
|
2041
|
+
});
|
|
1458
2042
|
}
|
|
1459
2043
|
const conditions = [
|
|
1460
2044
|
eq(schema.documents.accountId, input.accountId),
|
|
1461
2045
|
eq(schema.documents.status, "ready"),
|
|
1462
2046
|
isNotNull(schema.documents.indexSequence),
|
|
1463
|
-
gt(schema.documents.indexSequence,
|
|
2047
|
+
gt(schema.documents.indexSequence, topLevelAfter),
|
|
1464
2048
|
...documentAccessConditions(input.workspaceId, access)
|
|
1465
2049
|
];
|
|
1466
2050
|
if (topic) conditions.push(sql`${schema.documents.topics} ? ${topic}`);
|
|
1467
2051
|
if (sourceKinds.length > 0)
|
|
1468
2052
|
conditions.push(inArray(schema.documents.sourceKind, sourceKinds));
|
|
1469
|
-
const rows = await scopedDb.select(
|
|
2053
|
+
const rows = await scopedDb.select({
|
|
2054
|
+
document: schema.documents,
|
|
2055
|
+
citation: googleDriveCitationProjection(input.workspaceId, access),
|
|
2056
|
+
firstChunkId: schema.documentChunks.id
|
|
2057
|
+
}).from(schema.documents).leftJoin(
|
|
2058
|
+
schema.documentChunks,
|
|
2059
|
+
and(
|
|
2060
|
+
eq(schema.documentChunks.accountId, schema.documents.accountId),
|
|
2061
|
+
eq(schema.documentChunks.documentId, schema.documents.id),
|
|
2062
|
+
eq(schema.documentChunks.chunkIndex, 0)
|
|
2063
|
+
)
|
|
2064
|
+
).where(and(...conditions)).orderBy(asc(schema.documents.indexSequence)).limit(limit + 1);
|
|
1470
2065
|
const hasMore = rows.length > limit;
|
|
1471
2066
|
const page = rows.slice(0, limit);
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
2067
|
+
return selectKnowledgeBrowseRecords({
|
|
2068
|
+
entries: page.map((row) => {
|
|
2069
|
+
if (row.document.indexSequence === null) {
|
|
2070
|
+
throw new Error("ready knowledge document is missing its index revision");
|
|
2071
|
+
}
|
|
2072
|
+
return {
|
|
2073
|
+
record: knowledgeDocumentRecord(row.document, row.citation, row.firstChunkId),
|
|
2074
|
+
cursorAfter: encodeKnowledgeBrowseCursor(cursorScope, row.document.indexSequence)
|
|
2075
|
+
};
|
|
2076
|
+
}),
|
|
2077
|
+
hasMoreAfterEntries: hasMore
|
|
2078
|
+
});
|
|
1478
2079
|
}
|
|
1479
2080
|
);
|
|
1480
2081
|
}
|
|
1481
2082
|
function encodeKnowledgeBrowseCursor(scope, position) {
|
|
1482
2083
|
if (position < 0n) throw new Error("knowledge browse cursor position is invalid");
|
|
2084
|
+
const version = knowledgeBrowseCursorVersion(scope);
|
|
1483
2085
|
return Buffer.from(
|
|
1484
|
-
JSON.stringify({ v:
|
|
2086
|
+
JSON.stringify({ v: version, s: knowledgeBrowseCursorScope(scope), q: position.toString() }),
|
|
1485
2087
|
"utf8"
|
|
1486
2088
|
).toString("base64url");
|
|
1487
2089
|
}
|
|
@@ -1493,7 +2095,8 @@ function decodeKnowledgeBrowseCursor(value, scope) {
|
|
|
1493
2095
|
const bytes = Buffer.from(value, "base64url");
|
|
1494
2096
|
if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
|
|
1495
2097
|
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
1496
|
-
|
|
2098
|
+
const expectedVersion = knowledgeBrowseCursorVersion(scope);
|
|
2099
|
+
if (Object.keys(parsed).sort().join(",") !== "q,s,v" || parsed.v !== expectedVersion || typeof parsed.s !== "string" || typeof parsed.q !== "string" || !/^(0|[1-9][0-9]*)$/.test(parsed.q)) {
|
|
1497
2100
|
throw new Error("cursor payload");
|
|
1498
2101
|
}
|
|
1499
2102
|
if (parsed.s !== knowledgeBrowseCursorScope(scope)) {
|
|
@@ -1510,7 +2113,17 @@ function decodeKnowledgeBrowseCursor(value, scope) {
|
|
|
1510
2113
|
}
|
|
1511
2114
|
}
|
|
1512
2115
|
function knowledgeBrowseCursorScope(scope) {
|
|
1513
|
-
|
|
2116
|
+
const version = knowledgeBrowseCursorVersion(scope);
|
|
2117
|
+
const hash = createHash("sha256").update(`opengeni:knowledge-browse-cursor:v${version}\0`).update(scope.accountId).update("\0").update(scope.workspaceId).update("\0").update(canonicalEffectiveDocumentSubject(scope.initiatingSubjectId)).update("\0").update(scope.parentId ?? "").update("\0");
|
|
2118
|
+
if (version === 2) hash.update(scope.parentRevision).update("\0");
|
|
2119
|
+
return hash.update(scope.topic ?? "").update("\0").update([...scope.sourceKinds].sort().join("\0")).digest("hex");
|
|
2120
|
+
}
|
|
2121
|
+
function knowledgeBrowseCursorVersion(scope) {
|
|
2122
|
+
if (!scope.parentId) return 1;
|
|
2123
|
+
if (!scope.parentRevision || !/^[1-9][0-9]*$/.test(scope.parentRevision)) {
|
|
2124
|
+
throw new Error("knowledge browse parent cursor requires an exact document revision");
|
|
2125
|
+
}
|
|
2126
|
+
return 2;
|
|
1514
2127
|
}
|
|
1515
2128
|
function parseKnowledgeRecordId(value) {
|
|
1516
2129
|
const match = /^(document|document_chunk):([0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/iu.exec(
|
|
@@ -1519,7 +2132,7 @@ function parseKnowledgeRecordId(value) {
|
|
|
1519
2132
|
if (!match) throw new Error("invalid knowledge record id");
|
|
1520
2133
|
return { kind: match[1], id: match[2].toLowerCase() };
|
|
1521
2134
|
}
|
|
1522
|
-
function knowledgeDocumentRecord(document) {
|
|
2135
|
+
function knowledgeDocumentRecord(document, citation = null, firstChunkId = null) {
|
|
1523
2136
|
if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
|
|
1524
2137
|
const projected = projectKnowledgeRecord({
|
|
1525
2138
|
title: document.title,
|
|
@@ -1537,15 +2150,27 @@ function knowledgeDocumentRecord(document) {
|
|
|
1537
2150
|
authority: { kind: normalizeDocumentAuthorityKind(document.authorityKind) },
|
|
1538
2151
|
provenance: {
|
|
1539
2152
|
source: projected.source,
|
|
1540
|
-
indexedAt: document.indexedAt.toISOString()
|
|
2153
|
+
indexedAt: document.indexedAt.toISOString(),
|
|
2154
|
+
citation: parseKnowledgeProviderCitation(citation)
|
|
1541
2155
|
},
|
|
1542
2156
|
lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
|
|
1543
2157
|
quality: knowledgeQuality(document),
|
|
1544
|
-
links:
|
|
2158
|
+
links: [
|
|
2159
|
+
...firstChunkId ? [
|
|
2160
|
+
{
|
|
2161
|
+
relation: "contents",
|
|
2162
|
+
target: {
|
|
2163
|
+
kind: "knowledge",
|
|
2164
|
+
id: `document_chunk:${firstChunkId}`
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
] : [],
|
|
2168
|
+
...knowledgeSourceLinks(projected.source.uri)
|
|
2169
|
+
],
|
|
1545
2170
|
projection: projected.projection
|
|
1546
2171
|
};
|
|
1547
2172
|
}
|
|
1548
|
-
function knowledgeChunkRecord(document, chunk) {
|
|
2173
|
+
function knowledgeChunkRecord(document, chunk, citation = null, traversal = { previousChunkId: null, nextChunkId: null }) {
|
|
1549
2174
|
if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
|
|
1550
2175
|
const projected = projectKnowledgeRecord({
|
|
1551
2176
|
title: document.title,
|
|
@@ -1563,12 +2188,34 @@ function knowledgeChunkRecord(document, chunk) {
|
|
|
1563
2188
|
authority: { kind: normalizeDocumentAuthorityKind(document.authorityKind) },
|
|
1564
2189
|
provenance: {
|
|
1565
2190
|
source: projected.source,
|
|
1566
|
-
indexedAt: document.indexedAt.toISOString()
|
|
2191
|
+
indexedAt: document.indexedAt.toISOString(),
|
|
2192
|
+
citation: parseKnowledgeProviderCitation(citation)
|
|
1567
2193
|
},
|
|
1568
2194
|
lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
|
|
1569
2195
|
quality: knowledgeQuality(document),
|
|
1570
2196
|
links: [
|
|
1571
|
-
{
|
|
2197
|
+
{
|
|
2198
|
+
relation: "parent",
|
|
2199
|
+
target: { kind: "knowledge", id: `document:${document.id}` }
|
|
2200
|
+
},
|
|
2201
|
+
...traversal.previousChunkId ? [
|
|
2202
|
+
{
|
|
2203
|
+
relation: "previous",
|
|
2204
|
+
target: {
|
|
2205
|
+
kind: "knowledge",
|
|
2206
|
+
id: `document_chunk:${traversal.previousChunkId}`
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
] : [],
|
|
2210
|
+
...traversal.nextChunkId ? [
|
|
2211
|
+
{
|
|
2212
|
+
relation: "next",
|
|
2213
|
+
target: {
|
|
2214
|
+
kind: "knowledge",
|
|
2215
|
+
id: `document_chunk:${traversal.nextChunkId}`
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
] : [],
|
|
1572
2219
|
...knowledgeSourceLinks(projected.source.uri)
|
|
1573
2220
|
],
|
|
1574
2221
|
projection: projected.projection
|
|
@@ -1598,6 +2245,28 @@ function knowledgeQuality(document) {
|
|
|
1598
2245
|
function knowledgeSourceLinks(sourceUri) {
|
|
1599
2246
|
return sourceUri ? [{ relation: "source", target: { kind: "external", uri: sourceUri } }] : [];
|
|
1600
2247
|
}
|
|
2248
|
+
function knowledgePreviousChunkIdProjection() {
|
|
2249
|
+
return sql`(
|
|
2250
|
+
select knowledge_previous_chunk.id
|
|
2251
|
+
from document_chunks knowledge_previous_chunk
|
|
2252
|
+
where knowledge_previous_chunk.account_id = ${schema.documentChunks.accountId}
|
|
2253
|
+
and knowledge_previous_chunk.document_id = ${schema.documentChunks.documentId}
|
|
2254
|
+
and knowledge_previous_chunk.chunk_index < ${schema.documentChunks.chunkIndex}
|
|
2255
|
+
order by knowledge_previous_chunk.chunk_index desc
|
|
2256
|
+
limit 1
|
|
2257
|
+
)`;
|
|
2258
|
+
}
|
|
2259
|
+
function knowledgeNextChunkIdProjection() {
|
|
2260
|
+
return sql`(
|
|
2261
|
+
select knowledge_next_chunk.id
|
|
2262
|
+
from document_chunks knowledge_next_chunk
|
|
2263
|
+
where knowledge_next_chunk.account_id = ${schema.documentChunks.accountId}
|
|
2264
|
+
and knowledge_next_chunk.document_id = ${schema.documentChunks.documentId}
|
|
2265
|
+
and knowledge_next_chunk.chunk_index > ${schema.documentChunks.chunkIndex}
|
|
2266
|
+
order by knowledge_next_chunk.chunk_index asc
|
|
2267
|
+
limit 1
|
|
2268
|
+
)`;
|
|
2269
|
+
}
|
|
1601
2270
|
async function vectorSearchDocuments(db, input, limit, services) {
|
|
1602
2271
|
const queryEmbedding = await services.embedder.embedQuery(input.query);
|
|
1603
2272
|
validateEmbedding(queryEmbedding, services.embedder.dimensions, services.embedder.model);
|
|
@@ -1629,8 +2298,9 @@ async function vectorSearchDocuments(db, input, limit, services) {
|
|
|
1629
2298
|
authorityKind: schema.documents.authorityKind,
|
|
1630
2299
|
authorityWorkspaceId: schema.documents.authorityWorkspaceId,
|
|
1631
2300
|
authoritySubjectId: schema.documents.authoritySubjectId,
|
|
2301
|
+
citation: googleDriveCitationProjection(input.workspaceId, input.access),
|
|
1632
2302
|
distance
|
|
1633
|
-
}).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(and(...documentSearchConditions(input, services.embedder.model))).orderBy(distance).limit(limit)
|
|
2303
|
+
}).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(and(...documentSearchConditions(input, services.embedder.model))).orderBy(distance, asc(schema.documentChunks.id)).limit(limit)
|
|
1634
2304
|
);
|
|
1635
2305
|
return rows.map((row) => ({
|
|
1636
2306
|
...mapSearchRowBase(row),
|
|
@@ -1667,13 +2337,14 @@ async function keywordSearchDocuments(db, input, limit) {
|
|
|
1667
2337
|
authorityKind: schema.documents.authorityKind,
|
|
1668
2338
|
authorityWorkspaceId: schema.documents.authorityWorkspaceId,
|
|
1669
2339
|
authoritySubjectId: schema.documents.authoritySubjectId,
|
|
2340
|
+
citation: googleDriveCitationProjection(input.workspaceId, input.access),
|
|
1670
2341
|
rank
|
|
1671
2342
|
}).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
|
|
1672
2343
|
and(
|
|
1673
2344
|
...documentSearchConditions(input),
|
|
1674
2345
|
sql`to_tsvector('simple', ${schema.documentChunks.text}) @@ plainto_tsquery('simple', ${input.query})`
|
|
1675
2346
|
)
|
|
1676
|
-
).orderBy(desc(rank)).limit(limit)
|
|
2347
|
+
).orderBy(desc(rank), asc(schema.documentChunks.id)).limit(limit)
|
|
1677
2348
|
);
|
|
1678
2349
|
return rows.map((row) => ({
|
|
1679
2350
|
...mapSearchRowBase(row),
|
|
@@ -1708,7 +2379,8 @@ async function getDocumentChunk(db, accountId, workspaceId, chunkId, access) {
|
|
|
1708
2379
|
aclTags: schema.documents.aclTags,
|
|
1709
2380
|
authorityKind: schema.documents.authorityKind,
|
|
1710
2381
|
authorityWorkspaceId: schema.documents.authorityWorkspaceId,
|
|
1711
|
-
authoritySubjectId: schema.documents.authoritySubjectId
|
|
2382
|
+
authoritySubjectId: schema.documents.authoritySubjectId,
|
|
2383
|
+
citation: googleDriveCitationProjection(workspaceId, access)
|
|
1712
2384
|
}).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
|
|
1713
2385
|
and(
|
|
1714
2386
|
eq(schema.documents.accountId, accountId),
|
|
@@ -1768,6 +2440,21 @@ async function assertDocumentAccountWorkspace(db, accountId, workspaceId) {
|
|
|
1768
2440
|
}
|
|
1769
2441
|
return context;
|
|
1770
2442
|
}
|
|
2443
|
+
async function resolveEffectiveDocumentAccess(_db, input) {
|
|
2444
|
+
if (input.surface === "human") {
|
|
2445
|
+
return { viewerSubjectId: input.initiatingSubjectId };
|
|
2446
|
+
}
|
|
2447
|
+
return {
|
|
2448
|
+
agentOnly: true,
|
|
2449
|
+
viewerSubjectId: input.initiatingSubjectId,
|
|
2450
|
+
authorizedPersonalAttempt: input.agentAuthority ? {
|
|
2451
|
+
accountId: input.accountId,
|
|
2452
|
+
workspaceId: input.workspaceId,
|
|
2453
|
+
sessionId: input.agentAuthority.sessionId,
|
|
2454
|
+
attemptId: input.agentAuthority.attemptId
|
|
2455
|
+
} : void 0
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
1771
2458
|
function documentAccessConditions(workspaceId, access) {
|
|
1772
2459
|
const organization = eq(schema.documents.authorityKind, "organization");
|
|
1773
2460
|
const workspace = and(
|
|
@@ -1775,20 +2462,45 @@ function documentAccessConditions(workspaceId, access) {
|
|
|
1775
2462
|
eq(schema.documents.authorityWorkspaceId, workspaceId)
|
|
1776
2463
|
);
|
|
1777
2464
|
const viewer = cleanString(access?.viewerSubjectId ?? null);
|
|
2465
|
+
const personalAttempt = access?.authorizedPersonalAttempt;
|
|
2466
|
+
const authorizedPersonal = personalAttempt ? sql`${schema.documents.id} IN (
|
|
2467
|
+
SELECT resolve_session_attempt_personal_document_reads(
|
|
2468
|
+
${personalAttempt.accountId}::uuid,
|
|
2469
|
+
${personalAttempt.workspaceId}::uuid,
|
|
2470
|
+
${personalAttempt.sessionId}::uuid,
|
|
2471
|
+
${personalAttempt.attemptId}::uuid
|
|
2472
|
+
)
|
|
2473
|
+
)` : sql`false`;
|
|
1778
2474
|
const personal = viewer ? and(
|
|
1779
2475
|
eq(schema.documents.authorityKind, "personal"),
|
|
1780
|
-
eq(schema.documents.
|
|
1781
|
-
|
|
2476
|
+
eq(schema.documents.authoritySubjectId, viewer),
|
|
2477
|
+
access?.agentOnly ? or(
|
|
2478
|
+
and(
|
|
2479
|
+
isNull(schema.documents.authorityId),
|
|
2480
|
+
eq(schema.documents.authorityWorkspaceId, workspaceId)
|
|
2481
|
+
),
|
|
2482
|
+
and(isNotNull(schema.documents.authorityId), authorizedPersonal)
|
|
2483
|
+
) ?? authorizedPersonal : or(
|
|
2484
|
+
eq(schema.documents.authorityWorkspaceId, workspaceId),
|
|
2485
|
+
isNull(schema.documents.authorityWorkspaceId)
|
|
2486
|
+
) ?? eq(schema.documents.authorityWorkspaceId, workspaceId)
|
|
1782
2487
|
) : void 0;
|
|
1783
2488
|
const authority = viewer ? or(organization, workspace, personal) ?? organization : or(organization, workspace) ?? organization;
|
|
2489
|
+
const viewerSql = viewer ? sql`${viewer}` : sql`NULL::text`;
|
|
2490
|
+
const providerAuthorization = sql`google_drive_file_authorized(
|
|
2491
|
+
${schema.documents.accountId},
|
|
2492
|
+
${workspaceId}::uuid,
|
|
2493
|
+
${viewerSql},
|
|
2494
|
+
${schema.documents.fileId}
|
|
2495
|
+
)`;
|
|
1784
2496
|
if (access?.agentOnly) {
|
|
1785
|
-
return [eq(schema.documents.agentAccess, true), authority];
|
|
2497
|
+
return [eq(schema.documents.agentAccess, true), authority, providerAuthorization];
|
|
1786
2498
|
}
|
|
1787
|
-
return [authority];
|
|
2499
|
+
return [authority, providerAuthorization];
|
|
1788
2500
|
}
|
|
1789
2501
|
function documentMatchesAccess(document, workspaceId, access) {
|
|
1790
2502
|
if (access?.agentOnly) {
|
|
1791
|
-
return document.agentAccess && canViewDocument(document, access.viewerSubjectId, workspaceId);
|
|
2503
|
+
return document.agentAccess && (document.authorityKind !== "personal" || document.authorityId === null) && canViewDocument(document, access.viewerSubjectId, workspaceId);
|
|
1792
2504
|
}
|
|
1793
2505
|
return canViewDocument(document, access?.viewerSubjectId, workspaceId);
|
|
1794
2506
|
}
|
|
@@ -1806,16 +2518,16 @@ function canViewDocument(document, viewerSubjectId, workspaceId) {
|
|
|
1806
2518
|
return document.authorityWorkspaceId === null && document.authoritySubjectId === null;
|
|
1807
2519
|
}
|
|
1808
2520
|
const normalizedWorkspaceId = cleanString(workspaceId ?? null);
|
|
1809
|
-
if (!normalizedWorkspaceId
|
|
2521
|
+
if (!normalizedWorkspaceId) {
|
|
1810
2522
|
return false;
|
|
1811
2523
|
}
|
|
1812
2524
|
if (document.authorityKind === "workspace") {
|
|
1813
|
-
return document.authoritySubjectId === null;
|
|
2525
|
+
return document.authorityWorkspaceId === normalizedWorkspaceId && document.authoritySubjectId === null;
|
|
1814
2526
|
}
|
|
1815
2527
|
if (document.authorityKind === "personal") {
|
|
1816
2528
|
const authoritySubjectId = canonicalDocumentAuthoritySubject(document.authoritySubjectId);
|
|
1817
2529
|
const viewer = canonicalDocumentAuthoritySubject(viewerSubjectId);
|
|
1818
|
-
return !!authoritySubjectId && authoritySubjectId === viewer;
|
|
2530
|
+
return !!authoritySubjectId && authoritySubjectId === viewer && (document.authorityWorkspaceId === null || document.authorityWorkspaceId === normalizedWorkspaceId);
|
|
1819
2531
|
}
|
|
1820
2532
|
return false;
|
|
1821
2533
|
}
|
|
@@ -1863,9 +2575,24 @@ function mapSearchRowBase(row) {
|
|
|
1863
2575
|
aclTags: cleanStringArray(row.aclTags),
|
|
1864
2576
|
authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
|
|
1865
2577
|
authorityWorkspaceId: row.authorityWorkspaceId,
|
|
1866
|
-
authoritySubjectId: row.authoritySubjectId
|
|
2578
|
+
authoritySubjectId: row.authoritySubjectId,
|
|
2579
|
+
citation: parseKnowledgeProviderCitation(row.citation)
|
|
1867
2580
|
};
|
|
1868
2581
|
}
|
|
2582
|
+
function googleDriveCitationProjection(workspaceId, access) {
|
|
2583
|
+
const viewer = cleanString(access?.viewerSubjectId ?? null);
|
|
2584
|
+
const viewerSql = viewer ? sql`${viewer}` : sql`NULL::text`;
|
|
2585
|
+
return sql`google_drive_document_citation(
|
|
2586
|
+
${schema.documents.accountId},
|
|
2587
|
+
${workspaceId}::uuid,
|
|
2588
|
+
${viewerSql},
|
|
2589
|
+
${schema.documents.id},
|
|
2590
|
+
${schema.documents.fileId}
|
|
2591
|
+
)`;
|
|
2592
|
+
}
|
|
2593
|
+
function parseKnowledgeProviderCitation(value) {
|
|
2594
|
+
return value === null || value === void 0 ? null : KnowledgeProviderCitation.parse(value);
|
|
2595
|
+
}
|
|
1869
2596
|
function mergeDocumentSearchRows(rows, mode) {
|
|
1870
2597
|
const byChunk = /* @__PURE__ */ new Map();
|
|
1871
2598
|
for (const row of rows) {
|
|
@@ -1890,7 +2617,7 @@ function mergeDocumentSearchRows(rows, mode) {
|
|
|
1890
2617
|
matchType
|
|
1891
2618
|
};
|
|
1892
2619
|
}).sort(
|
|
1893
|
-
(left, right) => right.score - left.score || (right.vectorScore ?? 0) - (left.vectorScore ?? 0) || (right.keywordScore ?? 0) - (left.keywordScore ?? 0) || left.chunkIndex - right.chunkIndex
|
|
2620
|
+
(left, right) => right.score - left.score || (right.vectorScore ?? 0) - (left.vectorScore ?? 0) || (right.keywordScore ?? 0) - (left.keywordScore ?? 0) || left.chunkIndex - right.chunkIndex || (left.chunkId === right.chunkId ? 0 : left.chunkId < right.chunkId ? -1 : 1)
|
|
1894
2621
|
);
|
|
1895
2622
|
}
|
|
1896
2623
|
function combinedSearchScore(mode, vectorScore, keywordScore, matchType) {
|
|
@@ -1952,10 +2679,16 @@ function deterministicEmbedding(text, dimensions = DEFAULT_DOCUMENT_EMBEDDING_DI
|
|
|
1952
2679
|
const norm = Math.hypot(...values) || 1;
|
|
1953
2680
|
return values.map((value) => Number((value / norm).toFixed(6)));
|
|
1954
2681
|
}
|
|
1955
|
-
async function requireReadyFile(db,
|
|
1956
|
-
const file = await
|
|
2682
|
+
async function requireReadyFile(db, input) {
|
|
2683
|
+
const [file] = await getFilesForSubject(db, {
|
|
2684
|
+
accountId: input.accountId,
|
|
2685
|
+
workspaceId: input.workspaceId,
|
|
2686
|
+
subjectId: input.subjectId,
|
|
2687
|
+
fileIds: [input.fileId]
|
|
2688
|
+
});
|
|
2689
|
+
if (!file) throw new Error(`File not found: ${input.fileId}`);
|
|
1957
2690
|
if (file.status !== "ready") {
|
|
1958
|
-
throw new Error(`File ${fileId} is ${file.status}`);
|
|
2691
|
+
throw new Error(`File ${input.fileId} is ${file.status}`);
|
|
1959
2692
|
}
|
|
1960
2693
|
return file;
|
|
1961
2694
|
}
|
|
@@ -2071,6 +2804,7 @@ function mapDocument(row) {
|
|
|
2071
2804
|
authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
|
|
2072
2805
|
authorityWorkspaceId: row.authorityWorkspaceId,
|
|
2073
2806
|
authoritySubjectId: row.authoritySubjectId,
|
|
2807
|
+
authorityId: row.authorityId,
|
|
2074
2808
|
visibility: normalizeDocumentVisibility(row.visibility),
|
|
2075
2809
|
createdBy: row.createdBy,
|
|
2076
2810
|
agentAccess: row.agentAccess,
|
|
@@ -2175,10 +2909,14 @@ export {
|
|
|
2175
2909
|
getDocument,
|
|
2176
2910
|
getDocumentBase,
|
|
2177
2911
|
getDocumentChunk,
|
|
2912
|
+
getDocumentForIndexing,
|
|
2178
2913
|
getDocumentInventory,
|
|
2914
|
+
getDocumentOriginalFile,
|
|
2179
2915
|
getEffectiveKnowledgeRecord,
|
|
2180
2916
|
heuristicCuration,
|
|
2181
2917
|
indexDocumentNow,
|
|
2918
|
+
listAccessibleDocuments,
|
|
2919
|
+
listDocumentAuthorityReclassifications,
|
|
2182
2920
|
listDocumentBases,
|
|
2183
2921
|
listDocumentBasesEnsuringDefault,
|
|
2184
2922
|
listDocuments,
|
|
@@ -2188,9 +2926,15 @@ export {
|
|
|
2188
2926
|
parseDocumentBytes,
|
|
2189
2927
|
projectKnowledgeRecord,
|
|
2190
2928
|
queueDocumentForReindex,
|
|
2929
|
+
reclassifyDocumentAuthority,
|
|
2191
2930
|
resolveDocumentAuthority,
|
|
2931
|
+
resolveEffectiveDocumentAccess,
|
|
2932
|
+
runDocumentDefaultCollectionBackfill,
|
|
2192
2933
|
searchDocuments,
|
|
2193
2934
|
searchEffectiveDocuments,
|
|
2194
|
-
searchEffectiveKnowledge
|
|
2935
|
+
searchEffectiveKnowledge,
|
|
2936
|
+
selectDocumentSearchCandidateWindow,
|
|
2937
|
+
selectKnowledgeBrowseRecords,
|
|
2938
|
+
selectKnowledgeSearchResults
|
|
2195
2939
|
};
|
|
2196
2940
|
//# sourceMappingURL=index.js.map
|