Skip to content

Commit d12717f

Browse files
committed
improvement(search): preserve existing ranking policy
1 parent 149b689 commit d12717f

8 files changed

Lines changed: 179 additions & 830 deletions

File tree

apps/sim/lib/core/rate-limiter/provider-admission.test.ts

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -35,66 +35,7 @@ describe('provider admission', () => {
3535
consumeTokens.mockResolvedValue({ allowed: true, tokensRemaining: 1, resetAt: new Date() })
3636
})
3737

38-
afterEach(() => {
39-
vi.useRealTimers()
40-
vi.unstubAllEnvs()
41-
})
42-
43-
it.each([
44-
{ requestsPerMinute: 1, capacity: 1 },
45-
{ requestsPerMinute: 60, capacity: 2 },
46-
{ requestsPerMinute: 120, capacity: 2 },
47-
{ requestsPerMinute: 121, capacity: 3 },
48-
{ requestsPerMinute: 600, capacity: 10 },
49-
])(
50-
'admits a complete rerank refill at $requestsPerMinute requests per minute',
51-
async ({ requestsPerMinute, capacity }) => {
52-
vi.stubEnv('KB_CONFIG_RERANK_REQUESTS_PER_MINUTE', String(requestsPerMinute))
53-
54-
await waitForProviderAdmission({ ...INPUT, operation: 'rerank', providerId: 'cohere' })
55-
56-
expect(consumeTokens).toHaveBeenCalledExactlyOnceWith(
57-
[
58-
{
59-
key: 'provider:rerank:cohere:hashed-credential:requests',
60-
cost: 1,
61-
config: {
62-
maxTokens: capacity,
63-
refillRate: requestsPerMinute / 60,
64-
refillIntervalMs: 1000,
65-
},
66-
},
67-
],
68-
expect.objectContaining({
69-
cooldownKeys: [
70-
'provider:rerank:cohere:hashed-credential:cooldown',
71-
'provider:rerank:cohere:hashed-credential:quota',
72-
],
73-
})
74-
)
75-
}
76-
)
77-
78-
it.each([
79-
{ operation: 'embedding' as const, capacity: 8, refillRate: 10 },
80-
{ operation: 'ocr' as const, capacity: 2, refillRate: 1 },
81-
{ operation: 'rerank' as const, capacity: 2, refillRate: 1 },
82-
])('preserves default $operation request limits', async ({ operation, capacity, refillRate }) => {
83-
vi.stubEnv('KB_CONFIG_EMBEDDING_REQUESTS_PER_MINUTE', undefined)
84-
vi.stubEnv('KB_CONFIG_OCR_REQUESTS_PER_MINUTE', undefined)
85-
vi.stubEnv('KB_CONFIG_RERANK_REQUESTS_PER_MINUTE', undefined)
86-
87-
await waitForProviderAdmission({ ...INPUT, operation })
88-
89-
expect(consumeTokens.mock.calls[0][0]).toEqual(
90-
expect.arrayContaining([
91-
expect.objectContaining({
92-
key: `provider:${operation}:openai:hashed-credential:requests`,
93-
config: { maxTokens: capacity, refillRate, refillIntervalMs: 1000 },
94-
}),
95-
])
96-
)
97-
})
38+
afterEach(() => vi.useRealTimers())
9839

9940
it('shares both credential dimensions in one reservation across concurrent callers', async () => {
10041
await Promise.all([waitForProviderAdmission(INPUT), waitForProviderAdmission(INPUT)])

apps/sim/lib/core/rate-limiter/provider-admission.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,6 @@ export async function waitForProviderAdmission(input: ProviderAdmissionInput): P
4646
: input.operation === 'ocr'
4747
? envNumber(env.KB_CONFIG_OCR_REQUESTS_PER_MINUTE, 60, { min: 1 })
4848
: envNumber(env.KB_CONFIG_RERANK_REQUESTS_PER_MINUTE, 60, { min: 1 })
49-
/** Rerank capacity must retain a complete refill from the backends' whole-second ticks. */
50-
const requestBurst =
51-
input.operation === 'embedding'
52-
? 8
53-
: input.operation === 'rerank'
54-
? Math.max(2, Math.ceil(requestsPerMinute / 60))
55-
: 2
5649
const reservations: TokenBucketReservation[] = []
5750
if (input.operation === 'embedding' && input.inputTokens) {
5851
const tokensPerMinute = envNumber(env.KB_CONFIG_EMBEDDING_TOKENS_PER_MINUTE, 600_000, {
@@ -75,7 +68,7 @@ export async function waitForProviderAdmission(input: ProviderAdmissionInput): P
7568
key: `${key}:requests`,
7669
cost: 1,
7770
config: {
78-
maxTokens: Math.min(requestBurst, requestsPerMinute),
71+
maxTokens: Math.min(input.operation === 'embedding' ? 8 : 2, requestsPerMinute),
7972
refillRate: requestsPerMinute / 60,
8073
refillIntervalMs: 1000,
8174
},

apps/sim/lib/knowledge/application/search.test.ts

Lines changed: 61 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { member } from '@sim/db/schema'
66
import { queueTableRows, resetDbChainMock } from '@sim/testing'
77
import { beforeEach, describe, expect, it, vi } from 'vitest'
88
import { OrchestrationError } from '@/lib/core/orchestration/types'
9-
import { EXACT_EMPTY_DURABLE_SECRET_PROVENANCE } from '@/lib/execution/durable-secret-provenance'
109

1110
const mocks = vi.hoisted(() => ({
1211
resolveWorkspace: vi.fn(),
@@ -188,143 +187,65 @@ describe('knowledge search application use case', () => {
188187
expect(result.totalResults).toBe(0)
189188
})
190189

191-
it('uses provider-reported rerank units for the returned usage cost', async () => {
192-
mocks.rerank.mockResolvedValue({
193-
results: [{ item: { id: 'embedding-1' }, relevanceScore: 0.9 }],
194-
isBYOK: false,
195-
billedSearchUnits: 3,
196-
})
197-
const result = await searchKnowledge.execute({
198-
principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' },
199-
input: {
200-
workspaceId: 'workspace-1',
201-
knowledgeBaseIds: ['knowledge-1'],
202-
query: 'answer',
203-
topK: 5,
204-
rerankerEnabled: true,
205-
rerankerModel: 'rerank-v4.0-fast',
206-
},
190+
describe.each(['workspace', 'organization'] as const)('%s ranking policy', (scope) => {
191+
beforeEach(() => {
192+
if (scope === 'organization') {
193+
mocks.getKnowledgeBase.mockResolvedValue({
194+
...knowledgeBase,
195+
workspaceId: null,
196+
organizationId: 'org-canonical',
197+
isSearchIndex: true,
198+
})
199+
queueTableRows(member, [{ role: 'member' }])
200+
}
207201
})
208-
expect(result.cost?.rerankerSearchUnits).toBe(3)
209-
expect(result.cost?.rerankerCost).toBe(3 * 0.002)
210-
expect(mocks.rerank).toHaveBeenCalledWith(
211-
'answer',
212-
[{ id: 'embedding-1', text: 'answer' }],
213-
expect.objectContaining({ timeoutMs: undefined })
214-
)
215-
})
216202

217-
describe('organization reranking', () => {
218-
beforeEach(() => {
219-
mocks.getKnowledgeBase.mockResolvedValue({
220-
...knowledgeBase,
221-
workspaceId: null,
222-
organizationId: 'org-canonical',
223-
isSearchIndex: true,
224-
})
225-
queueTableRows(member, [{ role: 'member' }])
226-
mocks.importProvenance.mockResolvedValue({
227-
imported: true,
228-
unrecordedCount: 0,
229-
documentMetadata: {
230-
'document-1': {
231-
filename: 'REV-781 approval',
232-
sourceUrl: null,
233-
provenance: EXACT_EMPTY_DURABLE_SECRET_PROVENANCE,
203+
const principal = { kind: 'session', userId: 'user-1', sessionId: 'session-1' } as const
204+
const input = { knowledgeBaseIds: ['knowledge-1'], query: 'answer', topK: 10 }
205+
206+
it.each([undefined, false])(
207+
'preserves retrieval without reranking when enabled is %s',
208+
async (rerankerEnabled) => {
209+
const result = await searchKnowledge.execute({
210+
principal,
211+
input: {
212+
...input,
213+
...(rerankerEnabled === undefined ? {} : { rerankerEnabled }),
234214
},
235-
},
236-
})
237-
mocks.rerank.mockResolvedValue({
215+
})
216+
217+
expect(mocks.executeSearch).toHaveBeenCalledWith(expect.objectContaining({ topK: 10 }))
218+
expect(mocks.rerank).not.toHaveBeenCalled()
219+
expect(mocks.importProvenance).not.toHaveBeenCalled()
220+
expect(result.rerankerStatus).toBe('not_requested')
221+
expect(result.cost?.rerankerSearchUnits).toBeUndefined()
222+
expect(result.results[0]).toMatchObject({ embeddingId: 'embedding-1', content: 'answer' })
223+
}
224+
)
225+
226+
it('preserves the existing explicit reranking option', async () => {
227+
mocks.rerank.mockResolvedValueOnce({
238228
results: [{ item: { id: 'embedding-1' }, relevanceScore: 0.9 }],
239229
isBYOK: false,
240-
billedSearchUnits: 1,
241230
})
242-
})
243-
244-
const principal = { kind: 'session', userId: 'user-1', sessionId: 'session-1' } as const
245-
const input = { knowledgeBaseIds: ['knowledge-1'], query: 'approval', topK: 10 }
246231

247-
it('uses the canonical organization owner to apply bounded title-aware reranking', async () => {
248-
const result = await searchKnowledge.execute({ principal, input })
249-
expect(mocks.executeSearch).toHaveBeenCalledWith(expect.objectContaining({ topK: 50 }))
250-
expect(mocks.importProvenance).toHaveBeenCalledWith(
251-
expect.objectContaining({ includeDocumentNames: true })
252-
)
253-
expect(mocks.rerank).toHaveBeenCalledWith(
254-
'approval',
255-
[{ id: 'embedding-1', text: 'Title: REV-781 approval\n\nanswer' }],
256-
expect.objectContaining({ model: 'rerank-v4.0-fast', topN: 10, timeoutMs: 3_000 })
257-
)
258-
expect(result.rerankerStatus).toBe('applied')
259-
expect(result.results[0].documentName).toBe('REV-781 approval')
260-
expect(result.cost?.rerankerModel).toBe('rerank-v4.0-fast')
261-
})
262-
263-
it('honors an explicit opt-out without expanding candidates or importing titles', async () => {
264232
const result = await searchKnowledge.execute({
265233
principal,
266-
input: { ...input, rerankerEnabled: false },
234+
input: {
235+
...input,
236+
rerankerEnabled: true,
237+
rerankerModel: 'rerank-v4.0-pro',
238+
rerankerInputCount: 20,
239+
},
267240
})
268-
expect(mocks.executeSearch).toHaveBeenCalledWith(expect.objectContaining({ topK: 10 }))
269-
expect(mocks.rerank).not.toHaveBeenCalled()
270-
expect(mocks.importProvenance).not.toHaveBeenCalled()
271-
expect(result.rerankerStatus).toBe('not_requested')
272-
})
273241

274-
it('preserves explicit model and candidate choices within the organization latency budget', async () => {
275-
await searchKnowledge.execute({
276-
principal,
277-
input: { ...input, rerankerModel: 'rerank-v4.0-pro', rerankerInputCount: 20 },
278-
})
279242
expect(mocks.executeSearch).toHaveBeenCalledWith(expect.objectContaining({ topK: 20 }))
280243
expect(mocks.rerank).toHaveBeenCalledWith(
281-
expect.anything(),
282-
expect.anything(),
283-
expect.objectContaining({ model: 'rerank-v4.0-pro', timeoutMs: 3_000 })
284-
)
285-
})
286-
287-
it('does not send a title to a model when its provenance snapshot is missing', async () => {
288-
mocks.importProvenance.mockResolvedValue({
289-
imported: true,
290-
unrecordedCount: 0,
291-
documentMetadata: {},
292-
})
293-
await expect(searchKnowledge.execute({ principal, input })).rejects.toThrow(
294-
'Knowledge result secret provenance is unavailable'
244+
'answer',
245+
[{ id: 'embedding-1', text: 'answer' }],
246+
expect.objectContaining({ model: 'rerank-v4.0-pro', topN: 10 })
295247
)
296-
expect(mocks.rerank).not.toHaveBeenCalled()
297-
})
298-
299-
it('does not disguise a provenance refusal as provider fallback', async () => {
300-
mocks.importProvenance.mockResolvedValue({
301-
imported: false,
302-
unrecordedCount: 0,
303-
documentMetadata: {},
304-
})
305-
await expect(searchKnowledge.execute({ principal, input })).rejects.toThrow(
306-
'Knowledge result secret provenance is unavailable'
307-
)
308-
expect(mocks.rerank).not.toHaveBeenCalled()
309-
})
310-
311-
it('preserves authorized results and reports provider failure', async () => {
312-
mocks.rerank.mockRejectedValue(new Error('Reranker unavailable'))
313-
const result = await searchKnowledge.execute({ principal, input })
314-
expect(result.rerankerStatus).toBe('unavailable')
315-
expect(result.results).toHaveLength(1)
316-
expect(result.results[0].rerankerScore).toBeUndefined()
317-
expect(result.cost?.rerankerSearchUnits).toBeUndefined()
318-
})
319-
320-
it('skips reranking when retrieval finds no authorized candidates', async () => {
321-
mocks.executeSearch.mockResolvedValue([])
322-
const result = await searchKnowledge.execute({
323-
principal,
324-
input,
325-
})
326-
expect(mocks.rerank).not.toHaveBeenCalled()
327-
expect(result.rerankerStatus).toBe('skipped')
248+
expect(result.rerankerStatus).toBe('applied')
328249
})
329250
})
330251

@@ -671,7 +592,6 @@ describe('knowledge search application use case', () => {
671592

672593
expect(mocks.importProvenance).toHaveBeenCalledWith({
673594
registry,
674-
includeDocumentNames: false,
675595
results: expect.arrayContaining([
676596
expect.objectContaining({ id: 'embedding-1', documentId: 'document-1' }),
677597
]),
@@ -755,6 +675,22 @@ describe('knowledge search application use case', () => {
755675
expect(result.results[0]).not.toHaveProperty('rerankerScore')
756676
})
757677

678+
/**
679+
* A resolved call with an empty ordering leaves the caller in the same place a
680+
* thrown one does — vector order, no `rerankerScore` — so it reports the same
681+
* status. It is not "the reranker matched nothing": `rerank` sends a non-empty
682+
* document list and asks for `top_n` of it, so an empty array means the
683+
* response carried nothing usable rather than a legitimate empty ranking.
684+
*/
685+
it('reports unavailable when the call resolves without a usable ordering', async () => {
686+
mocks.rerank.mockResolvedValueOnce({ results: [], isBYOK: false })
687+
688+
const result = await rerankedSearch(true)
689+
690+
expect(result.rerankerStatus).toBe('unavailable')
691+
expect(result.results[0]).not.toHaveProperty('rerankerScore')
692+
})
693+
758694
it('reports skipped for a tag-only search, which has no query to rank against', async () => {
759695
mocks.getTagDefinitions.mockResolvedValue([
760696
{ tagSlot: 'tag1', displayName: 'team', fieldType: 'text' },

0 commit comments

Comments
 (0)