Skip to content

Commit 6a6d747

Browse files
committed
improvement(search): multiple native queries per account with rank fusion and per-provider query guides
1 parent aced5f7 commit 6a6d747

11 files changed

Lines changed: 432 additions & 155 deletions

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

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -78,36 +78,39 @@ describe('Assistant execution contracts', () => {
7878
expect(searchWorkspaceInputSchema.safeParse({}).success).toBe(false)
7979
})
8080

81-
it('accepts one native query per provider account and kind', () => {
81+
it('accepts up to four distinct native queries per provider account', () => {
8282
const accepts = (nativeQueries: Record<string, string>[]) =>
8383
searchWorkspaceInputSchema.safeParse({ query: 'launch', nativeQueries }).success
84-
const github = { provider: 'github', accountId: 'account', query: 'repo:org/repo launch' }
85-
expect(
86-
accepts([
87-
{ ...github, kind: 'issues' },
88-
{ ...github, kind: 'commits' },
89-
])
90-
).toBe(true)
91-
expect(
92-
accepts([
93-
{ ...github, kind: 'issues' },
94-
{ ...github, kind: 'issues' },
95-
])
96-
).toBe(false)
97-
expect(accepts([{ ...github, kind: 'issues' }, github])).toBe(false)
84+
const slack = { provider: 'slack', accountId: 'account' }
85+
const alternatives = ['trip', 'travel', 'visiting', 'vacation', 'holiday'].map((query) => ({
86+
...slack,
87+
query,
88+
}))
89+
expect(accepts(alternatives.slice(0, 4))).toBe(true)
90+
expect(accepts(alternatives)).toBe(false)
9891
expect(
99-
accepts([
100-
{ ...github, kind: 'issues' },
101-
{ ...github, accountId: 'other', kind: 'issues' },
102-
])
92+
accepts([...alternatives.slice(0, 4), { ...alternatives[4]!, accountId: 'other' }])
10393
).toBe(true)
104-
const gmail = { provider: 'gmail', query: 'subject:launch' }
105-
expect(
106-
accepts([
107-
{ ...gmail, kind: 'issues' },
108-
{ ...gmail, kind: 'code' },
109-
])
110-
).toBe(false)
94+
})
95+
96+
it('rejects a native query that repeats a search on the same account', () => {
97+
const accepts = (nativeQueries: Record<string, string>[]) =>
98+
searchWorkspaceInputSchema.safeParse({ query: 'launch', nativeQueries }).success
99+
const trip = { provider: 'slack', accountId: 'account', query: 'trip' }
100+
expect(accepts([trip, trip])).toBe(false)
101+
expect(accepts([trip, { provider: 'slack', query: 'trip' }])).toBe(false)
102+
expect(accepts([trip, { ...trip, kind: 'issues' }])).toBe(false)
103+
expect(accepts([trip, { ...trip, accountId: 'other' }])).toBe(true)
104+
})
105+
106+
it('takes one GitHub or GitLab query per account and kind', () => {
107+
const accepts = (nativeQueries: Record<string, string>[]) =>
108+
searchWorkspaceInputSchema.safeParse({ query: 'launch', nativeQueries }).success
109+
const github = { provider: 'github', accountId: 'account', query: 'repo:org/repo launch' }
110+
const kinds = ['issues', 'commits', 'code', 'repositories'].map((kind) => ({ ...github, kind }))
111+
expect(accepts(kinds)).toBe(true)
112+
expect(accepts([kinds[0]!, { ...kinds[0]!, query: 'repo:org/repo deploy' }])).toBe(false)
113+
expect(accepts([kinds[0]!, github])).toBe(false)
111114
expect(
112115
searchWorkspaceInputSchema.safeParse({ nativeQueries: [{ provider: 'github', query: '' }] })
113116
.success

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

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ import { LIVE_SEARCH_PROVIDER_IDS } from '@/lib/sim-search/live/provider-catalog
44
export const liveSearchProviderSchema = z.enum(LIVE_SEARCH_PROVIDER_IDS)
55
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>
66

7+
/**
8+
* Native queries one call may send to the same provider account. Alternatives run as separate
9+
* provider searches and fuse into one ranking, so the bound keeps a call within the provider's
10+
* burst limits (Slack allows about ten searches per user per minute) while leaving room for the
11+
* four GitHub or GitLab kinds.
12+
*/
13+
export const MAX_NATIVE_QUERIES_PER_ACCOUNT = 4
14+
15+
/**
16+
* Providers whose `kind` selects a separate search endpoint. They take one query per kind: their
17+
* query languages already join alternatives with OR, and each extra query fans out into several
18+
* repository or project requests against strict search rate limits.
19+
*/
20+
const KIND_PROVIDERS: ReadonlySet<LiveSearchProvider> = new Set(['github', 'gitlab'])
21+
722
const nativeSearchKindSchema = z.enum([
823
'issues',
924
'code',
@@ -13,9 +28,6 @@ const nativeSearchKindSchema = z.enum([
1328
'wiki',
1429
])
1530

16-
/** Providers whose `kind` selects a separate search endpoint; others ignore it. */
17-
const KIND_PROVIDERS: ReadonlySet<LiveSearchProvider> = new Set(['github', 'gitlab'])
18-
1931
/** Queries are data for fixed read-only provider endpoints, never URLs or credentials. */
2032
export const nativeSearchQuerySchema = z
2133
.object({
@@ -37,38 +49,38 @@ export const nativeSearchQueriesSchema = z
3749
.min(1)
3850
.max(9)
3951
.superRefine((queries, context) => {
40-
/**
41-
* Each account runs one query per kind: GitHub and GitLab kinds are separate endpoints, so
42-
* one call can search several of them for the same account. A query without a kind covers
43-
* the provider's default kinds and conflicts with any other query for that account.
44-
*/
45-
const kindOf = (query: NativeSearchQuery) =>
46-
KIND_PROVIDERS.has(query.provider) ? query.kind : undefined
52+
/** A query without an account ID targets every account of its provider. */
53+
const overlaps = (left: NativeSearchQuery, right: NativeSearchQuery) =>
54+
left.provider === right.provider &&
55+
(!left.accountId || !right.accountId || left.accountId === right.accountId)
56+
/** The search a query runs, ignoring its account and any kind its provider does not use. */
57+
const searchKey = ({ accountId: _, kind, ...query }: NativeSearchQuery) =>
58+
JSON.stringify({ ...query, kind: KIND_PROVIDERS.has(query.provider) ? kind : undefined })
4759
for (const [index, query] of queries.entries()) {
48-
if (
49-
queries
50-
.slice(0, index)
51-
.some(
52-
(previous) =>
53-
previous.provider === query.provider &&
54-
(!previous.accountId || !query.accountId || previous.accountId === query.accountId) &&
55-
(!kindOf(previous) || !kindOf(query) || kindOf(previous) === kindOf(query))
56-
)
60+
const addIssue = (message: string) =>
61+
context.addIssue({ code: 'custom', path: [index], message })
62+
const earlier = queries.slice(0, index).filter((previous) => overlaps(previous, query))
63+
if (earlier.some((previous) => searchKey(previous) === searchKey(query)))
64+
addIssue('Duplicate native query.')
65+
else if (
66+
KIND_PROVIDERS.has(query.provider) &&
67+
earlier.some((previous) => !previous.kind || !query.kind || previous.kind === query.kind)
5768
)
58-
context.addIssue({
59-
code: 'custom',
60-
path: [index],
61-
message:
62-
'Use one query per provider account and kind per call. Combine alternatives with OR in one query, or refine in another call.',
63-
})
69+
addIssue(
70+
'Send one GitHub or GitLab query per account and kind; join alternatives with OR in one query (GitHub code search has no OR, so search code alternatives in another call).'
71+
)
72+
else if (earlier.length >= MAX_NATIVE_QUERIES_PER_ACCOUNT)
73+
addIssue(
74+
`Send at most ${MAX_NATIVE_QUERIES_PER_ACCOUNT} native queries per provider account in one call; queries without an accountId count toward every account of their provider.`
75+
)
6476
}
6577
})
6678

6779
export const liveSearchAccountStatusSchema = z.object({
6880
accountId: z.string(),
6981
provider: liveSearchProviderSchema,
70-
/** The native query kind this status and its cursor belong to. */
71-
kind: nativeSearchKindSchema.optional(),
82+
/** Index of the native query in the request that this status and its cursor belong to. */
83+
queryIndex: z.number().int().min(0).optional(),
7284
displayName: z.string(),
7385
status: z.enum(['ok', 'partial', 'reconnect', 'rate_limited', 'unavailable', 'timeout']),
7486
message: z.string().optional(),
@@ -137,7 +149,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
137149
nativeQueries: nativeSearchQueriesSchema
138150
.optional()
139151
.describe(
140-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
152+
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS). Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
141153
),
142154
query: z
143155
.string()

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

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ export const liveSearchProviderSchema = z.enum([
1616
])
1717
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>
1818

19+
/**
20+
* Native queries one call may send to the same provider account. Alternatives run as separate
21+
* provider searches and fuse into one ranking, so the bound keeps a call within the provider's
22+
* burst limits (Slack allows about ten searches per user per minute) while leaving room for the
23+
* four GitHub or GitLab kinds.
24+
*/
25+
export const MAX_NATIVE_QUERIES_PER_ACCOUNT = 4
26+
27+
/**
28+
* Providers whose `kind` selects a separate search endpoint. They take one query per kind: their
29+
* query languages already join alternatives with OR, and each extra query fans out into several
30+
* repository or project requests against strict search rate limits.
31+
*/
32+
const KIND_PROVIDERS: ReadonlySet<LiveSearchProvider> = new Set(['github', 'gitlab'])
33+
1934
const nativeSearchKindSchema = z.enum([
2035
'issues',
2136
'code',
@@ -25,9 +40,6 @@ const nativeSearchKindSchema = z.enum([
2540
'wiki',
2641
])
2742

28-
/** Providers whose `kind` selects a separate search endpoint; others ignore it. */
29-
const KIND_PROVIDERS: ReadonlySet<LiveSearchProvider> = new Set(['github', 'gitlab'])
30-
3143
/** Queries are data for fixed read-only provider endpoints, never URLs or credentials. */
3244
export const nativeSearchQuerySchema = z
3345
.object({
@@ -49,38 +61,38 @@ export const nativeSearchQueriesSchema = z
4961
.min(1)
5062
.max(9)
5163
.superRefine((queries, context) => {
52-
/**
53-
* Each account runs one query per kind: GitHub and GitLab kinds are separate endpoints, so
54-
* one call can search several of them for the same account. A query without a kind covers
55-
* the provider's default kinds and conflicts with any other query for that account.
56-
*/
57-
const kindOf = (query: NativeSearchQuery) =>
58-
KIND_PROVIDERS.has(query.provider) ? query.kind : undefined
64+
/** A query without an account ID targets every account of its provider. */
65+
const overlaps = (left: NativeSearchQuery, right: NativeSearchQuery) =>
66+
left.provider === right.provider &&
67+
(!left.accountId || !right.accountId || left.accountId === right.accountId)
68+
/** The search a query runs, ignoring its account and any kind its provider does not use. */
69+
const searchKey = ({ accountId: _, kind, ...query }: NativeSearchQuery) =>
70+
JSON.stringify({ ...query, kind: KIND_PROVIDERS.has(query.provider) ? kind : undefined })
5971
for (const [index, query] of queries.entries()) {
60-
if (
61-
queries
62-
.slice(0, index)
63-
.some(
64-
(previous) =>
65-
previous.provider === query.provider &&
66-
(!previous.accountId || !query.accountId || previous.accountId === query.accountId) &&
67-
(!kindOf(previous) || !kindOf(query) || kindOf(previous) === kindOf(query))
68-
)
72+
const addIssue = (message: string) =>
73+
context.addIssue({ code: 'custom', path: [index], message })
74+
const earlier = queries.slice(0, index).filter((previous) => overlaps(previous, query))
75+
if (earlier.some((previous) => searchKey(previous) === searchKey(query)))
76+
addIssue('Duplicate native query.')
77+
else if (
78+
KIND_PROVIDERS.has(query.provider) &&
79+
earlier.some((previous) => !previous.kind || !query.kind || previous.kind === query.kind)
6980
)
70-
context.addIssue({
71-
code: 'custom',
72-
path: [index],
73-
message:
74-
'Use one query per provider account and kind per call. Combine alternatives with OR in one query, or refine in another call.',
75-
})
81+
addIssue(
82+
'Send one GitHub or GitLab query per account and kind; join alternatives with OR in one query (GitHub code search has no OR, so search code alternatives in another call).'
83+
)
84+
else if (earlier.length >= MAX_NATIVE_QUERIES_PER_ACCOUNT)
85+
addIssue(
86+
`Send at most ${MAX_NATIVE_QUERIES_PER_ACCOUNT} native queries per provider account in one call; queries without an accountId count toward every account of their provider.`
87+
)
7688
}
7789
})
7890

7991
export const liveSearchAccountStatusSchema = z.object({
8092
accountId: z.string(),
8193
provider: liveSearchProviderSchema,
82-
/** The native query kind this status and its cursor belong to. */
83-
kind: nativeSearchKindSchema.optional(),
94+
/** Index of the native query in the request that this status and its cursor belong to. */
95+
queryIndex: z.number().int().min(0).optional(),
8496
displayName: z.string(),
8597
status: z.enum(['ok', 'partial', 'reconnect', 'rate_limited', 'unavailable', 'timeout']),
8698
message: z.string().optional(),
@@ -149,7 +161,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
149161
nativeQueries: nativeSearchQueriesSchema
150162
.optional()
151163
.describe(
152-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
164+
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS). Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
153165
),
154166
query: z
155167
.string()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6041,7 +6041,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
60416041
},
60426042
nativeQueries: {
60436043
description:
6044-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
6044+
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS). Up to 4 per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.",
60456045
minItems: 1,
60466046
maxItems: 9,
60476047
type: 'array',

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5972,7 +5972,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
59725972
},
59735973
nativeQueries: {
59745974
description:
5975-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). GitHub kind commits searches commit messages with author:, committer:, author-date:, and repo: qualifiers. Send one query per account, or one per kind for GitHub and GitLab (e.g. issues and commits together); combine alternatives with OR. Omit for simple cross-provider terms. Use the returned live guidance and account IDs.',
5975+
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS). Up to 4 per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.",
59765976
minItems: 1,
59775977
maxItems: 9,
59785978
type: 'array',

0 commit comments

Comments
 (0)