Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions apps/sim/lib/api/contracts/mothership-assistant-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { LIVE_SEARCH_PROVIDER_IDS } from '@/lib/sim-search/live/provider-catalog
export const liveSearchProviderSchema = z.enum(LIVE_SEARCH_PROVIDER_IDS)
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>

export const NOTION_SEARCH_TERMS_REQUIRED =
'Notion requires search terms. Add keywords or a concise question.'

/**
* Native queries one call may send to the same provider account. Alternatives run as separate
* provider searches and fuse into one ranking, so the bound keeps a call within the provider's
Expand Down Expand Up @@ -42,6 +45,14 @@ export const nativeSearchQuerySchema = z
keywordOnly: z.boolean().optional(),
})
.strict()
.superRefine((input, context) => {
if (input.provider === 'notion' && !input.query)
context.addIssue({
code: 'custom',
path: ['query'],
message: NOTION_SEARCH_TERMS_REQUIRED,
})
})
export type NativeSearchQuery = z.output<typeof nativeSearchQuerySchema>

export const nativeSearchQueriesSchema = z
Expand Down Expand Up @@ -161,15 +172,15 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
nativeQueries: nativeSearchQueriesSchema
.optional()
.describe(
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). 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.`
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Notion requires nonempty search terms even with dates or sorting. 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.`
),
query: z
.string()
.trim()
.max(2000)
.default('')
.describe(
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.'
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest where supported; Notion requires search terms.'
),
topK: z
.number()
Expand Down
15 changes: 13 additions & 2 deletions apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export const liveSearchProviderSchema = z.enum([
])
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>

export const NOTION_SEARCH_TERMS_REQUIRED =
'Notion requires search terms. Add keywords or a concise question.'

/**
* Native queries one call may send to the same provider account. Alternatives run as separate
* provider searches and fuse into one ranking, so the bound keeps a call within the provider's
Expand Down Expand Up @@ -58,6 +61,14 @@ export const nativeSearchQuerySchema = z
keywordOnly: z.boolean().optional(),
})
.strict()
.superRefine((input, context) => {
if (input.provider === 'notion' && !input.query)
context.addIssue({
code: 'custom',
path: ['query'],
message: NOTION_SEARCH_TERMS_REQUIRED,
})
})
export type NativeSearchQuery = z.output<typeof nativeSearchQuerySchema>

export const nativeSearchQueriesSchema = z
Expand Down Expand Up @@ -177,15 +188,15 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
nativeQueries: nativeSearchQueriesSchema
.optional()
.describe(
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). 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.`
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Notion requires nonempty search terms even with dates or sorting. 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.`
),
query: z
.string()
.trim()
.max(2000)
.default('')
.describe(
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.'
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest where supported; Notion requires search terms.'
),
topK: z
.number()
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/mothership/generated/tool-catalog-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6096,7 +6096,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
},
nativeQueries: {
description:
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). 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.",
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Notion requires nonempty search terms even with dates or sorting. 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.",
minItems: 1,
maxItems: 9,
type: 'array',
Expand Down Expand Up @@ -6140,7 +6140,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
query: {
default: '',
description:
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.',
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest where supported; Notion requires search terms.',
type: 'string',
maxLength: 2000,
},
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/mothership/generated/tool-schemas-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6044,7 +6044,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
nativeQueries: {
description:
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). 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.",
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Notion requires nonempty search terms even with dates or sorting. 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.",
minItems: 1,
maxItems: 9,
type: 'array',
Expand Down Expand Up @@ -6114,7 +6114,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
query: {
default: '',
description:
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.',
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest where supported; Notion requires search terms.',
type: 'string',
maxLength: 2000,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,25 @@ describe('Assistant retrieval tools', () => {
next: null,
})
})
it.each([{ startDate: '2026-09-01T00:00:00Z' }, { sortBy: 'newest' }, { sortBy: 'oldest' }])(
'returns actionable validation for empty Notion native queries with %j',
async (bound) => {
setEnvFlags({ isLiveEnterpriseSearchEnabled: true })
const result = await searchWorkspaceServerTool.execute(
{
...bound,
query: 'fallback terms',
nativeQueries: [{ provider: 'notion', query: ' \t ' }],
},
{ ...context, assistantSearch: undefined }
)
expect(result).toMatchObject({
success: false,
message: 'Notion requires search terms. Add keywords or a concise question.',
})
expect(result).not.toHaveProperty('data')
}
)
it('returns a safe permanent configuration failure instead of empty results or opaque error', async () => {
mocks.search.mockRejectedValue(new EmbeddingConfigurationError())
const result = await searchWorkspaceServerTool.execute({ query: 'policy' }, context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
readDocumentInputSchema,
searchWorkspaceInputSchema,
} from '@/lib/api/contracts/mothership-assistant-tools'
import { getValidationErrorMessage } from '@/lib/api/server/validation'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { EmbeddingConfigurationError } from '@/lib/embeddings/configuration-error'
import { sourceAuthor } from '@/lib/knowledge/search/author'
Expand Down Expand Up @@ -211,7 +212,7 @@ export const searchWorkspaceServerTool: BaseServerTool = {
error instanceof SearchDeadlineError
? error.message
: error instanceof z.ZodError
? 'Invalid search arguments'
? getValidationErrorMessage(error, 'Invalid search arguments')
: messageForCopilotKnowledgeError(error),
}
}
Expand Down
27 changes: 27 additions & 0 deletions apps/sim/lib/sim-search/live/application.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,33 @@ describe('authorized live retrieval', () => {
})
mocks.adminVerify.mockResolvedValue(true)
})
it.each([
{ startDate: '2026-08-01T00:00:00Z' },
{ source: ' notion ', startDate: '2026-08-01T00:00:00Z' },
{ sortBy: 'newest' as const },
{ sortBy: 'oldest' as const },
])('rejects a Notion-only live listing with %j', async (bound) => {
await expect(
searchLiveKnowledge.execute({
principal,
input: { ...input, query: ' \t ', filters: { source: 'notion', ...bound } },
})
).rejects.toMatchObject({
code: 'validation',
message: 'Notion requires search terms. Add keywords or a concise question.',
})
})
it.each([undefined, 'google_drive'])(
'preserves date-only live results with source %s',
async (source) => {
const result = await searchLiveKnowledge.execute({
principal,
input: { ...input, query: '', filters: { source, startDate: '2026-08-01T00:00:00Z' } },
})
expect(result.results.map((row) => decodeLiveReference(row.documentId).id)).toEqual(['doc'])
expect(result.live?.accounts[0]?.status).toBe('ok')
}
)
it('filters admin-token GitLab results through the reader ACL before projection', async () => {
const gitlab = {
...account,
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/lib/sim-search/live/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type LiveSearchAccountStatus,
liveSearchProviderSchema,
type NativeSearchQuery,
NOTION_SEARCH_TERMS_REQUIRED,
nativeSearchQueriesSchema,
workspaceSearchFiltersSchema,
} from '@/lib/api/contracts/mothership-assistant-tools'
Expand Down Expand Up @@ -312,6 +313,8 @@ export const searchLiveKnowledge = defineAuthorizedKnowledgeUseCase({
if (input.filters)
input = { ...input, filters: workspaceSearchFiltersSchema.parse(input.filters) }
const filters = input.filters
if (!queries && filters?.source === 'notion' && !input.query.trim())
throw new OrchestrationError('validation', NOTION_SEARCH_TERMS_REQUIRED)
if (
filters?.startDate &&
filters.endDate &&
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/sim-search/live/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export const LIVE_SEARCH_PROVIDERS = {
transport: 'managed_mcp',
guide: {
syntax:
'Natural-language or plain keyword content search through Notion MCP. Availability depends on the connected account and plan; results are restricted to Notion pages, excluding connected apps.',
'Natural-language or plain keyword content search through Notion MCP. Search terms are required even with dates or sorting. Availability depends on the connected account and plan; results are restricted to Notion pages, excluding connected apps.',
scope:
'project optionally takes a known Notion page URL when the advertised tool supports page scoping. Dates use explicit last-edited timestamps; results without those timestamps cannot satisfy date filters. Read a result for page content.',
example: 'deployment rollback checklist',
Expand Down
Loading