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
11 changes: 11 additions & 0 deletions apps/sim/lib/api/contracts/knowledge/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,15 @@ describe('workspaceKnowledgeSearchBodySchema', () => {
)
expect(workspaceKnowledgeSearchBodySchema.safeParse(body).success).toBe(false)
})
it('accepts a newest- or oldest-first listing without terms, as the Assistant contract does', () => {
const body = { workspaceId: 'workspace-1', query: '' }
for (const sortBy of ['newest', 'oldest'] as const)
expect(
workspaceKnowledgeSearchBodySchema.safeParse({ ...body, filters: { sortBy } }).success
).toBe(true)
expect(
workspaceKnowledgeSearchBodySchema.safeParse({ ...body, filters: { sortBy: 'relevance' } })
.success
).toBe(false)
})
})
8 changes: 5 additions & 3 deletions apps/sim/lib/api/contracts/knowledge/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,19 +195,21 @@ export const workspaceKnowledgeSearchBodySchema = resourceOwnerSchema
nativeQueries: nativeSearchQueriesSchema.optional(),
})
.superRefine((body, ctx) => {
const { modifiedAfter, modifiedBefore, startDate, endDate } = body.filters ?? {}
const { modifiedAfter, modifiedBefore, startDate, endDate, sortBy } = body.filters ?? {}
if (
!body.query &&
!body.nativeQueries?.some((query) => query.query) &&
!startDate &&
!endDate &&
!modifiedAfter &&
!modifiedBefore
!modifiedBefore &&
sortBy !== 'newest' &&
sortBy !== 'oldest'
)
ctx.addIssue({
code: 'custom',
path: ['query'],
message: 'A search query, native query, or date bound is required',
message: 'A search query, native query, date bound, or newest or oldest sort is required',
})
if (startDate && endDate && Date.parse(endDate) <= Date.parse(startDate))
ctx.addIssue({
Expand Down
31 changes: 13 additions & 18 deletions apps/sim/lib/api/contracts/mothership-assistant-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export const workspaceSearchFiltersSchema = z.object({
.enum(['relevance', 'newest', 'oldest'])
.optional()
.describe(
'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.'
'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. Without search terms or dates, newest or oldest lists items up to now.'
),
source: z
.string()
Expand Down Expand Up @@ -169,7 +169,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
.max(2000)
.default('')
.describe(
'Search terms, without dates already supplied as filters. May be empty for a live date-bounded listing.'
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.'
),
topK: z
.number()
Expand All @@ -182,18 +182,19 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
),
})
.superRefine((input, context) => {
if (
!input.query &&
!input.nativeQueries?.some((query) => query.query) &&
!input.startDate &&
!input.endDate &&
!input.modifiedAfter &&
!input.modifiedBefore
const bounded = Boolean(
input.startDate ||
input.endDate ||
input.modifiedAfter ||
input.modifiedBefore ||
input.sortBy === 'newest' ||
input.sortBy === 'oldest'
)
if (!input.query && !input.nativeQueries?.some((query) => query.query) && !bounded)
context.addIssue({
code: 'custom',
path: ['query'],
message: 'Supply search terms, a native query, or a date bound.',
message: 'Supply search terms, a native query, a date bound, or sortBy newest or oldest.',
})
if (
input.startDate &&
Expand All @@ -205,17 +206,11 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
path: ['endDate'],
message: 'endDate must be after startDate.',
})
if (
input.nativeQueries?.some((query) => !query.query) &&
!input.startDate &&
!input.endDate &&
!input.modifiedAfter &&
!input.modifiedBefore
)
if (input.nativeQueries?.some((query) => !query.query) && !bounded)
context.addIssue({
code: 'custom',
path: ['nativeQueries'],
message: 'Empty native queries require a date bound.',
message: 'Empty native queries require a date bound or sortBy newest or oldest.',
})
})

Expand Down
31 changes: 13 additions & 18 deletions apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export const workspaceSearchFiltersSchema = z.object({
.enum(['relevance', 'newest', 'oldest'])
.optional()
.describe(
'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.'
'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. Without search terms or dates, newest or oldest lists items up to now.'
),
source: z
.string()
Expand Down Expand Up @@ -181,7 +181,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
.max(2000)
.default('')
.describe(
'Search terms, without dates already supplied as filters. May be empty for a live date-bounded listing.'
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.'
),
topK: z
.number()
Expand All @@ -194,18 +194,19 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
),
})
.superRefine((input, context) => {
if (
!input.query &&
!input.nativeQueries?.some((query) => query.query) &&
!input.startDate &&
!input.endDate &&
!input.modifiedAfter &&
!input.modifiedBefore
const bounded = Boolean(
input.startDate ||
input.endDate ||
input.modifiedAfter ||
input.modifiedBefore ||
input.sortBy === 'newest' ||
input.sortBy === 'oldest'
)
if (!input.query && !input.nativeQueries?.some((query) => query.query) && !bounded)
context.addIssue({
code: 'custom',
path: ['query'],
message: 'Supply search terms, a native query, or a date bound.',
message: 'Supply search terms, a native query, a date bound, or sortBy newest or oldest.',
})
if (
input.startDate &&
Expand All @@ -217,17 +218,11 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
path: ['endDate'],
message: 'endDate must be after startDate.',
})
if (
input.nativeQueries?.some((query) => !query.query) &&
!input.startDate &&
!input.endDate &&
!input.modifiedAfter &&
!input.modifiedBefore
)
if (input.nativeQueries?.some((query) => !query.query) && !bounded)
context.addIssue({
code: 'custom',
path: ['nativeQueries'],
message: 'Empty native queries require a date bound.',
message: 'Empty native queries require a date bound or sortBy newest or oldest.',
})
})

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 @@ -6021,7 +6021,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
},
sortBy: {
description:
'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.',
'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. Without search terms or dates, newest or oldest lists items up to now.',
type: 'string',
enum: ['relevance', 'newest', 'oldest'],
},
Expand Down Expand Up @@ -6095,7 +6095,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
query: {
default: '',
description:
'Search terms, without dates already supplied as filters. May be empty for a live date-bounded listing.',
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.',
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 @@ -5948,7 +5948,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
sortBy: {
description:
'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.',
'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. Without search terms or dates, newest or oldest lists items up to now.',
type: 'string',
enum: ['relevance', 'newest', 'oldest'],
},
Expand Down Expand Up @@ -6052,7 +6052,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 date-bounded listing.',
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.',
type: 'string',
maxLength: 2000,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const searchWorkspaceServerTool: BaseServerTool = {
})
return {
success: true,
message: `Found ${data.results.length} live results. Read documentIds for more content. ${CITATION_INSTRUCTION}`,
message: `Found ${data.results.length} live results. Read a documentId when its passage does not answer the question or more context is needed. ${CITATION_INSTRUCTION}`,
data: {
...data,
results: data.results.map((item) => ({
Expand Down
30 changes: 30 additions & 0 deletions apps/sim/lib/sim-search/live/application.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,36 @@ describe('authorized live retrieval', () => {
})
expect(result.retrieval.status).toBe('partial')
})
it('lists newest first up to now when no terms or dates are given, and only then', async () => {
vi.useFakeTimers({ now: new Date('2026-09-25T02:00:00Z'), toFake: ['Date'] })
try {
await searchLiveKnowledge.execute({
principal,
input: { ...input, query: '', filters: { sortBy: 'newest' } },
})
expect(mocks.search).toHaveBeenLastCalledWith(
'google_drive',
expect.anything(),
expect.objectContaining({
filters: { sortBy: 'newest', endDate: '2026-09-25T02:00:00.000Z' },
})
)
await searchLiveKnowledge.execute({
principal,
input: { ...input, filters: { sortBy: 'newest' } },
})
expect(mocks.search).toHaveBeenLastCalledWith(
'google_drive',
expect.anything(),
expect.objectContaining({ filters: { sortBy: 'newest' } })
)
await expect(
searchLiveKnowledge.execute({ principal, input: { ...input, query: '' } })
).rejects.toThrow('Invalid live search query')
} finally {
vi.useRealTimers()
}
})
it('reports candidates that could not be verified and keeps the verified ones', async () => {
mocks.search.mockResolvedValue({
documents: [document, { ...document, id: 'other', url: 'https://docs.google.com/other' }],
Expand Down
6 changes: 6 additions & 0 deletions apps/sim/lib/sim-search/live/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
matchesSourceDates,
sourceDate,
sourceDateType,
withImpliedListingBound,
} from '@/lib/sim-search/live/dates'
import { NativeSearchError } from '@/lib/sim-search/live/http'
import { joinMessages } from '@/lib/sim-search/live/pages'
Expand Down Expand Up @@ -293,6 +294,11 @@ export const searchLiveKnowledge = defineAuthorizedKnowledgeUseCase({
const queries = input.nativeQueries
? nativeSearchQueriesSchema.parse(input.nativeQueries)
: undefined
if (
(!input.query.trim() && !queries?.some((query) => query.query)) ||
queries?.some((query) => !query.query)
)
input = { ...input, filters: withImpliedListingBound(input.filters, new Date()) }
Comment thread
waleedlatif1 marked this conversation as resolved.
if (
(!input.query.trim() &&
!hasDateBounds(input.filters) &&
Expand Down
31 changes: 29 additions & 2 deletions apps/sim/lib/sim-search/live/dates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { searchWorkspaceInputSchema } from '@/lib/api/contracts/mothership-assis
import { intersectWorkspaceSearchFilters } from '@/lib/knowledge/search/filters'
import { searchAtlassian } from '@/lib/sim-search/live/atlassian'
import { searchCoda } from '@/lib/sim-search/live/coda'
import { matchesSourceDates, sourceDate, sourceDateType } from '@/lib/sim-search/live/dates'
import {
matchesSourceDates,
sourceDate,
sourceDateType,
withImpliedListingBound,
} from '@/lib/sim-search/live/dates'
import { searchGitHub } from '@/lib/sim-search/live/github'
import { searchGitLab } from '@/lib/sim-search/live/gitlab'
import { searchCalendar, searchDrive, searchGmail } from '@/lib/sim-search/live/google'
Expand All @@ -29,6 +34,28 @@ const doc: NativeDocument = {
}

describe('generic live search dates', () => {
it('lists newest or oldest first up to now when no terms or dates are given', () => {
const now = new Date('2026-09-25T02:00:00Z')
for (const sortBy of ['newest', 'oldest'] as const) {
expect(searchWorkspaceInputSchema.safeParse({ sortBy }).success).toBe(true)
expect(
searchWorkspaceInputSchema.safeParse({
sortBy,
nativeQueries: [{ provider: 'slack', query: '', modifiers: 'in:<#D1>' }],
}).success
).toBe(true)
expect(withImpliedListingBound({ sortBy }, now)).toEqual({
sortBy,
endDate: '2026-09-25T02:00:00.000Z',
})
}
expect(searchWorkspaceInputSchema.safeParse({ sortBy: 'relevance' }).success).toBe(false)
expect(withImpliedListingBound({ sortBy: 'relevance' }, now)).toEqual({ sortBy: 'relevance' })
expect(withImpliedListingBound(undefined, now)).toBeUndefined()
expect(
withImpliedListingBound({ sortBy: 'newest', startDate: filters.startDate }, now)
).toEqual({ sortBy: 'newest', startDate: filters.startDate })
})
it('accepts a date-only request and rejects invalid or unbounded listings', () => {
expect(searchWorkspaceInputSchema.parse(filters)).toMatchObject({
...filters,
Expand All @@ -38,7 +65,7 @@ describe('generic live search dates', () => {
for (const value of [
{},
{ query: '' },
{ sortBy: 'oldest' },
{ sortBy: 'relevance' },
{ startDate: 'today' },
{ ...filters, endDate: filters.startDate },
{ ...filters, endDate: '2026-09-21T00:00:00Z' },
Expand Down
13 changes: 13 additions & 0 deletions apps/sim/lib/sim-search/live/dates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ export function dateSortDirection(filters?: WorkspaceSearchFilters): 'asc' | 'de
return filters.sortBy === 'oldest' ? 'asc' : 'desc'
}

/**
* A newest- or oldest-first listing without terms or dates lists items up to now, the default
* Slack's conversations.history applies to an omitted `latest`. Providers need a bound to list
* without search terms, so the implied one is made explicit and checked like any other.
*/
export function withImpliedListingBound(
filters: WorkspaceSearchFilters | undefined,
now: Date
): WorkspaceSearchFilters | undefined {
if (!dateSortDirection(filters) || hasDateBounds(filters)) return filters
return { ...filters, endDate: now.toISOString() }
}

/** Native bounds may be widened for provider precision; returned metadata is checked exactly. */
export function nativeDateBounds(input: NativeSearchInput): { start?: string; end?: string } {
const filters = input.filters
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/lib/sim-search/live/providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,14 +567,16 @@ describe('native search endpoints', () => {
has_more: true,
})
.mockResolvedValueOnce({ ok: true, permalink: 'https://team.slack.com/archives/C1/p123456' })
.mockResolvedValueOnce({ ok: true, user: { profile: { display_name: 'Sid' } } })
const result = await readSlack(api, '123.456', 'C1')
expect(api.json.mock.calls[0]).toEqual([
'/api/conversations.replies',
{ query: { channel: 'C1', ts: '123.456', limit: '100' } },
])
expect(result.content).toContain('Reply evidence')
expect(api.json.mock.calls[2]).toEqual(['/api/users.info', { query: { user: 'U1' } }])
expect(result.content).toContain('Sid: Reply evidence')
expect(result.content).toContain('Thread continues')
expect(api.json).toHaveBeenCalledTimes(2)
expect(api.json).toHaveBeenCalledTimes(3)
})
it('applies Slack modifiers and date bounds without requiring term clauses', async () => {
const api = client()
Expand Down
Loading
Loading