Skip to content

Commit ea7c26d

Browse files
j15zclaude
andcommitted
feat(copilot): expose KB tag definitions in VFS meta.json
Surface each knowledge base's defined tags (displayName -> tagSlot) inline in its meta.json via serializeKBMeta, loaded in one batched query (loadKbTagDefinitions), so the agent can bind a knowledge-tag filter to a real tag slot instead of guessing a tag name it cannot otherwise see. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 201b55a commit ea7c26d

3 files changed

Lines changed: 96 additions & 2 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { serializeKBMeta } from '@/lib/copilot/vfs/serializers'
6+
7+
describe('serializeKBMeta', () => {
8+
const baseKb = {
9+
id: 'kb-1',
10+
name: 'Support Docs',
11+
description: null,
12+
embeddingModel: 'text-embedding-3-small',
13+
embeddingDimension: 1536,
14+
tokenCount: 42,
15+
createdAt: new Date('2026-01-01T00:00:00.000Z'),
16+
updatedAt: new Date('2026-01-02T00:00:00.000Z'),
17+
documentCount: 3,
18+
}
19+
20+
it('includes tag definitions when present', () => {
21+
const json = JSON.parse(
22+
serializeKBMeta({
23+
...baseKb,
24+
tagDefinitions: [
25+
{ displayName: 'Important', tagSlot: 'tag1', fieldType: 'text' },
26+
{ displayName: 'Department', tagSlot: 'tag2', fieldType: 'text' },
27+
],
28+
})
29+
)
30+
31+
expect(json.tagDefinitions).toEqual([
32+
{ displayName: 'Important', tagSlot: 'tag1', fieldType: 'text' },
33+
{ displayName: 'Department', tagSlot: 'tag2', fieldType: 'text' },
34+
])
35+
})
36+
37+
it('omits tag definitions when empty or undefined', () => {
38+
const empty = JSON.parse(serializeKBMeta({ ...baseKb, tagDefinitions: [] }))
39+
const missing = JSON.parse(serializeKBMeta(baseKb))
40+
41+
expect(empty).not.toHaveProperty('tagDefinitions')
42+
expect(missing).not.toHaveProperty('tagDefinitions')
43+
})
44+
})

apps/sim/lib/copilot/vfs/serializers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ export function serializeRecentExecutions(
8484
}
8585

8686
/**
87-
* Serialize knowledge base metadata for VFS meta.json
87+
* Serialize knowledge base metadata for VFS meta.json.
88+
*
89+
* `tagDefinitions` exposes the KB's defined tags (`displayName` → `tagSlot`) so the
90+
* agent can select and bind a knowledge-tag filter correctly instead of guessing a
91+
* tag name it cannot otherwise see.
8892
*/
8993
export function serializeKBMeta(kb: {
9094
id: string
@@ -97,6 +101,7 @@ export function serializeKBMeta(kb: {
97101
updatedAt: Date
98102
documentCount: number
99103
connectorTypes?: string[]
104+
tagDefinitions?: Array<{ displayName: string; tagSlot: string; fieldType: string }>
100105
}): string {
101106
return JSON.stringify(
102107
{
@@ -109,6 +114,8 @@ export function serializeKBMeta(kb: {
109114
documentCount: kb.documentCount,
110115
connectorTypes:
111116
kb.connectorTypes && kb.connectorTypes.length > 0 ? kb.connectorTypes : undefined,
117+
tagDefinitions:
118+
kb.tagDefinitions && kb.tagDefinitions.length > 0 ? kb.tagDefinitions : undefined,
112119
createdAt: kb.createdAt.toISOString(),
113120
updatedAt: kb.updatedAt.toISOString(),
114121
},

apps/sim/lib/copilot/vfs/workspace-vfs.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
customTools as customToolsTable,
77
document,
88
jobExecutionLogs,
9+
knowledgeBaseTagDefinitions,
910
knowledgeConnector,
1011
mcpServers as mcpServersTable,
1112
skill as skillTable,
@@ -18,7 +19,7 @@ import {
1819
} from '@sim/db/schema'
1920
import { createLogger } from '@sim/logger'
2021
import { toError } from '@sim/utils/errors'
21-
import { and, desc, eq, isNotNull, isNull, ne, or, sql } from 'drizzle-orm'
22+
import { and, desc, eq, inArray, isNotNull, isNull, ne, or, sql } from 'drizzle-orm'
2223
import { listApiKeys } from '@/lib/api-key/service'
2324
import {
2425
buildWorkspaceContextMd,
@@ -1455,6 +1456,8 @@ export class WorkspaceVFS {
14551456
): Promise<WorkspaceMdData['knowledgeBases']> {
14561457
const kbs = await getKnowledgeBases(userId, workspaceId)
14571458

1459+
const tagDefinitionsByKb = await this.loadKbTagDefinitions(kbs.map((kb) => kb.id))
1460+
14581461
await Promise.all(
14591462
kbs.map(async (kb) => {
14601463
const safeName = sanitizeName(kb.name)
@@ -1473,6 +1476,7 @@ export class WorkspaceVFS {
14731476
updatedAt: kb.updatedAt,
14741477
documentCount: kb.docCount,
14751478
connectorTypes: kb.connectorTypes,
1479+
tagDefinitions: tagDefinitionsByKb.get(kb.id),
14761480
})
14771481
)
14781482

@@ -1544,6 +1548,45 @@ export class WorkspaceVFS {
15441548
}))
15451549
}
15461550

1551+
/**
1552+
* Load tag definitions for the given knowledge bases in a single query, grouped by
1553+
* KB id and ordered by tag slot. Surfaced inline in each KB's meta.json so the agent
1554+
* knows which tags exist (and their slot binding) when editing a knowledge-tag filter.
1555+
*/
1556+
private async loadKbTagDefinitions(
1557+
kbIds: string[]
1558+
): Promise<Map<string, Array<{ displayName: string; tagSlot: string; fieldType: string }>>> {
1559+
const byKb = new Map<
1560+
string,
1561+
Array<{ displayName: string; tagSlot: string; fieldType: string }>
1562+
>()
1563+
if (kbIds.length === 0) return byKb
1564+
1565+
const rows = await db
1566+
.select({
1567+
knowledgeBaseId: knowledgeBaseTagDefinitions.knowledgeBaseId,
1568+
tagSlot: knowledgeBaseTagDefinitions.tagSlot,
1569+
displayName: knowledgeBaseTagDefinitions.displayName,
1570+
fieldType: knowledgeBaseTagDefinitions.fieldType,
1571+
})
1572+
.from(knowledgeBaseTagDefinitions)
1573+
.where(inArray(knowledgeBaseTagDefinitions.knowledgeBaseId, kbIds))
1574+
.orderBy(knowledgeBaseTagDefinitions.tagSlot)
1575+
1576+
for (const row of rows) {
1577+
const entry = {
1578+
displayName: row.displayName,
1579+
tagSlot: row.tagSlot,
1580+
fieldType: row.fieldType,
1581+
}
1582+
const existing = byKb.get(row.knowledgeBaseId)
1583+
if (existing) existing.push(entry)
1584+
else byKb.set(row.knowledgeBaseId, [entry])
1585+
}
1586+
1587+
return byKb
1588+
}
1589+
15471590
/**
15481591
* Materialize tables using the shared listTables function.
15491592
* Returns a summary for WORKSPACE.md generation.

0 commit comments

Comments
 (0)