Skip to content

Commit 93ea661

Browse files
committed
fix(knowledge): keep a bundle's document token totals on import
- Imported documents took their token total from the embedding tokenizer's per-chunk counts, which estimate differently from the chunker that produced the source total, so totals read about 15% high. The prepared index now carries the manifest's total, matching the source document. - Bound manifest token and character counts to the integer columns they are written to, so an out-of-range bundle is refused before any work instead of failing each document at its final write. - Log "indexing with bundled vectors" instead of "generating embeddings" when a document's vectors come from the bundle.
1 parent 17389db commit 93ea661

6 files changed

Lines changed: 41 additions & 15 deletions

File tree

‎apps/sim/lib/knowledge/__integration__/bundle-import.integration.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ async function importedDocuments(knowledgeBaseId: string) {
202202
enabled: document.enabled,
203203
processingStatus: document.processingStatus,
204204
chunkCount: document.chunkCount,
205+
tokenCount: document.tokenCount,
205206
tag1: document.tag1,
206207
})
207208
.from(document)
@@ -308,6 +309,7 @@ describe('knowledge base bundle import', () => {
308309
enabled: true,
309310
processingStatus: 'completed',
310311
chunkCount: 2,
312+
tokenCount: 8,
311313
tag1: 'public',
312314
})
313315
expect(handbook.fileUrl).toMatch(/^\/api\/files\/serve\/kb\/.+\?context=knowledge-base$/)
@@ -328,6 +330,7 @@ describe('knowledge base bundle import', () => {
328330
enabled: false,
329331
processingStatus: 'completed',
330332
chunkCount: 1,
333+
tokenCount: 4,
331334
})
332335
expect(
333336
(
@@ -380,7 +383,7 @@ describe('knowledge base bundle import', () => {
380383
)
381384
)
382385
expect(queued).toHaveLength(1)
383-
expect(notes.processingStatus).toBe('completed')
386+
expect(notes).toMatchObject({ processingStatus: 'completed', tokenCount: 4 })
384387
expect(fixture.embedded).toBe(1)
385388
expect(await importedChunks(notes.id)).toEqual([
386389
{

‎apps/sim/lib/knowledge/documents/service.ts‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,12 +1459,17 @@ export interface PreparedDocumentIndex {
14591459
readonly vectors?: readonly number[][]
14601460
/** Characters in the document's source text; chunks overlap, so their lengths do not sum to it. */
14611461
readonly characterCount: number
1462+
/**
1463+
* The document total its chunker estimated for these chunks. Chunkers estimate
1464+
* differently from the embedding tokenizer the pass applies to each chunk, so
1465+
* summing per-chunk counts would not match the source document.
1466+
*/
1467+
readonly tokenCount: number
14621468
}
14631469

14641470
/**
1465-
* The processor's result shape for chunks that were never parsed here. Token
1466-
* counts are left to the pass, which estimates every chunk against this
1467-
* base's model whichever way the chunks arrived.
1471+
* The processor's result shape for chunks that were never parsed here. The
1472+
* pass still estimates each chunk against this base's model.
14681473
*/
14691474
function preparedProcessingResult(prepared: PreparedDocumentIndex) {
14701475
return {
@@ -1474,7 +1479,7 @@ function preparedProcessingResult(prepared: PreparedDocumentIndex) {
14741479
})),
14751480
metadata: {
14761481
chunkCount: prepared.chunks.length,
1477-
tokenCount: null,
1482+
tokenCount: prepared.tokenCount,
14781483
characterCount: prepared.characterCount,
14791484
},
14801485
}
@@ -2037,7 +2042,9 @@ export async function processDocumentAsync(
20372042
const now = new Date()
20382043

20392044
logger.info(
2040-
`[${documentId}] Document parsed successfully, generating embeddings for ${processed.chunks.length} chunks`
2045+
prepared?.vectors
2046+
? `[${documentId}] Indexing ${processed.chunks.length} chunks with their bundled vectors`
2047+
: `[${documentId}] Document parsed successfully, generating embeddings for ${processed.chunks.length} chunks`
20412048
)
20422049

20432050
const chunkTexts = processed.chunks.map((chunk) => chunk.text)
@@ -2297,9 +2304,7 @@ export async function processDocumentAsync(
22972304
.update(document)
22982305
.set({
22992306
chunkCount: processed.metadata.chunkCount,
2300-
tokenCount:
2301-
processed.metadata.tokenCount ??
2302-
chunkTokenCounts.reduce((sum, count) => sum + count, 0),
2307+
tokenCount: processed.metadata.tokenCount,
23032308
characterCount: processed.metadata.characterCount,
23042309
processingStatus: 'completed',
23052310
processingCompletedAt: now,

‎apps/sim/lib/knowledge/transfer/bundle.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ const MAX_BUNDLE_TEXT_LENGTH = 255
5757
*/
5858
const MAX_BUNDLE_TAG_VALUE_LENGTH = KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH
5959

60+
/** Largest value the `integer` count columns an import writes can hold. */
61+
const MAX_INTEGER_COLUMN = 2_147_483_647
62+
6063
const tagSlotSchema = z.enum(ALL_TAG_SLOTS)
6164

6265
export const knowledgeBundleTagSchema = z
@@ -81,8 +84,8 @@ export const knowledgeBundleDocumentSchema = z
8184
file: z.string().nullable(),
8285
chunks: z.string().nullable(),
8386
chunkCount: z.number().int().min(0).max(MAX_DOCUMENT_CHUNKS),
84-
tokenCount: z.number().int().min(0),
85-
characterCount: z.number().int().min(0),
87+
tokenCount: z.number().int().min(0).max(MAX_INTEGER_COLUMN),
88+
characterCount: z.number().int().min(0).max(MAX_INTEGER_COLUMN),
8689
})
8790
.strict()
8891
.superRefine((document, ctx) => {

‎apps/sim/lib/knowledge/transfer/import-archive.test.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ function manifest(overrides: Record<string, unknown> = {}, document: Record<stri
5151
}
5252
}
5353

54-
const CHUNKS = { chunks: `chunks/${DOCUMENT_ID}.ndjson`, chunkCount: 2, characterCount: 10 }
54+
const CHUNKS = {
55+
chunks: `chunks/${DOCUMENT_ID}.ndjson`,
56+
chunkCount: 2,
57+
characterCount: 10,
58+
tokenCount: 4,
59+
}
5560

5661
function chunkLine(index: number, extra: Record<string, unknown> = {}): string {
5762
return JSON.stringify({
@@ -139,6 +144,11 @@ describe('openKnowledgeBundle', () => {
139144
},
140145
'duplicate document id doc-1',
141146
],
147+
[
148+
'a document count no integer column can hold',
149+
() => zipOf(validEntries(undefined, manifest({}, { tokenCount: 2_147_483_648 }))),
150+
'manifest documents.0.tokenCount',
151+
],
142152
[
143153
'a manifest naming an entry the archive lacks',
144154
() => {

‎apps/sim/lib/knowledge/transfer/import-archive.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ export interface KnowledgeBundleReader {
4444
* line must carry a vector of the manifest's dimension; without, none is decoded.
4545
*/
4646
readChunks(
47-
document: Pick<KnowledgeBundleDocument, 'chunkCount' | 'characterCount'> & { chunks: string },
47+
document: Pick<KnowledgeBundleDocument, 'chunkCount' | 'characterCount' | 'tokenCount'> & {
48+
chunks: string
49+
},
4850
vectors: boolean
4951
): Promise<PreparedDocumentIndex>
5052
close(): void
@@ -231,8 +233,10 @@ export async function openKnowledgeBundle(path: string): Promise<KnowledgeBundle
231233
chunks.push(chunk)
232234
if (vector) decoded.push(vector)
233235
})
234-
const { characterCount } = document
235-
return vectors ? { chunks, vectors: decoded, characterCount } : { chunks, characterCount }
236+
const { characterCount, tokenCount } = document
237+
return vectors
238+
? { chunks, vectors: decoded, characterCount, tokenCount }
239+
: { chunks, characterCount, tokenCount }
236240
},
237241
close: () => zip.close(),
238242
}

‎apps/sim/lib/knowledge/transfer/import-runner.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ async function importDocument(
153153
chunks: document.chunks,
154154
chunkCount: document.chunkCount,
155155
characterCount: document.characterCount,
156+
tokenCount: document.tokenCount,
156157
},
157158
reuseVectors
158159
)

0 commit comments

Comments
 (0)