Skip to content
Merged
Prev Previous commit
Next Next commit
feat(copilot): specify tag name and legal operators in KB meta.json
`tagDefinitions` exposed `displayName`, but a `tagFilters` entry must carry the
key `tagName`. An entry written with `displayName` passes validation and
persists, then filters nothing -- a silent failure. Rename the field at the
serializer boundary; the DB column is untouched.

Also emit the operators legal for each tag's `fieldType`, reusing
`getOperatorsForFieldType`. `between` is valid for number and date but not for
text or boolean, and the agent has no way to infer that. An unrecognized
fieldType yields an empty list rather than throwing.

Still unspecified, and deliberately out of scope: a filter entry's value key is
`tagValue` (but `value` on documentTags), and `between` needs `valueTo`. Those
describe the subblock entry shape, not the knowledge base, so meta.json is the
wrong place for them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  • Loading branch information
j15z and claude committed Jul 10, 2026
commit 7e69f8d9c99e279e2132a6b9aee1b33662a957ce
36 changes: 32 additions & 4 deletions apps/sim/lib/copilot/vfs/serializers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,46 @@ describe('serializeKBMeta', () => {
serializeKBMeta({
...baseKb,
tagDefinitions: [
{ displayName: 'Important', tagSlot: 'tag1', fieldType: 'text' },
{ displayName: 'Department', tagSlot: 'tag2', fieldType: 'text' },
{ tagName: 'Important', tagSlot: 'tag1', fieldType: 'text' },
{ tagName: 'Department', tagSlot: 'tag2', fieldType: 'text' },
],
})
)

const textOperators = ['eq', 'neq', 'contains', 'not_contains', 'starts_with', 'ends_with']
expect(json.tagDefinitions).toEqual([
{ displayName: 'Important', tagSlot: 'tag1', fieldType: 'text' },
{ displayName: 'Department', tagSlot: 'tag2', fieldType: 'text' },
{ tagName: 'Important', tagSlot: 'tag1', fieldType: 'text', operators: textOperators },
{ tagName: 'Department', tagSlot: 'tag2', fieldType: 'text', operators: textOperators },
])
})

// `between` is legal for number/date but not text/boolean -- the agent cannot infer this.
it.each([
['number', ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between']],
['date', ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between']],
['boolean', ['eq', 'neq']],
])('exposes the operators legal for a %s tag', (fieldType, expected) => {
const json = JSON.parse(
serializeKBMeta({
...baseKb,
tagDefinitions: [{ tagName: 'Tag', tagSlot: 'tag1', fieldType }],
})
)

expect(json.tagDefinitions[0].operators).toEqual(expected)
})

it('emits an empty operator list for an unrecognized field type rather than throwing', () => {
const json = JSON.parse(
serializeKBMeta({
...baseKb,
tagDefinitions: [{ tagName: 'Tag', tagSlot: 'tag1', fieldType: 'mystery' }],
})
)

expect(json.tagDefinitions[0].operators).toEqual([])
})

it('omits tag definitions when empty or undefined', () => {
const empty = JSON.parse(serializeKBMeta({ ...baseKb, tagDefinitions: [] }))
const missing = JSON.parse(serializeKBMeta(baseKb))
Expand Down
33 changes: 26 additions & 7 deletions apps/sim/lib/copilot/vfs/serializers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions'
import { isHosted } from '@/lib/core/config/env-flags'
import type { TagDefinition } from '@/lib/knowledge/types'
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
import { isSubBlockHidden } from '@/lib/workflows/subblocks/visibility'
import { isCustomBlockType } from '@/blocks/custom/build-config'
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
Expand Down Expand Up @@ -84,15 +84,27 @@ export function serializeRecentExecutions(
)
}

/** A knowledge base tag definition, reduced to the fields the agent needs to bind a tag filter. */
export type KbTagDefinitionSummary = Pick<TagDefinition, 'displayName' | 'tagSlot' | 'fieldType'>
/**
* A knowledge base tag definition, reduced to the fields the agent needs to bind a tag filter.
*
* @remarks
* `tagName` is the DB's `displayName`. It is renamed at this boundary because that is the key
* a `tagFilters` entry must carry -- an entry written with `displayName` validates and persists
* but never filters anything.
*/
export interface KbTagDefinitionSummary {
tagName: string
tagSlot: string
fieldType: string
}

/**
* Serialize knowledge base metadata for VFS meta.json.
*
* `tagDefinitions` exposes the KB's defined tags (`displayName` → `tagSlot`) so the
* agent can select and bind a knowledge-tag filter correctly instead of guessing a
* tag name it cannot otherwise see.
* `tagDefinitions` exposes the KB's defined tags (`tagName` → `tagSlot`) plus the operators
* legal for each tag's `fieldType`, so the agent can bind a knowledge-tag filter without
* guessing a tag name it cannot otherwise see or an operator the field does not accept
* (`between` is valid for number/date but not text/boolean).
*/
export function serializeKBMeta(kb: {
id: string
Expand All @@ -119,7 +131,14 @@ export function serializeKBMeta(kb: {
connectorTypes:
kb.connectorTypes && kb.connectorTypes.length > 0 ? kb.connectorTypes : undefined,
tagDefinitions:
kb.tagDefinitions && kb.tagDefinitions.length > 0 ? kb.tagDefinitions : undefined,
kb.tagDefinitions && kb.tagDefinitions.length > 0
? kb.tagDefinitions.map((tag) => ({
...tag,
operators: getOperatorsForFieldType(tag.fieldType as FilterFieldType).map(
(op) => op.value
),
}))
: undefined,
createdAt: kb.createdAt.toISOString(),
updatedAt: kb.updatedAt.toISOString(),
},
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/copilot/vfs/workspace-vfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1591,7 +1591,7 @@ export class WorkspaceVFS {

for (const row of rows) {
const entry = {
displayName: row.displayName,
tagName: row.displayName,
tagSlot: row.tagSlot,
fieldType: row.fieldType,
}
Expand Down
Loading