@opengeni/documents 0.5.41 → 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 +169 -5
- package/dist/index.js +792 -125
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +1101 -129
package/dist/index.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import {
|
|
3
|
-
KnowledgeProviderCitation
|
|
3
|
+
KnowledgeProviderCitation,
|
|
4
|
+
DocumentAuthorityReclassification,
|
|
5
|
+
ListDocumentAuthorityReclassificationsResponse,
|
|
6
|
+
DocumentDefaultCollectionBackfill
|
|
4
7
|
} from "@opengeni/contracts";
|
|
5
8
|
import {
|
|
9
|
+
createPersonalDocumentAuthority,
|
|
6
10
|
getFilesForSubject,
|
|
11
|
+
resolveDocumentOriginalFileForSubject,
|
|
7
12
|
rlsContextForWorkspace,
|
|
8
13
|
setSubjectRlsContext,
|
|
9
14
|
withRlsContext,
|
|
@@ -11,9 +16,20 @@ import {
|
|
|
11
16
|
withWorkspaceSubjectRls
|
|
12
17
|
} from "@opengeni/db";
|
|
13
18
|
import * as schema from "@opengeni/db/schema";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
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";
|
|
17
33
|
|
|
18
34
|
// src/knowledge-projection.ts
|
|
19
35
|
import {
|
|
@@ -697,6 +713,135 @@ async function listDocumentBasesEnsuringDefault(db, input) {
|
|
|
697
713
|
await ensureDefaultBase(db, input);
|
|
698
714
|
return await listDocumentBases(db, input.workspaceId);
|
|
699
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
|
+
}
|
|
700
845
|
async function addDocumentToBase(db, input) {
|
|
701
846
|
return await withRlsContext(
|
|
702
847
|
db,
|
|
@@ -776,33 +921,47 @@ async function addDocumentToBase(db, input) {
|
|
|
776
921
|
).returning();
|
|
777
922
|
return mapDocument(updated ?? existing);
|
|
778
923
|
}
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
+
});
|
|
806
965
|
if (!row) throw new Error("Failed to create document");
|
|
807
966
|
return mapDocument(row);
|
|
808
967
|
}
|
|
@@ -817,7 +976,7 @@ async function moveDocumentToBase(db, input) {
|
|
|
817
976
|
if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
|
|
818
977
|
const [row] = await scopedDb.select().from(schema.documents).where(
|
|
819
978
|
and(
|
|
820
|
-
eq(schema.documents.
|
|
979
|
+
eq(schema.documents.accountId, input.accountId),
|
|
821
980
|
eq(schema.documents.id, input.documentId),
|
|
822
981
|
...documentAccessConditions(input.workspaceId, input.access)
|
|
823
982
|
)
|
|
@@ -830,11 +989,11 @@ async function moveDocumentToBase(db, input) {
|
|
|
830
989
|
throw new Error("document has no suggested base; pass targetBaseId");
|
|
831
990
|
}
|
|
832
991
|
if (targetBaseId === row.baseId) return mapDocument(row);
|
|
833
|
-
const base = await getDocumentBase(scopedDb,
|
|
992
|
+
const base = await getDocumentBase(scopedDb, row.workspaceId, targetBaseId);
|
|
834
993
|
if (!base) throw new Error(`Document base not found: ${targetBaseId}`);
|
|
835
994
|
const [conflict] = await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
|
|
836
995
|
and(
|
|
837
|
-
eq(schema.documents.workspaceId,
|
|
996
|
+
eq(schema.documents.workspaceId, row.workspaceId),
|
|
838
997
|
eq(schema.documents.baseId, targetBaseId),
|
|
839
998
|
...row.knowledgeSourceIdentity ? [eq(schema.documents.knowledgeSourceIdentity, row.knowledgeSourceIdentity)] : [eq(schema.documents.fileId, row.fileId)]
|
|
840
999
|
)
|
|
@@ -850,7 +1009,7 @@ async function moveDocumentToBase(db, input) {
|
|
|
850
1009
|
updatedAt: now
|
|
851
1010
|
}).where(
|
|
852
1011
|
and(
|
|
853
|
-
eq(schema.documents.workspaceId,
|
|
1012
|
+
eq(schema.documents.workspaceId, row.workspaceId),
|
|
854
1013
|
eq(schema.documents.id, input.documentId),
|
|
855
1014
|
...documentAccessConditions(input.workspaceId, input.access)
|
|
856
1015
|
)
|
|
@@ -858,7 +1017,7 @@ async function moveDocumentToBase(db, input) {
|
|
|
858
1017
|
if (updated) {
|
|
859
1018
|
await tx.update(schema.documentChunks).set({ baseId: targetBaseId }).where(
|
|
860
1019
|
and(
|
|
861
|
-
eq(schema.documentChunks.workspaceId,
|
|
1020
|
+
eq(schema.documentChunks.workspaceId, row.workspaceId),
|
|
862
1021
|
eq(schema.documentChunks.documentId, input.documentId)
|
|
863
1022
|
)
|
|
864
1023
|
);
|
|
@@ -879,7 +1038,7 @@ async function deleteDocumentFromBase(db, input) {
|
|
|
879
1038
|
if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
|
|
880
1039
|
const [document] = await scopedDb.select().from(schema.documents).where(
|
|
881
1040
|
and(
|
|
882
|
-
eq(schema.documents.
|
|
1041
|
+
eq(schema.documents.accountId, input.accountId),
|
|
883
1042
|
eq(schema.documents.id, input.documentId),
|
|
884
1043
|
...documentAccessConditions(input.workspaceId, input.access)
|
|
885
1044
|
)
|
|
@@ -896,7 +1055,7 @@ async function deleteDocumentFromBase(db, input) {
|
|
|
896
1055
|
}
|
|
897
1056
|
await scopedDb.delete(schema.documents).where(
|
|
898
1057
|
and(
|
|
899
|
-
eq(schema.documents.
|
|
1058
|
+
eq(schema.documents.accountId, input.accountId),
|
|
900
1059
|
eq(schema.documents.id, input.documentId),
|
|
901
1060
|
...documentAccessConditions(input.workspaceId, input.access)
|
|
902
1061
|
)
|
|
@@ -916,6 +1075,12 @@ async function listDocuments(db, workspaceId, baseId, access) {
|
|
|
916
1075
|
return rows.map(mapDocument);
|
|
917
1076
|
});
|
|
918
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
|
+
}
|
|
919
1084
|
async function listEffectiveIndexedDocuments(db, input) {
|
|
920
1085
|
const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
|
|
921
1086
|
const limit = input.limit ?? 50;
|
|
@@ -927,10 +1092,13 @@ async function listEffectiveIndexedDocuments(db, input) {
|
|
|
927
1092
|
workspaceId: input.workspaceId,
|
|
928
1093
|
initiatingSubjectId
|
|
929
1094
|
}) : 0n;
|
|
930
|
-
const access = {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1095
|
+
const access = await resolveEffectiveDocumentAccess(db, {
|
|
1096
|
+
accountId: input.accountId,
|
|
1097
|
+
workspaceId: input.workspaceId,
|
|
1098
|
+
initiatingSubjectId,
|
|
1099
|
+
surface: "agent",
|
|
1100
|
+
agentAuthority: input.agentAuthority
|
|
1101
|
+
});
|
|
934
1102
|
const rows = await withDocumentAccountRls(
|
|
935
1103
|
db,
|
|
936
1104
|
input.accountId,
|
|
@@ -1014,15 +1182,21 @@ function canonicalEffectiveDocumentSubject(value) {
|
|
|
1014
1182
|
async function getDocument(db, workspaceId, documentId, access) {
|
|
1015
1183
|
return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
|
|
1016
1184
|
const [row] = await scopedDb.select().from(schema.documents).where(
|
|
1017
|
-
and(
|
|
1018
|
-
eq(schema.documents.workspaceId, workspaceId),
|
|
1019
|
-
eq(schema.documents.id, documentId),
|
|
1020
|
-
...documentAccessConditions(workspaceId, access)
|
|
1021
|
-
)
|
|
1185
|
+
and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
|
|
1022
1186
|
).limit(1);
|
|
1023
1187
|
return row ? mapDocument(row) : null;
|
|
1024
1188
|
});
|
|
1025
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
|
+
}
|
|
1026
1200
|
async function getDocumentForIndexing(db, workspaceId, documentId) {
|
|
1027
1201
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
1028
1202
|
const [row] = await scopedDb.select().from(schema.documents).where(
|
|
@@ -1033,12 +1207,8 @@ async function getDocumentForIndexing(db, workspaceId, documentId) {
|
|
|
1033
1207
|
}
|
|
1034
1208
|
async function queueDocumentForReindex(db, workspaceId, documentId, access, organizationAuthorityGranted) {
|
|
1035
1209
|
return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
|
|
1036
|
-
const [document] = await scopedDb.select(
|
|
1037
|
-
and(
|
|
1038
|
-
eq(schema.documents.workspaceId, workspaceId),
|
|
1039
|
-
eq(schema.documents.id, documentId),
|
|
1040
|
-
...documentAccessConditions(workspaceId, access)
|
|
1041
|
-
)
|
|
1210
|
+
const [document] = await scopedDb.select().from(schema.documents).where(
|
|
1211
|
+
and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
|
|
1042
1212
|
).limit(1);
|
|
1043
1213
|
if (!document) throw new Error(`Document not found: ${documentId}`);
|
|
1044
1214
|
assertOrganizationDocumentAuthority(document.authorityKind, organizationAuthorityGranted);
|
|
@@ -1047,11 +1217,7 @@ async function queueDocumentForReindex(db, workspaceId, documentId, access, orga
|
|
|
1047
1217
|
error: null,
|
|
1048
1218
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1049
1219
|
}).where(
|
|
1050
|
-
and(
|
|
1051
|
-
eq(schema.documents.workspaceId, workspaceId),
|
|
1052
|
-
eq(schema.documents.id, documentId),
|
|
1053
|
-
...documentAccessConditions(workspaceId, access)
|
|
1054
|
-
)
|
|
1220
|
+
and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
|
|
1055
1221
|
).returning();
|
|
1056
1222
|
if (!row) throw new Error(`Document not found: ${documentId}`);
|
|
1057
1223
|
return mapDocument(row);
|
|
@@ -1090,7 +1256,11 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
|
|
|
1090
1256
|
);
|
|
1091
1257
|
});
|
|
1092
1258
|
try {
|
|
1093
|
-
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;
|
|
1094
1264
|
const parsed = await services.parser.parse(bytes, file);
|
|
1095
1265
|
if (document.curationStatus === "pending") {
|
|
1096
1266
|
document = await curateDroppedDocument(db, services, document, parsed, file);
|
|
@@ -1284,6 +1454,9 @@ async function curateDroppedDocument(db, services, document, parsed, file) {
|
|
|
1284
1454
|
return updated ?? document;
|
|
1285
1455
|
}
|
|
1286
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) {
|
|
1287
1460
|
await assertDocumentAccountWorkspace(db, input.accountId, input.workspaceId);
|
|
1288
1461
|
const mode = input.mode ?? "hybrid";
|
|
1289
1462
|
const limit = Math.min(Math.max(input.limit ?? 5, 1), 50);
|
|
@@ -1308,13 +1481,34 @@ async function searchDocuments(db, input, services = createDocumentServices()) {
|
|
|
1308
1481
|
if (mode === "keyword" || mode === "hybrid") {
|
|
1309
1482
|
rows.push(...await keywordSearchDocuments(db, input, candidateLimit));
|
|
1310
1483
|
}
|
|
1311
|
-
|
|
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 };
|
|
1312
1499
|
}
|
|
1313
1500
|
async function searchEffectiveDocuments(db, input, services = createDocumentServices()) {
|
|
1314
1501
|
const initiatingSubjectId = cleanString(input.initiatingSubjectId);
|
|
1315
1502
|
if (!initiatingSubjectId) {
|
|
1316
1503
|
throw new Error("effective document retrieval requires an initiating subject");
|
|
1317
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
|
+
});
|
|
1318
1512
|
return await searchDocuments(
|
|
1319
1513
|
db,
|
|
1320
1514
|
{
|
|
@@ -1328,23 +1522,50 @@ async function searchEffectiveDocuments(db, input, services = createDocumentServ
|
|
|
1328
1522
|
aclTags: input.aclTags,
|
|
1329
1523
|
// Construct the lower-level access filter here instead of spreading the
|
|
1330
1524
|
// caller input, so an untyped/legacy access override is always ignored.
|
|
1331
|
-
access
|
|
1332
|
-
viewerSubjectId: initiatingSubjectId,
|
|
1333
|
-
...input.surface === "agent" ? { agentOnly: true } : {}
|
|
1334
|
-
}
|
|
1525
|
+
access
|
|
1335
1526
|
},
|
|
1336
1527
|
services
|
|
1337
1528
|
);
|
|
1338
1529
|
}
|
|
1339
1530
|
async function searchEffectiveKnowledge(db, input, services = createDocumentServices()) {
|
|
1340
1531
|
const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
|
|
1341
|
-
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(
|
|
1342
1542
|
db,
|
|
1343
|
-
{
|
|
1344
|
-
|
|
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
|
+
}
|
|
1345
1559
|
);
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
+
}
|
|
1348
1569
|
const current = await withDocumentAccountRls(
|
|
1349
1570
|
db,
|
|
1350
1571
|
input.accountId,
|
|
@@ -1353,7 +1574,9 @@ async function searchEffectiveKnowledge(db, input, services = createDocumentServ
|
|
|
1353
1574
|
async (scopedDb) => await scopedDb.select({
|
|
1354
1575
|
chunk: schema.documentChunks,
|
|
1355
1576
|
document: schema.documents,
|
|
1356
|
-
citation: googleDriveCitationProjection(input.workspaceId, access)
|
|
1577
|
+
citation: googleDriveCitationProjection(input.workspaceId, access),
|
|
1578
|
+
previousChunkId: knowledgePreviousChunkIdProjection(),
|
|
1579
|
+
nextChunkId: knowledgeNextChunkIdProjection()
|
|
1357
1580
|
}).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
|
|
1358
1581
|
and(
|
|
1359
1582
|
eq(schema.documents.accountId, input.accountId),
|
|
@@ -1368,28 +1591,310 @@ async function searchEffectiveKnowledge(db, input, services = createDocumentServ
|
|
|
1368
1591
|
)
|
|
1369
1592
|
);
|
|
1370
1593
|
const currentByChunkId = new Map(current.map((row) => [row.chunk.id, row]));
|
|
1371
|
-
return {
|
|
1372
|
-
|
|
1594
|
+
return selectKnowledgeSearchResults({
|
|
1595
|
+
rankedCandidateCount: ranked.length,
|
|
1596
|
+
requestedLimit,
|
|
1597
|
+
alreadyBelowRelevanceFloor: rankedSelection.belowRelevanceFloor,
|
|
1598
|
+
candidates: ranked.flatMap((rankedResult) => {
|
|
1373
1599
|
const row = currentByChunkId.get(rankedResult.chunkId);
|
|
1374
1600
|
if (!row) return [];
|
|
1375
1601
|
return [
|
|
1376
1602
|
{
|
|
1377
|
-
record: knowledgeChunkRecord(row.document, row.chunk, row.citation
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
|
1384
1611
|
}
|
|
1385
1612
|
];
|
|
1386
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
|
+
}
|
|
1850
|
+
};
|
|
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
|
+
}
|
|
1387
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");
|
|
1388
1887
|
}
|
|
1389
1888
|
async function getEffectiveKnowledgeRecord(db, input) {
|
|
1390
1889
|
const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
|
|
1391
1890
|
const target = parseKnowledgeRecordId(input.id);
|
|
1392
|
-
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
|
+
});
|
|
1393
1898
|
return await withDocumentAccountRls(
|
|
1394
1899
|
db,
|
|
1395
1900
|
input.accountId,
|
|
@@ -1399,8 +1904,16 @@ async function getEffectiveKnowledgeRecord(db, input) {
|
|
|
1399
1904
|
if (target.kind === "document") {
|
|
1400
1905
|
const [row2] = await scopedDb.select({
|
|
1401
1906
|
document: schema.documents,
|
|
1402
|
-
citation: googleDriveCitationProjection(input.workspaceId, access)
|
|
1403
|
-
|
|
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(
|
|
1404
1917
|
and(
|
|
1405
1918
|
eq(schema.documents.accountId, input.accountId),
|
|
1406
1919
|
eq(schema.documents.id, target.id),
|
|
@@ -1408,12 +1921,14 @@ async function getEffectiveKnowledgeRecord(db, input) {
|
|
|
1408
1921
|
...documentAccessConditions(input.workspaceId, access)
|
|
1409
1922
|
)
|
|
1410
1923
|
).limit(1);
|
|
1411
|
-
return row2 ? knowledgeDocumentRecord(row2.document, row2.citation) : null;
|
|
1924
|
+
return row2 ? knowledgeDocumentRecord(row2.document, row2.citation, row2.firstChunkId) : null;
|
|
1412
1925
|
}
|
|
1413
1926
|
const [row] = await scopedDb.select({
|
|
1414
1927
|
chunk: schema.documentChunks,
|
|
1415
1928
|
document: schema.documents,
|
|
1416
|
-
citation: googleDriveCitationProjection(input.workspaceId, access)
|
|
1929
|
+
citation: googleDriveCitationProjection(input.workspaceId, access),
|
|
1930
|
+
previousChunkId: knowledgePreviousChunkIdProjection(),
|
|
1931
|
+
nextChunkId: knowledgeNextChunkIdProjection()
|
|
1417
1932
|
}).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
|
|
1418
1933
|
and(
|
|
1419
1934
|
eq(schema.documents.accountId, input.accountId),
|
|
@@ -1423,7 +1938,10 @@ async function getEffectiveKnowledgeRecord(db, input) {
|
|
|
1423
1938
|
...documentAccessConditions(input.workspaceId, access)
|
|
1424
1939
|
)
|
|
1425
1940
|
).limit(1);
|
|
1426
|
-
return row ? knowledgeChunkRecord(row.document, row.chunk, row.citation
|
|
1941
|
+
return row ? knowledgeChunkRecord(row.document, row.chunk, row.citation, {
|
|
1942
|
+
previousChunkId: row.previousChunkId,
|
|
1943
|
+
nextChunkId: row.nextChunkId
|
|
1944
|
+
}) : null;
|
|
1427
1945
|
}
|
|
1428
1946
|
);
|
|
1429
1947
|
}
|
|
@@ -1453,11 +1971,14 @@ async function browseEffectiveKnowledge(db, input) {
|
|
|
1453
1971
|
topic,
|
|
1454
1972
|
sourceKinds
|
|
1455
1973
|
};
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
+
});
|
|
1461
1982
|
return await withDocumentAccountRls(
|
|
1462
1983
|
db,
|
|
1463
1984
|
input.accountId,
|
|
@@ -1465,7 +1986,7 @@ async function browseEffectiveKnowledge(db, input) {
|
|
|
1465
1986
|
access,
|
|
1466
1987
|
async (scopedDb) => {
|
|
1467
1988
|
if (parent) {
|
|
1468
|
-
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(
|
|
1469
1990
|
and(
|
|
1470
1991
|
eq(schema.documents.accountId, input.accountId),
|
|
1471
1992
|
eq(schema.documents.id, parent.id),
|
|
@@ -1473,34 +1994,57 @@ async function browseEffectiveKnowledge(db, input) {
|
|
|
1473
1994
|
...documentAccessConditions(input.workspaceId, access)
|
|
1474
1995
|
)
|
|
1475
1996
|
).limit(1);
|
|
1476
|
-
if (!authorizedParent)
|
|
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
|
+
}
|
|
1477
2011
|
const rows2 = await scopedDb.select({
|
|
1478
2012
|
chunk: schema.documentChunks,
|
|
1479
2013
|
document: schema.documents,
|
|
1480
|
-
citation: googleDriveCitationProjection(input.workspaceId, access)
|
|
2014
|
+
citation: googleDriveCitationProjection(input.workspaceId, access),
|
|
2015
|
+
previousChunkId: knowledgePreviousChunkIdProjection(),
|
|
2016
|
+
nextChunkId: knowledgeNextChunkIdProjection()
|
|
1481
2017
|
}).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
|
|
1482
2018
|
and(
|
|
1483
2019
|
eq(schema.documentChunks.accountId, input.accountId),
|
|
1484
2020
|
eq(schema.documentChunks.documentId, parent.id),
|
|
1485
2021
|
gt(schema.documentChunks.chunkIndex, Number(after) - 1),
|
|
1486
2022
|
eq(schema.documents.status, "ready"),
|
|
2023
|
+
eq(schema.documents.indexSequence, authorizedParent.indexSequence),
|
|
1487
2024
|
...documentAccessConditions(input.workspaceId, access)
|
|
1488
2025
|
)
|
|
1489
2026
|
).orderBy(asc(schema.documentChunks.chunkIndex)).limit(limit + 1);
|
|
1490
2027
|
const hasMore2 = rows2.length > limit;
|
|
1491
2028
|
const page2 = rows2.slice(0, limit);
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
+
});
|
|
1498
2042
|
}
|
|
1499
2043
|
const conditions = [
|
|
1500
2044
|
eq(schema.documents.accountId, input.accountId),
|
|
1501
2045
|
eq(schema.documents.status, "ready"),
|
|
1502
2046
|
isNotNull(schema.documents.indexSequence),
|
|
1503
|
-
gt(schema.documents.indexSequence,
|
|
2047
|
+
gt(schema.documents.indexSequence, topLevelAfter),
|
|
1504
2048
|
...documentAccessConditions(input.workspaceId, access)
|
|
1505
2049
|
];
|
|
1506
2050
|
if (topic) conditions.push(sql`${schema.documents.topics} ? ${topic}`);
|
|
@@ -1508,23 +2052,38 @@ async function browseEffectiveKnowledge(db, input) {
|
|
|
1508
2052
|
conditions.push(inArray(schema.documents.sourceKind, sourceKinds));
|
|
1509
2053
|
const rows = await scopedDb.select({
|
|
1510
2054
|
document: schema.documents,
|
|
1511
|
-
citation: googleDriveCitationProjection(input.workspaceId, access)
|
|
1512
|
-
|
|
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);
|
|
1513
2065
|
const hasMore = rows.length > limit;
|
|
1514
2066
|
const page = rows.slice(0, limit);
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
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
|
+
});
|
|
1521
2079
|
}
|
|
1522
2080
|
);
|
|
1523
2081
|
}
|
|
1524
2082
|
function encodeKnowledgeBrowseCursor(scope, position) {
|
|
1525
2083
|
if (position < 0n) throw new Error("knowledge browse cursor position is invalid");
|
|
2084
|
+
const version = knowledgeBrowseCursorVersion(scope);
|
|
1526
2085
|
return Buffer.from(
|
|
1527
|
-
JSON.stringify({ v:
|
|
2086
|
+
JSON.stringify({ v: version, s: knowledgeBrowseCursorScope(scope), q: position.toString() }),
|
|
1528
2087
|
"utf8"
|
|
1529
2088
|
).toString("base64url");
|
|
1530
2089
|
}
|
|
@@ -1536,7 +2095,8 @@ function decodeKnowledgeBrowseCursor(value, scope) {
|
|
|
1536
2095
|
const bytes = Buffer.from(value, "base64url");
|
|
1537
2096
|
if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
|
|
1538
2097
|
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
1539
|
-
|
|
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)) {
|
|
1540
2100
|
throw new Error("cursor payload");
|
|
1541
2101
|
}
|
|
1542
2102
|
if (parsed.s !== knowledgeBrowseCursorScope(scope)) {
|
|
@@ -1553,7 +2113,17 @@ function decodeKnowledgeBrowseCursor(value, scope) {
|
|
|
1553
2113
|
}
|
|
1554
2114
|
}
|
|
1555
2115
|
function knowledgeBrowseCursorScope(scope) {
|
|
1556
|
-
|
|
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;
|
|
1557
2127
|
}
|
|
1558
2128
|
function parseKnowledgeRecordId(value) {
|
|
1559
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(
|
|
@@ -1562,7 +2132,7 @@ function parseKnowledgeRecordId(value) {
|
|
|
1562
2132
|
if (!match) throw new Error("invalid knowledge record id");
|
|
1563
2133
|
return { kind: match[1], id: match[2].toLowerCase() };
|
|
1564
2134
|
}
|
|
1565
|
-
function knowledgeDocumentRecord(document, citation = null) {
|
|
2135
|
+
function knowledgeDocumentRecord(document, citation = null, firstChunkId = null) {
|
|
1566
2136
|
if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
|
|
1567
2137
|
const projected = projectKnowledgeRecord({
|
|
1568
2138
|
title: document.title,
|
|
@@ -1585,11 +2155,22 @@ function knowledgeDocumentRecord(document, citation = null) {
|
|
|
1585
2155
|
},
|
|
1586
2156
|
lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
|
|
1587
2157
|
quality: knowledgeQuality(document),
|
|
1588
|
-
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
|
+
],
|
|
1589
2170
|
projection: projected.projection
|
|
1590
2171
|
};
|
|
1591
2172
|
}
|
|
1592
|
-
function knowledgeChunkRecord(document, chunk, citation = null) {
|
|
2173
|
+
function knowledgeChunkRecord(document, chunk, citation = null, traversal = { previousChunkId: null, nextChunkId: null }) {
|
|
1593
2174
|
if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
|
|
1594
2175
|
const projected = projectKnowledgeRecord({
|
|
1595
2176
|
title: document.title,
|
|
@@ -1613,7 +2194,28 @@ function knowledgeChunkRecord(document, chunk, citation = null) {
|
|
|
1613
2194
|
lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
|
|
1614
2195
|
quality: knowledgeQuality(document),
|
|
1615
2196
|
links: [
|
|
1616
|
-
{
|
|
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
|
+
] : [],
|
|
1617
2219
|
...knowledgeSourceLinks(projected.source.uri)
|
|
1618
2220
|
],
|
|
1619
2221
|
projection: projected.projection
|
|
@@ -1643,6 +2245,28 @@ function knowledgeQuality(document) {
|
|
|
1643
2245
|
function knowledgeSourceLinks(sourceUri) {
|
|
1644
2246
|
return sourceUri ? [{ relation: "source", target: { kind: "external", uri: sourceUri } }] : [];
|
|
1645
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
|
+
}
|
|
1646
2270
|
async function vectorSearchDocuments(db, input, limit, services) {
|
|
1647
2271
|
const queryEmbedding = await services.embedder.embedQuery(input.query);
|
|
1648
2272
|
validateEmbedding(queryEmbedding, services.embedder.dimensions, services.embedder.model);
|
|
@@ -1676,7 +2300,7 @@ async function vectorSearchDocuments(db, input, limit, services) {
|
|
|
1676
2300
|
authoritySubjectId: schema.documents.authoritySubjectId,
|
|
1677
2301
|
citation: googleDriveCitationProjection(input.workspaceId, input.access),
|
|
1678
2302
|
distance
|
|
1679
|
-
}).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)
|
|
1680
2304
|
);
|
|
1681
2305
|
return rows.map((row) => ({
|
|
1682
2306
|
...mapSearchRowBase(row),
|
|
@@ -1720,7 +2344,7 @@ async function keywordSearchDocuments(db, input, limit) {
|
|
|
1720
2344
|
...documentSearchConditions(input),
|
|
1721
2345
|
sql`to_tsvector('simple', ${schema.documentChunks.text}) @@ plainto_tsquery('simple', ${input.query})`
|
|
1722
2346
|
)
|
|
1723
|
-
).orderBy(desc(rank)).limit(limit)
|
|
2347
|
+
).orderBy(desc(rank), asc(schema.documentChunks.id)).limit(limit)
|
|
1724
2348
|
);
|
|
1725
2349
|
return rows.map((row) => ({
|
|
1726
2350
|
...mapSearchRowBase(row),
|
|
@@ -1816,6 +2440,21 @@ async function assertDocumentAccountWorkspace(db, accountId, workspaceId) {
|
|
|
1816
2440
|
}
|
|
1817
2441
|
return context;
|
|
1818
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
|
+
}
|
|
1819
2458
|
function documentAccessConditions(workspaceId, access) {
|
|
1820
2459
|
const organization = eq(schema.documents.authorityKind, "organization");
|
|
1821
2460
|
const workspace = and(
|
|
@@ -1823,10 +2462,28 @@ function documentAccessConditions(workspaceId, access) {
|
|
|
1823
2462
|
eq(schema.documents.authorityWorkspaceId, workspaceId)
|
|
1824
2463
|
);
|
|
1825
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`;
|
|
1826
2474
|
const personal = viewer ? and(
|
|
1827
2475
|
eq(schema.documents.authorityKind, "personal"),
|
|
1828
|
-
eq(schema.documents.
|
|
1829
|
-
|
|
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)
|
|
1830
2487
|
) : void 0;
|
|
1831
2488
|
const authority = viewer ? or(organization, workspace, personal) ?? organization : or(organization, workspace) ?? organization;
|
|
1832
2489
|
const viewerSql = viewer ? sql`${viewer}` : sql`NULL::text`;
|
|
@@ -1843,7 +2500,7 @@ function documentAccessConditions(workspaceId, access) {
|
|
|
1843
2500
|
}
|
|
1844
2501
|
function documentMatchesAccess(document, workspaceId, access) {
|
|
1845
2502
|
if (access?.agentOnly) {
|
|
1846
|
-
return document.agentAccess && canViewDocument(document, access.viewerSubjectId, workspaceId);
|
|
2503
|
+
return document.agentAccess && (document.authorityKind !== "personal" || document.authorityId === null) && canViewDocument(document, access.viewerSubjectId, workspaceId);
|
|
1847
2504
|
}
|
|
1848
2505
|
return canViewDocument(document, access?.viewerSubjectId, workspaceId);
|
|
1849
2506
|
}
|
|
@@ -1861,16 +2518,16 @@ function canViewDocument(document, viewerSubjectId, workspaceId) {
|
|
|
1861
2518
|
return document.authorityWorkspaceId === null && document.authoritySubjectId === null;
|
|
1862
2519
|
}
|
|
1863
2520
|
const normalizedWorkspaceId = cleanString(workspaceId ?? null);
|
|
1864
|
-
if (!normalizedWorkspaceId
|
|
2521
|
+
if (!normalizedWorkspaceId) {
|
|
1865
2522
|
return false;
|
|
1866
2523
|
}
|
|
1867
2524
|
if (document.authorityKind === "workspace") {
|
|
1868
|
-
return document.authoritySubjectId === null;
|
|
2525
|
+
return document.authorityWorkspaceId === normalizedWorkspaceId && document.authoritySubjectId === null;
|
|
1869
2526
|
}
|
|
1870
2527
|
if (document.authorityKind === "personal") {
|
|
1871
2528
|
const authoritySubjectId = canonicalDocumentAuthoritySubject(document.authoritySubjectId);
|
|
1872
2529
|
const viewer = canonicalDocumentAuthoritySubject(viewerSubjectId);
|
|
1873
|
-
return !!authoritySubjectId && authoritySubjectId === viewer;
|
|
2530
|
+
return !!authoritySubjectId && authoritySubjectId === viewer && (document.authorityWorkspaceId === null || document.authorityWorkspaceId === normalizedWorkspaceId);
|
|
1874
2531
|
}
|
|
1875
2532
|
return false;
|
|
1876
2533
|
}
|
|
@@ -1960,7 +2617,7 @@ function mergeDocumentSearchRows(rows, mode) {
|
|
|
1960
2617
|
matchType
|
|
1961
2618
|
};
|
|
1962
2619
|
}).sort(
|
|
1963
|
-
(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)
|
|
1964
2621
|
);
|
|
1965
2622
|
}
|
|
1966
2623
|
function combinedSearchScore(mode, vectorScore, keywordScore, matchType) {
|
|
@@ -2147,6 +2804,7 @@ function mapDocument(row) {
|
|
|
2147
2804
|
authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
|
|
2148
2805
|
authorityWorkspaceId: row.authorityWorkspaceId,
|
|
2149
2806
|
authoritySubjectId: row.authoritySubjectId,
|
|
2807
|
+
authorityId: row.authorityId,
|
|
2150
2808
|
visibility: normalizeDocumentVisibility(row.visibility),
|
|
2151
2809
|
createdBy: row.createdBy,
|
|
2152
2810
|
agentAccess: row.agentAccess,
|
|
@@ -2253,9 +2911,12 @@ export {
|
|
|
2253
2911
|
getDocumentChunk,
|
|
2254
2912
|
getDocumentForIndexing,
|
|
2255
2913
|
getDocumentInventory,
|
|
2914
|
+
getDocumentOriginalFile,
|
|
2256
2915
|
getEffectiveKnowledgeRecord,
|
|
2257
2916
|
heuristicCuration,
|
|
2258
2917
|
indexDocumentNow,
|
|
2918
|
+
listAccessibleDocuments,
|
|
2919
|
+
listDocumentAuthorityReclassifications,
|
|
2259
2920
|
listDocumentBases,
|
|
2260
2921
|
listDocumentBasesEnsuringDefault,
|
|
2261
2922
|
listDocuments,
|
|
@@ -2265,9 +2926,15 @@ export {
|
|
|
2265
2926
|
parseDocumentBytes,
|
|
2266
2927
|
projectKnowledgeRecord,
|
|
2267
2928
|
queueDocumentForReindex,
|
|
2929
|
+
reclassifyDocumentAuthority,
|
|
2268
2930
|
resolveDocumentAuthority,
|
|
2931
|
+
resolveEffectiveDocumentAccess,
|
|
2932
|
+
runDocumentDefaultCollectionBackfill,
|
|
2269
2933
|
searchDocuments,
|
|
2270
2934
|
searchEffectiveDocuments,
|
|
2271
|
-
searchEffectiveKnowledge
|
|
2935
|
+
searchEffectiveKnowledge,
|
|
2936
|
+
selectDocumentSearchCandidateWindow,
|
|
2937
|
+
selectKnowledgeBrowseRecords,
|
|
2938
|
+
selectKnowledgeSearchResults
|
|
2272
2939
|
};
|
|
2273
2940
|
//# sourceMappingURL=index.js.map
|