Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion apps/docs/content/docs/integrations/knowledge.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ Search for similar content in a knowledge base using vector similarity

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base to search in |
| `knowledgeBaseId` | string | No | ID of the knowledge base to search in. Optional only when folderPath is supplied: provide either knowledgeBaseId or folderPath. |
| `folderPath` | string | No | Search every knowledge base in this folder, resolved when the workflow runs so a knowledge base added later is included. Provide either folderPath or knowledgeBaseId. Canonical folder path, percent-encoded, e.g. "/Support/Tier%201". The workspace root is "/". |
| `folderIncludeSubfolders` | boolean | No | Extend the folder scope to its subfolders. Off by default, so a folder means only the knowledge bases directly inside it. |
| `query` | string | No | Search query text \(optional when using tag filters\) |
| `topK` | number | No | Number of most similar results to return \(1-100\) |
| `tagFilters` | array | No | Array of tag filters with tagName and tagValue properties |
Expand Down Expand Up @@ -429,4 +431,79 @@ Trigger a manual sync for a knowledge base connector
| `connectorId` | string | ID of the connector that was synced |
| `message` | string | Status message from the sync trigger |

### List Knowledge Folders

List what is inside a knowledge folder: its subfolders and its knowledge bases together. Lists direct children by default; set Recursive to walk the whole subtree.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `path` | string | No | Folder to list. Omit to list from the workspace root. Canonical folder path, percent-encoded, e.g. "/Reports/Q3%20Results". The workspace root is "/". |
| `recursive` | boolean | No | List everything beneath the path rather than only its direct children. Each entry carries its depth below the listed folder. |
| `depth` | number | No | Deepest level to include when recursive, counted from the listed folder. 1 is direct children. |
| `search` | string | No | Case-insensitive substring match against an entry name. Filters the result, so a deep match is still reported even when its parent folders do not match. |
| `limit` | number | No | Most entries to return, 200 by default. A listing cut short comes back with truncated set. |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `path` | string | The folder that was listed. |
| `entries` | array | What the folder holds. Each entry has kind "folder" or "knowledge_base", a name, and its depth below the listed folder. A folder carries its own canonical path; a knowledge base carries its id, description, document and token counts, and the canonical path of the folder holding it. |
| `truncated` | boolean | True when the limit cut the listing short, so more entries exist. |

### Create Knowledge Folder

Create a knowledge folder at a path. Parent folders are created as needed. Fails if a folder already exists at the path.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `path` | string | Yes | Path of the folder to create. Canonical folder path, percent-encoded, e.g. "/Reports/Q3%20Results". The workspace root is "/". |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `folder` | object | The created folder, with its id, name, canonical path, parent path, and timestamps. |

### Move Knowledge Folder

Move or rename a knowledge folder by giving its full destination path. Everything inside the folder moves with it.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `path` | string | Yes | Folder to move. Canonical folder path, percent-encoded, e.g. "/Reports/Q3%20Results". The workspace root is "/". |
| `destinationPath` | string | Yes | Full path the folder should have afterwards. Renaming is a destination whose parent is unchanged. Canonical folder path, percent-encoded, e.g. "/Reports/Q3%20Results". The workspace root is "/". |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `folder` | object | The folder at its new path. |
| `previousPath` | string | The path the folder had before the move. |

### Delete Knowledge Folder

Delete a knowledge folder. Deleting a folder that still has contents requires the recursive option.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `path` | string | Yes | Folder to delete. Canonical folder path, percent-encoded, e.g. "/Reports/Q3%20Results". The workspace root is "/". |
| `recursive` | boolean | No | Also delete the folder’s nested folders and knowledge bases. Without it, deleting a non-empty folder fails. |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `path` | string | The folder that was deleted. |
| `deleted` | boolean | Always true when the operation succeeded. |
| `deletedItems` | object | Counts of the folders and knowledge bases deleted alongside it. |


Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import { X } from '@sim/emcn/icons'
import { useQueries } from '@tanstack/react-query'
import { useParams } from 'next/navigation'
import { PackageSearchIcon } from '@/components/icons'
import { ROOT_FOLDER_PATH } from '@/lib/folders/paths'
import { collectFolderDepths } from '@/lib/folders/subtree'
import type { KnowledgeBaseData } from '@/lib/knowledge/types'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { readFolderPath } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sim-folder-tree-selector/selection'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useResourceFolders } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-resource-folders'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
import type { SubBlockConfig } from '@/blocks/types'
Expand All @@ -25,6 +29,11 @@ interface KnowledgeBaseSelectorProps {
onKnowledgeBaseSelect?: (knowledgeBaseId: string | string[]) => void
isPreview?: boolean
previewValue?: string | null
/**
* A sibling folder field that narrows what this picker offers, and the switch
* saying whether that scope descends. See `SubBlockConfig.folderScope`.
*/
folderScope?: NonNullable<SubBlockConfig['folderScope']>
}

export function KnowledgeBaseSelector({
Expand All @@ -34,6 +43,7 @@ export function KnowledgeBaseSelector({
onKnowledgeBaseSelect,
isPreview = false,
previewValue,
folderScope,
}: KnowledgeBaseSelectorProps) {
const activeSearchTarget = useActiveSearchTarget()
const params = useParams()
Expand Down Expand Up @@ -81,18 +91,114 @@ export function KnowledgeBaseSelector({
})),
})

/*
* A sibling folder field narrows what this picker offers. Choosing a folder
* means the run only searches that folder, so listing knowledge bases from
* anywhere else would let a selection be built that the operation then
* ignores — the picker has to describe the same set the run will read.
*
* Falling back to this control's own id keeps the hook call unconditional for
* a picker with no folder scope; its own value is never a folder path, so the
* scope reads as absent.
*/
const resourceFolders = useResourceFolders(workspaceId, 'knowledge_base')
const [folderScopeValue] = useSubBlockValue<unknown>(blockId, folderScope?.fieldId ?? subBlock.id)
Comment thread
mzxchandra marked this conversation as resolved.
/*
* The advanced half of the same canonical pair. Only one half is ever filled —
* whichever mode the field is in — so reading just the basic id would see an
* empty value for an advanced-mode user and drop the scope entirely.
*/
const [manualFolderScopeValue] = useSubBlockValue<unknown>(
blockId,
folderScope?.manualFieldId ?? folderScope?.fieldId ?? subBlock.id
)
const [folderScopeRecursive] = useSubBlockValue<unknown>(
blockId,
folderScope?.recursiveFieldId ?? subBlock.id
)
/*
* `readFolderPath` is the shared parser the block's own params transformer
* uses. Reading the raw string here instead would miss the legacy array and
* JSON-array forms a persisted value can still take, and the picker would
* then scope differently from the run.
*/
const folderScopePath = folderScope
? readFolderPath(folderScopeValue) || readFolderPath(manualFolderScopeValue)
Comment thread
mzxchandra marked this conversation as resolved.
: ''
const folderScopeIncludesSubfolders =
folderScopeRecursive === true || folderScopeRecursive === 'true'

/**
* The folder ids the scope covers, walked through `parentId` rather than
* compared as path strings — a folder genuinely named `Q3/Q4` is one level in
* either spelling and only a parent walk sees that. An unresolvable path
* covers nothing, which reads as an empty picker rather than an unfiltered
* one.
*/
const folderScopeState = useMemo(():
| { kind: 'none' }
| { kind: 'pending' }
| { kind: 'unresolved' }
| { kind: 'resolved'; ids: Set<string> } => {
if (!folderScopePath || folderScopePath === ROOT_FOLDER_PATH) return { kind: 'none' }
/*
* Loading is not the same as empty. The knowledge-base query can resolve
* while the folder query is still in flight, and treating that window as an
* empty scope makes every selection look out of scope — which the cleanup
* below would act on and erase.
*/
if (resourceFolders.isLoading) return { kind: 'pending' }
const root = resourceFolders.byPath.get(folderScopePath)
Comment thread
mzxchandra marked this conversation as resolved.
Comment thread
mzxchandra marked this conversation as resolved.
if (!root) return { kind: 'unresolved' }
const ids = new Set<string>([root.id])
if (folderScopeIncludesSubfolders) {
for (const id of collectFolderDepths(resourceFolders.folders, root.id).keys()) ids.add(id)
}
return { kind: 'resolved', ids }
}, [folderScopePath, folderScopeIncludesSubfolders, resourceFolders])

/**
* Whether a knowledge base survives the folder scope.
*
* `pending` offers everything rather than flashing an empty picker while the
* folder query lands; `unresolved` offers nothing, because a path that names
* no folder selects no knowledge bases.
*/
const isInFolderScope = useCallback(
(knowledgeBase: { folderId: string | null }) => {
switch (folderScopeState.kind) {
case 'none':
case 'pending':
return true
case 'unresolved':
return false
case 'resolved':
return knowledgeBase.folderId !== null && folderScopeState.ids.has(knowledgeBase.folderId)
}
},
[folderScopeState]
)

const combinedKnowledgeBases = useMemo<KnowledgeBaseData[]>(() => {
const merged = new Map<string, KnowledgeBaseData>()
knowledgeBases.forEach((kb) => merged.set(kb.id, kb))
knowledgeBases.forEach((kb) => {
if (isInFolderScope(kb)) merged.set(kb.id, kb)
})

/*
* An already-chosen knowledge base stays listed even when the folder scope
* excludes it, or its chip loses its label. It is not silently dropped
* either: a picked knowledge base is what the run searches, the card
* sentence names it, and the Folder field says so.
*/
selectedKnowledgeBaseQueries.forEach((query) => {
if (query.data) {
merged.set(query.data.id, query.data)
}
})

return Array.from(merged.values())
}, [knowledgeBases, selectedKnowledgeBaseQueries])
}, [knowledgeBases, selectedKnowledgeBaseQueries, isInFolderScope])

/**
* Display names, with the folder path appended when two knowledge bases share
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ vi.mock(
ScheduleInfo: stubInput('schedule-info'),
SelectorInput: stubInput('selector-input'),
ShortInput: stubInput('short-input'),
SimFolderTreeSelector: stubInput('sim-folder-tree-selector'),
SkillInput: stubInput('skill-input'),
SliderInput: stubInput('slider-input'),
SortBuilder: stubInput('sort-builder'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,7 @@ function SubBlockComponent({
blockId={blockId}
subBlock={config}
disabled={isDisabled}
folderScope={config.folderScope}
isPreview={isPreview}
previewValue={previewValue as any}
/>
Expand Down
Loading