Skip to content

Commit

Permalink
refactor(workflow): migrate to Kahn's topological sort with ordered e…
Browse files Browse the repository at this point in the history
…dges

- Replace Tarjan's algorithm with Kahn's algorithm for topological sorting
- Add edge ordering support through IndexedOrder interface
- Consolidate node sorting logic from nodeSorting.ts into node-sorting.ts
- Improve node relationship terms using child/parent naming
- Enhance cycle detection through in-degree tracking
- Remove recursive DFS traversal for better performance
- Update workflow component with new sorting implementation
- Clean up debug logging and optimize queue management
  • Loading branch information
PriNova committed Feb 6, 2025
1 parent 0dc7338 commit 25bb365
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 447 deletions.
467 changes: 144 additions & 323 deletions vscode/src/workflow/node-sorting.ts

Large diffs are not rendered by default.

18 changes: 15 additions & 3 deletions vscode/src/workflow/workflow-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,6 @@ async function executeLLMNode(
//let chunksProcessed = 0
try {
for await (const msg of stream) {
//console.log('Chunks Processed: ', ++chunksProcessed)
if (abortSignal?.aborted) reject('LLM Node Aborted')
if (msg.type === 'change') {
const newText = msg.text.slice(accumulated.length)
Expand Down Expand Up @@ -763,8 +762,21 @@ function stringToContextItems(input: string[] | string | undefined): ContextItem
* @returns The sanitized string with backslashes and ${} escaped.
*/
export function sanitizeForShell(input: string): string {
// Only escape backslashes and ${} template syntax
return input.replace(/\\/g, '\\\\').replace(/\${/g, '\\${').replace(/"/g, '\\"')
// Replace backslashes first to avoid double escaping in subsequent steps
let sanitized = input.replace(/\\/g, '\\\\')

// Escape ${} templates
sanitized = sanitized.replace(/\${/g, '\\${')

// Escape quotes
sanitized = sanitized.replace(/"/g, '\\"')

// Check for any remaining forbidden characters and escape them as well
for (const char of ["'", ';']) {
sanitized = sanitized.replace(new RegExp(`\\${char}`, 'g'), `\\${char}`)
}

return sanitized
}

/**
Expand Down
43 changes: 6 additions & 37 deletions vscode/webviews/workflow/components/CustomOrderedEdge.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { BaseEdge, getSmoothStepPath } from '@xyflow/react'
import type { EdgeProps } from '@xyflow/react'
import type React from 'react'
import { memo, useMemo } from 'react'

interface IndexedOrder {
bySourceTarget: Map<string, number>
byTarget: Map<string, Edge[]>
}
import { memo } from 'react'

export interface Edge {
id: string
source: string
target: string
type?: string
style?: {
strokeWidth: 1
}
Expand All @@ -29,38 +25,14 @@ export const CustomOrderedEdgeComponent: React.FC<OrderedEdgeProps> = ({
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
// sourcePosition,
// targetPosition,
style,
markerEnd,
source,
target,
edges,
data,
}) => {
const edgeIndex = useMemo((): IndexedOrder => {
const bySourceTarget = new Map<string, number>()
const byTarget = new Map<string, Edge[]>()

if (!edges) return { bySourceTarget, byTarget }

// Index edges by target for quick parent edge lookups
for (const edge of edges) {
const targetEdges = byTarget.get(edge.target) || []
targetEdges.push(edge)
byTarget.set(edge.target, targetEdges)
}

// Precompute order numbers
for (const [targetId, targetEdges] of byTarget) {
targetEdges.forEach((edge, index) => {
const key = `${edge.source}-${targetId}`
bySourceTarget.set(key, index + 1)
})
}

return { bySourceTarget, byTarget }
}, [edges])

const [edgePath] = getSmoothStepPath({
sourceX,
sourceY,
Expand All @@ -70,10 +42,7 @@ export const CustomOrderedEdgeComponent: React.FC<OrderedEdgeProps> = ({
// targetPosition,
})

const orderNumber = useMemo(
() => edgeIndex.bySourceTarget.get(`${source}-${target}`),
[edgeIndex, source, target]
)
const orderNumber = data?.orderNumber

return (
<>
Expand Down
30 changes: 18 additions & 12 deletions vscode/webviews/workflow/components/Flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ export const Flow: React.FC<{
// UI state
const [isHelpOpen, setIsHelpOpen] = useState(false)

const { onEdgesChange, onConnect, onEdgesDelete, orderedEdges } = useEdgeOperations(
edges,
setEdges,
nodes
)

const { movingNodeId, onNodesChange, onNodeDragStart, onNodeAdd, onNodeUpdate } = useNodeOperations(
vscodeAPI,
nodes,
setNodes,
selectedNode,
setSelectedNode
)

const {
isExecuting,
executingNodeId,
Expand All @@ -58,16 +72,6 @@ export const Flow: React.FC<{
setNodeErrors,
} = useWorkflowExecution(vscodeAPI, nodes, edges, setNodes, setEdges)

const { movingNodeId, onNodesChange, onNodeDragStart, onNodeAdd, onNodeUpdate } = useNodeOperations(
vscodeAPI,
nodes,
setNodes,
selectedNode,
setSelectedNode
)

const { onEdgesChange, onConnect, getEdgesWithOrder } = useEdgeOperations(edges, setEdges, nodes)

const { onSave, onLoad, calculatePreviewNodeTokens, handleNodeApproval } = useWorkflowActions(
vscodeAPI,
nodes,
Expand Down Expand Up @@ -137,6 +141,7 @@ export const Flow: React.FC<{
data: { ...node.data }, // Ensure data object is properly cloned
}))
}, [nodesWithState, edges])

return (
<div className="tw-flex tw-h-screen tw-w-full tw-border-2 tw-border-solid tw-border-[var(--vscode-panel-border)] tw-text-[14px] tw-overflow-hidden">
<div
Expand Down Expand Up @@ -171,17 +176,18 @@ export const Flow: React.FC<{
<div className="tw-flex-1 tw-bg-[var(--vscode-editor-background)] tw-h-full">
<ReactFlow
nodes={nodesWithHandlers}
edges={getEdgesWithOrder}
edges={orderedEdges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onEdgesDelete={onEdgesDelete}
onConnect={onConnect}
onNodeClick={onNodeClick}
onNodeDragStart={onNodeDragStart}
deleteKeyCode={['Backspace', 'Delete']}
nodeTypes={nodeTypes}
edgeTypes={{
'ordered-edge': props => (
<CustomOrderedEdgeComponent {...props} edges={edges} />
<CustomOrderedEdgeComponent {...props} edges={orderedEdges} />
),
}}
fitView
Expand Down
105 changes: 62 additions & 43 deletions vscode/webviews/workflow/components/hooks/edgeOperations.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { type EdgeChange, addEdge, applyEdgeChanges } from '@xyflow/react'
import type React from 'react'
import { useCallback, useMemo } from 'react'
import type { Edge } from '../CustomOrderedEdge'
import type { WorkflowNodes } from '../nodes/Nodes'
import { memoizedTopologicalSort } from './nodeStateTransforming'

interface IndexedOrder {
bySourceTarget: Map<string, number>
byTarget: Map<string, Edge[]>
}

/**
* A React hook that provides functionality for managing edges in a workflow application.
Expand All @@ -20,72 +25,86 @@ export const useEdgeOperations = (
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>,
nodes: WorkflowNodes[]
) => {
// Memoize the topological sort results
const sortedNodes = useMemo(() => memoizedTopologicalSort(nodes, edges), [nodes, edges])
const onEdgesDelete = useCallback(
(deletedEdges: Edge[]) => {
setEdges(prevEdges => {
const updatedEdges = prevEdges.filter(
edge => !deletedEdges.some(deleted => deleted.id === edge.id)
)
return [...updatedEdges]
})
},
[setEdges]
)

// Memoize the edge order map calculation
const edgeOrder = useMemo(() => {
const orderMap = new Map<string, number>()
const edgesByTarget = new Map<string, Edge[]>()
// Reimplementing the edgeIndex logic from CustomOrderedEdgeComponent
const edgeIndex = useMemo((): IndexedOrder => {
const bySourceTarget = new Map<string, number>()
const byTarget = new Map<string, Edge[]>()

if (!edges) return { bySourceTarget, byTarget }

// Group edges by target
// Index edges by target for quick parent edge lookups
for (const edge of edges) {
const targetEdges = edgesByTarget.get(edge.target) || []
const targetEdges = byTarget.get(edge.target) || []
targetEdges.push(edge)
edgesByTarget.set(edge.target, targetEdges)
byTarget.set(edge.target, targetEdges)
}

// Calculate orders using our memoized sorted nodes
for (const targetEdges of edgesByTarget.values()) {
const sortedEdges = targetEdges.sort((a, b) => {
const aIndex = sortedNodes.findIndex(node => node.id === a.source)
const bIndex = sortedNodes.findIndex(node => node.id === b.source)
return aIndex - bIndex
})

sortedEdges.forEach((edge, index) => {
orderMap.set(edge.id, index + 1)
// Precompute order numbers
for (const [targetId, targetEdges] of byTarget) {
targetEdges.forEach((edge, index) => {
const key = `${edge.source}-${targetId}`
bySourceTarget.set(key, index + 1)
})
}

return orderMap
}, [edges, sortedNodes])
return { bySourceTarget, byTarget }
}, [edges])

// Memoize the final edges with order data
const edgesWithOrder = useMemo(
() =>
edges.map(edge => ({
// Memoize the edge order map calculation
const edgesWithOrder = useMemo(() => {
const ordered = edges.map(edge => {
const orderNumber = edgeIndex.bySourceTarget.get(`${edge.source}-${edge.target}`) || 0
return {
...edge,
type: 'ordered-edge',
data: {
orderNumber: edgeOrder.get(edge.id) || 0,
orderNumber: orderNumber,
},
})),
[edges, edgeOrder]
)
}
})
return [...ordered]
}, [edges, edgeIndex])

const onEdgesChange = useCallback(
(changes: EdgeChange[]) => setEdges(eds => applyEdgeChanges(changes, eds) as typeof edges),
(changes: EdgeChange[]) => {
setEdges(edges => {
const updatedEdges = applyEdgeChanges(changes, edges)
return [...updatedEdges]
})
},
[setEdges]
)

const onConnect = useCallback(
(params: any) =>
setEdges(eds =>
addEdge(
{
...params,
type: 'smoothstep',
},
eds
)
),
[setEdges]
(params: any) => {
setEdges(eds => {
const newEdge = {
...params,
type: 'smoothstep',
} as Edge
const updatedEdges = addEdge(newEdge, eds)
return [...updatedEdges]
})
},
[setEdges] // ADDED creationOrderedEdges to dependencies to get the updated value in the log
)

return {
onEdgesChange,
onConnect,
getEdgesWithOrder: edgesWithOrder,
onEdgesDelete,
orderedEdges: edgesWithOrder,
}
}
21 changes: 3 additions & 18 deletions vscode/webviews/workflow/components/hooks/nodeStateTransforming.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { memoize } from 'lodash'
import { useMemo } from 'react'
import { processGraphComposition } from '../../../../src/workflow/node-sorting'
import type { Edge } from '../CustomOrderedEdge'
Expand Down Expand Up @@ -117,20 +116,6 @@ export function getInactiveNodes(edges: Edge[], startNodeId: string): Set<string
* @param edges - The edges between the workflow nodes.
* @returns The workflow nodes in a sorted order.
*/
export const memoizedTopologicalSort = memoize(
(nodes: WorkflowNodes[], edges: Edge[]) => {
return processGraphComposition(nodes, edges, false)
},
// Keep existing memoization key generator
(nodes: WorkflowNodes[], edges: Edge[]) => {
const nodeKey = nodes
.map(n => `${n.id}-${n.data.title}-${n.data.active}`)
.sort()
.join('|')
const edgeKey = edges
.map(e => `${e.source}-${e.target}`)
.sort()
.join('|')
return `${nodeKey}:${edgeKey}`
}
)
export const memoizedTopologicalSort = (nodes: WorkflowNodes[], edges: Edge[]) => {
return processGraphComposition(nodes, edges, false)
}
4 changes: 2 additions & 2 deletions vscode/webviews/workflow/components/nodes/LLM_Node.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Model } from '@sourcegraph/cody-shared'
import { Handle, Position } from '@xyflow/react'
import type React from 'react'
import { CodyAILogo } from '../../icons/CodyAILogo'
import { CodyLogoBW } from '../../../icons/CodyLogo'
import {
type BaseNodeData,
type BaseNodeProps,
Expand Down Expand Up @@ -56,7 +56,7 @@ export const LLMNode: React.FC<BaseNodeProps> = ({ data, selected }) => (
alignItems: 'center',
}}
>
<CodyAILogo size={14} className="tw-mr-2 tw-ml-1" />
<CodyLogoBW size={14} className="tw-mr-2 tw-ml-1" />
<div
className="tw-text-center tw-flex-grow"
style={{
Expand Down
Loading

0 comments on commit 25bb365

Please sign in to comment.