Skip to content

Commit cc35da9

Browse files
fix: fallback to highest-similarity chunk when isFirstChunk metadata missing
Documents uploaded before isFirstChunk was added had no chunk marked as first, causing filterAndFormatContext to return 0 results when all similarities were below 0.5. Now falls back to the highest-similarity chunk per document when no isFirstChunk metadata exists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ded81d8 commit cc35da9

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

__tests__/prepareContext.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,44 @@ describe('filterAndFormatContext', () => {
8282
// 2 first chunks + 2 relevant chunks = 4
8383
expect(result).toHaveLength(4);
8484
});
85+
86+
it('includes highest-similarity chunk per document as fallback when no isFirstChunk metadata', () => {
87+
// Documents uploaded before isFirstChunk was added
88+
const chunks = [
89+
makeChunk('Intro content', 0.35, 1),
90+
makeChunk('Middle content', 0.30, 1),
91+
makeChunk('End content', 0.20, 1),
92+
];
93+
const result = filterAndFormatContext(chunks);
94+
// No chunks pass the 0.5 threshold and none are marked isFirstChunk,
95+
// but the highest-similarity chunk per document should be included as fallback
96+
expect(result).toHaveLength(1);
97+
expect(result[0]).toContain('Intro content');
98+
});
99+
100+
it('fallback chunk does not duplicate an already-included first chunk', () => {
101+
const chunks = [
102+
makeChunk('Real first', 0.35, 1, true),
103+
makeChunk('Other', 0.30, 1),
104+
];
105+
const result = filterAndFormatContext(chunks);
106+
// First chunk is included, no relevant chunks pass threshold,
107+
// but fallback should not duplicate — still just 1
108+
expect(result).toHaveLength(1);
109+
expect(result[0]).toContain('Real first');
110+
});
111+
112+
it('fallback works per-document when multiple documents lack isFirstChunk', () => {
113+
const chunks = [
114+
makeChunk('Doc1 best', 0.40, 1),
115+
makeChunk('Doc1 other', 0.20, 1),
116+
makeChunk('Doc2 best', 0.35, 2),
117+
makeChunk('Doc2 other', 0.10, 2),
118+
];
119+
const result = filterAndFormatContext(chunks);
120+
// One fallback per document = 2
121+
expect(result).toHaveLength(2);
122+
expect(result[0]).toContain('Doc1 best');
123+
expect(result[1]).toContain('Doc2 best');
124+
});
85125
});

utils/contextUtils.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,41 @@ interface ContextChunk {
1414
export const filterAndFormatContext = (chunks: ContextChunk[]): string[] => {
1515
if (chunks.length === 0) return [];
1616

17+
// First chunks marked with metadata
1718
const firstChunks = chunks.filter((c) => c.metadata?.isFirstChunk);
19+
20+
// For documents without any isFirstChunk metadata, use the highest-similarity
21+
// chunk as a fallback (covers documents uploaded before isFirstChunk was added)
22+
const documentIds = [...new Set(chunks.map((c) => c.metadata?.documentId))];
23+
const fallbackChunks: ContextChunk[] = [];
24+
for (const docId of documentIds) {
25+
const hasFirst = firstChunks.some(
26+
(c) => c.metadata?.documentId === docId
27+
);
28+
if (!hasFirst) {
29+
const docChunks = chunks
30+
.filter((c) => c.metadata?.documentId === docId)
31+
.sort((a, b) => b.similarity - a.similarity);
32+
if (docChunks.length > 0) {
33+
fallbackChunks.push(docChunks[0]);
34+
}
35+
}
36+
}
37+
38+
const alwaysIncluded = [...firstChunks, ...fallbackChunks];
39+
40+
// Relevant chunks above threshold (excluding already-included ones)
41+
const alwaysIncludedSet = new Set(alwaysIncluded);
1842
const relevantChunks = chunks
19-
.filter((c) => !c.metadata?.isFirstChunk && c.similarity >= SIMILARITY_THRESHOLD)
43+
.filter(
44+
(c) =>
45+
!alwaysIncludedSet.has(c) &&
46+
c.similarity >= SIMILARITY_THRESHOLD
47+
)
2048
.sort((a, b) => b.similarity - a.similarity)
2149
.slice(0, MAX_RELEVANT_CHUNKS);
2250

23-
const selected = [...firstChunks, ...relevantChunks];
51+
const selected = [...alwaysIncluded, ...relevantChunks];
2452

2553
return selected.map((item, index) => {
2654
const documentName =

0 commit comments

Comments
 (0)