Skip to content

Commit 9221551

Browse files
committed
Add date-aware live enterprise search and bounded listings
1 parent c011716 commit 9221551

24 files changed

Lines changed: 1092 additions & 111 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/knowledge-search-results.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function toSource(
7171
connectorType: result.connectorType ?? undefined,
7272
snippet: matchSnippet(result.content, query),
7373
author: result.author ?? undefined,
74-
updatedAt: result.sourceModifiedAt ?? undefined,
74+
updatedAt: result.sourceDate ?? result.sourceModifiedAt ?? undefined,
7575
}
7676
}
7777

‎apps/sim/lib/api/contracts/knowledge/search.ts‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,23 @@ export type WorkspaceKnowledgeSearchLimit =
189189
export const workspaceKnowledgeSearchBodySchema = resourceOwnerSchema
190190
.safeExtend({
191191
filters: workspaceSearchFiltersSchema.optional(),
192-
query: z.string().trim().min(1, 'A search query is required').max(2000, 'Query is too long'),
192+
query: z.string().trim().max(2000, 'Query is too long').default(''),
193193
topK: z.number().int().min(1).max(50).optional().default(20),
194194
})
195195
.superRefine((body, ctx) => {
196-
const { modifiedAfter, modifiedBefore } = body.filters ?? {}
196+
const { modifiedAfter, modifiedBefore, startDate, endDate } = body.filters ?? {}
197+
if (!body.query && !startDate && !endDate && !modifiedAfter && !modifiedBefore)
198+
ctx.addIssue({
199+
code: 'custom',
200+
path: ['query'],
201+
message: 'A search query or date bound is required',
202+
})
203+
if (startDate && endDate && Date.parse(endDate) <= Date.parse(startDate))
204+
ctx.addIssue({
205+
code: 'custom',
206+
path: ['filters', 'endDate'],
207+
message: 'endDate must be after startDate',
208+
})
197209
if (modifiedAfter && modifiedBefore && Date.parse(modifiedBefore) < Date.parse(modifiedAfter)) {
198210
ctx.addIssue({
199211
code: 'custom',

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ describe('Assistant execution contracts', () => {
6767
documentId: 'doc-1',
6868
limit: 3,
6969
})
70-
expect(TOOL_CATALOG.search_workspace?.parameters).toMatchObject({ required: ['query'] })
70+
expect(TOOL_CATALOG.search_workspace?.parameters).not.toHaveProperty('required')
71+
expect(searchWorkspaceInputSchema.parse({ startDate: '2026-09-22T00:00:00Z' })).toMatchObject({
72+
query: '',
73+
topK: 20,
74+
})
7175
expect(TOOL_CATALOG.read_document?.parameters).toMatchObject({ required: ['documentId'] })
7276
})
7377
})

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

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>
1717
export const nativeSearchQuerySchema = z
1818
.object({
1919
provider: liveSearchProviderSchema,
20-
query: z.string().trim().min(1).max(2000),
20+
query: z.string().trim().max(2000),
2121
accountId: z.string().min(1).max(200).optional(),
2222
kind: z.enum(['issues', 'code', 'repositories', 'merge_requests', 'wiki']).optional(),
2323
project: z.string().min(1).max(300).optional(),
@@ -72,6 +72,26 @@ export const liveSearchCoverageSchema = z.object({
7272

7373
/** Connected-source filters are shared by composer search and Assistant retrieval. */
7474
export const workspaceSearchFiltersSchema = z.object({
75+
startDate: z
76+
.string()
77+
.datetime({ offset: true })
78+
.optional()
79+
.describe(
80+
'Live search: inclusive lower date bound. Calendar uses scheduled event start; Gmail/Slack use message time; other sources use modification time. Include the user’s timezone offset.'
81+
),
82+
endDate: z
83+
.string()
84+
.datetime({ offset: true })
85+
.optional()
86+
.describe(
87+
'Live search: exclusive upper bound on the same date as startDate. For a whole day, use the next local midnight.'
88+
),
89+
sortBy: z
90+
.enum(['relevance', 'newest', 'oldest'])
91+
.optional()
92+
.describe(
93+
'Live search ordering by relevance or the provider date used by startDate/endDate. Date sorting covers retrieved results; inspect partial coverage before claiming latest or earliest overall.'
94+
),
7595
source: z
7696
.string()
7797
.trim()
@@ -99,28 +119,67 @@ export const workspaceSearchFiltersSchema = z.object({
99119
),
100120
})
101121

102-
export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema.extend({
103-
nativeQueries: nativeSearchQueriesSchema
104-
.optional()
105-
.describe(
106-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
107-
),
108-
query: z
109-
.string()
110-
.trim()
111-
.min(1)
112-
.max(2000)
113-
.describe('Search query describing the information needed.'),
114-
topK: z
115-
.number()
116-
.int()
117-
.min(1)
118-
.max(50)
119-
.default(20)
120-
.describe(
121-
'Maximum matching passage previews; retrieval ranking is independent of preview length.'
122-
),
123-
})
122+
export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
123+
.extend({
124+
nativeQueries: nativeSearchQueriesSchema
125+
.optional()
126+
.describe(
127+
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
128+
),
129+
query: z
130+
.string()
131+
.trim()
132+
.max(2000)
133+
.default('')
134+
.describe(
135+
'Search terms, without dates already supplied as filters. May be empty for a live date-bounded listing.'
136+
),
137+
topK: z
138+
.number()
139+
.int()
140+
.min(1)
141+
.max(50)
142+
.default(20)
143+
.describe(
144+
'Maximum matching passage previews; retrieval ranking is independent of preview length.'
145+
),
146+
})
147+
.superRefine((input, context) => {
148+
if (
149+
!input.query &&
150+
!input.startDate &&
151+
!input.endDate &&
152+
!input.modifiedAfter &&
153+
!input.modifiedBefore
154+
)
155+
context.addIssue({
156+
code: 'custom',
157+
path: ['query'],
158+
message: 'Supply search terms or a date bound.',
159+
})
160+
if (
161+
input.startDate &&
162+
input.endDate &&
163+
Date.parse(input.startDate) >= Date.parse(input.endDate)
164+
)
165+
context.addIssue({
166+
code: 'custom',
167+
path: ['endDate'],
168+
message: 'endDate must be after startDate.',
169+
})
170+
if (
171+
input.nativeQueries?.some((query) => !query.query) &&
172+
!input.startDate &&
173+
!input.endDate &&
174+
!input.modifiedAfter &&
175+
!input.modifiedBefore
176+
)
177+
context.addIssue({
178+
code: 'custom',
179+
path: ['nativeQueries'],
180+
message: 'Empty native queries require a date bound.',
181+
})
182+
})
124183

125184
export const readDocumentInputSchema = z.object({
126185
documentId: z
@@ -261,6 +320,8 @@ export const workspaceKnowledgeSearchResultSchema = z.object({
261320
sourceUrl: z.string().nullable(),
262321
connectorType: z.string().nullable(),
263322
sourceModifiedAt: z.string().nullable(),
323+
sourceDate: z.string().nullable().optional(),
324+
sourceDateType: z.enum(['event_start', 'message', 'modified']).optional(),
264325
/** The person behind the document, from its author-like tag; null when the source names none. */
265326
author: z.string().nullable(),
266327
content: z.string(),

‎apps/sim/lib/knowledge/application/workspace-search.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ type ScopedSearchInput = Omit<SearchKnowledgeInput, 'knowledgeBaseIds'>
9595
* index is read twice. Surfaces differ only in how they name the owner and find the index.
9696
*/
9797
function defineScopedSearchUseCase<
98-
I extends Pick<SearchKnowledgeInput, 'query' | 'surface' | 'signal'>,
98+
I extends Pick<SearchKnowledgeInput, 'query' | 'surface' | 'signal' | 'filters'>,
9999
>(surface: {
100100
resolveContext: (input: I) => Promise<KnowledgeResourceContext>
101101
findIndex: (context: KnowledgeResourceContext) => Promise<ActiveKnowledgeBaseReference | null>
@@ -107,6 +107,16 @@ function defineScopedSearchUseCase<
107107
measureSearchStage('scope_resolution', () => surface.resolveContext(input)),
108108
async execute({ principal, input, context }): Promise<ScopedSearchResult> {
109109
input.signal?.throwIfAborted()
110+
if (
111+
!input.query?.trim() ||
112+
input.filters?.startDate ||
113+
input.filters?.endDate ||
114+
input.filters?.sortBy
115+
)
116+
throw new OrchestrationError(
117+
'validation',
118+
'Date-only search, startDate/endDate and sorting require live search. Use a text query and modification filters for indexed search.'
119+
)
110120
const index = await measureSearchStage('index_resolution', () => surface.findIndex(context))
111121
if (!index) return searchWithoutIndex(principal, context, input)
112122
const searchInput: SearchKnowledgeInput = {

‎apps/sim/lib/knowledge/search/filters.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { OrchestrationError } from '@/lib/core/orchestration/types'
22

33
/** Document constraints shared by Search and Assistant, applied before ranking. */
44
export interface WorkspaceSearchFilters {
5+
startDate?: string
6+
endDate?: string
7+
sortBy?: 'relevance' | 'newest' | 'oldest'
58
source?: string
69
modifiedAfter?: string
710
modifiedBefore?: string
@@ -36,7 +39,21 @@ export function intersectWorkspaceSearchFilters(
3639
if (modifiedAfter && modifiedBefore && Date.parse(modifiedBefore) < Date.parse(modifiedAfter)) {
3740
throw new OrchestrationError('validation', 'The requested dates are outside this search')
3841
}
42+
const startDate = [requested.startDate, scope.startDate]
43+
.filter((value): value is string => Boolean(value))
44+
.sort((a, b) => Date.parse(a) - Date.parse(b))
45+
.at(-1)
46+
const endDate = [requested.endDate, scope.endDate]
47+
.filter((value): value is string => Boolean(value))
48+
.sort((a, b) => Date.parse(a) - Date.parse(b))
49+
.at(0)
50+
if (startDate && endDate && Date.parse(endDate) <= Date.parse(startDate))
51+
throw new OrchestrationError('validation', 'The requested dates are outside this search')
52+
const sortBy = requested.sortBy ?? scope.sortBy
3953
return {
54+
...(startDate ? { startDate } : {}),
55+
...(endDate ? { endDate } : {}),
56+
...(sortBy ? { sortBy } : {}),
4057
...(scope.source || requested.source ? { source: scope.source ?? requested.source } : {}),
4158
...(modifiedAfter ? { modifiedAfter } : {}),
4259
...(modifiedBefore ? { modifiedBefore } : {}),

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

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>
2020
export const nativeSearchQuerySchema = z
2121
.object({
2222
provider: liveSearchProviderSchema,
23-
query: z.string().trim().min(1).max(2000),
23+
query: z.string().trim().max(2000),
2424
accountId: z.string().min(1).max(200).optional(),
2525
kind: z.enum(['issues', 'code', 'repositories', 'merge_requests', 'wiki']).optional(),
2626
project: z.string().min(1).max(300).optional(),
@@ -75,6 +75,26 @@ export const liveSearchCoverageSchema = z.object({
7575

7676
/** Connected-source filters are shared by composer search and Assistant retrieval. */
7777
export const workspaceSearchFiltersSchema = z.object({
78+
startDate: z
79+
.string()
80+
.datetime({ offset: true })
81+
.optional()
82+
.describe(
83+
'Live search: inclusive lower date bound. Calendar uses scheduled event start; Gmail/Slack use message time; other sources use modification time. Include the user’s timezone offset.'
84+
),
85+
endDate: z
86+
.string()
87+
.datetime({ offset: true })
88+
.optional()
89+
.describe(
90+
'Live search: exclusive upper bound on the same date as startDate. For a whole day, use the next local midnight.'
91+
),
92+
sortBy: z
93+
.enum(['relevance', 'newest', 'oldest'])
94+
.optional()
95+
.describe(
96+
'Live search ordering by relevance or the provider date used by startDate/endDate. Date sorting covers retrieved results; inspect partial coverage before claiming latest or earliest overall.'
97+
),
7898
source: z
7999
.string()
80100
.trim()
@@ -102,28 +122,67 @@ export const workspaceSearchFiltersSchema = z.object({
102122
),
103123
})
104124

105-
export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema.extend({
106-
nativeQueries: nativeSearchQueriesSchema
107-
.optional()
108-
.describe(
109-
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
110-
),
111-
query: z
112-
.string()
113-
.trim()
114-
.min(1)
115-
.max(2000)
116-
.describe('Search query describing the information needed.'),
117-
topK: z
118-
.number()
119-
.int()
120-
.min(1)
121-
.max(50)
122-
.default(20)
123-
.describe(
124-
'Maximum matching passage previews; retrieval ranking is independent of preview length.'
125-
),
126-
})
125+
export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
126+
.extend({
127+
nativeQueries: nativeSearchQueriesSchema
128+
.optional()
129+
.describe(
130+
'Live search only: provider-native queries (Drive q, Gmail operators, Jira JQL, Confluence CQL, GitHub qualifiers, Slack RTS). Omit for simple cross-provider terms. Use the returned live guidance and account IDs.'
131+
),
132+
query: z
133+
.string()
134+
.trim()
135+
.max(2000)
136+
.default('')
137+
.describe(
138+
'Search terms, without dates already supplied as filters. May be empty for a live date-bounded listing.'
139+
),
140+
topK: z
141+
.number()
142+
.int()
143+
.min(1)
144+
.max(50)
145+
.default(20)
146+
.describe(
147+
'Maximum matching passage previews; retrieval ranking is independent of preview length.'
148+
),
149+
})
150+
.superRefine((input, context) => {
151+
if (
152+
!input.query &&
153+
!input.startDate &&
154+
!input.endDate &&
155+
!input.modifiedAfter &&
156+
!input.modifiedBefore
157+
)
158+
context.addIssue({
159+
code: 'custom',
160+
path: ['query'],
161+
message: 'Supply search terms or a date bound.',
162+
})
163+
if (
164+
input.startDate &&
165+
input.endDate &&
166+
Date.parse(input.startDate) >= Date.parse(input.endDate)
167+
)
168+
context.addIssue({
169+
code: 'custom',
170+
path: ['endDate'],
171+
message: 'endDate must be after startDate.',
172+
})
173+
if (
174+
input.nativeQueries?.some((query) => !query.query) &&
175+
!input.startDate &&
176+
!input.endDate &&
177+
!input.modifiedAfter &&
178+
!input.modifiedBefore
179+
)
180+
context.addIssue({
181+
code: 'custom',
182+
path: ['nativeQueries'],
183+
message: 'Empty native queries require a date bound.',
184+
})
185+
})
127186

128187
export const readDocumentInputSchema = z.object({
129188
documentId: z
@@ -264,6 +323,8 @@ export const workspaceKnowledgeSearchResultSchema = z.object({
264323
sourceUrl: z.string().nullable(),
265324
connectorType: z.string().nullable(),
266325
sourceModifiedAt: z.string().nullable(),
326+
sourceDate: z.string().nullable().optional(),
327+
sourceDateType: z.enum(['event_start', 'message', 'modified']).optional(),
267328
/** The person behind the document, from its author-like tag; null when the source names none. */
268329
author: z.string().nullable(),
269330
content: z.string(),

0 commit comments

Comments
 (0)