Skip to content

Commit 0fcd526

Browse files
improvement(tool-input): general abstraction to enrich agent context, reuse visibility helpers (#2872)
* add abstraction for schema enrichment, improve agent KB block experience for tags, fix visibility of subblocks * cleanup code * consolidate * fix workflow tool react query * fix deployed context propagation * fix tests
1 parent b8b2057 commit 0fcd526

File tree

25 files changed

+967
-342
lines changed

25 files changed

+967
-342
lines changed

apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
22
import { createLogger } from '@sim/logger'
33
import { type NextRequest, NextResponse } from 'next/server'
44
import { z } from 'zod'
5-
import { getSession } from '@/lib/auth'
5+
import { checkHybridAuth } from '@/lib/auth/hybrid'
66
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
77
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
88
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
@@ -19,19 +19,32 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
1919
try {
2020
logger.info(`[${requestId}] Getting tag definitions for knowledge base ${knowledgeBaseId}`)
2121

22-
const session = await getSession()
23-
if (!session?.user?.id) {
24-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
22+
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
23+
if (!auth.success) {
24+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
2525
}
2626

27-
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, session.user.id)
28-
if (!accessCheck.hasAccess) {
29-
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
27+
// Only allow session and internal JWT auth (not API key)
28+
if (auth.authType === 'api_key') {
29+
return NextResponse.json(
30+
{ error: 'API key auth not supported for this endpoint' },
31+
{ status: 401 }
32+
)
33+
}
34+
35+
// For session auth, verify KB access. Internal JWT is trusted.
36+
if (auth.authType === 'session' && auth.userId) {
37+
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
38+
if (!accessCheck.hasAccess) {
39+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
40+
}
3041
}
3142

3243
const tagDefinitions = await getTagDefinitions(knowledgeBaseId)
3344

34-
logger.info(`[${requestId}] Retrieved ${tagDefinitions.length} tag definitions`)
45+
logger.info(
46+
`[${requestId}] Retrieved ${tagDefinitions.length} tag definitions (${auth.authType})`
47+
)
3548

3649
return NextResponse.json({
3750
success: true,
@@ -51,14 +64,25 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
5164
try {
5265
logger.info(`[${requestId}] Creating tag definition for knowledge base ${knowledgeBaseId}`)
5366

54-
const session = await getSession()
55-
if (!session?.user?.id) {
56-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
67+
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
68+
if (!auth.success) {
69+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
5770
}
5871

59-
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, session.user.id)
60-
if (!accessCheck.hasAccess) {
61-
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
72+
// Only allow session and internal JWT auth (not API key)
73+
if (auth.authType === 'api_key') {
74+
return NextResponse.json(
75+
{ error: 'API key auth not supported for this endpoint' },
76+
{ status: 401 }
77+
)
78+
}
79+
80+
// For session auth, verify KB access. Internal JWT is trusted.
81+
if (auth.authType === 'session' && auth.userId) {
82+
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
83+
if (!accessCheck.hasAccess) {
84+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
85+
}
6286
}
6387

6488
const body = await req.json()

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-selector/document-selector.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface DocumentSelectorProps {
1515
onDocumentSelect?: (documentId: string) => void
1616
isPreview?: boolean
1717
previewValue?: string | null
18+
previewContextValues?: Record<string, unknown>
1819
}
1920

2021
export function DocumentSelector({
@@ -24,9 +25,15 @@ export function DocumentSelector({
2425
onDocumentSelect,
2526
isPreview = false,
2627
previewValue,
28+
previewContextValues,
2729
}: DocumentSelectorProps) {
28-
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
29-
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
30+
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
31+
disabled,
32+
isPreview,
33+
previewContextValues,
34+
})
35+
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
36+
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
3037
const normalizedKnowledgeBaseId =
3138
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
3239
? knowledgeBaseIdValue

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface DocumentTagEntryProps {
3737
disabled?: boolean
3838
isPreview?: boolean
3939
previewValue?: any
40+
previewContextValues?: Record<string, unknown>
4041
}
4142

4243
/**
@@ -56,6 +57,7 @@ export function DocumentTagEntry({
5657
disabled = false,
5758
isPreview = false,
5859
previewValue,
60+
previewContextValues,
5961
}: DocumentTagEntryProps) {
6062
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlock.id)
6163
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
@@ -74,8 +76,12 @@ export function DocumentTagEntry({
7476
disabled,
7577
})
7678

77-
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
78-
const knowledgeBaseId = knowledgeBaseIdValue || null
79+
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
80+
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
81+
const knowledgeBaseId =
82+
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
83+
? knowledgeBaseIdValue
84+
: null
7985

8086
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
8187
const emitTagSelection = useTagSelection(blockId, subBlock.id)
@@ -131,11 +137,16 @@ export function DocumentTagEntry({
131137
}
132138

133139
/**
134-
* Removes a tag by ID (prevents removing the last tag)
140+
* Removes a tag by ID, or resets it if it's the last one
135141
*/
136142
const removeTag = (id: string) => {
137-
if (isReadOnly || tags.length === 1) return
138-
updateTags(tags.filter((t) => t.id !== id))
143+
if (isReadOnly) return
144+
if (tags.length === 1) {
145+
// Reset the last tag instead of removing it
146+
updateTags([createDefaultTag()])
147+
} else {
148+
updateTags(tags.filter((t) => t.id !== id))
149+
}
139150
}
140151

141152
/**
@@ -222,6 +233,7 @@ export function DocumentTagEntry({
222233

223234
/**
224235
* Renders the tag header with name, badge, and action buttons
236+
* Shows tag name only when collapsed (as summary), generic label when expanded
225237
*/
226238
const renderTagHeader = (tag: DocumentTag, index: number) => (
227239
<div
@@ -230,9 +242,11 @@ export function DocumentTagEntry({
230242
>
231243
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
232244
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
233-
{tag.tagName || `Tag ${index + 1}`}
245+
{tag.collapsed ? tag.tagName || `Tag ${index + 1}` : `Tag ${index + 1}`}
234246
</span>
235-
{tag.tagName && <Badge size='sm'>{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}</Badge>}
247+
{tag.collapsed && tag.tagName && (
248+
<Badge size='sm'>{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}</Badge>
249+
)}
236250
</div>
237251
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
238252
<Button
@@ -247,7 +261,7 @@ export function DocumentTagEntry({
247261
<Button
248262
variant='ghost'
249263
onClick={() => removeTag(tag.id)}
250-
disabled={isReadOnly || tags.length === 1}
264+
disabled={isReadOnly}
251265
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
252266
>
253267
<Trash className='h-[14px] w-[14px]' />
@@ -341,7 +355,7 @@ export function DocumentTagEntry({
341355

342356
const tagOptions: ComboboxOption[] = selectableTags.map((t) => ({
343357
value: t.displayName,
344-
label: `${t.displayName} (${FIELD_TYPE_LABELS[t.fieldType] || 'Text'})`,
358+
label: t.displayName,
345359
}))
346360

347361
return (

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ interface KnowledgeTagFiltersProps {
4040
disabled?: boolean
4141
isPreview?: boolean
4242
previewValue?: string | null
43+
previewContextValues?: Record<string, unknown>
4344
}
4445

4546
/**
@@ -60,14 +61,19 @@ export function KnowledgeTagFilters({
6061
disabled = false,
6162
isPreview = false,
6263
previewValue,
64+
previewContextValues,
6365
}: KnowledgeTagFiltersProps) {
6466
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
6567
const emitTagSelection = useTagSelection(blockId, subBlock.id)
6668
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
6769
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
6870

69-
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
70-
const knowledgeBaseId = knowledgeBaseIdValue || null
71+
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
72+
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
73+
const knowledgeBaseId =
74+
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
75+
? knowledgeBaseIdValue
76+
: null
7177

7278
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
7379
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
@@ -123,11 +129,16 @@ export function KnowledgeTagFilters({
123129
}
124130

125131
/**
126-
* Removes a filter by ID (prevents removing the last filter)
132+
* Removes a filter by ID, or resets it if it's the last one
127133
*/
128134
const removeFilter = (id: string) => {
129-
if (isReadOnly || filters.length === 1) return
130-
updateFilters(filters.filter((f) => f.id !== id))
135+
if (isReadOnly) return
136+
if (filters.length === 1) {
137+
// Reset the last filter instead of removing it
138+
updateFilters([createDefaultFilter()])
139+
} else {
140+
updateFilters(filters.filter((f) => f.id !== id))
141+
}
131142
}
132143

133144
/**
@@ -215,6 +226,7 @@ export function KnowledgeTagFilters({
215226

216227
/**
217228
* Renders the filter header with name, badge, and action buttons
229+
* Shows tag name only when collapsed (as summary), generic label when expanded
218230
*/
219231
const renderFilterHeader = (filter: TagFilter, index: number) => (
220232
<div
@@ -223,9 +235,11 @@ export function KnowledgeTagFilters({
223235
>
224236
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
225237
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
226-
{filter.tagName || `Filter ${index + 1}`}
238+
{filter.collapsed ? filter.tagName || `Filter ${index + 1}` : `Filter ${index + 1}`}
227239
</span>
228-
{filter.tagName && <Badge size='sm'>{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}</Badge>}
240+
{filter.collapsed && filter.tagName && (
241+
<Badge size='sm'>{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}</Badge>
242+
)}
229243
</div>
230244
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
231245
<Button variant='ghost' onClick={addFilter} disabled={isReadOnly} className='h-auto p-0'>
@@ -235,7 +249,7 @@ export function KnowledgeTagFilters({
235249
<Button
236250
variant='ghost'
237251
onClick={() => removeFilter(filter.id)}
238-
disabled={isReadOnly || filters.length === 1}
252+
disabled={isReadOnly}
239253
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
240254
>
241255
<Trash className='h-[14px] w-[14px]' />
@@ -324,7 +338,7 @@ export function KnowledgeTagFilters({
324338
const renderFilterContent = (filter: TagFilter) => {
325339
const tagOptions: ComboboxOption[] = tagDefinitions.map((tag) => ({
326340
value: tag.displayName,
327-
label: `${tag.displayName} (${FIELD_TYPE_LABELS[tag.fieldType] || 'Text'})`,
341+
label: tag.displayName,
328342
}))
329343

330344
const operators = getOperatorsForFieldType(filter.fieldType)

0 commit comments

Comments
 (0)