Explorar el Código

feat(goto-anything): add RAG pipeline node search (#25948)

tags/1.9.0
GuanMu hace 1 mes
padre
commit
ab910c736c
No account linked to committer's email address

+ 31
- 1
web/app/components/goto-anything/actions/index.ts Ver fichero

import { knowledgeAction } from './knowledge' import { knowledgeAction } from './knowledge'
import { pluginAction } from './plugin' import { pluginAction } from './plugin'
import { workflowNodesAction } from './workflow-nodes' import { workflowNodesAction } from './workflow-nodes'
import { ragPipelineNodesAction } from './rag-pipeline-nodes'
import type { ActionItem, SearchResult } from './types' import type { ActionItem, SearchResult } from './types'
import { slashAction } from './commands' import { slashAction } from './commands'
import { slashCommandRegistry } from './commands/registry' import { slashCommandRegistry } from './commands/registry'


// Create dynamic Actions based on context
export const createActions = (isWorkflowPage: boolean, isRagPipelinePage: boolean) => {
const baseActions = {
slash: slashAction,
app: appAction,
knowledge: knowledgeAction,
plugin: pluginAction,
}

// Add appropriate node search based on context
if (isRagPipelinePage) {
return {
...baseActions,
node: ragPipelineNodesAction,
}
}
else if (isWorkflowPage) {
return {
...baseActions,
node: workflowNodesAction,
}
}

// Default actions without node search
return baseActions
}

// Legacy export for backward compatibility
export const Actions = { export const Actions = {
slash: slashAction, slash: slashAction,
app: appAction, app: appAction,
locale: string, locale: string,
query: string, query: string,
actionItem?: ActionItem, actionItem?: ActionItem,
dynamicActions?: Record<string, ActionItem>,
): Promise<SearchResult[]> => { ): Promise<SearchResult[]> => {
if (actionItem) { if (actionItem) {
const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim() const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
if (query.startsWith('@') || query.startsWith('/')) if (query.startsWith('@') || query.startsWith('/'))
return [] return []


const globalSearchActions = Object.values(Actions)
const globalSearchActions = Object.values(dynamicActions || Actions)


// Use Promise.allSettled to handle partial failures gracefully // Use Promise.allSettled to handle partial failures gracefully
const searchPromises = globalSearchActions.map(async (action) => { const searchPromises = globalSearchActions.map(async (action) => {

+ 24
- 0
web/app/components/goto-anything/actions/rag-pipeline-nodes.tsx Ver fichero

import type { ActionItem } from './types'

// Create the RAG pipeline nodes action
export const ragPipelineNodesAction: ActionItem = {
key: '@node',
shortcut: '@node',
title: 'Search RAG Pipeline Nodes',
description: 'Find and jump to nodes in the current RAG pipeline by name or type',
searchFn: undefined, // Will be set by useRagPipelineSearch hook
search: async (_, searchTerm = '', _locale) => {
try {
// Use the searchFn if available (set by useRagPipelineSearch hook)
if (ragPipelineNodesAction.searchFn)
return ragPipelineNodesAction.searchFn(searchTerm)

// If not in RAG pipeline context, return empty array
return []
}
catch (error) {
console.warn('RAG pipeline nodes search failed:', error)
return []
}
},
}

+ 1
- 1
web/app/components/goto-anything/actions/workflow-nodes.tsx Ver fichero

title: 'Search Workflow Nodes', title: 'Search Workflow Nodes',
description: 'Find and jump to nodes in the current workflow by name or type', description: 'Find and jump to nodes in the current workflow by name or type',
searchFn: undefined, // Will be set by useWorkflowSearch hook searchFn: undefined, // Will be set by useWorkflowSearch hook
search: async (_, searchTerm = '', locale) => {
search: async (_, searchTerm = '', _locale) => {
try { try {
// Use the searchFn if available (set by useWorkflowSearch hook) // Use the searchFn if available (set by useWorkflowSearch hook)
if (workflowNodesAction.searchFn) if (workflowNodesAction.searchFn)

+ 20
- 4
web/app/components/goto-anything/context.tsx Ver fichero

* Whether the current page is a workflow page * Whether the current page is a workflow page
*/ */
isWorkflowPage: boolean isWorkflowPage: boolean
/**
* Whether the current page is a RAG pipeline page
*/
isRagPipelinePage: boolean
} }


// Create context with default values // Create context with default values
const GotoAnythingContext = createContext<GotoAnythingContextType>({ const GotoAnythingContext = createContext<GotoAnythingContextType>({
isWorkflowPage: false, isWorkflowPage: false,
isRagPipelinePage: false,
}) })


/** /**
*/ */
export const GotoAnythingProvider: React.FC<GotoAnythingProviderProps> = ({ children }) => { export const GotoAnythingProvider: React.FC<GotoAnythingProviderProps> = ({ children }) => {
const [isWorkflowPage, setIsWorkflowPage] = useState(false) const [isWorkflowPage, setIsWorkflowPage] = useState(false)
const [isRagPipelinePage, setIsRagPipelinePage] = useState(false)
const pathname = usePathname() const pathname = usePathname()


// Update context based on current pathname
// Update context based on current pathname using more robust route matching
useEffect(() => { useEffect(() => {
// Check if current path contains workflow
const isWorkflow = pathname?.includes('/workflow') || false
if (!pathname) {
setIsWorkflowPage(false)
setIsRagPipelinePage(false)
return
}

// Workflow pages: /app/[appId]/workflow or /workflow/[token] (shared)
const isWorkflow = /^\/app\/[^/]+\/workflow$/.test(pathname) || /^\/workflow\/[^/]+$/.test(pathname)
// RAG Pipeline pages: /datasets/[datasetId]/pipeline
const isRagPipeline = /^\/datasets\/[^/]+\/pipeline$/.test(pathname)

setIsWorkflowPage(isWorkflow) setIsWorkflowPage(isWorkflow)
setIsRagPipelinePage(isRagPipeline)
}, [pathname]) }, [pathname])


return ( return (
<GotoAnythingContext.Provider value={{ isWorkflowPage }}>
<GotoAnythingContext.Provider value={{ isWorkflowPage, isRagPipelinePage }}>
{children} {children}
</GotoAnythingContext.Provider> </GotoAnythingContext.Provider>
) )

+ 22
- 24
web/app/components/goto-anything/index.tsx Ver fichero

import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app/components/workflow/utils/common' import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app/components/workflow/utils/common'
import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation' import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation'
import { RiSearchLine } from '@remixicon/react' import { RiSearchLine } from '@remixicon/react'
import { Actions as AllActions, type SearchResult, matchAction, searchAnything } from './actions'
import { type SearchResult, createActions, matchAction, searchAnything } from './actions'
import { GotoAnythingProvider, useGotoAnythingContext } from './context' import { GotoAnythingProvider, useGotoAnythingContext } from './context'
import { slashCommandRegistry } from './actions/commands/registry' import { slashCommandRegistry } from './actions/commands/registry'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
}) => { }) => {
const router = useRouter() const router = useRouter()
const defaultLocale = useGetLanguage() const defaultLocale = useGetLanguage()
const { isWorkflowPage } = useGotoAnythingContext()
const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext()
const { t } = useTranslation() const { t } = useTranslation()
const [show, setShow] = useState<boolean>(false) const [show, setShow] = useState<boolean>(false)
const [searchQuery, setSearchQuery] = useState<string>('') const [searchQuery, setSearchQuery] = useState<string>('')


// Filter actions based on context // Filter actions based on context
const Actions = useMemo(() => { const Actions = useMemo(() => {
// Create a filtered copy of actions based on current page context
if (isWorkflowPage) {
// Include all actions on workflow pages
return AllActions
}
else {
const { app, knowledge, plugin, slash } = AllActions
return { app, knowledge, plugin, slash }
}
}, [isWorkflowPage])
// Create actions based on current page context
return createActions(isWorkflowPage, isRagPipelinePage)
}, [isWorkflowPage, isRagPipelinePage])


const [activePlugin, setActivePlugin] = useState<Plugin>() const [activePlugin, setActivePlugin] = useState<Plugin>()




const query = searchQueryDebouncedValue.toLowerCase() const query = searchQueryDebouncedValue.toLowerCase()
const action = matchAction(query, Actions) const action = matchAction(query, Actions)
return action
? (action.key === '/' ? '@command' : action.key)
: 'general'

if (!action)
return 'general'

return action.key === '/' ? '@command' : action.key
}, [searchQueryDebouncedValue, Actions, isCommandsMode, searchQuery]) }, [searchQueryDebouncedValue, Actions, isCommandsMode, searchQuery])


const { data: searchResults = [], isLoading, isError, error } = useQuery( const { data: searchResults = [], isLoading, isError, error } = useQuery(
searchQueryDebouncedValue, searchQueryDebouncedValue,
searchMode, searchMode,
isWorkflowPage, isWorkflowPage,
isRagPipelinePage,
defaultLocale, defaultLocale,
Object.keys(Actions).sort().join(','), Object.keys(Actions).sort().join(','),
], ],
queryFn: async () => { queryFn: async () => {
const query = searchQueryDebouncedValue.toLowerCase() const query = searchQueryDebouncedValue.toLowerCase()
const action = matchAction(query, Actions) const action = matchAction(query, Actions)
return await searchAnything(defaultLocale, query, action)
return await searchAnything(defaultLocale, query, action, Actions)
}, },
enabled: !!searchQueryDebouncedValue && !isCommandsMode, enabled: !!searchQueryDebouncedValue && !isCommandsMode,
staleTime: 30000, staleTime: 30000,
) : ( ) : (
<> <>
<span className='opacity-60'> <span className='opacity-60'>
{isCommandsMode
? t('app.gotoAnything.selectToNavigate')
: searchQuery.trim()
? t('app.gotoAnything.searching')
: t('app.gotoAnything.startTyping')
}
{(() => {
if (isCommandsMode)
return t('app.gotoAnything.selectToNavigate')

if (searchQuery.trim())
return t('app.gotoAnything.searching')

return t('app.gotoAnything.startTyping')
})()}
</span> </span>
<span className='opacity-60'> <span className='opacity-60'>
{searchQuery.trim() || isCommandsMode {searchQuery.trim() || isCommandsMode
? t('app.gotoAnything.tips') ? t('app.gotoAnything.tips')
: t('app.gotoAnything.pressEscToClose')
}
: t('app.gotoAnything.pressEscToClose')}
</span> </span>
</> </>
)} )}

+ 4
- 0
web/app/components/rag-pipeline/components/rag-pipeline-children.tsx Ver fichero

} from '@/app/components/workflow/hooks' } from '@/app/components/workflow/hooks'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import PublishToast from './publish-toast' import PublishToast from './publish-toast'
import { useRagPipelineSearch } from '../hooks/use-rag-pipeline-search'


const RagPipelineChildren = () => { const RagPipelineChildren = () => {
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
handleExportDSL, handleExportDSL,
} = useDSL() } = useDSL()


// Initialize RAG pipeline search functionality
useRagPipelineSearch()

eventEmitter?.useSubscription((v: any) => { eventEmitter?.useSubscription((v: any) => {
if (v.type === DSL_EXPORT_CHECK) if (v.type === DSL_EXPORT_CHECK)
setSecretEnvList(v.payload.data as EnvironmentVariable[]) setSecretEnvList(v.payload.data as EnvironmentVariable[])

+ 168
- 0
web/app/components/rag-pipeline/hooks/use-rag-pipeline-search.tsx Ver fichero

'use client'

import { useCallback, useEffect, useMemo } from 'react'
import { useNodes } from 'reactflow'
import { useNodesInteractions } from '@/app/components/workflow/hooks/use-nodes-interactions'
import type { CommonNodeType } from '@/app/components/workflow/types'
import { ragPipelineNodesAction } from '@/app/components/goto-anything/actions/rag-pipeline-nodes'
import BlockIcon from '@/app/components/workflow/block-icon'
import { setupNodeSelectionListener } from '@/app/components/workflow/utils/node-navigation'
import { BlockEnum } from '@/app/components/workflow/types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types'
import type { KnowledgeRetrievalNodeType } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import { useGetToolIcon } from '@/app/components/workflow/hooks/use-tool-icon'

/**
* Hook to register RAG pipeline nodes search functionality
*/
export const useRagPipelineSearch = () => {
const nodes = useNodes()
const { handleNodeSelect } = useNodesInteractions()
const getToolIcon = useGetToolIcon()

// Process nodes to create searchable data structure
const searchableNodes = useMemo(() => {
return nodes.map((node) => {
const nodeData = node.data as CommonNodeType
const title = nodeData.title || nodeData.type || 'Untitled Node'
let desc = nodeData.desc || ''

// Keep the original node title for consistency with workflow display
// Only enhance description for better search context
if (nodeData.type === BlockEnum.Tool) {
const toolData = nodeData as ToolNodeType
desc = toolData.tool_description || toolData.tool_label || desc
}

if (nodeData.type === BlockEnum.LLM) {
const llmData = nodeData as LLMNodeType
if (llmData.model?.provider && llmData.model?.name)
desc = `${llmData.model.name} (${llmData.model.provider}) - ${llmData.model.mode || desc}`
}

if (nodeData.type === BlockEnum.KnowledgeRetrieval) {
const knowledgeData = nodeData as KnowledgeRetrievalNodeType
if (knowledgeData.dataset_ids?.length)
desc = `Knowledge Retrieval with ${knowledgeData.dataset_ids.length} datasets - ${desc}`
}

return {
id: node.id,
title,
desc,
type: nodeData.type,
blockType: nodeData.type,
nodeData,
toolIcon: getToolIcon(nodeData),
modelInfo: nodeData.type === BlockEnum.LLM ? {
provider: (nodeData as LLMNodeType).model?.provider,
name: (nodeData as LLMNodeType).model?.name,
mode: (nodeData as LLMNodeType).model?.mode,
} : {
provider: undefined,
name: undefined,
mode: undefined,
},
}
})
}, [nodes, getToolIcon])

// Calculate relevance score for search results
const calculateScore = useCallback((node: {
title: string;
type: string;
desc: string;
modelInfo: { provider?: string; name?: string; mode?: string }
}, searchTerm: string): number => {
if (!searchTerm) return 1

let score = 0
const term = searchTerm.toLowerCase()

// Title match (highest priority)
if (node.title.toLowerCase().includes(term))
score += 10

// Type match
if (node.type.toLowerCase().includes(term))
score += 8

// Description match
if (node.desc.toLowerCase().includes(term))
score += 5

// Model info matches (for LLM nodes)
if (node.modelInfo.provider?.toLowerCase().includes(term))
score += 6
if (node.modelInfo.name?.toLowerCase().includes(term))
score += 6
if (node.modelInfo.mode?.toLowerCase().includes(term))
score += 4

return score
}, [])

// Create search function for RAG pipeline nodes
const searchRagPipelineNodes = useCallback((query: string) => {
if (!searchableNodes.length) return []

const searchTerm = query.toLowerCase().trim()

const results = searchableNodes
.map((node) => {
const score = calculateScore(node, searchTerm)

return score > 0 ? {
id: node.id,
title: node.title,
description: node.desc || node.type,
type: 'workflow-node' as const,
path: `#${node.id}`,
icon: (
<BlockIcon
type={node.blockType}
className="shrink-0"
size="sm"
toolIcon={node.toolIcon}
/>
),
metadata: {
nodeId: node.id,
nodeData: node.nodeData,
},
data: node.nodeData,
score,
} : null
})
.filter((node): node is NonNullable<typeof node> => node !== null)
.sort((a, b) => {
// If no search term, sort alphabetically
if (!searchTerm) return a.title.localeCompare(b.title)
// Sort by relevance score (higher score first)
return (b.score || 0) - (a.score || 0)
})

return results
}, [searchableNodes, calculateScore])

// Directly set the search function on the action object
useEffect(() => {
if (searchableNodes.length > 0) {
// Set the search function directly on the action
ragPipelineNodesAction.searchFn = searchRagPipelineNodes
}

return () => {
// Clean up when component unmounts
ragPipelineNodesAction.searchFn = undefined
}
}, [searchableNodes, searchRagPipelineNodes])

// Set up node selection event listener using the utility function
useEffect(() => {
return setupNodeSelectionListener(handleNodeSelect)
}, [handleNodeSelect])

return null
}

Cargando…
Cancelar
Guardar