Skip to content

Commit 7e11748

Browse files
authored
improvement(search): accept several same-kind native queries per account and cap oversized read limits (#8407)
* improvement(search): accept several same-kind native queries per account and cap oversized read limits * fix(search): describe same-kind native queries and test the read cap on the contract * fix(search): test the read cap through tool output and describe kindless native queries * fix(search): state what a kindless GitHub, GitLab, or HubSpot query covers * fix(search): kindless queries fan out across default collections * fix(search): kindless GitHub queries search code only without date bounds or boolean operators * test(search): keep only the refusal the contract still enforces
1 parent 85b96fb commit 7e11748

6 files changed

Lines changed: 41 additions & 29 deletions

File tree

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,12 @@ describe('Assistant execution contracts', () => {
5757
expect(accepts([trip, { ...trip, accountId: 'other' }])).toBe(true)
5858
})
5959

60-
it('takes one GitHub or GitLab query per account and kind', () => {
60+
it('refuses a kindless GitHub query alongside kinded queries on one account', () => {
6161
const accepts = (nativeQueries: Record<string, string>[]) =>
6262
searchWorkspaceInputSchema.safeParse({ query: 'launch', nativeQueries }).success
6363
const github = { provider: 'github', accountId: 'account', query: 'repo:org/repo launch' }
6464
const kinds = ['issues', 'commits', 'code', 'repositories'].map((kind) => ({ ...github, kind }))
6565
expect(accepts(kinds)).toBe(true)
66-
expect(accepts([kinds[0]!, { ...kinds[0]!, query: 'repo:org/repo deploy' }])).toBe(false)
6766
expect(accepts([kinds[0]!, github])).toBe(false)
6867
expect(
6968
searchWorkspaceInputSchema.safeParse({ nativeQueries: [{ provider: 'github', query: '' }] })

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ export const NOTION_SEARCH_TERMS_REQUIRED =
1616
export const MAX_NATIVE_QUERIES_PER_ACCOUNT = 4
1717

1818
/**
19-
* Providers whose kind selects a distinct search collection. One query per kind bounds fanout
20-
* while retaining independently searchable collections within the per-account request limit.
19+
* Providers whose kind selects a distinct search collection. A query without a kind fans out
20+
* across its provider's default collections, so it cannot share an account with kinded queries;
21+
* the per-account request limit bounds the rest.
2122
*/
2223
const PROVIDER_KIND_SCHEMAS = {
2324
github: z.enum(['issues', 'code', 'repositories', 'commits']),
@@ -102,10 +103,10 @@ export const nativeSearchQueriesSchema = z
102103
addIssue('Duplicate native query.')
103104
else if (
104105
hasSearchKinds(query.provider) &&
105-
earlier.some((previous) => !previous.kind || !query.kind || previous.kind === query.kind)
106+
earlier.some((previous) => !previous.kind || !query.kind)
106107
)
107108
addIssue(
108-
'Send one query per account and kind for GitHub, GitLab, or HubSpot. Use provider-supported operators for alternatives, or send another call.'
109+
'A GitHub, GitLab, or HubSpot query without a kind already searches every kind; give each query on this account a kind.'
109110
)
110111
else if (busiestAccountLoad(earlier) >= MAX_NATIVE_QUERIES_PER_ACCOUNT)
111112
addIssue(
@@ -187,7 +188,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
187188
nativeQueries: nativeSearchQueriesSchema
188189
.optional()
189190
.describe(
190-
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; GitHub, GitLab, and HubSpot take one per kind. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
191+
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; one GitHub, GitLab, or HubSpot query without a kind searches GitHub issues (plus code when the query has no date bound or boolean operators, as its status message says), GitLab issues, merge requests, and code, or every HubSpot CRM kind; other collections, and multiple queries on one account, each need a kind, which may repeat. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
191192
),
192193
query: z
193194
.string()
@@ -246,11 +247,12 @@ export const readDocumentInputSchema = z.object({
246247
.min(1)
247248
.max(4000)
248249
.describe('Canonical document ID returned by search or selected document context.'),
250+
/** A larger request is capped rather than refused: the server returns at most 8 chunks anyway. */
249251
limit: z
250-
.number()
251-
.int()
252-
.min(1)
253-
.max(8)
252+
.preprocess(
253+
(limit) => (typeof limit === 'number' && limit > 8 ? 8 : limit),
254+
z.number().int().min(1).max(8)
255+
)
254256
.default(3)
255257
.describe(
256258
'Maximum number of chunks, from 1 to 8 (default 3); the server may return fewer to fit its text budget. Follow next for more context.'

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ export const NOTION_SEARCH_TERMS_REQUIRED =
3333
export const MAX_NATIVE_QUERIES_PER_ACCOUNT = 4
3434

3535
/**
36-
* Providers whose kind selects a distinct search collection. One query per kind bounds fanout
37-
* while retaining independently searchable collections within the per-account request limit.
36+
* Providers whose kind selects a distinct search collection. A query without a kind fans out
37+
* across its provider's default collections, so it cannot share an account with kinded queries;
38+
* the per-account request limit bounds the rest.
3839
*/
3940
const PROVIDER_KIND_SCHEMAS = {
4041
github: z.enum(['issues', 'code', 'repositories', 'commits']),
@@ -119,10 +120,10 @@ export const nativeSearchQueriesSchema = z
119120
addIssue('Duplicate native query.')
120121
else if (
121122
hasSearchKinds(query.provider) &&
122-
earlier.some((previous) => !previous.kind || !query.kind || previous.kind === query.kind)
123+
earlier.some((previous) => !previous.kind || !query.kind)
123124
)
124125
addIssue(
125-
'Send one query per account and kind for GitHub, GitLab, or HubSpot. Use provider-supported operators for alternatives, or send another call.'
126+
'A GitHub, GitLab, or HubSpot query without a kind already searches every kind; give each query on this account a kind.'
126127
)
127128
else if (busiestAccountLoad(earlier) >= MAX_NATIVE_QUERIES_PER_ACCOUNT)
128129
addIssue(
@@ -204,7 +205,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
204205
nativeQueries: nativeSearchQueriesSchema
205206
.optional()
206207
.describe(
207-
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; GitHub, GitLab, and HubSpot take one per kind. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
208+
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; one GitHub, GitLab, or HubSpot query without a kind searches GitHub issues (plus code when the query has no date bound or boolean operators, as its status message says), GitLab issues, merge requests, and code, or every HubSpot CRM kind; other collections, and multiple queries on one account, each need a kind, which may repeat. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
208209
),
209210
query: z
210211
.string()
@@ -263,11 +264,12 @@ export const readDocumentInputSchema = z.object({
263264
.min(1)
264265
.max(4000)
265266
.describe('Canonical document ID returned by search or selected document context.'),
267+
/** A larger request is capped rather than refused: the server returns at most 8 chunks anyway. */
266268
limit: z
267-
.number()
268-
.int()
269-
.min(1)
270-
.max(8)
269+
.preprocess(
270+
(limit) => (typeof limit === 'number' && limit > 8 ? 8 : limit),
271+
z.number().int().min(1).max(8)
272+
)
271273
.default(3)
272274
.describe(
273275
'Maximum number of chunks, from 1 to 8 (default 3); the server may return fewer to fit its text budget. Follow next for more context.'

‎apps/sim/lib/mothership/generated/tool-catalog-v1.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5196,7 +5196,6 @@ export const ReadDocument: ToolCatalogEntry = {
51965196
description: 'Canonical document ID returned by search or selected document context.',
51975197
},
51985198
limit: {
5199-
default: 3,
52005199
description:
52015200
'Maximum number of chunks, from 1 to 8 (default 3); the server may return fewer to fit its text budget. Follow next for more context.',
52025201
type: 'integer',
@@ -6096,7 +6095,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
60966095
},
60976096
nativeQueries: {
60986097
description:
6099-
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to 4 per account run separately and merge; GitHub, GitLab, and HubSpot take one per kind. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.",
6098+
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to 4 per account run separately and merge; one GitHub, GitLab, or HubSpot query without a kind searches GitHub issues (plus code when the query has no date bound or boolean operators, as its status message says), GitLab issues, merge requests, and code, or every HubSpot CRM kind; other collections, and multiple queries on one account, each need a kind, which may repeat. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.",
61006099
minItems: 1,
61016100
maxItems: 9,
61026101
type: 'array',

‎apps/sim/lib/mothership/generated/tool-schemas-v1.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5184,7 +5184,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
51845184
description: 'Canonical document ID returned by search or selected document context.',
51855185
},
51865186
limit: {
5187-
default: 3,
51885187
description:
51895188
'Maximum number of chunks, from 1 to 8 (default 3); the server may return fewer to fit its text budget. Follow next for more context.',
51905189
type: 'integer',
@@ -6044,7 +6043,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
60446043
},
60456044
nativeQueries: {
60466045
description:
6047-
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to 4 per account run separately and merge; GitHub, GitLab, and HubSpot take one per kind. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.",
6046+
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to 4 per account run separately and merge; one GitHub, GitLab, or HubSpot query without a kind searches GitHub issues (plus code when the query has no date bound or boolean operators, as its status message says), GitLab issues, merge requests, and code, or every HubSpot CRM kind; other collections, and multiple queries on one account, each need a kind, which may repeat. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.",
60486047
minItems: 1,
60496048
maxItems: 9,
60506049
type: 'array',

‎apps/sim/lib/mothership/tools/server/knowledge/workspace-search.test.ts‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ describe('Assistant retrieval tools', () => {
376376
expect(mocks.search).not.toHaveBeenCalled()
377377
expect(mocks.read).not.toHaveBeenCalled()
378378
})
379-
it('reads a selected document through the shared use case and rejects unbounded pages', async () => {
379+
it('reads a selected document through the shared use case and caps oversized pages', async () => {
380380
expect(
381381
await readDocumentServerTool.execute({ documentId: 'doc', startChunkIndex: 20 }, context)
382382
).toMatchObject({ success: true })
@@ -390,9 +390,20 @@ describe('Assistant retrieval tools', () => {
390390
}),
391391
})
392392
)
393-
expect(
394-
await readDocumentServerTool.execute({ documentId: 'doc', limit: 9 }, context)
395-
).toMatchObject({ success: false })
396-
expect(mocks.read).toHaveBeenCalledOnce()
393+
mocks.read.mockImplementationOnce(async ({ input }: { input: { limit: number } }) => ({
394+
knowledgeBaseId: 'index',
395+
documentId: 'doc',
396+
documentName: 'Title',
397+
sourceUrl: 'https://source.test/doc',
398+
chunks: Array.from({ length: input.limit }, (_, chunkIndex) => ({
399+
content: 'body',
400+
chunkIndex,
401+
})),
402+
hasMore: true,
403+
next: null,
404+
}))
405+
const capped = await readDocumentServerTool.execute({ documentId: 'doc', limit: 9 }, context)
406+
expect(capped).toMatchObject({ success: true })
407+
expect((capped as { data: { chunks: unknown[] } }).data.chunks).toHaveLength(8)
397408
})
398409
})

0 commit comments

Comments
 (0)