mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-21 04:48:00 -05:00
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
This commit is contained in:
committed by
GitHub
parent
b8b20576d3
commit
0fcd52683a
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
|
||||
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
|
||||
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
|
||||
@@ -19,19 +19,32 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
try {
|
||||
logger.info(`[${requestId}] Getting tag definitions for knowledge base ${knowledgeBaseId}`)
|
||||
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
|
||||
if (!auth.success) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, session.user.id)
|
||||
if (!accessCheck.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
// Only allow session and internal JWT auth (not API key)
|
||||
if (auth.authType === 'api_key') {
|
||||
return NextResponse.json(
|
||||
{ error: 'API key auth not supported for this endpoint' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// For session auth, verify KB access. Internal JWT is trusted.
|
||||
if (auth.authType === 'session' && auth.userId) {
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
||||
if (!accessCheck.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
const tagDefinitions = await getTagDefinitions(knowledgeBaseId)
|
||||
|
||||
logger.info(`[${requestId}] Retrieved ${tagDefinitions.length} tag definitions`)
|
||||
logger.info(
|
||||
`[${requestId}] Retrieved ${tagDefinitions.length} tag definitions (${auth.authType})`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
@@ -51,14 +64,25 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
try {
|
||||
logger.info(`[${requestId}] Creating tag definition for knowledge base ${knowledgeBaseId}`)
|
||||
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
|
||||
if (!auth.success) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, session.user.id)
|
||||
if (!accessCheck.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
// Only allow session and internal JWT auth (not API key)
|
||||
if (auth.authType === 'api_key') {
|
||||
return NextResponse.json(
|
||||
{ error: 'API key auth not supported for this endpoint' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// For session auth, verify KB access. Internal JWT is trusted.
|
||||
if (auth.authType === 'session' && auth.userId) {
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
||||
if (!accessCheck.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
|
||||
@@ -15,6 +15,7 @@ interface DocumentSelectorProps {
|
||||
onDocumentSelect?: (documentId: string) => void
|
||||
isPreview?: boolean
|
||||
previewValue?: string | null
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function DocumentSelector({
|
||||
@@ -24,9 +25,15 @@ export function DocumentSelector({
|
||||
onDocumentSelect,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: DocumentSelectorProps) {
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
|
||||
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
||||
const normalizedKnowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
|
||||
@@ -37,6 +37,7 @@ interface DocumentTagEntryProps {
|
||||
disabled?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: any
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +57,7 @@ export function DocumentTagEntry({
|
||||
disabled = false,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: DocumentTagEntryProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlock.id)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
@@ -74,8 +76,12 @@ export function DocumentTagEntry({
|
||||
disabled,
|
||||
})
|
||||
|
||||
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseId = knowledgeBaseIdValue || null
|
||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
||||
const knowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
: null
|
||||
|
||||
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
|
||||
const emitTagSelection = useTagSelection(blockId, subBlock.id)
|
||||
@@ -131,11 +137,16 @@ export function DocumentTagEntry({
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag by ID (prevents removing the last tag)
|
||||
* Removes a tag by ID, or resets it if it's the last one
|
||||
*/
|
||||
const removeTag = (id: string) => {
|
||||
if (isReadOnly || tags.length === 1) return
|
||||
updateTags(tags.filter((t) => t.id !== id))
|
||||
if (isReadOnly) return
|
||||
if (tags.length === 1) {
|
||||
// Reset the last tag instead of removing it
|
||||
updateTags([createDefaultTag()])
|
||||
} else {
|
||||
updateTags(tags.filter((t) => t.id !== id))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,6 +233,7 @@ export function DocumentTagEntry({
|
||||
|
||||
/**
|
||||
* Renders the tag header with name, badge, and action buttons
|
||||
* Shows tag name only when collapsed (as summary), generic label when expanded
|
||||
*/
|
||||
const renderTagHeader = (tag: DocumentTag, index: number) => (
|
||||
<div
|
||||
@@ -230,9 +242,11 @@ export function DocumentTagEntry({
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||
{tag.tagName || `Tag ${index + 1}`}
|
||||
{tag.collapsed ? tag.tagName || `Tag ${index + 1}` : `Tag ${index + 1}`}
|
||||
</span>
|
||||
{tag.tagName && <Badge size='sm'>{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}</Badge>}
|
||||
{tag.collapsed && tag.tagName && (
|
||||
<Badge size='sm'>{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
@@ -247,7 +261,7 @@ export function DocumentTagEntry({
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => removeTag(tag.id)}
|
||||
disabled={isReadOnly || tags.length === 1}
|
||||
disabled={isReadOnly}
|
||||
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
|
||||
>
|
||||
<Trash className='h-[14px] w-[14px]' />
|
||||
@@ -341,7 +355,7 @@ export function DocumentTagEntry({
|
||||
|
||||
const tagOptions: ComboboxOption[] = selectableTags.map((t) => ({
|
||||
value: t.displayName,
|
||||
label: `${t.displayName} (${FIELD_TYPE_LABELS[t.fieldType] || 'Text'})`,
|
||||
label: t.displayName,
|
||||
}))
|
||||
|
||||
return (
|
||||
|
||||
@@ -40,6 +40,7 @@ interface KnowledgeTagFiltersProps {
|
||||
disabled?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: string | null
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,14 +61,19 @@ export function KnowledgeTagFilters({
|
||||
disabled = false,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: KnowledgeTagFiltersProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
|
||||
const emitTagSelection = useTagSelection(blockId, subBlock.id)
|
||||
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
|
||||
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
|
||||
|
||||
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseId = knowledgeBaseIdValue || null
|
||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
||||
const knowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
: null
|
||||
|
||||
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
@@ -123,11 +129,16 @@ export function KnowledgeTagFilters({
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a filter by ID (prevents removing the last filter)
|
||||
* Removes a filter by ID, or resets it if it's the last one
|
||||
*/
|
||||
const removeFilter = (id: string) => {
|
||||
if (isReadOnly || filters.length === 1) return
|
||||
updateFilters(filters.filter((f) => f.id !== id))
|
||||
if (isReadOnly) return
|
||||
if (filters.length === 1) {
|
||||
// Reset the last filter instead of removing it
|
||||
updateFilters([createDefaultFilter()])
|
||||
} else {
|
||||
updateFilters(filters.filter((f) => f.id !== id))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,6 +226,7 @@ export function KnowledgeTagFilters({
|
||||
|
||||
/**
|
||||
* Renders the filter header with name, badge, and action buttons
|
||||
* Shows tag name only when collapsed (as summary), generic label when expanded
|
||||
*/
|
||||
const renderFilterHeader = (filter: TagFilter, index: number) => (
|
||||
<div
|
||||
@@ -223,9 +235,11 @@ export function KnowledgeTagFilters({
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||
{filter.tagName || `Filter ${index + 1}`}
|
||||
{filter.collapsed ? filter.tagName || `Filter ${index + 1}` : `Filter ${index + 1}`}
|
||||
</span>
|
||||
{filter.tagName && <Badge size='sm'>{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}</Badge>}
|
||||
{filter.collapsed && filter.tagName && (
|
||||
<Badge size='sm'>{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant='ghost' onClick={addFilter} disabled={isReadOnly} className='h-auto p-0'>
|
||||
@@ -235,7 +249,7 @@ export function KnowledgeTagFilters({
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => removeFilter(filter.id)}
|
||||
disabled={isReadOnly || filters.length === 1}
|
||||
disabled={isReadOnly}
|
||||
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
|
||||
>
|
||||
<Trash className='h-[14px] w-[14px]' />
|
||||
@@ -324,7 +338,7 @@ export function KnowledgeTagFilters({
|
||||
const renderFilterContent = (filter: TagFilter) => {
|
||||
const tagOptions: ComboboxOption[] = tagDefinitions.map((tag) => ({
|
||||
value: tag.displayName,
|
||||
label: `${tag.displayName} (${FIELD_TYPE_LABELS[tag.fieldType] || 'Text'})`,
|
||||
label: tag.displayName,
|
||||
}))
|
||||
|
||||
const operators = getOperatorsForFieldType(filter.fieldType)
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
Code,
|
||||
FileSelectorInput,
|
||||
FileUpload,
|
||||
FolderSelectorInput,
|
||||
LongInput,
|
||||
ProjectSelectorInput,
|
||||
SheetSelectorInput,
|
||||
@@ -45,7 +46,9 @@ import {
|
||||
TimeInput,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components'
|
||||
import { DocumentSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-selector/document-selector'
|
||||
import { DocumentTagEntry } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry'
|
||||
import { KnowledgeBaseSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector'
|
||||
import { KnowledgeTagFilters } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters'
|
||||
import {
|
||||
type CustomTool,
|
||||
CustomToolModal,
|
||||
@@ -75,6 +78,13 @@ import {
|
||||
isPasswordParameter,
|
||||
type ToolParameterConfig,
|
||||
} from '@/tools/params'
|
||||
import {
|
||||
buildCanonicalIndex,
|
||||
buildPreviewContextValues,
|
||||
type CanonicalIndex,
|
||||
evaluateSubBlockCondition,
|
||||
type SubBlockCondition,
|
||||
} from '@/tools/params-resolver'
|
||||
|
||||
const logger = createLogger('ToolInput')
|
||||
|
||||
@@ -304,6 +314,42 @@ function SheetSelectorSyncWrapper({
|
||||
)
|
||||
}
|
||||
|
||||
function FolderSelectorSyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
value,
|
||||
onChange,
|
||||
uiComponent,
|
||||
disabled,
|
||||
previewContextValues,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
uiComponent: any
|
||||
disabled: boolean
|
||||
previewContextValues?: Record<string, any>
|
||||
}) {
|
||||
return (
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
<FolderSelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={{
|
||||
id: paramId,
|
||||
type: 'folder-selector' as const,
|
||||
title: paramId,
|
||||
serviceId: uiComponent.serviceId,
|
||||
requiredScopes: uiComponent.requiredScopes || [],
|
||||
placeholder: uiComponent.placeholder,
|
||||
dependsOn: uiComponent.dependsOn,
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function KnowledgeBaseSelectorSyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
@@ -342,6 +388,7 @@ function DocumentSelectorSyncWrapper({
|
||||
onChange,
|
||||
uiComponent,
|
||||
disabled,
|
||||
previewContextValues,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
@@ -349,6 +396,7 @@ function DocumentSelectorSyncWrapper({
|
||||
onChange: (value: string) => void
|
||||
uiComponent: any
|
||||
disabled: boolean
|
||||
previewContextValues?: Record<string, any>
|
||||
}) {
|
||||
return (
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
@@ -361,6 +409,67 @@ function DocumentSelectorSyncWrapper({
|
||||
dependsOn: ['knowledgeBaseId'],
|
||||
}}
|
||||
disabled={disabled}
|
||||
previewContextValues={previewContextValues}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function DocumentTagEntrySyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
previewContextValues,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
disabled: boolean
|
||||
previewContextValues?: Record<string, any>
|
||||
}) {
|
||||
return (
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
<DocumentTagEntry
|
||||
blockId={blockId}
|
||||
subBlock={{
|
||||
id: paramId,
|
||||
type: 'document-tag-entry',
|
||||
}}
|
||||
disabled={disabled}
|
||||
previewContextValues={previewContextValues}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function KnowledgeTagFiltersSyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
previewContextValues,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
disabled: boolean
|
||||
previewContextValues?: Record<string, any>
|
||||
}) {
|
||||
return (
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
<KnowledgeTagFilters
|
||||
blockId={blockId}
|
||||
subBlock={{
|
||||
id: paramId,
|
||||
type: 'knowledge-tag-filters',
|
||||
}}
|
||||
disabled={disabled}
|
||||
previewContextValues={previewContextValues}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
@@ -497,11 +606,15 @@ function CheckboxListSyncWrapper({
|
||||
}
|
||||
|
||||
function ComboboxSyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
value,
|
||||
onChange,
|
||||
uiComponent,
|
||||
disabled,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
uiComponent: any
|
||||
@@ -512,13 +625,15 @@ function ComboboxSyncWrapper({
|
||||
)
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={uiComponent.placeholder || 'Select option'}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={uiComponent.placeholder || 'Select option'}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -597,6 +712,8 @@ function SlackSelectorSyncWrapper({
|
||||
}
|
||||
|
||||
function WorkflowSelectorSyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
value,
|
||||
onChange,
|
||||
uiComponent,
|
||||
@@ -604,6 +721,8 @@ function WorkflowSelectorSyncWrapper({
|
||||
workspaceId,
|
||||
currentWorkflowId,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
uiComponent: any
|
||||
@@ -623,15 +742,17 @@ function WorkflowSelectorSyncWrapper({
|
||||
}))
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={uiComponent.placeholder || 'Select workflow'}
|
||||
disabled={disabled || isLoading}
|
||||
searchable
|
||||
searchPlaceholder='Search workflows...'
|
||||
/>
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={uiComponent.placeholder || 'Select workflow'}
|
||||
disabled={disabled || isLoading}
|
||||
searchable
|
||||
searchPlaceholder='Search workflows...'
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1792,57 +1913,13 @@ export function ToolInput({
|
||||
return toolParams?.toolConfig?.oauth
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates parameter conditions to determine if a parameter should be visible.
|
||||
*
|
||||
* @remarks
|
||||
* Supports field value matching with arrays, negation via `not`, and
|
||||
* compound conditions via `and`. Used for conditional parameter visibility.
|
||||
*
|
||||
* @param param - The parameter configuration with optional condition
|
||||
* @param tool - The current tool instance with its parameter values
|
||||
* @returns `true` if the parameter should be shown based on its condition
|
||||
*/
|
||||
const evaluateParameterCondition = (param: any, tool: StoredTool): boolean => {
|
||||
if (!('uiComponent' in param) || !param.uiComponent?.condition) return true
|
||||
|
||||
const condition = param.uiComponent.condition
|
||||
const currentValues: Record<string, any> = {
|
||||
operation: tool.operation,
|
||||
...tool.params,
|
||||
}
|
||||
|
||||
const fieldValue = currentValues[condition.field]
|
||||
let result = false
|
||||
|
||||
if (Array.isArray(condition.value)) {
|
||||
result = condition.value.includes(fieldValue)
|
||||
} else {
|
||||
result = fieldValue === condition.value
|
||||
}
|
||||
|
||||
if (condition.not) {
|
||||
result = !result
|
||||
}
|
||||
|
||||
if (condition.and) {
|
||||
const andFieldValue = currentValues[condition.and.field]
|
||||
let andResult = false
|
||||
|
||||
if (Array.isArray(condition.and.value)) {
|
||||
andResult = condition.and.value.includes(andFieldValue)
|
||||
} else {
|
||||
andResult = andFieldValue === condition.and.value
|
||||
}
|
||||
|
||||
if (condition.and.not) {
|
||||
andResult = !andResult
|
||||
}
|
||||
|
||||
result = result && andResult
|
||||
}
|
||||
|
||||
return result
|
||||
const currentValues: Record<string, any> = { operation: tool.operation, ...tool.params }
|
||||
return evaluateSubBlockCondition(
|
||||
param.uiComponent.condition as SubBlockCondition,
|
||||
currentValues
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1961,7 +2038,7 @@ export function ToolInput({
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
previewContextValues={currentToolParams}
|
||||
selectorType='channel-selector'
|
||||
/>
|
||||
)
|
||||
@@ -1975,7 +2052,7 @@ export function ToolInput({
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
previewContextValues={currentToolParams}
|
||||
selectorType='user-selector'
|
||||
/>
|
||||
)
|
||||
@@ -1995,7 +2072,7 @@ export function ToolInput({
|
||||
}}
|
||||
onProjectSelect={onChange}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -2020,7 +2097,7 @@ export function ToolInput({
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -2033,7 +2110,20 @@ export function ToolInput({
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'folder-selector':
|
||||
return (
|
||||
<FolderSelectorSyncWrapper
|
||||
blockId={blockId}
|
||||
paramId={param.id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -2052,6 +2142,8 @@ export function ToolInput({
|
||||
case 'combobox':
|
||||
return (
|
||||
<ComboboxSyncWrapper
|
||||
blockId={blockId}
|
||||
paramId={param.id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
@@ -2110,6 +2202,8 @@ export function ToolInput({
|
||||
case 'workflow-selector':
|
||||
return (
|
||||
<WorkflowSelectorSyncWrapper
|
||||
blockId={blockId}
|
||||
paramId={param.id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
@@ -2167,6 +2261,31 @@ export function ToolInput({
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'document-tag-entry':
|
||||
return (
|
||||
<DocumentTagEntrySyncWrapper
|
||||
blockId={blockId}
|
||||
paramId={param.id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'knowledge-tag-filters':
|
||||
return (
|
||||
<KnowledgeTagFiltersSyncWrapper
|
||||
blockId={blockId}
|
||||
paramId={param.id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -2225,9 +2344,27 @@ export function ToolInput({
|
||||
// Get tool parameters using the new utility with block type for UI components
|
||||
const toolParams =
|
||||
!isCustomTool && !isMcpTool && currentToolId
|
||||
? getToolParametersConfig(currentToolId, tool.type)
|
||||
? getToolParametersConfig(currentToolId, tool.type, {
|
||||
operation: tool.operation,
|
||||
...tool.params,
|
||||
})
|
||||
: null
|
||||
|
||||
// Build canonical index for proper dependency resolution
|
||||
const toolCanonicalIndex: CanonicalIndex | null = toolBlock?.subBlocks
|
||||
? buildCanonicalIndex(toolBlock.subBlocks)
|
||||
: null
|
||||
|
||||
// Build preview context with canonical resolution
|
||||
const toolContextValues = toolCanonicalIndex
|
||||
? buildPreviewContextValues(tool.params || {}, {
|
||||
blockType: tool.type,
|
||||
subBlocks: toolBlock!.subBlocks,
|
||||
canonicalIndex: toolCanonicalIndex,
|
||||
values: { operation: tool.operation, ...tool.params },
|
||||
})
|
||||
: tool.params || {}
|
||||
|
||||
// For custom tools, resolve from reference (new format) or use inline (legacy)
|
||||
const resolvedCustomTool = isCustomTool
|
||||
? resolveCustomToolFromReference(tool, customTools)
|
||||
@@ -2590,7 +2727,7 @@ export function ToolInput({
|
||||
{param.required && param.visibility === 'user-only' && (
|
||||
<span className='ml-1'>*</span>
|
||||
)}
|
||||
{(!param.required || param.visibility !== 'user-only') && (
|
||||
{param.visibility === 'user-or-llm' && (
|
||||
<span className='ml-[6px] text-[12px] text-[var(--text-tertiary)]'>
|
||||
(optional)
|
||||
</span>
|
||||
@@ -2603,7 +2740,7 @@ export function ToolInput({
|
||||
tool.params?.[param.id] || '',
|
||||
(value) => handleParamChange(toolIndex, param.id, value),
|
||||
toolIndex,
|
||||
tool.params || {}
|
||||
toolContextValues as Record<string, string>
|
||||
)
|
||||
) : (
|
||||
<ShortInput
|
||||
|
||||
@@ -535,6 +535,51 @@ Return ONLY the search query - no explanations.`,
|
||||
value: ['linear_read_issues', 'linear_search_issues', 'linear_list_projects'],
|
||||
},
|
||||
},
|
||||
// Issue filtering options for read_issues (advanced)
|
||||
{
|
||||
id: 'labelIds',
|
||||
title: 'Label IDs',
|
||||
type: 'short-input',
|
||||
placeholder: 'Array of label IDs to filter by',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'linear_read_issues',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'createdAfter',
|
||||
title: 'Created After',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter issues created after this date (ISO 8601 format)',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'linear_read_issues',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'updatedAfter',
|
||||
title: 'Updated After',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter issues updated after this date (ISO 8601 format)',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'linear_read_issues',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'orderBy',
|
||||
title: 'Order By',
|
||||
type: 'short-input',
|
||||
placeholder: 'Sort order: "createdAt" or "updatedAt" (default: "updatedAt")',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'linear_read_issues',
|
||||
},
|
||||
},
|
||||
// Cycle ID
|
||||
{
|
||||
id: 'cycleId',
|
||||
@@ -2188,6 +2233,16 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
estimate: { type: 'string', description: 'Estimate points' },
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
includeArchived: { type: 'boolean', description: 'Include archived items' },
|
||||
labelIds: { type: 'array', description: 'Array of label IDs to filter by' },
|
||||
createdAfter: {
|
||||
type: 'string',
|
||||
description: 'Filter issues created after this date (ISO 8601)',
|
||||
},
|
||||
updatedAfter: {
|
||||
type: 'string',
|
||||
description: 'Filter issues updated after this date (ISO 8601)',
|
||||
},
|
||||
orderBy: { type: 'string', description: 'Sort order: createdAt or updatedAt' },
|
||||
cycleId: { type: 'string', description: 'Cycle identifier' },
|
||||
startDate: { type: 'string', description: 'Start date' },
|
||||
endDate: { type: 'string', description: 'End date' },
|
||||
|
||||
@@ -321,6 +321,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
_context: {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
},
|
||||
false,
|
||||
@@ -1074,6 +1075,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
workflowVariables: ctx.workflowVariables || {},
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
})
|
||||
|
||||
return this.processProviderResponse(response, block, responseFormat)
|
||||
|
||||
@@ -78,6 +78,7 @@ export class ApiBlockHandler implements BlockHandler {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
executionId: ctx.executionId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -40,6 +40,7 @@ export async function evaluateConditionExpression(
|
||||
_context: {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -38,6 +38,7 @@ export class FunctionBlockHandler implements BlockHandler {
|
||||
_context: {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -66,6 +66,7 @@ export class GenericBlockHandler implements BlockHandler {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
executionId: ctx.executionId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -627,6 +627,7 @@ export class HumanInTheLoopBlockHandler implements BlockHandler {
|
||||
_context: {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
blockData: blockDataWithPause,
|
||||
blockNameMapping: blockNameMappingWithPause,
|
||||
|
||||
@@ -52,7 +52,8 @@ export function useWorkflowInputFields(workflowId: string | undefined) {
|
||||
queryKey: workflowKeys.inputFields(workflowId),
|
||||
queryFn: () => fetchWorkflowInputFields(workflowId!),
|
||||
enabled: Boolean(workflowId),
|
||||
staleTime: 60 * 1000, // 1 minute cache
|
||||
staleTime: 0,
|
||||
refetchOnMount: 'always',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@ export async function executeWorkflowCore(
|
||||
executionId,
|
||||
workspaceId: providedWorkspaceId,
|
||||
userId,
|
||||
isDeployedContext: triggerType !== 'manual',
|
||||
isDeployedContext: !metadata.isClientSession,
|
||||
onBlockStart,
|
||||
onBlockComplete: wrappedOnBlockComplete,
|
||||
onStream,
|
||||
|
||||
@@ -167,6 +167,7 @@ export interface ProviderRequest {
|
||||
reasoningEffort?: string
|
||||
verbosity?: string
|
||||
thinkingLevel?: string
|
||||
isDeployedContext?: boolean
|
||||
}
|
||||
|
||||
export const providers: Record<string, ProviderConfig> = {}
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import type { ProviderId, ProviderToolConfig } from '@/providers/types'
|
||||
import { useCustomToolsStore } from '@/stores/custom-tools/store'
|
||||
import { useProvidersStore } from '@/stores/providers/store'
|
||||
import { deepMergeInputMapping } from '@/tools/params'
|
||||
import { mergeToolParameters } from '@/tools/params'
|
||||
|
||||
const logger = createLogger('ProviderUtils')
|
||||
|
||||
@@ -981,39 +981,14 @@ export function prepareToolExecution(
|
||||
workflowVariables?: Record<string, any>
|
||||
blockData?: Record<string, any>
|
||||
blockNameMapping?: Record<string, string>
|
||||
isDeployedContext?: boolean
|
||||
}
|
||||
): {
|
||||
toolParams: Record<string, any>
|
||||
executionParams: Record<string, any>
|
||||
} {
|
||||
const filteredUserParams: Record<string, any> = {}
|
||||
if (tool.params) {
|
||||
for (const [key, value] of Object.entries(tool.params)) {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
filteredUserParams[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start with LLM params as base
|
||||
const toolParams: Record<string, any> = { ...llmArgs }
|
||||
|
||||
// Apply user params with special handling for inputMapping
|
||||
for (const [key, userValue] of Object.entries(filteredUserParams)) {
|
||||
if (key === 'inputMapping') {
|
||||
// Deep merge inputMapping so LLM values fill in empty user fields
|
||||
const llmInputMapping = llmArgs.inputMapping as Record<string, any> | undefined
|
||||
toolParams.inputMapping = deepMergeInputMapping(llmInputMapping, userValue)
|
||||
} else {
|
||||
// Normal override for other params
|
||||
toolParams[key] = userValue
|
||||
}
|
||||
}
|
||||
|
||||
// If LLM provided inputMapping but user didn't, ensure it's included
|
||||
if (llmArgs.inputMapping && !filteredUserParams.inputMapping) {
|
||||
toolParams.inputMapping = llmArgs.inputMapping
|
||||
}
|
||||
// Use centralized merge logic from tools/params
|
||||
const toolParams = mergeToolParameters(tool.params || {}, llmArgs) as Record<string, any>
|
||||
|
||||
const executionParams = {
|
||||
...toolParams,
|
||||
@@ -1024,6 +999,9 @@ export function prepareToolExecution(
|
||||
...(request.workspaceId ? { workspaceId: request.workspaceId } : {}),
|
||||
...(request.chatId ? { chatId: request.chatId } : {}),
|
||||
...(request.userId ? { userId: request.userId } : {}),
|
||||
...(request.isDeployedContext !== undefined
|
||||
? { isDeployedContext: request.isDeployedContext }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { KnowledgeCreateDocumentResponse } from '@/tools/knowledge/types'
|
||||
import { formatDocumentTagsForAPI, parseDocumentTags } from '@/tools/params'
|
||||
import { enrichKBTagsSchema } from '@/tools/schema-enrichers'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumentResponse> = {
|
||||
@@ -26,61 +28,18 @@ export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumen
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Content of the document',
|
||||
},
|
||||
tag1: {
|
||||
type: 'string',
|
||||
documentTags: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 1 value for the document',
|
||||
description: 'Document tags',
|
||||
},
|
||||
tag2: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 2 value for the document',
|
||||
},
|
||||
tag3: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 3 value for the document',
|
||||
},
|
||||
tag4: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 4 value for the document',
|
||||
},
|
||||
tag5: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 5 value for the document',
|
||||
},
|
||||
tag6: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 6 value for the document',
|
||||
},
|
||||
tag7: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 7 value for the document',
|
||||
},
|
||||
documentTagsData: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Structured tag data with names, types, and values',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tagName: { type: 'string' },
|
||||
tagValue: { type: 'string' },
|
||||
tagType: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
schemaEnrichment: {
|
||||
documentTags: {
|
||||
dependsOn: 'knowledgeBaseId',
|
||||
enrichSchema: enrichKBTagsSchema,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -118,24 +77,9 @@ export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumen
|
||||
|
||||
const dataUri = `data:text/plain;base64,${base64Content}`
|
||||
|
||||
const tagData: Record<string, string> = {}
|
||||
|
||||
if (params.documentTags) {
|
||||
let parsedTags = params.documentTags
|
||||
|
||||
// Handle both string (JSON) and array formats
|
||||
if (typeof params.documentTags === 'string') {
|
||||
try {
|
||||
parsedTags = JSON.parse(params.documentTags)
|
||||
} catch (error) {
|
||||
parsedTags = []
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(parsedTags)) {
|
||||
tagData.documentTagsData = JSON.stringify(parsedTags)
|
||||
}
|
||||
}
|
||||
// Parse document tags from various formats (object, array, JSON string)
|
||||
const parsedTags = parseDocumentTags(params.documentTags)
|
||||
const tagData = formatDocumentTagsForAPI(parsedTags)
|
||||
|
||||
const documents = [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { StructuredFilter } from '@/lib/knowledge/types'
|
||||
import type { KnowledgeSearchResponse } from '@/tools/knowledge/types'
|
||||
import { parseTagFilters } from '@/tools/params'
|
||||
import { enrichKBTagFiltersSchema } from '@/tools/schema-enrichers'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
|
||||
@@ -42,6 +43,13 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
|
||||
},
|
||||
},
|
||||
|
||||
schemaEnrichment: {
|
||||
tagFilters: {
|
||||
dependsOn: 'knowledgeBaseId',
|
||||
enrichSchema: enrichKBTagFiltersSchema,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/knowledge/search',
|
||||
method: 'POST',
|
||||
@@ -54,40 +62,8 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
|
||||
// Use single knowledge base ID
|
||||
const knowledgeBaseIds = [params.knowledgeBaseId]
|
||||
|
||||
// Parse dynamic tag filters
|
||||
let structuredFilters: StructuredFilter[] = []
|
||||
if (params.tagFilters) {
|
||||
let tagFilters = params.tagFilters
|
||||
|
||||
// Handle both string (JSON) and array formats
|
||||
if (typeof tagFilters === 'string') {
|
||||
try {
|
||||
tagFilters = JSON.parse(tagFilters)
|
||||
} catch {
|
||||
tagFilters = []
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(tagFilters)) {
|
||||
// Send full filter objects with operator support
|
||||
structuredFilters = tagFilters
|
||||
.filter((filter: Record<string, unknown>) => {
|
||||
// For boolean, any value is valid; for others, check for non-empty string
|
||||
if (filter.fieldType === 'boolean') {
|
||||
return filter.tagName && filter.tagValue !== undefined
|
||||
}
|
||||
return filter.tagName && filter.tagValue && String(filter.tagValue).trim().length > 0
|
||||
})
|
||||
.map((filter: Record<string, unknown>) => ({
|
||||
tagName: filter.tagName as string,
|
||||
tagSlot: (filter.tagSlot as string) || '', // Will be resolved by API from tagName
|
||||
fieldType: (filter.fieldType as string) || 'text',
|
||||
operator: (filter.operator as string) || 'eq',
|
||||
value: filter.tagValue as string | number | boolean,
|
||||
valueTo: filter.valueTo as string | number | undefined,
|
||||
}))
|
||||
}
|
||||
}
|
||||
// Parse tag filters from various formats (array, JSON string)
|
||||
const structuredFilters = parseTagFilters(params.tagFilters)
|
||||
|
||||
const requestBody = {
|
||||
knowledgeBaseIds,
|
||||
|
||||
46
apps/sim/tools/params-resolver.ts
Normal file
46
apps/sim/tools/params-resolver.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
buildCanonicalIndex,
|
||||
type CanonicalIndex,
|
||||
evaluateSubBlockCondition,
|
||||
getCanonicalValues,
|
||||
isCanonicalPair,
|
||||
resolveCanonicalMode,
|
||||
type SubBlockCondition,
|
||||
} from '@/lib/workflows/subblocks/visibility'
|
||||
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
|
||||
|
||||
export {
|
||||
buildCanonicalIndex,
|
||||
type CanonicalIndex,
|
||||
evaluateSubBlockCondition,
|
||||
type SubBlockCondition,
|
||||
}
|
||||
|
||||
export interface ToolParamContext {
|
||||
blockType: string
|
||||
subBlocks: BlockSubBlockConfig[]
|
||||
canonicalIndex: CanonicalIndex
|
||||
values: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build preview context values for selectors that need dependency resolution.
|
||||
* Resolves canonical values so selectors get the correct credential/dependency values.
|
||||
*/
|
||||
export function buildPreviewContextValues(
|
||||
params: Record<string, unknown>,
|
||||
context: ToolParamContext
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = { ...params }
|
||||
|
||||
for (const [canonicalId, group] of Object.entries(context.canonicalIndex.groupsById)) {
|
||||
if (isCanonicalPair(group)) {
|
||||
const mode = resolveCanonicalMode(group, context.values)
|
||||
const { basicValue, advancedValue } = getCanonicalValues(group, context.values)
|
||||
result[canonicalId] =
|
||||
mode === 'advanced' ? (advancedValue ?? basicValue) : (basicValue ?? advancedValue)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,11 +1,216 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { StructuredFilter } from '@/lib/knowledge/types'
|
||||
import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format'
|
||||
import {
|
||||
evaluateSubBlockCondition,
|
||||
type SubBlockCondition,
|
||||
} from '@/lib/workflows/subblocks/visibility'
|
||||
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
|
||||
import type { ParameterVisibility, ToolConfig } from '@/tools/types'
|
||||
import { getTool } from '@/tools/utils'
|
||||
|
||||
const logger = createLogger('ToolsParams')
|
||||
type ToolParamDefinition = ToolConfig['params'][string]
|
||||
|
||||
/**
|
||||
* Checks if a value is non-empty (not undefined, null, or empty string)
|
||||
*/
|
||||
export function isNonEmpty(value: unknown): boolean {
|
||||
return value !== undefined && value !== null && value !== ''
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tag/Value Parsing Utilities
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Document tag entry format used in create_document tool
|
||||
*/
|
||||
export interface DocumentTagEntry {
|
||||
tagName: string
|
||||
value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag filter entry format used in search tool
|
||||
*/
|
||||
export interface TagFilterEntry {
|
||||
tagName: string
|
||||
tagSlot?: string
|
||||
tagValue: string | number | boolean
|
||||
fieldType?: string
|
||||
operator?: string
|
||||
valueTo?: string | number
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a tag value is effectively empty (unfilled/default entry)
|
||||
*/
|
||||
function isEmptyTagEntry(entry: Record<string, unknown>): boolean {
|
||||
if (!entry.tagName || (typeof entry.tagName === 'string' && entry.tagName.trim() === '')) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a tag-based value is effectively empty (only contains default/unfilled entries).
|
||||
* Works for both documentTags and tagFilters parameters in various formats.
|
||||
*
|
||||
* @param value - The tag value to check (can be JSON string, array, or object)
|
||||
* @returns true if the value is empty or only contains unfilled entries
|
||||
*/
|
||||
export function isEmptyTagValue(value: unknown): boolean {
|
||||
if (!value) return true
|
||||
|
||||
// Handle JSON string format
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
if (!Array.isArray(parsed)) return false
|
||||
if (parsed.length === 0) return true
|
||||
return parsed.every((entry: Record<string, unknown>) => isEmptyTagEntry(entry))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Handle array format directly
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return true
|
||||
return value.every((entry: Record<string, unknown>) => isEmptyTagEntry(entry))
|
||||
}
|
||||
|
||||
// Handle object format (LLM format: { "Category": "foo", "Priority": 5 })
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const entries = Object.entries(value)
|
||||
if (entries.length === 0) return true
|
||||
return entries.every(([, val]) => val === undefined || val === null || val === '')
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters valid document tags from an array, removing empty entries
|
||||
*/
|
||||
function filterValidDocumentTags(tags: unknown[]): DocumentTagEntry[] {
|
||||
return tags
|
||||
.filter((entry): entry is Record<string, unknown> => {
|
||||
if (typeof entry !== 'object' || entry === null) return false
|
||||
const e = entry as Record<string, unknown>
|
||||
if (!e.tagName || (typeof e.tagName === 'string' && e.tagName.trim() === '')) return false
|
||||
if (e.value === undefined || e.value === null || e.value === '') return false
|
||||
return true
|
||||
})
|
||||
.map((entry) => ({
|
||||
tagName: String(entry.tagName),
|
||||
value: String(entry.value),
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses document tags from various formats into a normalized array format.
|
||||
* Used by create_document tool to handle tags from both UI and LLM sources.
|
||||
*
|
||||
* @param value - Document tags in object, array, or JSON string format
|
||||
* @returns Normalized array of document tag entries, or empty array if invalid
|
||||
*/
|
||||
export function parseDocumentTags(value: unknown): DocumentTagEntry[] {
|
||||
if (!value) return []
|
||||
|
||||
// Handle object format from LLM: { "Category": "foo", "Priority": 5 }
|
||||
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
|
||||
return Object.entries(value)
|
||||
.filter(([tagName, tagValue]) => {
|
||||
if (!tagName || tagName.trim() === '') return false
|
||||
if (tagValue === undefined || tagValue === null || tagValue === '') return false
|
||||
return true
|
||||
})
|
||||
.map(([tagName, tagValue]) => ({
|
||||
tagName,
|
||||
value: String(tagValue),
|
||||
}))
|
||||
}
|
||||
|
||||
// Handle JSON string format from UI
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
if (Array.isArray(parsed)) {
|
||||
return filterValidDocumentTags(parsed)
|
||||
}
|
||||
} catch {
|
||||
// Invalid JSON, return empty
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// Handle array format directly
|
||||
if (Array.isArray(value)) {
|
||||
return filterValidDocumentTags(value)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses tag filters from various formats into a normalized StructuredFilter array.
|
||||
* Used by search tool to handle tag filters from both UI and LLM sources.
|
||||
*
|
||||
* @param value - Tag filters in array or JSON string format
|
||||
* @returns Normalized array of structured filters, or empty array if invalid
|
||||
*/
|
||||
export function parseTagFilters(value: unknown): StructuredFilter[] {
|
||||
if (!value) return []
|
||||
|
||||
let tagFilters = value
|
||||
|
||||
// Handle JSON string format
|
||||
if (typeof tagFilters === 'string') {
|
||||
try {
|
||||
tagFilters = JSON.parse(tagFilters)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Must be an array at this point
|
||||
if (!Array.isArray(tagFilters)) return []
|
||||
|
||||
return tagFilters
|
||||
.filter((filter): filter is Record<string, unknown> => {
|
||||
if (typeof filter !== 'object' || filter === null) return false
|
||||
const f = filter as Record<string, unknown>
|
||||
if (!f.tagName || (typeof f.tagName === 'string' && f.tagName.trim() === '')) return false
|
||||
if (f.fieldType === 'boolean') {
|
||||
return f.tagValue !== undefined
|
||||
}
|
||||
if (f.tagValue === undefined || f.tagValue === null) return false
|
||||
if (typeof f.tagValue === 'string' && f.tagValue.trim().length === 0) return false
|
||||
return true
|
||||
})
|
||||
.map((filter) => ({
|
||||
tagName: filter.tagName as string,
|
||||
tagSlot: (filter.tagSlot as string) || '',
|
||||
fieldType: (filter.fieldType as string) || 'text',
|
||||
operator: (filter.operator as string) || 'eq',
|
||||
value: filter.tagValue as string | number | boolean,
|
||||
valueTo: filter.valueTo as string | number | undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts parsed document tags to the format expected by the create document API.
|
||||
* Returns the documentTagsData JSON string if there are valid tags.
|
||||
*/
|
||||
export function formatDocumentTagsForAPI(tags: DocumentTagEntry[]): { documentTagsData?: string } {
|
||||
if (tags.length === 0) return {}
|
||||
return {
|
||||
documentTagsData: JSON.stringify(tags),
|
||||
}
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
label: string
|
||||
value: string
|
||||
@@ -39,7 +244,13 @@ export interface UIComponentConfig {
|
||||
multiple?: boolean
|
||||
multiSelect?: boolean
|
||||
maxSize?: number
|
||||
dependsOn?: string[]
|
||||
dependsOn?: string[] | { all?: string[]; any?: string[] }
|
||||
/** Canonical parameter ID if this is part of a canonical group */
|
||||
canonicalParamId?: string
|
||||
/** The mode of the source subblock (basic/advanced/both) */
|
||||
mode?: 'basic' | 'advanced' | 'both' | 'trigger'
|
||||
/** The actual subblock ID this config was derived from */
|
||||
actualSubBlockId?: string
|
||||
}
|
||||
|
||||
export interface SubBlockConfig {
|
||||
@@ -113,12 +324,6 @@ export interface ToolWithParameters {
|
||||
|
||||
let blockConfigCache: Record<string, BlockConfig> | null = null
|
||||
|
||||
const workflowInputFieldsCache = new Map<
|
||||
string,
|
||||
{ fields: Array<{ name: string; type: string }>; timestamp: number }
|
||||
>()
|
||||
const WORKFLOW_CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
function getBlockConfigurations(): Record<string, BlockConfig> {
|
||||
if (!blockConfigCache) {
|
||||
try {
|
||||
@@ -136,13 +341,56 @@ function getBlockConfigurations(): Record<string, BlockConfig> {
|
||||
return blockConfigCache
|
||||
}
|
||||
|
||||
function resolveSubBlockForParam(
|
||||
paramId: string,
|
||||
subBlocks: SubBlockConfig[],
|
||||
valuesWithOperation: Record<string, unknown>,
|
||||
paramType: string
|
||||
): BlockSubBlockConfig | undefined {
|
||||
const blockSubBlocks = subBlocks as BlockSubBlockConfig[]
|
||||
|
||||
// First pass: find subblock with matching condition
|
||||
let fallbackMatch: BlockSubBlockConfig | undefined
|
||||
|
||||
for (const sb of blockSubBlocks) {
|
||||
const matches = sb.id === paramId || sb.canonicalParamId === paramId
|
||||
if (!matches) continue
|
||||
|
||||
// Remember first match as fallback (for condition-based filtering in UI)
|
||||
if (!fallbackMatch) fallbackMatch = sb
|
||||
|
||||
if (
|
||||
!sb.condition ||
|
||||
evaluateSubBlockCondition(sb.condition as SubBlockCondition, valuesWithOperation)
|
||||
) {
|
||||
return sb
|
||||
}
|
||||
}
|
||||
|
||||
// Return fallback so its condition can be used for UI filtering
|
||||
if (fallbackMatch) return fallbackMatch
|
||||
|
||||
// Check if boolean param is part of a checkbox-list
|
||||
if (paramType === 'boolean') {
|
||||
return blockSubBlocks.find(
|
||||
(sb) =>
|
||||
sb.type === 'checkbox-list' &&
|
||||
Array.isArray(sb.options) &&
|
||||
(sb.options as Array<{ id?: string }>).some((opt) => opt.id === paramId)
|
||||
)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all parameters for a tool, categorized by their usage
|
||||
* Also includes UI component information from block configurations
|
||||
*/
|
||||
export function getToolParametersConfig(
|
||||
toolId: string,
|
||||
blockType?: string
|
||||
blockType?: string,
|
||||
currentValues?: Record<string, unknown>
|
||||
): ToolWithParameters | null {
|
||||
try {
|
||||
const toolConfig = getTool(toolId)
|
||||
@@ -209,6 +457,17 @@ export function getToolParametersConfig(
|
||||
blockConfig = blockConfigs[blockType] || null
|
||||
}
|
||||
|
||||
// Build values for condition evaluation
|
||||
// Operation should come from currentValues if provided, otherwise extract from toolId
|
||||
const values = currentValues || {}
|
||||
const valuesWithOperation = { ...values }
|
||||
if (valuesWithOperation.operation === undefined) {
|
||||
// Fallback: extract operation from tool ID (e.g., 'slack_message' -> 'message')
|
||||
const parts = toolId.split('_')
|
||||
valuesWithOperation.operation =
|
||||
parts.length >= 3 ? parts.slice(2).join('_') : parts[parts.length - 1]
|
||||
}
|
||||
|
||||
// Convert tool params to our standard format with UI component info
|
||||
const allParameters: ToolParameterConfig[] = Object.entries(toolConfig.params).map(
|
||||
([paramId, param]) => {
|
||||
@@ -221,63 +480,21 @@ export function getToolParametersConfig(
|
||||
default: param.default,
|
||||
}
|
||||
|
||||
// Add UI component information from block config if available
|
||||
if (blockConfig) {
|
||||
// For multi-operation tools, find the subblock that matches both the parameter ID
|
||||
// and the current tool operation
|
||||
let subBlock = blockConfig.subBlocks?.find((sb: SubBlockConfig) => {
|
||||
if (sb.id !== paramId) return false
|
||||
|
||||
// If there's a condition, check if it matches the current tool
|
||||
if (sb.condition && sb.condition.field === 'operation') {
|
||||
// First try exact match with full tool ID
|
||||
if (sb.condition.value === toolId) return true
|
||||
|
||||
// Then try extracting operation from tool ID
|
||||
// For tools like 'google_calendar_quick_add', extract 'quick_add'
|
||||
const parts = toolId.split('_')
|
||||
if (parts.length >= 3) {
|
||||
// Join everything after the provider prefix (e.g., 'google_calendar_')
|
||||
const operation = parts.slice(2).join('_')
|
||||
if (sb.condition.value === operation) return true
|
||||
}
|
||||
|
||||
// Fallback to last part only
|
||||
const operation = parts[parts.length - 1]
|
||||
return sb.condition.value === operation
|
||||
}
|
||||
|
||||
// If no condition, it's a global parameter (like apiKey)
|
||||
return !sb.condition
|
||||
})
|
||||
|
||||
// Fallback: if no operation-specific match, find any matching parameter
|
||||
if (!subBlock) {
|
||||
subBlock = blockConfig.subBlocks?.find((sb: SubBlockConfig) => sb.id === paramId)
|
||||
}
|
||||
|
||||
// Special case: Check if this boolean parameter is part of a checkbox-list
|
||||
if (!subBlock && param.type === 'boolean' && blockConfig) {
|
||||
// Look for a checkbox-list that includes this parameter as an option
|
||||
const checkboxListBlock = blockConfig.subBlocks?.find(
|
||||
(sb: SubBlockConfig) =>
|
||||
sb.type === 'checkbox-list' &&
|
||||
Array.isArray(sb.options) &&
|
||||
sb.options.some((opt: any) => opt.id === paramId)
|
||||
)
|
||||
|
||||
if (checkboxListBlock) {
|
||||
subBlock = checkboxListBlock
|
||||
}
|
||||
}
|
||||
const subBlock = resolveSubBlockForParam(
|
||||
paramId,
|
||||
blockConfig.subBlocks || [],
|
||||
valuesWithOperation,
|
||||
param.type
|
||||
)
|
||||
|
||||
if (subBlock) {
|
||||
toolParam.uiComponent = {
|
||||
type: subBlock.type,
|
||||
options: subBlock.options,
|
||||
options: subBlock.options as Option[] | undefined,
|
||||
placeholder: subBlock.placeholder,
|
||||
password: subBlock.password,
|
||||
condition: subBlock.condition,
|
||||
condition: subBlock.condition as ComponentCondition | undefined,
|
||||
title: subBlock.title,
|
||||
value: subBlock.value,
|
||||
serviceId: subBlock.serviceId,
|
||||
@@ -290,10 +507,13 @@ export function getToolParametersConfig(
|
||||
integer: subBlock.integer,
|
||||
language: subBlock.language,
|
||||
generationType: subBlock.generationType,
|
||||
acceptedTypes: subBlock.acceptedTypes,
|
||||
acceptedTypes: subBlock.acceptedTypes ? [subBlock.acceptedTypes] : undefined,
|
||||
multiple: subBlock.multiple,
|
||||
maxSize: subBlock.maxSize,
|
||||
dependsOn: subBlock.dependsOn,
|
||||
canonicalParamId: subBlock.canonicalParamId,
|
||||
mode: subBlock.mode,
|
||||
actualSubBlockId: subBlock.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -396,20 +616,41 @@ export async function createLLMToolSchema(
|
||||
|
||||
// Only include parameters that the LLM should/can provide
|
||||
for (const [paramId, param] of Object.entries(toolConfig.params)) {
|
||||
// Check if this param has schema enrichment config
|
||||
const enrichmentConfig = toolConfig.schemaEnrichment?.[paramId]
|
||||
|
||||
// Special handling for workflow_executor's inputMapping parameter
|
||||
// Always include in LLM schema so LLM can provide dynamic input values
|
||||
// even if user has configured empty/partial inputMapping in the UI
|
||||
const isWorkflowInputMapping =
|
||||
toolConfig.id === 'workflow_executor' && paramId === 'inputMapping'
|
||||
|
||||
if (!isWorkflowInputMapping) {
|
||||
const isUserProvided =
|
||||
userProvidedParams[paramId] !== undefined &&
|
||||
userProvidedParams[paramId] !== null &&
|
||||
userProvidedParams[paramId] !== ''
|
||||
// Parameters with enrichment config are treated specially:
|
||||
// - Include them if dependency value is available (even if normally hidden)
|
||||
// - Skip them if dependency value is not available
|
||||
if (enrichmentConfig) {
|
||||
const dependencyValue = userProvidedParams[enrichmentConfig.dependsOn] as string
|
||||
if (!dependencyValue) {
|
||||
continue
|
||||
}
|
||||
|
||||
const propertySchema = buildParameterSchema(toolConfig.id, paramId, param)
|
||||
const enrichedSchema = await enrichmentConfig.enrichSchema(dependencyValue)
|
||||
|
||||
if (enrichedSchema) {
|
||||
Object.assign(propertySchema, enrichedSchema)
|
||||
schema.properties[paramId] = propertySchema
|
||||
|
||||
if (param.required) {
|
||||
schema.required.push(paramId)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (!isWorkflowInputMapping) {
|
||||
// Skip parameters that user has already provided
|
||||
if (isUserProvided) {
|
||||
if (isNonEmpty(userProvidedParams[paramId])) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -479,18 +720,12 @@ async function applyDynamicSchemaForWorkflow(
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to fetch workflow input fields with caching
|
||||
* Fetches workflow input fields from the API.
|
||||
* No local caching - relies on React Query caching on the client side.
|
||||
*/
|
||||
async function fetchWorkflowInputFields(
|
||||
workflowId: string
|
||||
): Promise<Array<{ name: string; type: string }>> {
|
||||
const cached = workflowInputFieldsCache.get(workflowId)
|
||||
const now = Date.now()
|
||||
|
||||
if (cached && now - cached.timestamp < WORKFLOW_CACHE_TTL) {
|
||||
return cached.fields
|
||||
}
|
||||
|
||||
try {
|
||||
const { buildAuthHeaders, buildAPIUrl } = await import('@/executor/utils/http')
|
||||
|
||||
@@ -503,10 +738,7 @@ async function fetchWorkflowInputFields(
|
||||
}
|
||||
|
||||
const { data } = await response.json()
|
||||
const fields = extractInputFieldsFromBlocks(data?.state?.blocks)
|
||||
workflowInputFieldsCache.set(workflowId, { fields, timestamp: now })
|
||||
|
||||
return fields
|
||||
return extractInputFieldsFromBlocks(data?.state?.blocks)
|
||||
} catch (error) {
|
||||
logger.error('Error fetching workflow input fields:', error)
|
||||
return []
|
||||
@@ -591,8 +823,7 @@ export function deepMergeInputMapping(
|
||||
|
||||
for (const [key, userValue] of Object.entries(parsedUserMapping)) {
|
||||
// Only override LLM value if user provided a non-empty value
|
||||
// Note: Using strict inequality (!==) so 0 and false are correctly preserved
|
||||
if (userValue !== undefined && userValue !== null && userValue !== '') {
|
||||
if (isNonEmpty(userValue)) {
|
||||
merged[key] = userValue
|
||||
}
|
||||
}
|
||||
@@ -612,11 +843,15 @@ export function mergeToolParameters(
|
||||
userProvidedParams: Record<string, unknown>,
|
||||
llmGeneratedParams: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
// Filter out empty strings from user-provided params
|
||||
// Filter out empty and effectively-empty values from user-provided params
|
||||
// so that cleared fields don't override LLM values
|
||||
const filteredUserParams: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(userProvidedParams)) {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (isNonEmpty(value)) {
|
||||
// Skip tag-based params if they're effectively empty (only default/unfilled entries)
|
||||
if ((key === 'documentTags' || key === 'tagFilters') && isEmptyTagValue(value)) {
|
||||
continue
|
||||
}
|
||||
filteredUserParams[key] = value
|
||||
}
|
||||
}
|
||||
@@ -664,11 +899,7 @@ export function filterSchemaForLLM(
|
||||
|
||||
// Remove user-provided parameters from the schema
|
||||
Object.keys(userProvidedParams).forEach((paramKey) => {
|
||||
if (
|
||||
userProvidedParams[paramKey] !== undefined &&
|
||||
userProvidedParams[paramKey] !== null &&
|
||||
userProvidedParams[paramKey] !== ''
|
||||
) {
|
||||
if (isNonEmpty(userProvidedParams[paramKey])) {
|
||||
delete filteredProperties[paramKey]
|
||||
const reqIndex = filteredRequired.indexOf(paramKey)
|
||||
if (reqIndex > -1) {
|
||||
|
||||
127
apps/sim/tools/schema-enrichers.ts
Normal file
127
apps/sim/tools/schema-enrichers.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
|
||||
const logger = createLogger('SchemaEnrichers')
|
||||
|
||||
interface TagDefinition {
|
||||
id: string
|
||||
tagSlot: string
|
||||
displayName: string
|
||||
fieldType: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps KB field types to JSON schema types
|
||||
*/
|
||||
function mapFieldTypeToSchemaType(fieldType: string): string {
|
||||
switch (fieldType) {
|
||||
case 'number':
|
||||
return 'number'
|
||||
case 'boolean':
|
||||
return 'boolean'
|
||||
case 'date':
|
||||
case 'text':
|
||||
default:
|
||||
return 'string'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches tag definitions from knowledge base
|
||||
*/
|
||||
async function fetchTagDefinitions(knowledgeBaseId: string): Promise<TagDefinition[]> {
|
||||
try {
|
||||
const { buildAuthHeaders, buildAPIUrl } = await import('@/executor/utils/http')
|
||||
|
||||
const headers = await buildAuthHeaders()
|
||||
const url = buildAPIUrl(`/api/knowledge/${knowledgeBaseId}/tag-definitions`)
|
||||
|
||||
logger.info(`Fetching tag definitions for KB ${knowledgeBaseId} from ${url.toString()}`)
|
||||
|
||||
const response = await fetch(url.toString(), { headers })
|
||||
if (!response.ok) {
|
||||
logger.warn(`Failed to fetch tag definitions for KB ${knowledgeBaseId}: ${response.status}`)
|
||||
return []
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const tagDefinitions = result.data || []
|
||||
logger.info(`Found ${tagDefinitions.length} tag definitions for KB ${knowledgeBaseId}`)
|
||||
return tagDefinitions
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch tag definitions:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches KB tag definitions and builds a schema for LLM consumption.
|
||||
* Returns an object schema where each property is a tag the LLM can set.
|
||||
*/
|
||||
export async function enrichKBTagsSchema(knowledgeBaseId: string): Promise<{
|
||||
type: string
|
||||
properties?: Record<string, { type: string; description?: string }>
|
||||
description?: string
|
||||
required?: string[]
|
||||
} | null> {
|
||||
const tagDefinitions = await fetchTagDefinitions(knowledgeBaseId)
|
||||
|
||||
if (tagDefinitions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const properties: Record<string, { type: string; description?: string }> = {}
|
||||
const tagDescriptions: string[] = []
|
||||
|
||||
for (const def of tagDefinitions) {
|
||||
const schemaType = mapFieldTypeToSchemaType(def.fieldType)
|
||||
|
||||
properties[def.displayName] = {
|
||||
type: schemaType,
|
||||
description: `${def.fieldType} tag`,
|
||||
}
|
||||
tagDescriptions.push(`${def.displayName} (${def.fieldType})`)
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
properties,
|
||||
description: `Document tags. Available tags: ${tagDescriptions.join(', ')}`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches KB tag definitions and builds a schema for tag filters.
|
||||
* Returns an array schema where each item is a filter with tagName and tagValue.
|
||||
*/
|
||||
export async function enrichKBTagFiltersSchema(knowledgeBaseId: string): Promise<{
|
||||
type: string
|
||||
items?: Record<string, unknown>
|
||||
description?: string
|
||||
} | null> {
|
||||
const tagDefinitions = await fetchTagDefinitions(knowledgeBaseId)
|
||||
|
||||
if (tagDefinitions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const tagDescriptions = tagDefinitions.map((def) => `${def.displayName} (${def.fieldType})`)
|
||||
|
||||
return {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tagName: {
|
||||
type: 'string',
|
||||
description: `Name of the tag to filter by. Available: ${tagDescriptions.join(', ')}`,
|
||||
},
|
||||
tagValue: {
|
||||
type: 'string',
|
||||
description: 'Value to filter by',
|
||||
},
|
||||
},
|
||||
required: ['tagName', 'tagValue'],
|
||||
},
|
||||
description: `Tag filters for search. Available tags: ${tagDescriptions.join(', ')}`,
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,12 @@ export interface ToolConfig<P = any, R = any> {
|
||||
* If provided, this will be called instead of making an HTTP request.
|
||||
*/
|
||||
directExecution?: (params: P) => Promise<ToolResponse>
|
||||
|
||||
/**
|
||||
* Optional dynamic schema enrichment for specific params.
|
||||
* Maps param IDs to their enrichment configuration.
|
||||
*/
|
||||
schemaEnrichment?: Record<string, SchemaEnrichmentConfig>
|
||||
}
|
||||
|
||||
export interface TableRow {
|
||||
@@ -137,3 +143,19 @@ export interface ToolFileData {
|
||||
url?: string // URL to download file from
|
||||
size?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for dynamically enriching a parameter's schema at runtime.
|
||||
* Used when a parameter's schema depends on runtime values (e.g., KB tags, workflow inputs).
|
||||
*/
|
||||
export interface SchemaEnrichmentConfig {
|
||||
/** The param ID that this enrichment depends on (e.g., 'knowledgeBaseId', 'workflowId') */
|
||||
dependsOn: string
|
||||
/** Function to fetch and build dynamic schema based on the dependency value */
|
||||
enrichSchema: (dependencyValue: string) => Promise<{
|
||||
type: string
|
||||
properties?: Record<string, { type: string; description?: string }>
|
||||
description?: string
|
||||
required?: string[]
|
||||
} | null>
|
||||
}
|
||||
|
||||
@@ -16,10 +16,42 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: { firstName: 'John', lastName: 'Doe', age: 30 },
|
||||
triggerType: 'api',
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
it.concurrent('should use deployed state when isDeployedContext is true', () => {
|
||||
const params = {
|
||||
workflowId: 'test-workflow-id',
|
||||
inputMapping: { name: 'Test' },
|
||||
_context: { isDeployedContext: true },
|
||||
}
|
||||
|
||||
const result = buildBody(params)
|
||||
|
||||
expect(result).toEqual({
|
||||
input: { name: 'Test' },
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
})
|
||||
})
|
||||
|
||||
it.concurrent('should use draft state when isDeployedContext is false', () => {
|
||||
const params = {
|
||||
workflowId: 'test-workflow-id',
|
||||
inputMapping: { name: 'Test' },
|
||||
_context: { isDeployedContext: false },
|
||||
}
|
||||
|
||||
const result = buildBody(params)
|
||||
|
||||
expect(result).toEqual({
|
||||
input: { name: 'Test' },
|
||||
triggerType: 'api',
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
it.concurrent('should parse JSON string inputMapping (UI-provided via tool-input)', () => {
|
||||
const params = {
|
||||
workflowId: 'test-workflow-id',
|
||||
@@ -31,7 +63,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: { firstName: 'John', lastName: 'Doe' },
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,7 +78,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: { user: { name: 'John', email: 'john@example.com' }, count: 5 },
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -61,7 +93,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: { tags: ['a', 'b', 'c'], ids: [1, 2, 3] },
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -76,7 +108,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: {},
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -91,7 +123,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: {},
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -106,7 +138,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: {},
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -121,7 +153,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: {},
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -136,7 +168,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: {},
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -151,7 +183,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: {},
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -166,7 +198,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: { message: 'Hello\nWorld', path: 'C:\\Users' },
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -181,7 +213,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: { greeting: 'こんにちは', emoji: '👋' },
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -196,7 +228,7 @@ describe('workflowExecutorTool', () => {
|
||||
expect(result).toEqual({
|
||||
input: { data: '{"nested": "json"}' },
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -42,10 +42,12 @@ export const workflowExecutorTool: ToolConfig<
|
||||
inputData = {}
|
||||
}
|
||||
}
|
||||
// Use draft state for manual runs (not deployed), deployed state for deployed runs
|
||||
const isDeployedContext = params._context?.isDeployedContext
|
||||
return {
|
||||
input: inputData,
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
useDraftState: !isDeployedContext,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,6 +4,13 @@ export interface WorkflowExecutorParams {
|
||||
workflowId: string
|
||||
/** Can be a JSON string (from tool-input UI) or an object (from LLM args) */
|
||||
inputMapping?: Record<string, any> | string
|
||||
/** Execution context passed by handlers */
|
||||
_context?: {
|
||||
workflowId?: string
|
||||
workspaceId?: string
|
||||
executionId?: string
|
||||
isDeployedContext?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface WorkflowExecutorResponse extends ToolResponse {
|
||||
|
||||
Reference in New Issue
Block a user