Skip to content

Commit d07eaf5

Browse files
j15zclaude
andcommitted
improvement(copilot): share the unmounted-docs list, shrink the search default
Two places decide what the docs/ corpus is: the manifest generator (what is readable) and the vector search's unscoped filter (what is findable). They each carried their own copy of the excluded-section list. If they drift, a hit in a section that is indexed but not mounted comes back as a chunk the agent cannot then read — dropped as stale, silently shrinking the result set. UNMOUNTED_DOCS_SECTIONS is now the one list both import. search_docs returns 5 chunks by default instead of 10; raise topK when a pass genuinely comes back thin. A truncated docs page now routes to one more fetch instead of two. grep and read cost the same single uncached fetch of the page, so grep is an alternative to a read here, never a step after one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 01c4d70 commit d07eaf5

7 files changed

Lines changed: 55 additions & 23 deletions

File tree

apps/sim/lib/copilot/docs/docs-path.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,28 @@
1515
/** Suffix that marks a section overview page on disk. */
1616
export const DOCS_INDEX_SUFFIX = '/index.mdx'
1717

18+
/**
19+
* Top-level docs sections deliberately left out of the copilot's `docs/` tree.
20+
*
21+
* Two places must agree on this list or the corpus goes subtly wrong: the
22+
* manifest generator (which decides what is readable) and the vector search's
23+
* unscoped filter (which decides what is findable). If search still matched an
24+
* unmounted section, every hit there would be a chunk the agent cannot then
25+
* `read` — dropped as stale, silently shrinking the result set.
26+
*
27+
* Mounting a section later is not uniform work, so plan per section:
28+
* - `academy` is plain mdx under `apps/docs/content/docs/en/academy` and is
29+
* already indexed in `docs_embeddings` — removing it here and regenerating
30+
* the manifest is the whole change.
31+
* - `api-reference` is mostly generated from `apps/docs/openapi.json` at build
32+
* time, so its pages have no source mdx for the generator to walk (only the
33+
* four handwritten ones: authentication, getting-started, python, typescript).
34+
* Mounting it properly needs the spec served publicly again — the
35+
* `apps/docs/app/openapi.json` route existed for exactly this and was
36+
* reverted — plus a generator branch that walks the spec's tags.
37+
*/
38+
export const UNMOUNTED_DOCS_SECTIONS = ['academy', 'api-reference'] as const
39+
1840
/**
1941
* Fold an `en`-relative mdx file path onto its public path — the value used as
2042
* both the `docs/`-relative VFS path and the docs.sim.ai URL path.

apps/sim/lib/copilot/docs/docs-search.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,9 @@ describe('searchDocs topK clamping', () => {
241241
mockGenerateSearchEmbedding.mockResolvedValue({ embedding: [0.1, 0.2] })
242242
})
243243

244-
it('defaults to 10 when unspecified', async () => {
244+
it('defaults to 5 when unspecified', async () => {
245245
await searchDocs('cron')
246-
expect(capturedLimit.value).toBe(10)
246+
expect(capturedLimit.value).toBe(5)
247247
})
248248

249249
it('caps at 25 — the documented max, which the old tool never enforced', async () => {
@@ -266,10 +266,10 @@ describe('searchDocs topK clamping', () => {
266266
it('falls back to the default rather than passing NaN to the query', async () => {
267267
// Math.min/Math.max propagate NaN, so a bare clamp would reach `.limit(NaN)`.
268268
await searchDocs('cron', { topK: Number.NaN })
269-
expect(capturedLimit.value).toBe(10)
269+
expect(capturedLimit.value).toBe(5)
270270
await searchDocs('cron', { topK: 'twelve' as unknown as number })
271-
expect(capturedLimit.value).toBe(10)
271+
expect(capturedLimit.value).toBe(5)
272272
await searchDocs('cron', { topK: Number.POSITIVE_INFINITY })
273-
expect(capturedLimit.value).toBe(10)
273+
expect(capturedLimit.value).toBe(5)
274274
})
275275
})

apps/sim/lib/copilot/docs/docs-search.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { docsEmbeddings } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { and, eq, like, notLike, or, sql } from 'drizzle-orm'
55
import { docsPathForSourceDocument, isDocsDir, isDocsPage } from '@/lib/copilot/docs/docs-corpus'
6-
import { docsSourceCandidates } from '@/lib/copilot/docs/docs-path'
6+
import { docsSourceCandidates, UNMOUNTED_DOCS_SECTIONS } from '@/lib/copilot/docs/docs-path'
77
import { generateSearchEmbedding } from '@/lib/knowledge/embeddings'
88

99
const logger = createLogger('DocsSearch')
1010

1111
const SIMILARITY_THRESHOLD = 0.3
12-
const DEFAULT_TOP_K = 10
12+
const DEFAULT_TOP_K = 5
1313
const MAX_TOP_K = 25
1414

1515
export interface DocsSearchResult {
@@ -58,16 +58,17 @@ export class DocsSearchScopeError extends Error {
5858
* `workflows/index.mdx` (or `workflows.mdx`) on disk. A directory scope covers
5959
* the whole subtree plus the overview in either layout.
6060
*
61-
* Returns undefined for an unscoped search, which excludes `academy/` and
62-
* `api-reference/`: both are indexed but neither is mounted in the VFS, so a hit
63-
* there would be a chunk the agent cannot then read.
61+
* An unscoped search excludes every {@link UNMOUNTED_DOCS_SECTIONS} section:
62+
* they are indexed but not mounted in the VFS, so a hit there would be a chunk
63+
* the agent cannot then read.
6464
*/
6565
function scopeCondition(path?: string) {
6666
const normalized = (path ?? '').trim().replace(/^\/+/, '').replace(/\/+$/, '')
6767
if (normalized === '' || normalized === 'docs') {
6868
return and(
69-
notLike(docsEmbeddings.sourceDocument, 'academy/%'),
70-
notLike(docsEmbeddings.sourceDocument, 'api-reference/%')
69+
...UNMOUNTED_DOCS_SECTIONS.map((section) =>
70+
notLike(docsEmbeddings.sourceDocument, `${section}/%`)
71+
)
7172
)
7273
}
7374

apps/sim/lib/copilot/generated/tool-catalog-v1.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3675,7 +3675,7 @@ export const SearchDocs: ToolCatalogEntry = {
36753675
'Optional docs/ VFS path (a page such as docs/workflows/blocks/agent.mdx, or a section such as docs/workflows) that limits the search scope',
36763676
},
36773677
query: { type: 'string', description: 'The search query' },
3678-
topK: { type: 'number', description: 'Number of results (default 10, max 25)' },
3678+
topK: { type: 'number', description: 'Number of results (default 5, max 25)' },
36793679
},
36803680
required: ['query'],
36813681
},
@@ -3690,7 +3690,7 @@ export const SearchDocumentation: ToolCatalogEntry = {
36903690
type: 'object',
36913691
properties: {
36923692
query: { type: 'string', description: 'The search query' },
3693-
topK: { type: 'number', description: 'Number of results (default 10, max 25)' },
3693+
topK: { type: 'number', description: 'Number of results (default 5, max 25)' },
36943694
},
36953695
required: ['query'],
36963696
},

apps/sim/lib/copilot/generated/tool-schemas-v1.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3459,7 +3459,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
34593459
},
34603460
topK: {
34613461
type: 'number',
3462-
description: 'Number of results (default 10, max 25)',
3462+
description: 'Number of results (default 5, max 25)',
34633463
},
34643464
},
34653465
required: ['query'],
@@ -3476,7 +3476,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
34763476
},
34773477
topK: {
34783478
type: 'number',
3479-
description: 'Number of results (default 10, max 25)',
3479+
description: 'Number of results (default 5, max 25)',
34803480
},
34813481
},
34823482
required: ['query'],

apps/sim/lib/copilot/tools/handlers/vfs.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@ function truncateDocsPageToInlineCap(page: { content: string; totalLines: number
9797
returnedLines: number
9898
} {
9999
const lines = page.content.split('\n')
100+
// Route to ONE more fetch, not two. Telling the model to grep and then read
101+
// costs two more uncached fetches of a page it already partly has; grep and
102+
// read cost the same single fetch, so grep is an alternative to a read here,
103+
// never a step before one.
100104
const notice = (shown: number) =>
101-
`\n\n[Page truncated: showing lines 1-${shown} of ${page.totalLines}. Grep this path for the section you need, then read with offset/limit.]`
105+
`\n\n[Page truncated: returned lines 1-${shown} of ${page.totalLines}. To continue, read this path with offset: ${shown}. To jump straight to a section, grep this path INSTEAD of reading it — grep is the same single fetch and returns only matching lines with their numbers.]`
102106

103107
let kept = lines.length
104108
let content = page.content

scripts/sync-docs-manifest.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
* into their parent URL; `/workflows/index.mdx`
1616
* is a 404 on the site)
1717
*
18-
* Excluded, and intentionally absent from the VFS: `academy/` and
19-
* `api-reference/` (fetch those with the scrape tool if ever needed), the root
20-
* `index.mdx` (its URL is `/`, which redirects), and every non-`en` locale.
18+
* Excluded, and intentionally absent from the VFS: every section in
19+
* `UNMOUNTED_DOCS_SECTIONS` (fetch those with the scrape tool if ever needed),
20+
* the root `index.mdx` (its URL is `/`, which redirects), and every non-`en`
21+
* locale.
2122
*
2223
* Usage:
2324
* bun run docs-manifest:generate # write the manifest
@@ -26,16 +27,20 @@
2627
import { readdir, readFile, writeFile } from 'node:fs/promises'
2728
import { dirname, resolve } from 'node:path'
2829
import { fileURLToPath } from 'node:url'
29-
import { foldDocsIndexPath } from '../apps/sim/lib/copilot/docs/docs-path'
30+
import { foldDocsIndexPath, UNMOUNTED_DOCS_SECTIONS } from '../apps/sim/lib/copilot/docs/docs-path'
3031
import { formatGeneratedSource } from './format-generated-source'
3132

3233
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
3334
const ROOT = resolve(SCRIPT_DIR, '..')
3435
const DOCS_CONTENT_DIR = resolve(ROOT, 'apps/docs/content/docs/en')
3536
const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/docs-manifest.ts')
3637

37-
/** Top-level docs sections deliberately left out of the copilot's `docs/` tree. */
38-
const EXCLUDED_SECTIONS = new Set(['academy', 'api-reference'])
38+
/**
39+
* Top-level docs sections deliberately left out of the copilot's `docs/` tree.
40+
* Shared with the vector search's unscoped filter so readability and
41+
* findability cannot drift apart — see `UNMOUNTED_DOCS_SECTIONS`.
42+
*/
43+
const EXCLUDED_SECTIONS = new Set<string>(UNMOUNTED_DOCS_SECTIONS)
3944

4045
/** Collect every `.mdx` file under `dir`, as paths relative to {@link DOCS_CONTENT_DIR}. */
4146
async function collectMdxPaths(dir: string, prefix = ''): Promise<string[]> {

0 commit comments

Comments
 (0)