Skip to content

Commit eb4eb57

Browse files
authored
fix(search): preserve query attribution on fused results (#9730)
1 parent d703873 commit eb4eb57

6 files changed

Lines changed: 53 additions & 6 deletions

File tree

‎apps/sim/lib/api/contracts/mothership-assistant-tools.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,8 @@ export const workspaceKnowledgeSearchResultSchema = z.object({
458458
sourceDate: z.string().nullable().optional(),
459459
sourceContainerName: z.string().optional(),
460460
sourceContainerUrl: z.string().optional(),
461+
/** Zero-based nativeQueries entries that returned this result, retained through rank fusion. */
462+
queryIndexes: z.array(z.number().int().min(0).max(8)).min(1).max(9).optional(),
461463
sourceDateType: z.enum(['event_start', 'message', 'modified']).optional(),
462464
/** The person behind the document, from its author-like tag; null when the source names none. */
463465
author: z.string().nullable(),

‎apps/sim/lib/knowledge/mcp/server.protocol.test.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
33
import { createPersonalApiKeyPrincipal } from '@sim/testing/factories/principal.factory'
44
import { apiServerRoutesMock } from '@sim/testing/mocks/api-server-routes.mock'
55
import { urlsMockFns } from '@sim/testing/mocks/urls.mock'
6+
import { toArray, toRecord } from '@sim/utils/object'
67
import { NextRequest } from 'next/server'
78
import { describe, expect, it, vi } from 'vitest'
89

@@ -39,6 +40,7 @@ describe('Search MCP protocol', () => {
3940
content: 'Evidence',
4041
chunkIndex: 0,
4142
similarity: 0.5,
43+
queryIndexes: [0],
4244
}
4345
mocks.liveSearch.mockResolvedValue({
4446
results: [row],
@@ -74,12 +76,19 @@ describe('Search MCP protocol', () => {
7476
expect(searchSchema?.properties).toHaveProperty('nativeQueries')
7577
expect(readSchema?.properties).toHaveProperty('startOffset')
7678
expect(readSchema?.properties).not.toHaveProperty('url')
77-
const search = await client.callTool({ name: 'search', arguments: { query: 'release' } })
79+
const search = await client.callTool({
80+
name: 'search',
81+
arguments: {
82+
query: 'release',
83+
nativeQueries: [{ provider: 'google_drive', query: "name contains 'release'" }],
84+
},
85+
})
7886
expect(search.isError).not.toBe(true)
79-
expect(search.content).toEqual([{ type: 'text', text: expect.stringContaining(documentId) }])
80-
expect(mocks.liveSearch).toHaveBeenCalledWith(
81-
expect.objectContaining({ input: expect.objectContaining({ topK: 30 }) })
82-
)
87+
const searchContent = toRecord(toArray(search.content)[0])
88+
if (typeof searchContent.text !== 'string') throw new Error('Missing search payload')
89+
expect(JSON.parse(searchContent.text)).toMatchObject({
90+
results: [{ documentId, queryIndexes: [0] }],
91+
})
8392
const read = await client.callTool({ name: 'read_document', arguments: { documentId } })
8493
expect(read.isError).not.toBe(true)
8594
expect(read.content).toEqual([

‎apps/sim/lib/knowledge/mcp/server.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export function createKnowledgeMcpServer(context: KnowledgeMcpContext): McpServe
162162
sourceDate: row.sourceDate,
163163
sourceContainerName: row.sourceContainerName,
164164
sourceContainerUrl: row.sourceContainerUrl,
165+
...(row.queryIndexes ? { queryIndexes: row.queryIndexes } : {}),
165166
connectorType: row.connectorType,
166167
content: row.content,
167168
chunkIndex: row.chunkIndex,

‎apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,8 @@ export const workspaceKnowledgeSearchResultSchema = z.object({
480480
sourceDate: z.string().nullable().optional(),
481481
sourceContainerName: z.string().optional(),
482482
sourceContainerUrl: z.string().optional(),
483+
/** Zero-based nativeQueries entries that returned this result, retained through rank fusion. */
484+
queryIndexes: z.array(z.number().int().min(0).max(8)).min(1).max(9).optional(),
483485
sourceDateType: z.enum(['event_start', 'message', 'modified']).optional(),
484486
/** The person behind the document, from its author-like tag; null when the source names none. */
485487
author: z.string().nullable(),

‎apps/sim/lib/sim-search/live/application.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -972,7 +972,17 @@ const liveKnowledgeSearch = defineAuthorizedKnowledgeUseCase({
972972
})
973973
return {
974974
query: input.query,
975-
results: ranked.slice(0, input.topK).map(({ result }) => result),
975+
results: ranked.slice(0, input.topK).map(({ result, queries: matched }) => {
976+
const queryIndexes = [
977+
...new Set(
978+
matched.flatMap((index) => {
979+
const queryIndex = searched[index]?.status.queryIndex
980+
return queryIndex === undefined ? [] : [queryIndex]
981+
})
982+
),
983+
].sort((a, b) => a - b)
984+
return queryIndexes.length ? { ...result, queryIndexes } : result
985+
}),
976986
retrieval: {
977987
status: retrievalStatus,
978988
timedOutLegs: [],

‎apps/sim/lib/sim-search/live/managed-mcp.integration.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { sleep } from '@sim/utils/helpers'
2121
import { generateId } from '@sim/utils/id'
2222
import { eq, inArray } from 'drizzle-orm'
2323
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
24+
import { workspaceKnowledgeSearchDataSchema } from '@/lib/api/contracts/mothership-assistant-tools'
2425
import { env } from '@/lib/core/config/env'
2526
import { runWithOutboundOrganization } from '@/lib/core/network/context.server'
2627
import { createManagedMcpConnector } from '@/lib/credential-groups/managed-mcp-service'
@@ -493,6 +494,12 @@ describe('managed Search operation sessions', () => {
493494
'Second topology',
494495
TITLE,
495496
])
497+
expect(found.results).toEqual(
498+
expect.arrayContaining([
499+
expect.objectContaining({ documentName: TITLE, queryIndexes: [0] }),
500+
expect.objectContaining({ documentName: 'Second topology', queryIndexes: [1] }),
501+
])
502+
)
496503
expect(found.retrieval.status).toBe('complete')
497504
expect(
498505
found.live?.accounts.map(({ queryIndex, status }) => ({ queryIndex, status }))
@@ -576,6 +583,12 @@ describe('managed Search operation sessions', () => {
576583
'Second topology',
577584
TITLE,
578585
])
586+
expect(parallel.results).toEqual(
587+
expect.arrayContaining([
588+
expect.objectContaining({ documentName: TITLE, queryIndexes: [0] }),
589+
expect.objectContaining({ documentName: 'Second topology', queryIndexes: [1] }),
590+
])
591+
)
579592

580593
for (const options of [{ project: '42' }, { topK: 5 }, { startDate: '2026-09-01T00:00:00Z' }]) {
581594
expect((await browse(0, options)).live?.accounts[0]?.status).not.toBe('unavailable')
@@ -633,17 +646,27 @@ describe('managed Search operation sessions', () => {
633646
kind: 'lucidchart',
634647
accountId: actor.credentialId,
635648
},
649+
{
650+
provider: 'lucid',
651+
query: 'architecture',
652+
kind: 'lucidchart',
653+
accountId: actor.credentialId,
654+
},
636655
],
637656
},
638657
})
639658
expect(found.results).toHaveLength(1)
659+
expect(workspaceKnowledgeSearchDataSchema.parse(found).results[0]).toMatchObject({
660+
queryIndexes: [1, 2],
661+
})
640662
expect(found.live?.accounts).toEqual([
641663
expect.objectContaining({
642664
queryIndex: 0,
643665
folders: [{ id: '42', name: 'Architecture' }],
644666
nextCursor: expect.any(String),
645667
}),
646668
expect.objectContaining({ queryIndex: 1, status: 'ok' }),
669+
expect.objectContaining({ queryIndex: 2, status: 'ok' }),
647670
])
648671
expect(openTransports.size).toBe(0)
649672
})

0 commit comments

Comments
 (0)